Back to catalog
Season 47 7 Episodes 26 min 2026

CircuitPython

v10.1 — 2026 Edition. A technical audio course covering CircuitPython, its developer experience, architecture, hardware control, and scaling up with Blinka. Updated for version 10.1 (2026).

Embedded Systems Microcontrollers
CircuitPython
Now Playing
Click play to start
0:00
0:00
1
The CIRCUITPY Drive
This episode introduces CircuitPython and its unique USB mass storage workflow. Listeners will learn how saving a simple text file immediately executes code on a microcontroller, without the need for compilers or flashing tools.
3m 56s
2
Hardware as Code
Dive into the built-in modules that give CircuitPython its power to control the physical world. Listeners will learn how to interact with physical hardware pins using the board, digitalio, and time modules.
3m 52s
3
The Serial Console and REPL
Explore interactive debugging in CircuitPython using the Serial Console and the Read-Evaluate-Print-Loop (REPL). Listeners will learn how to pause execution and command their hardware live.
3m 44s
4
The Library Ecosystem
Manage external dependencies in CircuitPython using the library bundle. Listeners will learn the critical difference between raw Python files and compiled bytecode to optimize memory usage.
3m 34s
5
Frozen Libraries
Uncover the magic of frozen libraries in CircuitPython for memory-constrained boards. Listeners will understand library file priority and how firmware-baked modules free up filesystem space.
3m 45s
6
CircuitPython vs MicroPython
Examine the architectural differences between CircuitPython and its parent project, MicroPython. Listeners will learn why Adafruit forked the language to create a unified, beginner-friendly hardware API.
3m 58s
7
Blinka and Single Board Computers
Scale up your CircuitPython code to full Linux machines using Adafruit Blinka. Listeners will learn how this compatibility layer lets you use microcontroller APIs on single-board computers like the Raspberry Pi.
4m 00s

Episodes

1

The CIRCUITPY Drive

3m 56s

This episode introduces CircuitPython and its unique USB mass storage workflow. Listeners will learn how saving a simple text file immediately executes code on a microcontroller, without the need for compilers or flashing tools.

Download
Hi, this is Alex from DEV STORIES DOT EU. CircuitPython, episode 1 of 7. If you come from the world of traditional embedded systems, setting up a toolchain usually takes longer than writing the first program. You spend hours fighting with compilers, device drivers, and dedicated flashing utilities just to test a single line of logic. The CIRCUITPY Drive bypasses all of that entirely. Embedded developers often instinctively look for a flash tool, a compiler, or a specific Integrated Development Environment when starting a new project. With CircuitPython, you do not need them. There is no compile step. The operating system file system handles the deployment process. When you plug a CircuitPython-compatible microcontroller into your computer using a USB cable, the board presents itself to your operating system as a standard USB mass storage device. This removable drive mounts automatically and is named CIRCUITPY. To your computer, whether you use Windows, macOS, or Linux, it looks exactly like a standard thumb drive. You can open it in your file explorer, view the contents, and modify them. Inside this drive, the most important file is a text file typically named code dot py. This file acts as the entry point for your application. You can open it in any standard text editor. It does not require a specialized application; a basic text editor works perfectly. When you write a simple program—perhaps a loop that prints text or interacts with the board—and hit save, the deployment cycle begins automatically. Here is the key insight. You do not need to click a build button or run a command line utility to push the code to the hardware. The moment you save the file, the microcontroller detects the file system activity over the USB connection. This behavior is driven by a mechanism called auto-reload. CircuitPython actively monitors the state of the USB mass storage connection. When you initiate a save operation in your text editor, the computer writes data to the CIRCUITPY drive. CircuitPython waits until the file system write is complete. Once the file writing finishes, the auto-reload system triggers a soft reboot of the microcontroller. This soft reboot is extremely fast. It interrupts whatever the microcontroller was currently doing, safely tears down the current state, and completely clears the memory. Then, it immediately parses and executes the newly saved code dot py file from the top. The auto-reload mechanism creates a very tight iteration loop. You edit the code, save the file, and watch the hardware respond in less than a second. If there is a syntax error in your newly saved code, the execution halts safely. You can view the error output over a serial connection. You fix the typo, save the file again, and the board instantly reboots to try the updated code. The CIRCUITPY drive is also the mechanism for managing dependencies. If your project requires external helper code for specific hardware, you do not use a command-line package manager. You simply drag and drop the necessary library files directly into a designated folder named lib on the CIRCUITPY drive. The auto-reload system tracks changes in this directory as well. Before we wrap up, if you enjoy these technical episodes and want to support the show, you can search for DevStoriesEU on Patreon. The fundamental shift here is that the file system itself acts as the deployment pipeline, turning any machine with a basic text editor and a USB port into a fully capable embedded development workstation without installing a single driver. That is all for this one. Thanks for listening, and keep building!
2

Hardware as Code

3m 52s

Dive into the built-in modules that give CircuitPython its power to control the physical world. Listeners will learn how to interact with physical hardware pins using the board, digitalio, and time modules.

Download
Hi, this is Alex from DEV STORIES DOT EU. CircuitPython, episode 2 of 7. You write a script to turn on a light, save the file, and the light flashes for a fraction of a second before immediately going dark. Your code is perfectly fine. The issue is that when a script reaches the end and exits, the system automatically resets the hardware state. To keep a device running, you have to prevent the script from finishing. This is where the concept of Hardware as Code comes into play. In traditional programming, your code manipulates data in memory. In embedded programming, your code manipulates physical electrical signals. CircuitPython abstracts this complexity away using three built-in modules: board, digitalio, and time. You do not need to install these. They are baked directly into the firmware. The board module acts as a hardware map for your specific microcontroller. It knows which internal routing corresponds to physical pins. When you reference board dot LED, you are grabbing the correct hardware address for the built-in light, regardless of which physical board model you actually have in your hands. The digitalio module provides the classes needed to control the flow of electricity. It translates Python objects into digital signals. Finally, the time module handles pacing. Microcontrollers execute millions of instructions per second. Without the time module to pause execution, a blinking light would toggle so fast it would just look continuously dim to the human eye. Here is how these three modules work together to blink an onboard LED. First, you import board, digitalio, and time. Next, you set up the hardware. You create a new object using the DigitalInOut class from the digitalio module, and you pass it the board dot LED pin. Now you have a Python object representing that specific physical connection. Pins can either listen for electricity coming in, or send electricity out. You must explicitly tell CircuitPython what this pin is supposed to do. You set the direction property of your new pin object to output. This tells the microcontroller to push voltage out to the LED. Here is the key insight. Once the pin is configured as an output, controlling the physical electrical signal is exactly like assigning a variable. To turn the LED on, you set the value property of your pin object to True. Under the hood, CircuitPython translates that simple boolean assignment into a high voltage signal, pushing electricity through the circuit and illuminating the LED. To turn it off, you set the value property to False, which drops the voltage to zero. Now, back to the problem we started with. If you just set the value to True, then False, the script finishes. CircuitPython cleans up by resetting all pins, and your board goes dead. To make the LED blink continuously, you must trap the execution sequence. You wrap your commands in an infinite while True loop. Inside that loop, you set the pin value to True. Then, you use time dot sleep to pause the script for half a second. You set the pin value to False. You pause for another half second. Because it is an infinite loop, the script never reaches the end, the hardware never resets, and the LED keeps blinking as long as the board has power. When you write hardware as code, you are not just shifting bytes. You are physically opening and closing electrical gates by assigning True and False to Python objects. Thanks for tuning in. Until next time!
3

The Serial Console and REPL

3m 44s

Explore interactive debugging in CircuitPython using the Serial Console and the Read-Evaluate-Print-Loop (REPL). Listeners will learn how to pause execution and command their hardware live.

Download
Hi, this is Alex from DEV STORIES DOT EU. CircuitPython, episode 3 of 7. Most embedded debugging involves tweaking a line of code, waiting for a recompile, flashing the board, and hoping your output log catches the bug. But what if you could pause execution and command the hardware live? That is exactly what the Serial Console and REPL allow you to do. When you connect a CircuitPython board to your computer, it establishes a two-way serial connection over USB. You can access this using a terminal program like PuTTY on Windows, or the screen command on macOS and Linux. Many serial connections require you to match a specific baud rate, typically 115200. CircuitPython actually ignores this setting over USB, but it is standard practice to configure your terminal to that speed anyway. The serial console is your primary text-based window into the board. Whenever your script executes a print statement, the output appears here. More importantly, if your code encounters a fatal error and crashes, the Python traceback is printed to this console. Without a serial connection open, a crashed board gives you no diagnostic information. It simply stops working. The serial console is also your gateway to the REPL. REPL stands for Read, Evaluate, Print, Loop. It is an interactive Python prompt running directly on the microcontroller chip itself. Traditional firmware development requires writing code on a host machine, compiling it, and transferring the binary. The REPL changes that dynamic entirely. You type a command, the board reads it, evaluates the logic, prints the result, and loops back to wait for your next command. Consider a practical scenario. You have a script running on your board, but you need to figure out which hardware pin controls the built-in LED. You open your serial terminal and press Control C. This instantly interrupts the running script. You are prompted to press any key, which drops you into the REPL. You know you are in the right place when you see the standard Python prompt, which is three angle brackets. Now, you type import board. To discover what hardware is available, you pass the board module into the built-in dir function. The REPL prints out a list of every active pin name for your specific hardware. You see the LED pin in the list. Right there at the prompt, you import the digital IO module. You create a pin object for the LED, set its direction to output, and set its value to true. The physical LED on your desk instantly turns on. You just commanded the hardware manually, one line at a time. Here is the key insight. Code typed in the REPL is ephemeral. It only exists in the active memory of that current session. If you unplug the board or restart it, everything you typed vanishes. The REPL is an exploratory tool. It is a sandbox where you can test hardware limits, verify pin names, or check syntax on the fly. Once you find the exact sequence of commands that achieves your goal, you must manually recreate that logic inside your code dot py file on the USB drive to make it permanent. When you are done testing, you exit the REPL and restart your saved code by pressing Control D. This triggers a soft reboot, reloading your code dot py file from scratch. The REPL shifts hardware development from a slow cycle of guessing to instant, interactive exploration. Thanks for listening, happy coding everyone!
4

The Library Ecosystem

3m 34s

Manage external dependencies in CircuitPython using the library bundle. Listeners will learn the critical difference between raw Python files and compiled bytecode to optimize memory usage.

Download
Hi, this is Alex from DEV STORIES DOT EU. CircuitPython, episode 4 of 7. Microcontrollers have incredibly limited RAM, meaning a relatively short script can suddenly crash your board completely out of memory before executing a single command. The culprit is often how the board processes the external code you bring in, and solving that memory bottleneck is exactly what The Library Ecosystem is all about. CircuitPython comes with core modules built into the firmware, but for specific hardware like temperature sensors or LED displays, you need external libraries. Unlike a standard desktop Python environment, there is no package manager. You do not run a command to automatically fetch a driver. Dependency management here is manual by design. When you plug your microcontroller in via USB, it appears as a flash drive. At the root of this drive is a directory simply named lib. This folder is the dedicated home for all your external dependencies. To populate it, you download the Adafruit Library Bundle, a massive ZIP archive containing hundreds of official drivers and helper modules. Here is the key insight. When you go to download this bundle, you will see several versions available. A frequent error is simply downloading the newest bundle on the page. If your board is running CircuitPython version eight, but you download the version nine bundle, your code will fail. The major version of the bundle must exactly match the major version of the CircuitPython firmware installed on your board. If they do not match, the system throws incompatible bytecode errors and refuses to run. Inside that matching bundle, you will find the libraries offered in two distinct file formats: standard dot py files and dot mpy files. Dot py files are raw, human-readable Python source text. While your board can run them, the microcontroller must load all that raw text into its tiny RAM space and compile it into bytecode on the fly. This compilation overhead eats up memory incredibly fast. Dot mpy files solve this problem. They are pre-compiled bytecode. The heavy translation step has already been done on a computer before you ever downloaded the zip file. When the microcontroller loads a dot mpy file, it executes the instructions directly, bypassing the text parsing phase entirely. Say you are connecting a NeoPixel LED strip to your board. You write your control script, but the system needs the hardware driver to know how to communicate with those specific LEDs. You open your correctly versioned library bundle, locate the file named neopixel dot mpy, and drag it directly into the lib directory on the board. If you had dragged the raw neopixel dot py file instead, your microcontroller could easily run out of memory just trying to interpret the driver code. By using the pre-compiled dot mpy file, the board saves critical RAM for your actual application logic and lights up the pixels immediately. Managing dependencies manually forces you to be highly intentional about what code lives on your device. Relying on pre-compiled bytecode is the only way to abstract away complex hardware operations without instantly exhausting the tiny memory footprint of a microcontroller. That is all for this one. Thanks for listening, and keep building!
5

Frozen Libraries

3m 45s

Uncover the magic of frozen libraries in CircuitPython for memory-constrained boards. Listeners will understand library file priority and how firmware-baked modules free up filesystem space.

Download
Hi, this is Alex from DEV STORIES DOT EU. CircuitPython, episode 5 of 7. Sometimes, deleting a library from your microcontroller actually makes your project run better. You remove a large dependency from your drive, restart the board, and your code magically still works. This happens because of a feature called Frozen Libraries. Microcontrollers, especially smaller ones like the Circuit Playground Express, operate under extreme hardware limitations. Every standard library file in your lib folder takes up physical storage space on the tiny flash drive. More importantly, when your code runs, the device has to load that entire file into its random access memory. If you load too many files, or files that are too large, you run out of memory and the program crashes. Firmware developers solve this by freezing core libraries directly into the CircuitPython build. Instead of existing as an independent file on your visible USB drive, the library code is pre-compiled and packed straight into the base firmware file that you initially load onto the board. This provides a massive advantage. Frozen libraries take up zero space on your visible filesystem. Because they are part of the core system image, the microcontroller executes the code directly from flash memory. It skips the process of copying the library into random access memory entirely. This leads to a common trap. Many developers download a massive library bundle, copy everything they think they need into their lib folder, and accidentally duplicate libraries that are already frozen in their firmware. This wastes precious filesystem space. If you delete a library from your lib folder and your project continues to run flawlessly, that library was frozen. Here is the key insight. To understand how to avoid this overlap, you need to understand the import priority order controlled by the system path. When your code imports a module, CircuitPython does not just grab the first match it sees. It searches locations in a strict sequence. First, it checks the root directory of your drive. Second, it checks the lib folder. Third, and only if it fails to find the library in the first two locations, it checks the frozen libraries hidden inside the firmware. This priority order means any file you put on your drive overrides the frozen version. If you place an outdated or custom version of a library in your lib folder, CircuitPython runs that file. It completely ignores the optimized, memory-efficient frozen version inside the firmware. You are paying the memory cost for a file you did not need to provide. Within those directories, file extensions also follow a strict priority rule. A plain text source file always wins over a compiled file. If you have both a standard text file and a compiled version of the same library sitting in the exact same directory, CircuitPython loads the uncompiled text file. If you want to know exactly what is baked into your specific board, you can open the interactive prompt and type the word help, passing the word modules inside the parentheses. This prints a complete list of every frozen library available to you out of the box. The smartest way to manage memory on a constrained device is to let the firmware do the heavy lifting. Knowing exactly what is already frozen prevents duplicate files, stops accidental overrides, and keeps your available memory clear for your actual application logic. Thanks for listening. Take care, everyone.
6

CircuitPython vs MicroPython

3m 58s

Examine the architectural differences between CircuitPython and its parent project, MicroPython. Listeners will learn why Adafruit forked the language to create a unified, beginner-friendly hardware API.

Download
Hi, this is Alex from DEV STORIES DOT EU. CircuitPython, episode 6 of 7. You look at a hardware project and see that the developers deliberately removed advanced features like hardware interrupts from the core language. You might think this crippled the platform, but it actually made it wildly more successful. This is the architectural divergence between CircuitPython and MicroPython. Developers often assume these two platforms are identical. They both run Python on microcontrollers, and CircuitPython originally started as a fork of MicroPython. However, they serve completely different philosophies. MicroPython is designed to expose the raw power and specific features of the underlying microcontroller. CircuitPython is designed to enforce a strict, unified standard across all hardware. The first difference you will notice is the standard library. MicroPython uses a naming convention that prefixes standard modules with the letter u to indicate a micro version. You import uos or utime. CircuitPython completely drops this. It enforces standard desktop CPython naming. You simply import os or time. If you are a developer porting a MicroPython script to CircuitPython, you will need to rename those imports. In exchange for this minor adjustment, your code becomes highly portable. That portability is the core feature of CircuitPython. MicroPython exposes specific hardware registers and chip-specific pin numbers. CircuitPython abstracts the hardware completely using a unified API centered around a module called board. Instead of writing code that toggles GPIO pin 15 on an ESP32, you tell CircuitPython to toggle board dot D4. If you move that exact script to an entirely different microcontroller architecture, like a standard ARM Cortex chip, the hardware code runs entirely unchanged. To maintain this consistency, CircuitPython forces certain architectural choices that MicroPython leaves optional. For example, MicroPython allows you to compile firmware without floating-point number support to save memory. CircuitPython mandates floating-point support across every single build. Math operations behave exactly the same way regardless of how constrained the hardware is. CircuitPython also standardizes error messages, providing verbose, localized text instead of terse, bare-metal memory faults. Here is the key insight. The most polarizing difference is how the two handle concurrency. MicroPython fully supports hardware interrupts. An interrupt allows a physical pin change to immediately pause the main program, execute a specific function, and then resume. This provides high-performance timing, but it also introduces complex race conditions and unpredictable memory states that are incredibly difficult to debug. CircuitPython deliberately removes support for hardware interrupts. You cannot trigger Python code directly from a hardware interrupt pin. Instead, it enforces cooperative multitasking. You manage concurrency using standard asyncio features or by checking states sequentially in your main loop. By abandoning interrupts, CircuitPython ensures the execution flow remains entirely predictable and eliminates a massive category of silent crashes. MicroPython gives you the keys to the bare metal, prioritizing raw performance and hardware control. CircuitPython trades that control for an environment where your code is completely portable, predictable, and behaves exactly like the standard Python you run on a desktop. Thanks for listening, happy coding everyone!
7

Blinka and Single Board Computers

4m 00s

Scale up your CircuitPython code to full Linux machines using Adafruit Blinka. Listeners will learn how this compatibility layer lets you use microcontroller APIs on single-board computers like the Raspberry Pi.

Download
Hi, this is Alex from DEV STORIES DOT EU. CircuitPython, episode 7 of 7. You spend days writing and testing hardware interaction code for a sensor on a tiny five-dollar microcontroller. Later, the project requirements change, and you suddenly need the processing power of a full Linux computer to log that data to a remote cloud database. Normally, moving from a bare-metal microcontroller to a full operating system means completely rewriting your hardware code. Your target platform now has an OS kernel managing hardware access, requiring entirely different drivers and libraries. But what if the exact same Python script could run unchanged on both platforms? That is exactly what Adafruit Blinka allows you to do. Before explaining how it works, let us clear up a common misconception. Blinka does not replace the Linux operating system on your Raspberry Pi, BeagleBone, or other single-board computer. You do not flash a CircuitPython firmware file onto an SD card like you do with a microcontroller. Instead, Blinka is simply a Python package. You install it using standard pip. It acts as a compatibility layer that runs within standard desktop CPython. Its sole job is to translate CircuitPython API calls into the standard Linux system calls required to control hardware. When you write hardware code on a microcontroller, CircuitPython handles direct hardware access. On a Linux system, the operating system kernel controls the hardware, and user programs must request access. Blinka bridges this gap. When your code imports the board module or uses the digitalio module to toggle a pin, Blinka intercepts those commands. It maps the CircuitPython pin names to the specific hardware layout of your single-board computer, routing the request through the necessary Linux subsystems. Think about how this looks in practice with our temperature sensor. On the tiny microcontroller, your code imports board, configures an I2C bus, and continuously reads temperature values. To scale this operation up to a Raspberry Pi 5, you physically connect the sensor to the header pins on the Pi. Then, inside your Linux terminal, you use pip to install Adafruit Blinka and the specific Python driver for your temperature sensor. You copy your original Python script over, without altering a single line. When you execute the script using standard Python 3, the board module dynamically detects that it is running on a Raspberry Pi 5. It automatically provides the correct pin definitions for that specific board. The busio module seamlessly translates your I2C read requests into standard Linux I2C device calls. Your script reads the sensor data exactly as it did before. This is where it gets interesting. Because you are now running standard CPython on a full Linux machine, you are no longer constrained by the memory limits of a microcontroller. Your script can read the hardware sensor using the Blinka translation layer, and then immediately pass that data to heavy desktop libraries. You can use standard network requests to push the metrics to a cloud database, process the telemetry with data science tools, or even serve it up via a local web framework. Here is the key insight. The hardware interaction code remains identical across entirely different computing architectures. By decoupling the sensor logic from the underlying operating system, Blinka lets you prototype cheaply on bare metal and scale up to full Linux environments with zero hardware code rewrites. This wraps up our series on CircuitPython. I encourage you to read through the official documentation, try setting up a sensor with Blinka hands-on, or visit devstories dot eu to suggest topics for future series. That is all for this one. Thanks for listening, and keep building!