DEV Community

Cover image for Pure Python Web App: No Flask, No Django, No Frameworks!
Leapcell
Leapcell

Posted on

Pure Python Web App: No Flask, No Django, No Frameworks!

Image description

Leapcell: The Best of Serverless Web Hosting

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]

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

By default, Gunicorn will bind to port 8000. We can use curl to send requests for testing:

$ curl http://localhost:8000
Hello world!
Enter fullscreen mode Exit fullscreen mode

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!
Enter fullscreen mode Exit fullscreen mode

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 key CONTENT_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]

Enter fullscreen mode Exit fullscreen mode

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!
Enter fullscreen mode Exit fullscreen mode

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]

Enter fullscreen mode Exit fullscreen mode

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

Image description

🚀 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.

Image description

📖 Explore Our Documentation

🔹 Follow us on Twitter: @LeapcellHQ

Top comments (0)