Building OwnFlask - A Flask(like) Python Framework
In order to demistify `flask` these I had two options either to read Flask code end to end and understand or reverse engineer flask by building one on my own. I chose the latter and this blog is a ste
I have been wanting to demystify what goes behind the Python Flask framework. How does defining something as simple as app.route
handle HTTP Requests? How does app.run
creates a server and maintains it?
To demystify flask
these, I had two options Read Flask code end to end and understand or Reverse engineer flask by building one on my own. I chose the latter, and this blog is a step-by-step log of how it went.
Side Note
If you are new to Flask, then How to build your 1st flask app might be a good place to start.
Reverse Engineering
A simple Flask application looks something like this.
Reverse engineering started in my head. I am going to be working with just two files, ownflask.py
and demo.py
.
Here is how a simple flask application would look
Looking at this sample snippet, I want to mimic the same interface. Take a pass from top to bottom and see what all we need
We need a class
Flask
which initializes anapp
objectThe
Flask
class has a methodrun
, and it starts a serverThe
Flask
class also has aroute
method that registers the endpoints
Let's lay them down
That's gives us the basic skeleton. Let's add the functionality one by own. Python http module provides an HTTPServer
let's use that.
Starting a Server
In Flask, app.run
is responsible for starting a development webserver. The server then listens to all HTTP requests and responds to them.
In demo.py
, change from flask
to from ownflask
to work with the module we just created and run demo.py
. On hitting the http://127.0.0.1:8000
from the browser, you get a 501 error since we haven't implemented anything to handle the incoming request.
Mapping Requests
The app.route
method in Flask registers an endpoint. When an HTTP request comes, it maps it to the associated function call. These routes are maintained in a global object so that the request handler can refer to it. For our ownflask
, let's use a global dictionary.
Here I have two methods one to record the routes
to its associated functions and route_methods
to associate endpoints and its HTTPMethods.
Handling GET Request
When running our server, we have used a BaseHTTPRequestHandler
. From the Python documentation, it is clear that we have to extend it to support handling requests.
By itself, it cannot respond to any actual HTTP requests; it must be subclassed to handle each request method (e.g., GET or POST).
The above snippet sends Handling GET
as a response despite what the route function returns. Let's change that.
dir(self)
returns that self.path
is the URL, mapping that with routes dict, we can call the respective function.
Handling URL Params
Flask is known for passing URL params as a part of a URL string or a query string.
The 1st one would require some form of regex in the routes and the way we store them. Let's handle them later. Let's handle hello world
with the name http://127.0.0.1:8000?name=Joe
The current code fails with a KeyError
since the query string is also a part of the route.
To parse this and separate the URL path and the query params, we will use urllib
In Flask, the routing function can access the request params via the global Request
object. In our case, for the hello
route to access query params, we need the means to pass it to them.
Request Class
Let's pass this Request object to the route.
With the current state, hello() takes 0 positional arguments but 1 was given
let's capture request
Handling JSON Response
If we modify the hello
endpoint to return a dict
instead of str
, we will receive an error.
descriptor 'encode' for 'str' objects doesn't apply to a 'dict' object
It happens because we convert dict to a bytes object. To do this, we should convert the response dict to str
and then encode it.
Handling POST Request
For handling POST requests, you need to access the request body along with other parameters. Let's update the request class to support the same.
Let's consume the same via a POST API
Handling Unsupported Request
Right now, if you hit /todo
from the browser, you will get the response. This is wrong since we have clearly defined that /todo
on supports post request. This is where route_methods
comes in really handy.
Looks like we are repeating ourselves a lot; let's move them to a common function
The final do_GET
and do_POST
method looks like this.
We can further refactor them into
Introducing Multi-Threading
At this point, if you write a small multithreading script and hit our server, it will hang because HTTPServer
is not designed to handle multiple requests. Replacing it with ThreadingHTTPServer.
WSGI vs HTTP
At this point, I was happy with what I accomplished and already posted a tweet and ArunMozhi nudged me in the direction to explore WSGIServer
.
What started as an experiment to Demyistify flask and understand it better got me into a rabbit hole of new questions.
How is
WSGIServer
different fromHTTPServer
the interface looks the same?How can we plug the
ownflask
to work with GunicornHow to add async to ownflask?
Going one step further, How does Gunicorn work?
What are my unknown unknowns?
If you know the answer to any of these, you can send them to me via Twitter
Last updated