Microdot: Yet Another Python Web Framework

Posted by
on under

I just realized that I have never written on this blog about Microdot, my very own web framework for Python. I have released Microdot 2.0 a few days ago, so I guess this is a good time to make a belated announcement, and tell you why this world needs yet another Python web framework.

But before I tell you about the reasons and the history of Microdot, let me share some of its features:

  • Flask-like syntax, but without the magical/obscure parts (no application/request contexts)
  • Small enough to work with MicroPython, while also being compatible with CPython
  • Fully compatible with asyncio
  • Websocket support
  • Server-Sent Events (SSE) support
  • Templating support with Jinja (CPython) and uTemplate (MicroPython)
  • Cross-Origin Request Sharing (CORS) support
  • User sessions stored on cryptographically signed cookies
  • Uses its own minimal web server on MicroPython, and integrates with any ASGI or WSGI web servers on CPython
  • Included test client to use in unit tests

Interested? Keep reading to learn more about Microdot.

Why Microdot

Back in 2019, I was working on a hardware project based on the ESP8266 microcontroller. For the software portion of this project I used MicroPython, an alternative implementation of the Python language that is designed for small devices.

I wanted to host a small web-based interface on the device, and to my surprise I could not find any usable web frameworks. Things such as Flask or Bottle do not work under MicroPython because they are too big for it. The only MicroPython web framework I could find was one called "picoweb", which required an unofficial fork of the MicroPython language, which to me was a deal breaker.

Microdot was born out of needing to have a web framework that was as close as possible to Flask, but designed to run on an official and actively maintained version of MicroPython.

I ended up creating Microdot, and used it to complete my project. I also put the source code on GitHub, since it was obvious to me that there was a hole in the MicroPython ecosystem in terms of web frameworks. While it has received growing attention from hardware inclined developers, it managed to stay below the radar for a lot of people in the Python community for almost 5 years now. Here is a timeline of the major events in Microdot's history:

Date Event
April 2019 Microdot 0.1, first public release
August 2022 Microdot 1.0, with a synchronous base implementation and an asyncio extension
December 2023 Microdot 2.0, redesigned as 100% asynchronous

How Does Microdot Code Look Like?

Microdot is actually very similar to Flask. You write web routes as decorated functions:

from microdot import Microdot

app = Microdot()

@app.get('/')
async def index(request):
    return {'hello': 'world'}

app.run(debug=True)

The @app.get decorator is used to define GET requests. There are similar decorators for POST, PATCH, PUT and DELETE. Like Flask, Microdot handles OPTIONS and HEAD requests automatically.

Microdot 2 is asynchronous, so it is best to write handler functions as async def functions. Under CPython, regular def functions execute in a thread executor (like FastAPI), but most hardware devices that run MicroPython lack threading support, so regular functions on that platform just block for as long as they run.

One aspect in Flask that I do not like is the use of application and request contexts, as these add an unnecessary layer of complexity. I did not want to have that, so request handler functions in Microdot just receive a request object as a first positional argument.

As with Flask, the return value from a handler function is the response, and Microdot automatically formats the response as JSON if a dictionary is returned. It also supports second and third returned values for custom status code and headers. Microdot also copies the way streamed responses work in Flask with the use of generators, and also supports asynchronous generators.

I like the functionality offered by Blueprints in Flask, but here once again I feel Flask makes everything too complicated. In Microdot, you can create multiple application instances and use the mount() method to combine them:

api = Microdot()

@api.get('/users')
async def get_users():
    pass

main_app = Microdot()
main_app.mount(api, url_prefix='/api')  # /api/users route is added to main_app

Microdot has native support for WebSocket and Server-Sent Events. Here are example endpoints that use these features:

from microdot.websocket import with_websocket
from microdot.sse import with_sse

@app.route('/echo')
@with_websocket
async def echo(request, ws):
    while True:
        data = await ws.receive()
        await ws.send(data)

@app.route('/events')
@with_sse
async def events(request, sse):
    for i in range(10):
        await asyncio.sleep(1)
        await sse.send({'counter': i})

Hopefully there is nothing mysterious or magical in this code and you can understand it fully just from reading it.

If you want to render HTML templates, Microdot can use Jinja or uTemplate, the latter being the main (only?) templating library available for MicroPython. Templates can be rendered synchronously (blocking the asyncio loop) or asynchronously. For larger templates it is best to use the async interface as that improves concurrency. Templates can also be streamed as regular or asynchronous generators, one more way to improve performance and concurrency.

User sessions are implemented in a way that is also similar to Flask, but I decided to write user sessions as JWTs, which are fairly easy to debug.

If you want to learn more about Microdot's features, I have written documentation for it. I also offer a nice collection of examples in the GitHub repository.

How Small is Microdot?

This is not a straightforward question to answer, because unlike Flask, FastAPI and most other frameworks, Microdot has a modular structure, so there are a number of different configurations that can be measured depending on what features are used. When working with CPython this is not very important, but for MicroPython projects running on microcontrollers it is useful to be able to pick and choose exactly what you need and drop the parts that you don't.

The core Microdot framework comes in a single microdot.py Python source file. Below I used the cloc utility to count how many lines of code the current version of this file has:

files blank comment code
Microdot 2.0.1 (minimal) 1 216 456 728

Next I calculated the full Microdot configuration for the same version, including all of its features:

files blank comment code
Microdot 2.0.1 (full) 11 435 759 1577

Just so that you have an idea of how small this is, let's see how big Flask and FastAPI are. To make this a more consistent comparison, I've included Werkzeug with Flask, and Starlette with FastAPI.

files blank comment code
Flask 3.0.0 24 1775 3211 3854
Werkzeug 3.0.1 59 4043 5634 11704
Total 83 5818 8845 15558
files blank comment code
FastAPI 0.106.0 40 2052 5884 9839
Starlette 0.27.0 34 1008 477 4969
Total 74 3060 6361 14808

From this you can see that both Flask and FastAPI, when combined with their main supporting dependency have about 15K lines of code each, while Microdot has 1.5K or roughly 10% of the size of its bigger competitors (or 5% if you only need basic web routing features).

Since I'm counting Werkzeug and Starlette as part of Flask and FastAPI respectively, you may be curious about what third-party dependencies Microdot relies on, and if there are any big ones. The single-file minimal version does not require any dependencies at all, and it even includes its own web server. Some of the optional features do require third-party dependencies, and this is comparable to the larger frameworks. Like Flask, you will need to add a templating library (Jinja or uTemplate) to use templates. For user sessions, Microdot relies on PyJWT, while Flask uses its own Itsdangerous package.

Should I Switch to Microdot?

For a MicroPython project I think Microdot is a great framework to use, even though there are now a couple others that did not exist back in 2019.

If you are using CPython you are certainly welcome to switch if you want to, but my recommendation would be to stay with your current web framework if you are happy with it. A good reason to switch would be if you want to make better use of your server's resources. It's really hard to make predictions, but depending on the project you may be able to fit an extra web server worker or two on the same hardware, just from RAM savings after switching from a larger framework to Microdot. But as I said, I wouldn't go through the pain of a migration unless size is very important to you.

What I would like to ask is that you keep Microdot in mind for when you start a new project. If you end up using it, I would like to know how it works for you, so please reach out!

Become a Patron!

Hello, and thank you for visiting my blog! If you enjoyed this article, please consider supporting my work on this blog on Patreon!

22 comments
  • #1 Moses Koroma said

    I will like to explore the technology

  • #2 Sean Mayorga said

    This is awesome! Thanks for sharing with the world I'm going to use the heck out of this

  • #3 Nick Antonaccio said

    I'm looking forward to trying this out, thank you!

  • #4 Donald KANTI said

    Great technology
    Thanks for sharing

  • #5 Shahrukh said

    Would the flask packages like flask-alchemy and forms work with microdot too?

  • #6 Miguel Grinberg said

    @Shahrukh: No, there is no connection between Flask and Microdot. If you need database support for Microdot you can use Alchemical. Microdot includes its own basic form handling.

  • #7 mike said

    in the newest version how do you use a local css file does it go somewhere in Template('index.html').render()?

  • #8 Sandor said

    Miguel, which forms framework do you recommend to use with microdot (if any)? WTForms, as for flask?

  • #9 Miguel Grinberg said

    @Sandor: Microdot provides your form fields in the request.form dictionary. If you need to perform validation on these fields, then I would suggest Marshmallow or Pydantic.

  • #10 Miguel Grinberg said

    @mike: Static files are not the same as templates, as they do not need to be rendered. First of all you need to decide if your CSS is a template or a static file. For static files, you can put them anywhere you like. Normally I would create a static subdirectory and put them there.

  • #11 mike said

    @Miguel Grinberg: i tried as a static file at first i was using url_for thinking it was a jinja thing but turns out its a flask thing so my bad there, other than that when i just path to it normally in the static folder i keep getting a 404 the console just shots GET / 200, GET /static/index.css 404, im sure its a silly mistake somewhere i just cant seem to figure it out

  • #12 Miguel Grinberg said

    @mike: You may want to look at the static file examples in the Microdot repository. That will give you a working example that you can start from.

  • #13 Mateusz said

    Great work! Very nice framework. I was just looking for such a solution (after some troubles with picoweb). Just one question. Is version 2.0 still working on esp8266 or only on esp32? Thanks!

  • #14 Miguel Grinberg said

    @Mateusz: Microdot is not specific to any microcontroller, it should run just fine on any microcontroller that supports MicroPython. And version 2 is a bit smaller than version 1, so it should run even more comfortably on the ESP8266 than the previous version!

  • #15 CARLOS VILLEGAS said

    Hi Miguel! thanks for sharing this great tool! I've found your site doing some research on implementing web socket under Python. I'm working on a project for controlling a PingPong Robot from a web Page. It's working fine, but currently I'm confused on how to use "ws.send" instruction to update data from ESP32 to web page while the "await ws.receive()" instruction is blocking the program execution.
    I would really appreciate any feedback from your side!
    Thanks again for sharing this great tool!
    Greetings from Argentina.

    @app.route('/ws')
    @with_websocket
    async def echo(request, ws):
    while True:
    message = await ws.receive() # this line will block the program execution
    await ws.send(message) #How to update data from ESP32 to Web Page?

  • #16 Miguel Grinberg said

    @Carlos: It really depends on your application. If you do not need to receive anything, then remove the receive call and just send. Another option is to wrap the receive call with a asyncio.wait_for(), which allows you to put a timeout. Finally, you can run the receive call on a separate task. Which choice is best depends on the application, I recommend that you familiarize yourself with all the options the asyncio package offers to create concurrency.

  • #17 Mateusz said

    Thanks! I will test it. Just one more question. Using utemplate creates every time static html file (it makes sense) but... isn't it bad for esp8266 or 32 memory and write cycles?

  • #18 Miguel Grinberg said

    @Mateusz: I believe it compiles the template only when the compiled template does not exist, or when the timestamp on the template file is newer. I'll check and make sure that it only compiles when needed.

  • #19 Philippe Entzmann said

    Did you benchmark microdot on serverless cases ?
    I guess the light footprint of microdot should shine : especially for the expected reduced cold start time and reduced image size.

  • #20 Miguel Grinberg said

    @Philippe: I have not benchmarked Microdot for serverless, but I have heard of a few users that run it on AWS Lambda successfully.

  • #21 Maurício Silva said

    Hello Miguel Grinberg, how are you?
    I really liked the Microdot, congratulations!
    What is the possibility of you doing a "Megatutorial Microdot" at some point?

  • #22 Miguel Grinberg said

    @Mauricio: Thanks! I will consider it for the future!

Leave a Comment