Embedded Python Proficient¶
When you'd use this
Run Python on microcontrollers with MicroPython and CircuitPython.
Run Python on constrained devices (MicroPython/CircuitPython) for microcontroller projects and IoT.
What you'll learn¶
- What MicroPython and CircuitPython are
- Read sensors and control pins (GPIO)
- Work within tight memory constraints
- Choose between the two runtimes
- Understand what standard Python features you lose
This topic targets real microcontroller hardware
The code here runs on a microcontroller (Raspberry Pi Pico, ESP32, etc.), not on a desktop CPython, so the snippets can't be run-verified in this environment. They follow the documented MicroPython/CircuitPython APIs. To try them you need a supported board and its firmware flashed.
Python on a microcontroller¶
MicroPython/CircuitPython bring Python to tiny boards with kilobytes of RAM.
Microcontrollers — the tiny chips inside sensors, appliances, and IoT devices — traditionally run C. MicroPython and CircuitPython put a real Python interpreter on these chips, so you write firmware in Python instead. You lose some speed and RAM, but gain enormous development speed: edit a file, save, and it runs — no compile-and-flash cycle.
Desktop Python (CPython) Embedded Python
┌────────────────────┐ ┌──────────────────┐
│ GHz CPU, GBs of RAM │ │ ~100 MHz, ~256 KB │
│ full stdlib │ vs │ tiny subset │
│ pip / PyPI │ │ upip / bundles │
└────────────────────┘ └──────────────────┘
These are real Python (indentation, functions, classes, exceptions) — just a slimmed-down implementation tuned for kilobytes of RAM.
Blinking an LED (the "hello world" of hardware)¶
The canonical first program — toggle a pin to prove the toolchain works.
Controlling a GPIO pin — MicroPython style:
from machine import Pin
import time
led = Pin(25, Pin.OUT) # onboard LED on a Pico is GPIO 25
while True:
led.value(1) # on
time.sleep(0.5)
led.value(0) # off
time.sleep(0.5)
The same idea in CircuitPython (slightly different API):
import board
import digitalio
import time
led = digitalio.DigitalInOut(board.LED)
led.direction = digitalio.Direction.OUTPUT
while True:
led.value = True
time.sleep(0.5)
led.value = False
time.sleep(0.5)
GPIO (General-Purpose Input/Output) pins are how the chip talks to the physical world — set a pin high/low to drive an LED or motor, or read a pin to sense a button or sensor.
Reading a sensor¶
Sample a sensor over a bus (I2C/SPI/analog) and act on the value.
Reading an analog value (e.g. a temperature sensor or potentiometer) via the ADC (analog-to-digital converter):
from machine import ADC, Pin
import time
sensor = ADC(Pin(26)) # ADC on GPIO 26
while True:
raw = sensor.read_u16() # 0..65535
voltage = raw / 65535 * 3.3 # convert to volts
print(f"raw={raw} voltage={voltage:.2f}V")
time.sleep(1)
Digital input with a button:
from machine import Pin
button = Pin(15, Pin.IN, Pin.PULL_UP) # internal pull-up resistor
if button.value() == 0: # pressed pulls the pin low
print("button pressed")
Common protocols (I2C, SPI, UART) connect richer peripherals — displays, accelerometers, other chips — and both runtimes provide machine.I2C, machine.SPI, etc.
Living within constraints¶
Manage scarce RAM/flash and no OS on microcontrollers.
The defining challenge of embedded Python is scarcity — often 256 KB of RAM or less. This changes how you code:
- Memory is precious. A careless list comprehension can exhaust RAM. Prefer generators, reuse buffers, and avoid holding large structures.
- Watch garbage collection.
import gc; gc.collect()at safe points reclaims memory; unpredictable GC pauses matter more here (see Real-time Systems). - The stdlib is tiny. No
os.pathfull API, norequests, limitedjson. You get a curated subset plus hardware modules (machine,board). - No pip/PyPI. Libraries come as pre-bundled modules or via
mip/upip, not the full package ecosystem. - Floats may be limited. Some builds use single-precision floats or omit them; integer math is cheaper.
MicroPython vs CircuitPython¶
Two beginner-friendly embedded Pythons with different ecosystems.
They share a common ancestor (CircuitPython is a fork of MicroPython) but differ in philosophy:
| MicroPython | CircuitPython | |
|---|---|---|
| Origin | The original project | Adafruit's fork, education-focused |
| Editing | REPL + file upload | Board appears as a USB drive — edit code.py, it runs on save |
| Concurrency | Has _thread, asyncio | Simpler, no threading |
| Hardware breadth | Very wide (ESP32, Pico, STM32, ...) | Adafruit boards + many others |
| Best for | Performance, broad boards, advanced use | Beginners, rapid prototyping, great docs |
Rule of thumb: CircuitPython for the smoothest beginner experience (drag-and-drop editing, superb tutorials); MicroPython when you need more performance, threading, or a board CircuitPython doesn't cover.
When embedded Python fits (and when it doesn't)¶
Great for prototyping and I/O glue; not for hard real-time or tight timing.
Great for: prototyping, education, IoT sensors, hobby projects, and products where development speed beats squeezing every cycle.
Not ideal for: ultra-low-power designs counting microamps, hard real-time control loops (see Real-time Systems), or the smallest/cheapest chips where every byte and cent matters — there, C still rules. A common approach: prototype in MicroPython, then rewrite hot paths (or the whole thing) in C once the design is proven.
Practice exercises¶
- Write a MicroPython program that blinks an LED faster each time a button is pressed (read the button, adjust the sleep).
- Read an analog sensor and turn on an LED only when the value crosses a threshold.
- Use
gc.mem_free()to print free memory before and after allocating a large list, and observe the constraint. - Compare the LED-blink code in MicroPython vs CircuitPython and list every API difference.
- Explain a scenario where you'd prototype in MicroPython but ship in C, and what would drive that decision.
💬 Discussion
Have a question about this topic? Found an error? Share your thoughts below.