Build a Web App Using Pure Python (No Flask, No Django, No Frameworks!)
James Reed
Infrastructure Engineer ยท Leapcell

Building the Minimal Python Web Application Using WSGI
Those who have written Python web applications, especially those who have carried out online deployments, must have heard of the WSGI protocol. It defines the data exchange interface between Python's web servers and web applications. This description may be rather abstract, so let's explain it in detail through practical examples below.
Deployment of Web Applications in a Production Environment
Suppose we have developed a web application using a web application framework such as Django or Flask. The official documentation of these frameworks usually points out that the built-in servers of the frameworks, such as python manage.py runserver
in Django or flask --app hello run
in Flask, are only suitable for debugging in the development phase and cannot handle the traffic in a production environment. When deploying to a production environment, the web application needs to be run behind a web server. Common web servers include Gunicorn and uWSGI. The web server will provide concurrency options such as process models and thread models to improve the concurrency performance of the web application.
In the above simple scenario, there are four combinations of technical choices: Gunicorn + Django, Gunicorn + Flask, uWSGI + Django, and uWSGI + Flask. If each combination requires the web application framework to provide different web service adaptation codes, the complexity will reach $N^2$, which is obviously not cost-effective. The existence of WSGI is to define the interface between the web server and the web application, and framework developers only need to code for this interface. In this way, web application developers have more freedom in making choices. For example, the same Django code can run on both Gunicorn and uWSGI.
Without a Web Application Framework
Before discussing how Django and Flask adapt to WSGI, let's simplify the problem first. The role of a web framework is to provide some convenient functions, such as routing and HTTP request parsing, to help us develop web applications more easily and quickly. But for very simple applications, we can also choose not to use a framework.
The WSGI interface defined by PEP is very simple, and there is no need (and it should not be) to use any web framework:
HELLO_WORLD = b"Hello world!\n" def simple_app(environ, start_response): """Simplest possible application object""" status = '200 OK' response_headers = [('Content-type', 'text/plain')] start_response(status, response_headers) return [HELLO_WORLD]
This simple web application interacts with the web server through the environ
environment variable dictionary and the start_response
function, and the web server will ensure that the correct parameters are passed in.
Suppose the above code is saved as app.py
and Gunicorn has been installed. You can start the application using the following command:
gunicorn app:simple_app
By default, Gunicorn will bind to port 8000. We can use curl
to send requests for testing:
$ curl http://localhost:8000 Hello world!
As we can see, everything works as expected. At the same time, the code logic of this web application is very simple. It does not consider the request path (such as /
, /api
, etc.) and the request method (such as GET, POST, PUT, etc.), and always returns a status code of 200 and Hello World!
as the response body.
$ curl http://localhost:8080/not-found Hello world! $ curl -X POST http://localhost:8080 Hello world!
Beyond Hello World
As mentioned before, a normal web application usually has multiple endpoints and is expected to return different responses according to different requests.
The web server will store all the information of the request in the environ
dictionary, and it will also contain other environment variables. Among all the keys, we need to pay special attention to the following three:
REQUEST_METHOD
: The request method, such as GET, POST, etc.PATH_INFO
: The request path.wsgi.input
: A file object. When the request body contains data, it can be read through this object. Another keyCONTENT_LENGTH
will indicate the length of the request body, and the two are usually used together.
Suppose we want to implement a new POST interface on the /
path, which receives JSON-type parameters. When the user passes in {"name": "xxx"}
, the web application will return Hello, xxx!
, while the GET interface remains unchanged and continues to return Hello, World!
. The code is as follows:
import json def simple_app(environ, start_response): request_method = environ["REQUEST_METHOD"] path_info = environ["PATH_INFO"] response_headers = [('Content-type', 'text/plain')] if path_info == '/': status = '200 OK' if request_method == 'GET': body = b'Hello world!\n' elif request_method == 'POST': request_body_size = int(environ["CONTENT_LENGTH"]) request_body = environ["wsgi.input"].read(request_body_size) payload = json.loads(request_body) name = payload.get("name", "") body = f"Hello {name}!\n".encode("utf-8") else: status = '405 Method Not Allowed' body = b'Method Not Allowed!\n' else: status = '404 NOT FOUND' body = b'Not Found!\n' start_response(status, response_headers) return [body]
In addition to handling the request path and request method, we have also added some simple client error detection. For example, accessing a path other than /
will return a 404, and accessing /
with a method other than GET or POST will return a 405. Here are some simple tests:
$ curl http://localhost:8080/ Hello World! $ curl -X POST http://localhost:8080/ -d '{"name": "leapcell"}' Hello leapcell! $ curl -X PUT http://localhost:8080/ Method Not Allowed! $ curl http://localhost:8080/not-found Not Found!
Becoming More Like Flask
As the logic of the web application becomes more complex, the simple_app
function will become more and more lengthy. This kind of "spaghetti code" obviously does not conform to good programming practices. We can refer to the API of Flask for simple encapsulation.
For example, convert the function into a callable class so that web application developers can obtain the WSGI application; use routes
to store all the mappings from paths to handler functions; encapsulate environ
into a request
object, etc.
class MyWebFramework: def __init__(self): self.routes = {} def route(self, path): def wrapper(handler): self.routes[path] = handler return handler return wrapper def __call__(self, environ, start_response): request = self.assemble_request(environ) path_info = environ["PATH_INFO"] if path_info in self.routes: handler = self.routes[path_info] return handler(request) else: status = '404 NOT FOUND' response_headers = [('Content-type', 'text/plain')] start_response(status, response_headers) return [b'Not Found!\n'] app = MyWebFramework() @app.route("/my_endpoint") def my_endpoint_handler(request): # business logic here to handle request and assemble response response_headers = [('Content-type', 'text/plain')] status = '200 OK' body = b'Endpoint response!\n' return [body]
In this way, the MyWebFramework
part can gradually be abstracted into a web application framework, and the real business logic of the web application only needs to write each handler function. Referring to the source code of flask.app.Flask
, it also uses a similar implementation method. A Flask application is derived from the Flask
core class, and this class itself is a WSGI application.
Django's design is slightly different. It proposed and implemented the ASGI (Asynchronous Server Gateway Interface) protocol to support asynchronous requests. A Django application can be converted into an ASGI application or a WSGI application through internal functions. When we only focus on the WSGI part, we will find that its principle is similar to what was introduced before.
Recommended Reading
Leapcell: The Best of Serverless Web Hosting
Finally, I would like to recommend the best platform for deploying Python services: Leapcell
๐ Build with Your Favorite Language
Develop effortlessly in JavaScript, Python, Go, or Rust.
๐ Deploy Unlimited Projects for Free
Only pay for what you useโno requests, no charges.
โก Pay-as-You-Go, No Hidden Costs
No idle fees, just seamless scalability.
๐ Explore Our Documentation
๐น Follow us on Twitter: @LeapcellHQ