Back to catalog
Season 46 7 Episodes 25 min 2026

MicroPython

2026 Edition. A comprehensive guide to MicroPython for microcontrollers. Learn how to run a full Python 3 interpreter on bare-metal hardware.

Embedded Systems Microcontrollers Python Core
MicroPython
Now Playing
Click play to start
0:00
0:00
1
The Python that Fits in 256K
Discover how MicroPython squeezes a full Python 3 interpreter into bare-metal microcontrollers. We explore its core identity, its differences from CPython, and how it manages to run in highly constrained environments.
3m 54s
2
The Hardware Bridge: The machine Module
Learn how to control microcontroller peripherals directly from Python. We dive into the machine module, exploring how to interact with Pins, PWM, and raw memory.
3m 47s
3
Live Coding the MCU: REPL and mpremote
Revolutionize your embedded development workflow. We cover the MicroPython REPL and the mpremote command line tool for automating serial connections and live execution.
3m 39s
4
Three Lines to WiFi: The network Module
Transform a microcontroller into a connected IoT node. We explore the network module, detailing how to connect to WiFi as a station or host your own Access Point.
3m 41s
5
Surviving the Constraints: RAM and the GC
Master the art of writing memory-efficient Python. We discuss heap fragmentation, pre-allocating buffers, and manual garbage collection to keep your microcontroller running smoothly.
3m 25s
6
Compiled vs. Frozen: Deploying to Production
Learn how to deploy massive applications without running out of RAM. We explore pre-compiled .mpy files and freezing bytecode directly into the microcontroller's flash memory.
3m 47s
7
Determinism in Python: Timers and Interrupts
Achieve real-time behavior in MicroPython using hardware timers and interrupt service routines. We cover the strict rules of writing ISRs and avoiding memory allocation.
3m 44s

Episodes

1

The Python that Fits in 256K

3m 54s

Discover how MicroPython squeezes a full Python 3 interpreter into bare-metal microcontrollers. We explore its core identity, its differences from CPython, and how it manages to run in highly constrained environments.

Download
Hi, this is Alex from DEV STORIES DOT EU. MicroPython, episode 1 of 7. Desktop Python normally consumes megabytes of memory just to start up. Try putting that on a tiny, two-dollar chip and the system immediately runs out of resources. To write Python for microcontrollers, you need an interpreter that fits into a fraction of that space without losing the core language features you rely on. That is exactly what MicroPython provides. MicroPython is a lean, highly efficient implementation of the Python 3 programming language. It is designed specifically to run in constrained environments. Standard Python, often referred to as CPython, assumes it is running on top of a full operating system like Linux or Windows, with gigabytes of storage and ample RAM. MicroPython flips this assumption. It is built to run bare metal. There is no underlying operating system managing the hardware. Instead, MicroPython acts as the operating system. It interfaces directly with the hardware so you can write high-level scripts to control physical devices. The engineering behind this focuses entirely on severe resource constraints. MicroPython can execute entirely within just 256 kilobytes of compiled code space. Even more impressively, it requires a mere 16 kilobytes of RAM to operate. Within these tight boundaries, it delivers a fully functional Python environment. Standard Python includes a massive standard library, famously known as the batteries included approach. MicroPython strips that down to a carefully chosen subset of modules, keeping only what makes sense for an embedded system. Despite these heavy optimizations, the language itself remains authentic Python 3. This means the code you write on your desktop translates directly to the embedded device. MicroPython supports advanced language features like closures, list comprehensions, generators, and standard exception handling. It even handles arbitrary precision integers. If your logic requires multiplying massive numbers, MicroPython scales the memory dynamically to give you the correct answer without crashing into a hard architectural limit, exactly like desktop Python. Here is the key insight. Because the entire Python engine fits onto the chip, you get a live interactive prompt directly on the hardware. This read-eval-print loop changes how you write embedded code. Normally, programming a microcontroller involves writing code in C, compiling it on your desktop, flashing it to the board, and waiting for it to run. With MicroPython, you plug the board into your computer, open a serial terminal, and type Python commands directly into the chip. You can test logic, read sensor values, or trigger actions instantly. To make this possible, MicroPython includes specific built-in modules that go beyond standard Python. Since standard Python knows nothing about reading physical pins or configuring hardware timers, MicroPython provides specialized, highly optimized interfaces for these tasks. These are built directly into the firmware for your specific chip, giving you raw control over the physical world without bloating the memory footprint. By stripping away the operating system and shrinking the interpreter to fit in 256 kilobytes, MicroPython turns cheap, resource-starved hardware into a dynamic, interactive development environment. If you want to support the show, you can find us by searching for DevStoriesEU on Patreon. Thanks for tuning in. Until next time!
2

The Hardware Bridge: The machine Module

3m 47s

Learn how to control microcontroller peripherals directly from Python. We dive into the machine module, exploring how to interact with Pins, PWM, and raw memory.

Download
Hi, this is Alex from DEV STORIES DOT EU. MicroPython, episode 2 of 7. Stop recompiling C code and flashing firmware just to test a sensor or flip a switch. You want direct control over your microcontroller without the boilerplate, and that is exactly what the machine module provides. The machine module is your hardware bridge. It translates Python code into the electrical signals that drive the physical pins on your board. It provides access to core hardware features like central processing unit frequency, sleep modes, and digital input and output. But because it sits so close to the metal, it comes with a warning. This module does not hold your hand. If you configure the wrong hardware register or write to a protected memory address, your board will crash and reboot. The foundation of this module is the Pin class. A pin is a physical connection on the microcontroller that can send or receive electrical voltages. To control a basic component like an LED, you need to drive voltage to a specific pin. You do this by importing the machine module and creating a Pin object. You provide the physical pin number, and you define whether the pin should act as an input or an output. For an LED, you configure it as an output by passing an output constant. Once the object is created, you use it to set the state. Applying a value of one drives the hardware line high and turns the LED on. Applying a value of zero drives it low and turns it off. You are manipulating the digital state of the hardware in real time from a Python prompt. Sometimes basic on and off states are not enough. If you want to dim that LED or control the speed of a motor, you need Pulse Width Modulation, or PWM. The machine module has a dedicated PWM class for this. Instead of a steady stream of voltage, PWM turns the pin on and off extremely fast. You create a PWM object by giving it the Pin object you already configured. Then, you set two hardware parameters: frequency and duty cycle. Here is the key insight. The frequency dictates how many times per second the signal cycles, while the duty cycle dictates what percentage of that time the signal is actually on. If you set the duty cycle to fifty percent, the LED is only receiving power half the time, making it appear half as bright to the human eye. By gradually changing that duty cycle value inside a loop, you instruct the hardware to create a smooth fading effect. For advanced use cases, the machine module offers direct access to the underlying hardware registers through objects called mem8, mem16, and mem32. These allow you to read and write raw bytes, half-words, or 32-bit words directly to specific memory addresses. You do not use a Pin object for this. You use standard bracket notation, passing the physical memory address you want to access, exactly like looking up a key in a dictionary. Assigning a value to a mem32 address instantly overwrites that hardware register. This is how you interact with obscure microcontroller features that MicroPython has not explicitly wrapped in a class yet. It gives you absolute authority over the silicon. The true power of the machine module is not just that it makes hardware accessible, but that it completely removes the barrier between writing high-level software logic and manipulating physical voltage. Thanks for listening. Take care, everyone.
3

Live Coding the MCU: REPL and mpremote

3m 39s

Revolutionize your embedded development workflow. We cover the MicroPython REPL and the mpremote command line tool for automating serial connections and live execution.

Download
Hi, this is Alex from DEV STORIES DOT EU. MicroPython, episode 3 of 7. Forget the tedious compile, flash, and reboot cycle. You change one variable in a typical embedded project, wait minutes for a build, write to flash memory, and hope it works. MicroPython entirely bypasses this by letting you talk directly to the chip in real time. This episode covers Live Coding the MCU using the REPL and mpremote. The REPL stands for Read-Eval-Print Loop. It is an interactive prompt running directly on your microcontroller. You connect your board over USB, open a serial terminal, type Python code, and the chip executes it instantly. The standard REPL is designed for humans. It provides auto-indentation, command history, and basic auto-completion. It is perfect for testing a quick hardware pin toggle. However, there is a second mode called the raw REPL. People often confuse this with a limited version of MicroPython, but it is actually just a different input mode triggered by sending a specific control character over serial. The raw REPL disables character echo and auto-indentation. Tools use the raw REPL to programmatically inject large blocks of code into the microcontroller. If a script tried to paste text into the standard REPL, the human-friendly auto-indentation would cascade and completely mangle the Python formatting. The raw REPL guarantees the code goes in exactly as written. This brings us to mpremote. This is the official command-line tool for interacting with MicroPython devices, and it drives the raw REPL under the hood to perform heavy lifting. For live coding, mpremote offers two powerful commands: run and mount. You use the run command to execute a local script from your computer directly on the board. You write your code in your favorite text editor, open your terminal, and invoke mpremote run followed by your filename. Here is the key insight. The code is sent over the serial line and executed entirely in the microcontroller RAM. It is never written to the flash memory of the device. If you write a bad loop and crash the board, you just hard reset the chip. The flash remains untouched, saving write cycles, and your development loop takes seconds. When your project grows beyond a single script, you use the mount command. This command tells mpremote to take a local directory on your PC and map it over the serial connection. The microcontroller temporarily overrides its own storage and treats that folder on your computer as if it were its internal filesystem. Consider a scenario where you are writing a complex display driver. Usually, you would have to copy the driver file to the board, run your main script, observe a screen glitch, edit the file on your PC, and copy it over again. With the mount command, the microcontroller executes the driver directly from your PC storage. You hit save in your text editor on your laptop, restart the script on the board, and the changes apply immediately. You iterate as fast as you can type, completely eliminating the flashing step from your workflow. The true power of MicroPython is not just the Python syntax itself, but how tools like mpremote erase the boundary between your local development environment and the physical embedded hardware. That is all for this one. Thanks for listening, and keep building!
4

Three Lines to WiFi: The network Module

3m 41s

Transform a microcontroller into a connected IoT node. We explore the network module, detailing how to connect to WiFi as a station or host your own Access Point.

Download
Hi, this is Alex from DEV STORIES DOT EU. MicroPython, episode 4 of 7. Getting a bare microcontroller onto a WiFi network using traditional C code usually takes hundreds of lines of boilerplate just to initialize the radio and handle the handshake. It is tedious and error-prone. In MicroPython, you can bring up a wireless connection in literally three lines. That is the job of the network module, specifically the WLAN class. The network module abstracts the underlying hardware network interfaces. For WiFi operations, you rely on the network dot WLAN class. A standard WiFi chip can operate in two distinct roles. You must explicitly define which role you want when you create the WLAN object. The first role is Station mode, specified using the constant STA underscore IF. You use Station mode when your hardware needs to connect as a client to an upstream router, just like a smartphone joining a home network. The second role is Access Point mode, specified using AP underscore IF. In this mode, the microcontroller acts as a router itself, broadcasting its own wireless network so external devices can connect directly to it. Access Point mode is incredibly useful when deploying hardware in remote locations where no external network exists, allowing you to connect locally for configuration. Getting your board online requires a specific sequence. You first create the WLAN object using the Station mode constant. By default, the physical WiFi radio is powered down to conserve energy. You must turn it on by passing True to the active method on your WLAN object. Once the interface is active, you invoke the connect method with your wireless network name and password. Here is the key insight. The connect method is strictly non-blocking. When you invoke it, MicroPython hands the credentials to the network processor and immediately continues executing your script. It deliberately does not wait for the network authentication to finish. This design allows your main application to update a display or read a sensor while the radio negotiates with the router in the background. However, if your code attempts to send data immediately after calling connect, the operation will crash because the board does not actually have a connection yet. You manage this by polling the interface. The WLAN object provides a method called isconnected, which returns a simple boolean value. The standard pattern is to write a while loop that evaluates if isconnected is false. Inside this loop, you introduce a short delay, such as sleeping for half a second. The execution pauses here, repeatedly checking the hardware state until the router finally accepts the connection and isconnected returns true. Once the loop terminates, the hardware is officially online. To verify the network details, you call the ifconfig method on your WLAN object. This method outputs a collection of four specific items: the assigned IP address, the subnet mask, the gateway, and the primary DNS server. You can extract the first item from this collection and print it to the console to see exactly which IP address the router granted to your board. The non-blocking nature of the connect method means you are always responsible for verifying the hardware state before trying to route data over the network. That is it for today. Thanks for listening — go build something cool.
5

Surviving the Constraints: RAM and the GC

3m 25s

Master the art of writing memory-efficient Python. We discuss heap fragmentation, pre-allocating buffers, and manual garbage collection to keep your microcontroller running smoothly.

Download
Hi, this is Alex from DEV STORIES DOT EU. MicroPython, episode 5 of 7. Python loves to dynamically allocate memory, spinning up new objects for every calculation. That works perfectly on a server, but on a microcontroller, that habit will eventually crash your board, leaving you staring at a memory error even when you seemingly have RAM to spare. Today we are focusing on Surviving the Constraints: RAM and the GC. The memory where your Python objects live is called the heap. In MicroPython, the heap is incredibly small. When you create objects, they take up blocks of space. When those objects are no longer needed, they are cleared out. This leaves holes in your memory. Over time, as objects of varying sizes are created and destroyed, your heap looks like Swiss cheese. This is heap fragmentation. Here is the key insight. When you need to allocate a new object, MicroPython needs a single, contiguous block of memory to fit it. If your heap is heavily fragmented, you might have twenty kilobytes of total free RAM, but no single gap larger than one kilobyte. If you ask for two kilobytes, your program crashes. You need to stop wasting RAM, starting with your global variables. If you have integer values that never change, like pin numbers or hardware addresses, do not assign them normally. MicroPython provides a special declaration called const. If you wrap your integer in this const declaration, the compiler replaces the variable name with the actual number everywhere in your code during compilation. This prevents a variable from being created in RAM entirely. Next, look at your loops. Dynamic object creation inside a fast-running loop is the primary cause of fragmentation. You are reading a sensor over an SPI bus a hundred times a second. If you use a standard read command, MicroPython creates a brand new sequence of bytes in memory for every single reading. Those objects pile up rapidly, filling the gaps and destroying your contiguous space. Instead of making new objects, reuse the ones you have. Before your loop begins, create a bytearray of the correct size. A bytearray is a mutable block of memory. Then, inside your loop, use a hardware method called readinto. You pass your pre-existing bytearray to readinto. The hardware dumps the sensor data directly into that same block of memory, overwriting the old data. You process the data, the loop repeats, and zero new memory is allocated. Even with careful coding, some object creation is unavoidable, which brings us to the garbage collector. By default, the garbage collector runs automatically when memory allocation fails. It scans for unused objects and frees their space. However, this process takes time. If it triggers automatically during a time-sensitive operation, your code will pause unpredictably. To fix this, you take control. Call the collect function from the gc module explicitly. Find a safe spot in your code, like the end of your main loop, and trigger collection there. This keeps the heap clean on your schedule, preventing unexpected delays. In desktop Python, memory is something the language handles for you, but in MicroPython, memory is a physical container you must pack by hand. That is all for this one. Thanks for listening, and keep building!
6

Compiled vs. Frozen: Deploying to Production

3m 47s

Learn how to deploy massive applications without running out of RAM. We explore pre-compiled .mpy files and freezing bytecode directly into the microcontroller's flash memory.

Download
Hi, this is Alex from DEV STORIES DOT EU. MicroPython, episode 6 of 7. You upload a perfectly valid Python file to your microcontroller, type import, and the board instantly crashes with an out-of-memory error. The code itself is fine, but the process of loading it just consumed all your available memory. Fixing this requires bypassing the on-device compiler, which brings us to compiled versus frozen code for production deployments. When you run a standard Python script on a desktop, memory is practically infinite. On a microcontroller, memory is strictly constrained. When you drop a standard Python source file onto your device and run the import command, MicroPython does not just read the file. The compiler fires up right there on the microcontroller. It parses the text, builds an abstract syntax tree, and generates bytecode. Every single one of those steps allocates memory in RAM. Consider a large IoT framework that handles networking, security, and sensor data. If you try to import this massive module from a standard source file, the compiler will likely exhaust the available heap memory before the first line of your actual application ever executes. The spike in RAM usage during the compilation phase kills the process. To bypass this, you need to remove the compilation step from the microcontroller. You do this by pre-compiling your code using a desktop tool called the cross-compiler, or mpy-cross. You run this tool on your main computer. It ingests your Python source file and outputs a compiled file with an M-P-Y extension. This file contains the pre-generated bytecode. When you transfer this compiled file to the microcontroller and import it, MicroPython recognizes the format and skips the compilation phase entirely. You completely avoid the memory spike caused by the compiler. This is where it gets interesting. While pre-compiling saves you from the compilation memory spike, the microcontroller still loads the actual bytecode from the file system into RAM in order to execute it. If your IoT framework is large enough, the bytecode alone will still consume too much RAM, leaving you with very little space for your application variables and data. When pre-compiled files are not enough, the final step is freezing the bytecode. Freezing means baking the compiled code directly into the MicroPython firmware. Instead of copying the compiled file to the file system of the board, you place it into a specific modules directory within the MicroPython source code on your computer. You then rebuild the entire MicroPython firmware image from scratch and flash this custom firmware onto your board. This changes exactly how the code is executed. When you import a frozen module, MicroPython does not copy the bytecode into RAM at all. Because the code is baked into the firmware, it permanently resides in the microcontroller's flash memory. MicroPython executes the bytecode directly from flash. By executing from flash, the RAM footprint of importing that massive IoT framework drops to nearly zero. All of your RAM remains entirely free for processing real-time data. The fundamental difference comes down to where your code lives while it runs. Pre-compiling saves memory during the import step, but freezing saves memory for the entire life of the program by forcing execution to happen entirely in flash memory. If you would like to help support the show, you can search for DevStoriesEU on Patreon. That is all for this one. Thanks for listening, and keep building!
7

Determinism in Python: Timers and Interrupts

3m 44s

Achieve real-time behavior in MicroPython using hardware timers and interrupt service routines. We cover the strict rules of writing ISRs and avoiding memory allocation.

Download
Hi, this is Alex from DEV STORIES DOT EU. MicroPython, episode 7 of 7. Writing a hardware interrupt service routine in a high-level, garbage-collected language sounds like asking for an immediate system crash. But MicroPython actually allows it, as long as you obey one unbreakable rule: zero memory allocation. This episode covers Determinism in Python: Timers and Interrupts. When you need to read a sensor exactly one thousand times a second, a standard loop with a sleep function will fail. Garbage collection and background tasks introduce random delays, destroying your timing. To get deterministic behavior, you need a hardware timer. Using the machine module, you configure a Timer object to fire periodically and execute a specific callback function. This callback is your Interrupt Service Routine, or ISR. Timers trigger either soft or hard interrupts. A soft interrupt is scheduled by the MicroPython virtual machine when it is safe to do so. You write a soft ISR just like normal Python code. It can create objects, append to lists, and handle floating-point numbers. The downside is latency. If the garbage collector is actively running when the timer fires, your soft interrupt simply waits in line. If you need strict timing accuracy, you configure the timer to trigger a hard interrupt. A hard interrupt stops the processor immediately. It bypasses the virtual machine and runs your callback instantly. Here is the key insight. Because a hard interrupt preempts the current system state, it might interrupt the memory manager itself. If your ISR tries to allocate memory on the heap while the heap is in the middle of being modified, the microcontroller will crash. This constraint dictates exactly how you write hard ISRs. You cannot allocate any memory. No creating lists. No building dictionaries. No string manipulation. Even floating-point math is strictly forbidden, because in Python, every float is a newly allocated object on the heap. Your code must execute fast, rely on integer math, and use pre-existing memory structures. Consider a scenario where a hardware timer fires at 1000 Hertz. Every millisecond, the hard ISR reads a sensor value. Since you cannot append the reading to a list, you must pre-allocate your storage before starting the timer. You create a bytearray of one thousand bytes in your main program. You also define a global index variable. When the timer fires, the ISR reads the sensor. It updates the global index, stores the raw integer into the bytearray at that position, and exits. The memory space was reserved entirely up front. The interrupt just updates it in place, guaranteeing jitter-free execution. Once the buffer is full, the ISR needs to hand the data off to the main program. You cannot trigger a complex data processing function directly from the hard ISR. Instead, you use micropython dot schedule. This function takes a reference to a callback and queues it to run as soon as the system returns to a safe, soft execution state. Handling hardware interrupts forces you to stop thinking about Python as an infinite resource pool and start tracking exactly when and where your memory is assigned. Check the official MicroPython documentation for the specific interrupt behaviors of your hardware port, and try intentionally breaking the memory rules on your own board to see how the system reacts. If you want to suggest topics for our next series, drop by devstories dot eu. I would like to take a moment to thank you for listening — it helps us a lot. Have a great one!