The "impossibly small" Microdot web framework
Benefits for LWN subscribers The primary benefit from subscribing to LWN is helping to keep us publishing, but, beyond that, subscribers get immediate access to all site content and access to a number of extra site features. Please sign up today!
The Microdot web framework is quite small, as its name would imply; it supports both standard CPython and MicroPython, so it can be used on systems ranging from internet-of-things (IoT) devices all the way up to large, cloudy servers. It was developed by Miguel Grinberg, who gave a presentation about it at EuroPython 2025. His name may sound familiar from his well-known Flask Mega-Tutorial, which has introduced many to the Flask lightweight Python-based web framework. It should come as no surprise, then, that Microdot is inspired by its rather larger cousin, so Flask enthusiasts will find much to like in Microdot—and will come up to speed quickly should their needs turn toward smaller systems.
We have looked at various pieces of this software stack along the way: Microdot itself in January 2024, MicroPython in 2023, and Flask as part of a look at Python microframeworks in 2019.
Grinberg began his talk with an introduction. He has been living in Ireland for a few years and " I make stuff ". That includes open-source projects, blog posts (on a Flask-based blog platform that he wrote), and " a bunch of books ". He works for Elastic and is one of the maintainers of the Elasticsearch Python client, " so maybe you have used some of the things that I made for money ".
Why?
With a chuckle, he asked: " Why do we need another web framework? We have so many already. " The story starts with a move that he made to Ireland from the US in 2018; he rented a house with a "smart" heating controller and was excited to use it. There were two thermostats, one for each level of the house, and he was " really looking forward to the winter " to see the system in action.
As might be guessed, he could set target temperatures in each thermostat; they would communicate with the controller that would turn the heating on and off as needed. In addition, the system had a web server that could be used to query various parameters or to start and stop the heaters. You could even send commands via SMS text messages; " there's a SIM card somewhere in that box [...] very exciting stuff ".
When winter rolled around, it did not work that well, however; sometimes the house was too chilly or warm and he had to start and stop the heaters himself. He did some debugging and found that the thermostats were reporting temperatures that were off by ±3°C, " which is too much for trying to keep the house at 20° ". The owner of the house thought that he was too used to the US where things just work; " at least she thinks that in America everything is super-efficient, everything works, and she thought 'this is the way things work in Ireland' ". So he did not make any progress with the owner.
At that point, most people would probably just give up and live with the problem; " I hacked my heating controller instead ". He set the temperatures in both thermostats to zero, which effectively disabled their ability to affect the heaters at all, and built two small boards running MicroPython, each connected to a temperature and humidity sensor device. He wrote code that would check the temperature every five minutes and send the appropriate commands to start or stop the heaters based on what it found.
So the second half of his first winter in Ireland went great. The sensors are accurate to ±0.5°C, so " problem solved ". But, that led to a new problem for him. " I wanted to know things: What's the temperature right now? Is the heating running right now or not? How many hours did it run today compared to yesterday? " And so on.
He added a small LCD screen to display some information, but he had to actually go to the device and look at it; what he really wanted was to be able to talk to the device over WiFi and get information from the couch while he was watching TV. " I wanted to host a web server [...] that will show me a little dashboard ".
So he searched for web frameworks for MicroPython; in the winter of 2018-2019, " there were none ". Neither Flask nor Bottle, which is a good bit smaller, would run on MicroPython; both are too large for the devices, but, in addition, the standard library for MicroPython is a subset of that of CPython, so many things that they need are missing. A " normal person " would likely have just accepted that and moved on; " I created a web framework instead. "
Demo
He brought one of his thermostat devices to Prague for the conference and did a small demonstration of it operating during the talk. The device was connected to his laptop using USB, which provided power, but also a serial connection to the board. On the laptop, he used the rshell remote MicroPython shell to talk to the board, effectively using the laptop as a terminal.
He started the MicroPython read-eval-print loop (REPL) on the board in order to simulate the normal operation of the board. When it is plugged into the wall, rather than a laptop, it will boot to the web server, so he made that happen with a soft-reboot command. The device then connected to the conference WiFi and gave him the IP address (and port) where the server was running.
He switched over to Firefox on his laptop and visited the site, which showed a dashboard that had the current temperature (24.4°) and relative humidity (56.9%) of the room. He also used curl from the laptop to contact the api endpoint of the web application, which returned JSON with the two values and the time. There is no persistent clock on the board, so the application contacts an NTP server to pick up the time when it boots; that allows it to report the last time a measurement was taken.
Grinberg said that he wanted to set the expectations at the right level by looking at the capabilities of the microcontrollers he often uses with Microdot. For example, the ESP8266 in his thermostat device has 64KB of RAM and up to 4MB of flash. The ESP8266 is the smallest and least expensive (around €5) device with WiFi that he has found; there are many even smaller devices, but they lack the networking required for running a web server. The other devices he uses are the Raspberry Pi Pico W with 2MB of flash and 256KB of RAM and the ESP32 with up to 8MB of flash and 512KB of RAM. He contrasted those with his laptop, which has 32GB of RAM, so " you need 500,000 ESP8266s " to have the same amount of memory.
Features
The core framework of Microdot is in a single microdot.py file. It is fully asynchronous, using the MicroPython subset of the CPython asyncio module, so it can run on both interpreters. It uses asyncio because that is the only way to do concurrency on the microcontrollers; there is no support for processes or threads on those devices.
Microdot has Flask-style route decorators to define URLs for the application. It has Request and Response classes, as well as hooks to run before and after requests, he said. Handling query strings, form data, and JSON are all available in Microdot via normal Python dictionaries. Importantly, it can handle streaming requests and responses; because of the limited memory of these devices, it may be necessary to split up the handling of larger requests or responses.
It supports setting cookies and sending static files. Web applications can be constructed from a set of modules, using sub-applications, which are similar to Flask blueprints. It also has its own web server with TLS support. " I'm very proud of all the stuff I was able to fit in the core Microdot framework ", Grinberg said.
He hoped that attendees would have to think for a minute to come up with things that are missing from Microdot, but they definitely do exist. There are some officially maintained extensions, each in its own single .py file, to fill some of those holes. They encompass functionality that is important, but he did not want to add to the core because that would make it too large to fit on the low-end ESP8266 that he is using.
There is an extension for multipart forms, which includes file uploads; " this is extremely complicated to parse, it didn't make sense to add it into the core because most people don't do this ". There is support for WebSocket and server-sent events (SSE). Templates are supported using utemplate for both Python implementations or Jinja, which only works on CPython. There are extensions for basic and token-based authentication and for secure user logins with session data; the latter required a replacement for the CPython-only PyJWT, which Grinberg wrote and contributed to MicroPython as jwt.py . There is a small handful of other extensions that he quickly mentioned as well.
" I consider the documentation as part of the framework "; he is " kind of fanatical " about documenting everything. If there is something missing or not explained well, " it's a bug that I need to fix ". He writes books, so the documentation is organized similarly; it comes in at 9,267 words, which equates to around 47 minutes of reading time. There is 100% test coverage, he said, and there are around 30 examples, with more coming.
A design principle that he follows is " no dark magic ". An example of dark magic to him is the Flask application context, " which very few people understand ". In Microdot, the request object is explicitly passed to each route function. Another example is the dependency injection that is used by the FastAPI framework to add components; Microdot uses explicit decorators instead.
He used the cloc utility to count lines of code, while ignoring comments and blank lines. Using that, Django comes in at 110,000 lines, Flask plus its essential Werkzeug library is 15,500 lines, FastAPI with Starlette is 14,900 lines, Bottle is around 3,000 lines, while the Microdot core has 765 lines (" believe it or not ") and a full install with all the extensions on MicroPython comes in at just shy of 1,700 lines of code.
He ended with an example of how Microdot can be so small by comparing the URL matching in Flask with Microdot. The Flask version does lots more than Microdot, with more supported types of arguments in a URL and multiple classes in the werkzeug.routing module; it has 1,362 lines of code. For Microdot, there is a more limited set of URL arguments, though there is still the ability to define custom types, and a single URLPattern class; all of that is done in 63 lines of code. " I don't intend to support everything that Flask supports, in terms of routing, but I intend to support the 20% that covers 80% of the use cases. " That is the overall mechanism that he has used to get to something that is so small.
An audience member asked about whether the Microdot code was minified in order to get it to fit. Grinberg said that doing so was not all that useful for MicroPython, but the code is smaller on the board because it is precompiled on another system; that results in a microdot.mpy file, which is bytecode for MicroPython. For example, on the low-end device he is using for his thermostats, Microdot would not be able to be compiled on the device itself. There are some other tricks that can also be used for reducing the RAM requirements, like putting the code into the firmware as part of the MicroPython binary.
The final question was about performance, and how many requests per second could be handled. Grinberg said that he did not have any real numbers, but that the device he demonstrated is " really really slow ". That question led to a blog post in late July where Grinberg tried to more fully answer it.
[I would like to thank the Linux Foundation, LWN's travel sponsor, for travel assistance to Prague for EuroPython.]
Index entries for this article Conference EuroPython/2025 Python Web
to post comments