Parameter annotations in function definitions are a recent addition to the language. In this article we show how this feature can be put to good use when we build a simple web application server that checks its input parameters rigoreously.
Creating a simple web application server with HTTPServer
It is of course entirely possible to create a web application in a short time when you use an existing Python web application framework. In previous articles and my book on web applications I've used CherryPy extensively and although I recommend it for its flexibility an ease of use, it isn't all that difficult to create a web application framework from scratch.
https.server module provides us with the basic building blocks: the
HTTPServer class to handle incoming connections and a
BaseHTTPRequestHandler class that processes requests and returns an answer. The main part of developing an application server is therefore sub-classing the
BaseHTTPRequestHandler class. The minimum it will have to provide is a
do_GET() method that will return results based on any parameters it receives.
Using Python parameter annotations
CherryPy uses classes with methods that serve together as an application: requested URLs are mapped to these methods and any parameters are passed along. CherryPy uses an expose decorator to identify the methods that may be called. Non exposed methods are invisible, i.e. URLs that match those methods do not result in the invocation of that method. This behavior is what we like to mimic in our own web application server.
Another important concept in web applications is the screening of input: we would like to check that incoming data (i.e. the arguments that come along with a query) are within the range of things we deem acceptable. For example, a function that adds its arguments should reject anything that cannot be interpreted as a float. We could write code easily enough that checks any function parameters explicitly but wouldn't it be nice if there was a more syntactically pleasing way of writing this?
Enter Python's function parameter annotations. Python allows us to augment each function parameter with an expression that is evaluated when the function is defined and which is stores in the functions
__annotation__ field as a dictionary indexed by parameter name. Such an annotation might be as simple a single string but it can be anything, even a function reference. This function could be called with the value we would like to pass as an parameter to check if this value is ok. This could be done before the function is actually called, for example by the
do_GET() method of our request handler.
Assuming we have our
applicationserver module available, let's have a look what the definition of a new web application might look like:
from http.server import HTTPServer from applicationserver import ApplicationRequestHandler,IsExposed class Application: def donothing: pass def index(self) -> IsExposed: return 'index oink' def add(self,a:float,b:float) -> IsExposed: return str(a+b) def cat(self,a:str) -> IsExposed: return ' '.join(a) def opt(self,a:int=42) -> IsExposed: return str(a) class MyAppHandler(ApplicationRequestHandler): application=Application() appserver = HTTPServer(('',8088),MyAppHandler) appserver.serve_forever()The overall idea is to subclass the
ApplicationRequestHandlerand assign an instance of the
Applicationclass to its
applicationfield (line 21). This applicationhandler is then passed to a
HTTPServerinstance that will forward incoming requests to this handler (line 24).
ApplicationRequestHandler will try to map URLs of the form
http://hostname:8080/foo?a=1&b=2 to member functions of the
Application instance. It will only consider member function with a return annotation equal to an
IsExposed object. So even though we have defined a
donothing() function, it will not be executed when a URL like
http://hostname:8080/donothing is received.
We also use annotations to restrict input values for parameters to functions that are exposed. Remember that annotations can be any expression and here we employ that fact to annotate the
b parameters to the
add() method with a reference to the built-in
float() function. Our
ApplicationRequestHandler will pass an argument to any callable it finds as its corresponding annotation and will only execute the method if this callable returns a value (and not raise an exception). So a URL like
http://localhost:8080/add?a=1.23&b=4.56 will return a meaningful result while
http://localhost:8080/add?a=1.23&b=spam will fail with an error. Off course we are not restricted to built-in functions here: we can refer to functions that may perform elaborate checking as well, perhaps checking against regular expressions or performing lookups in database tables.
All this shows that Python's function annotations allow for a rather elegant way to describe the expected behavior of methods that perform some sort of action in a web application. In a future article I'll show how to implement the