Write Your Own Python Async ASGI Web App, No Framework Needed
Tags: python, asgi
Published: 2020-07-27 10:17:31

[ASGI] is the new [WSGI], with an [asynchronous](https://docs.python.org/library/asyncio.html) flair. [ASGI] is emerging as a new standard for asynchronous Python web apps. Because web applications spend a lot of time waiting, async web apps make a lot of sense, and have a [significant performance boost](https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=fortune&l=z8kflr-f&a=2) over synchronous apps. Speedy.

Thankfully, ASGI frameworks like [Starlette], [Quart], [Django Channels], and [BlackSheep] exist. With any of them, your high performance Python web app can be up and running stably and quickly.

But if you are a roll-your-own, do-it-yourself, disdain-for-abstractions, self-made, tired-of-hyphenation kind of developer, then you want to take a peek under the hood.

## The engine

```python
"""A barebones ASGI app that dumps scope."""

import pprint

def pretty_html_bytes(obj):
    """Pretty print a Python object in <pre> tags."""
    pp = pprint.PrettyPrinter(indent=2, width=256)
    prettified = pp.pformat(obj)
    return f"<pre>{prettified}</pre>".encode()

async def app(scope, receive, send):
    """The simplest of ASGI apps, displaying scope."""
    headers = [(b"content-type", b"text/html")]
    body = pretty_html_bytes(scope)
    await send({"type": "http.response.start", "status": 200, "headers": headers})
    await send({"type": "http.response.body", "body": body})

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8000)
``` 

Run the above, open a browser window with http://localhost:8000, and you should see a dump of the ASGI app's `scope`. While `scope["path"]` is likely the most interesting (change the path and watch that value change), `scope["client"]`, `scope["scheme"]`, and others are of interest as well.

You may also try out [the Repl.it](https://repl.it/@jonathanbowman/ASGI#main.py) I made with the above code.

## Customize

Try changing the body, but do make sure it is in bytes, not a string. Appening `.encode()`, such as in `my_string.encode()` will usually convert a string to bytes in the proper way. You can make the character encoding explicit with `my_string.encode("utf-8")`.

Add some routing with various `if scope["path"] == "/my/path":` and `elif`, etc. Use an `else` to catch paths that don't match, changing the status to 404.

A static handler is an option as well. I usually place static assets like CSS, Javascript, and images in a particular directory hierarchy. For instance, `if scope["path"].startswith("/assets/"):` makes a good start. Then open and read the file, setting the body to the bytes read. 

You may also try out the other ASGI servers in addition to [Uvicorn]: [Hypercorn] and [Daphne]. I also wrote [a summary article about all three](https://dev.to/bowmanjd/the-three-python-asgi-servers-5447).

Enjoy! This is why Python has such a proliferation of web frameworks. Easy to get started and hooked.



[ASGI]: https://asgi.readthedocs.io/en/latest/introduction.html
[WSGI]: https://docs.python.org/library/wsgiref.html
[Starlette]: https://pypi.org/project/starlette/
[Quart]: https://pypi.org/project/Quart/
[BlackSheep]: https://pypi.org/project/blacksheep/
[Django Channels]: https://channels.readthedocs.io/
[Uvicorn]: https://www.uvicorn.org/
[Daphne]: https://github.com/django/daphne
[Hypercorn]: https://pgjones.gitlab.io/hypercorn/