Skip to content

Repository files navigation


PyMCU

PyMCU

Python to bare-metal firmware — no runtime, no interpreter, no VM.
Explore the project »

Report a bug · Request a feature · Sponsor

PyPI version Python versions License Last commit Open issues Sponsor

Ecosystem Partner
Adafruit


Important

Alpha 10 is out — v0.1.0a10 release notes. The hardware-validation release. It came out of a sustained bug hunt on a real Arduino Uno with a logic analyzer, plus a sweep of the official MicroPython quickref and CircuitPython Essentials examples — 63 projects, 53 of which compile; the rest fail on purpose with a clear diagnostic.

What that hunt found was a class of silent miscompiles: uint32(float_var) emitting raw float bits, a global shadowing a function parameter (which had been driving the DHT start pulse for 250 ms instead of 18), millis() counting 1024 ms per second, and a timer's second PWM channel disconnecting the first. Each fix shipped with a regression test. Suites at that release: 517 unit, 508 driver, 1549 AVR integration.

Alpha 10 still has bugs, and the hunt did not stop. 145 more fixes have landed since it shipped, most of them turning a case the compiler used to answer blindly into a diagnostic that names what it cannot do. That work is heading for beta 1; watch the repo if you want to hear when it lands.

So: core compilation is stable and test-covered, the alpha is usable, and you will still find edges. If you hit one, please open an issue — that is how the list above got written.

Avoid pymcu.hal.*. The native HAL is deliberately low-level and may change between releases without a deprecation cycle. Use the MicroPython or CircuitPython compat API instead: those track APIs specified elsewhere, which is what makes them the surface designed to hold still.

PyMCU compiles a statically-typed subset of Python into bare-metal firmware for AVR, ARM (RP2040 / RP2350) and PIC — no runtime, no interpreter, no virtual machine. The same binary you would write in C.

PyMCU demo: MicroPython-flavoured blink compiled to 150 bytes and flashed to an Arduino Uno

A real session: 9 lines of Python → pymcu build150 bytes of flash → running on an Arduino Uno. Then the delay is edited, rebuilt and reflashed — the whole loop takes seconds.


The pitch in one table

LED blink for ATmega328P @ 16 MHz — all variants do the same thing: configure PB5 as output, then loop LED on → wait 500 ms → LED off → wait 500 ms forever.

Source Total flash SRAM
C (avr-gcc -Os) 176 B 0 B
PyMCU (native HAL) 150 B 0 B
PyMCU (MicroPython API) 150 B 0 B
PyMCU (CircuitPython API) 152 B 0 B
Arduino (IDE defaults) 924 B 9 B

PyMCU produces a smaller binary than C here. Why?

Pin("PB5", Pin.OUT) and delay_ms(500) are resolved entirely at compile time — the compiler sees through the Python objects and emits the same raw SBI/CBI port-toggle instructions a C programmer would write by hand. The rest of the difference is the delay: PyMCU emits one calibrated delay subroutine shared by both waits (rcall twice), where avr-libc's _delay_ms is inlined at each call site — and there is no call main / jmp _exit scaffolding around the program. The interrupt vector table and startup stub are identical fixed overhead in both toolchains.

Native HAL and MicroPython API produce byte-for-byte identical firmware — both compile down to the same SBI/CBI toggle and the same delay loop. The API is a zero-cost abstraction. CircuitPython is 2 bytes larger because the Direction.OUTPUT setter clears the PORT register before setting DDR, as the CircuitPython spec requires.

These numbers are for a minimal blink. Real programs that use SRAM (global variables, buffers) will emit a small zeroing loop at startup, just like C does.

For complex drivers (custom protocols, timing-critical bit-bang): expect 2-3x flash vs hand-written C. PyMCU is not competing with C — the goal is to make microcontroller development approachable in Python you already know, without the overhead of Arduino. The output is still 100-1000x smaller than any embedded Python interpreter.


Write code you already know

Pick the API that fits your background. Both compile to the same bare-metal firmware.

CircuitPython

# The exact same code that runs on a Pico under CircuitPython
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)

MicroPython

# The exact same code that runs on a Pico under MicroPython
from machine import Pin
from utime import sleep_ms

led = Pin(13, Pin.OUT)

while True:
    led.value(1)
    sleep_ms(500)
    led.value(0)
    sleep_ms(500)
pymcu build   # → dist/firmware.hex  (150 bytes flash, 0 bytes SRAM)
pymcu flash   # → avrdude upload to Arduino Uno

First binary in under 5 minutes

1. Install

pipx install --pip-args=--pre "pymcu-compiler[avr]"    # AVR (ATmega / ATtiny)   -- beta
pipx install --pip-args=--pre "pymcu-compiler[arm]"    # RP2040 / RP2350 (Pico)  -- alpha
pipx install --pip-args=--pre "pymcu-compiler[pic]"    # PIC16                   -- alpha
pipx install --pip-args=--pre "pymcu-compiler[all]"    # all of the above

Requires Python 3.11+ and pipx. Each extra bundles its full toolchain (compiler backend + assembler/linker binaries) — no system packages needed.

The extras are not equally mature. [avr] is beta; [arm] and [pic] are alpha, and [all] installs alpha backends alongside the beta one. See Supported targets for what alpha means here.

Package name: PyMCU is published as pymcu-compiler on PyPI while a PEP 541 request to reclaim the pymcu name is under review. Once approved, a pymcu metapackage will alias pymcu-compiler — installs and project configs will stay compatible.

2. Create a project

pymcu new blink --board arduino_uno --stdlib micropython
cd blink

That is the whole setup. pymcu new scaffolds a project that already builds: a pyproject.toml carrying the board, frequency, toolchain and dependencies, a src/main.py with a working blink, plus requirements.txt, a Makefile and VS Code tasks. Pass --stdlib circuitpython for the other API, or run it with no flags and it asks for what you left out.

Warning

pymcu new asks whether to install dependencies and defaults to no. The compat layer you picked is one of those dependencies, so declining leaves you with a project that scaffolds fine and a first pymcu build that fails with ImportError: Module not found: machine. If you already said no, install them from inside the project with uv sync, poetry install or pip install -e ..

3. The program it wrote for you

MicroPython style (--stdlib micropython):

# src/main.py
from machine import Pin
from time import sleep_ms

led = Pin(13, Pin.OUT)
while True:
    led.value(1)
    sleep_ms(500)
    led.value(0)
    sleep_ms(500)

CircuitPython style (--stdlib circuitpython):

# src/main.py
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)

Both compile to bare-metal firmware for the same chip. The Quick Start walks the same path in more detail, including what each scaffolded file is for.

4. Build and flash

pymcu build
# Compiling src/main.py...
# → dist/firmware.hex

pymcu flash --port /dev/cu.usbmodem*
# avrdude: flash verified

Choosing an API

Package API surface Install
pymcu-circuitpython digitalio, analogio, busio, pwmio, time, board, neopixel pip install pymcu-circuitpython
pymcu-micropython machine (Pin/UART/ADC/PWM/SPI/I2C/Timer/WDT), utime pip install pymcu-micropython
pymcu.hal.* Direct register-level HAL — lowest overhead pymcu-stdlib (installed automatically with pymcu-compiler)

Build against a compatibility layer, not against pymcu.hal.*.

The beta label covers the compiler frontend and the AVR backend. It is not a promise about every API those two can reach, and the difference between the three rows above matters:

  • pymcu-circuitpython and pymcu-micropython are the surfaces designed to hold still, because they do not define their own shape: they track digitalio / board and machine / utime as specified by CircuitPython and MicroPython. That is a reason you can check, not a guarantee we have earned yet, and both packages are still pre-1.0.
  • pymcu.hal.* is deliberately low-level, it is the layer being actively reshaped, and it may change between releases without a deprecation cycle. Reach for it when you need direct register access, @interrupt, asm() or @extern, and expect to revisit that code.

All three are deepest on the ATmega parts. board.* in particular is defined for the Arduino boards but not for ATtiny or for the RP2040 / RP2350, where you pass the pin number instead (digitalio.DigitalInOut(25)). On the alpha backends a compat call may resolve to a HAL module that does not implement it yet; the compiler says so at build time rather than emitting silently wrong code.


Supported targets

Not every backend is at the same maturity. The compiler frontend and the AVR backend are beta as of 0.1.0b1. ARM, PIC and RISC-V remain alpha.

Architecture Status Chips
AVR (ATmega) beta ATmega48/88/168/328P, ATmega2560, ATmega32U4
AVR (ATtiny) beta ATtiny25/45/85, ATtiny24/44/84, ATtiny13/13A, ATtiny2313/4313
ARM (Cortex-M0+ / M33) alpha RP2040 (Pico / Pico W), RP2350 (Pico 2 / Pico 2 W) — incl. PIO and CYW43 WiFi
PIC (mid-range) alpha PIC16F84A, PIC16F877A

What the labels mean.

  • beta: the language surface below is implemented and test-covered on this backend, and it is validated on real silicon (Arduino Uno, logic analyzer). The label covers the compiler frontend and the AVR backend, not the stability of every API you can reach through them: see Choosing an API for which surface to build against.
  • alpha: it builds and it runs, but parts of the language surface below are missing on this backend, it does not carry AVR's continuous silicon validation, and the API may change between releases. Specifically today: list[T] is AVR-only; try / except is unavailable on PIC; float, f-strings, generators and @interrupt are unavailable on PIC16 (PIC18 has float and generators); and PIC16F84A / PIC16F877A builds emit no configuration word, so the resulting image will not boot on real hardware until you program the fuses yourself. See Language Limitations.

How far each backend has been validated on real hardware. This is the part that most separates beta from alpha, so it is worth stating per backend rather than in one sentence:

Backend Hardware record
AVR Continuously validated on silicon. A logic-analyzer harness on an Arduino Uno decodes the board's UART and diffs it against CPython running the same source, so a semantic divergence is caught rather than argued about.
ARM Confirmed running on real Raspberry Pi silicon (Pico, Pico 2): blink, native f-strings, and the Python RTOS doing preemptive multitasking on the Cortex-M33, with clock and timer timing verified on a logic analyzer to better than 0.01%. What it does not yet have is the continuous differential harness AVR runs.
PIC Partially exercised on silicon (PIC18 GPIO, delay_ms, UART TX); most testing is on the PicSharp emulator. For the PIC16 parts listed above the build emits no configuration word, so the image does not boot until you program the fuses yourself.
RISC-V Emulation only, in qemu. It has never been run on a physical CH32V003.

RISC-V (CH32V003/V203) has a working backend in-tree but is not published on PyPI and has no install extra, so a pip/pipx install never provides it.


HAL coverage (ATmega328P / Arduino Uno)

Module Features
pymcu.hal.gpio Pin — high / low / toggle / irq / pulse_in
pymcu.hal.uart UART — write / read / println / RX interrupt
pymcu.hal.adc AnalogPin — poll + interrupt; internal temperature
pymcu.hal.timer Timer(n, prescaler) — CTC mode; millis() / micros()
pymcu.hal.pwm PWM — multi-channel; set_duty / set_freq
pymcu.hal.spi SPI
pymcu.hal.softspi SoftSPI — bit-bang, any GPIO
pymcu.hal.i2c I2C
pymcu.hal.softi2c SoftI2C — bit-bang, any GPIO
pymcu.hal.eeprom EEPROMwrite(addr, val) / read(addr)
pymcu.hal.watchdog Watchdogenable / disable / feed
pymcu.hal.power sleep_idle / sleep_adc_noise / sleep_power_down / sleep_power_save / sleep_standby / sleep_extended_standby

Drivers: DHT11, DS18B20, HD44780 LCD, SSD1306 OLED, MAX7219 8x8 matrix, BMP280, WS2812 NeoPixel.


What Python features are supported

PyMCU accepts Python syntax but enforces a strict compile-time type system.

The list below describes the AVR backend, which is the beta one. Items marked (AVR only) or with an explicit backend list are not available everywhere. See Supported targets.

Supported:

  • Integer types: uint8, int8, uint16, int16, uint32, int32 — with type inference for unannotated def parameters and returns
  • float (IEEE-754 single) on AVR, ARM and PIC18; PIC16 has no floating point at all
  • Fixed arrays buf: uint8[16] and bytearray everywhere; heap-bounded lists x: list[uint8] = list() (AVR only); on ARM and PIC, use a fixed array
  • Slices: equal-length assignment (including through __setitem__, so microcontroller.nvm[0:4] = b"..." compiles) and iteration with runtime bounds (for b in buf[0:n])
  • print() of a bytearray or a slice as the CPython repr, and of a float with two rounded decimals; s = "".join([chr(b) for b in buf]) for bytes-to-string
  • for, while, if, match / case, with, class, @inline, lambda
  • Generators (yield) on AVR, ARM and PIC18; async / await with asyncio.run / gather on AVR, ARM and PIC18, both unavailable on PIC16
  • dict / set literals as closed compile-time lookup tables, plus pymcu.collections.FixedDict for mutable fixed-capacity maps — still no heap
  • f-strings with runtime interpolations and format specs, as stream writes or values (AVR and ARM only: PIC and RISC-V have no string-building path)
  • try / except / raise / finally with cross-function propagation (AVR and ARM); on PIC, use return codes, since only the ZeroDivisionError guard exists there
  • @interrupt ISR handlers (not on PIC16), asm("...") inline assembly everywhere (with operands on ARM)
  • CircuitPython and MicroPython compat packages, plus pymcu lint to vet a port

Not supported:

  • Open-ended dict / set mutation beyond FixedDict's fixed capacity (no heap hash tables)
  • Closures capturing mutable variables — use explicit parameters
  • *args / **kwargs, reflection (getattr / setattr / eval)
  • Anything whose size is only known at runtime: a slice read bound to a name (b = buf[0:n]), a runtime tuple, a comprehension filtered on a runtime condition

The compiler rejects unsupported features with a clear error at compile time — including the ones the hardware cannot honour, such as a runtime pin number, an image larger than the chip's flash, or static data that does not fit in SRAM. See the Language Limitations page for the full list.


CLI reference

Command Description
pymcu new <name> Scaffold a new project
pymcu build Compile src/dist/firmware.hex
pymcu flash Upload via avrdude
pymcu clean Remove build artifacts

Written up elsewhere

  • Python on a Classic Uno on Arduino Project Hub — why the board that taught millions of us electronics never got to speak Python, and what changes once the interpreter is gone.

Sustainability

If PyMCU saves you time, consider sponsoring the project. The goal is $300/month, which covers the AI tooling, domains and hosting that keep development at its current pace. Sponsors at $10/month and above are listed in SPONSORS.md.

Sponsor on GitHub

Sponsors

PyMCU is built by one person, and these are the people helping keep it going.

Ecosystem Partner

Adafruit

Adafruit, the home of CircuitPython, sponsors PyMCU on GitHub. Thank you, pt and ladyada.


License

All components are licensed under the MIT License. Your compiled firmware output is entirely yours — no runtime license, no attribution required.


Contributing

See CONTRIBUTING.md and LANGUAGE_ROADMAP.md.

Credits

Special thanks to Richard Wardlow, creator of the original pyMCU project (2012). See CREDITS.md for the full acknowledgement.

About

A Python to ASM AOT compiler for microcontrollers

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

7 stars

Watchers

0 watching

Forks

Releases

Sponsor this project

Packages

Contributors

Languages