Language Limitations#
Read this before writing your first project.
PyMCU compiles a statically-typed, allocation-free subset of Python to bare-metal machine code. There is no runtime, no heap, no garbage collector, and no interpreter. Many standard Python features are therefore incompatible with this model.
Standard Library Philosophy
Because of the architectural differences between a PC and a bare-metal microcontroller, PyMCU does not attempt to replicate the CPython standard library 1:1.
Instead, PyMCU adopts the philosophy and API design of MicroPython and CircuitPython (specifically the machine and board modules) as its official user-facing standard library. This ensures that code written for PyMCU looks familiar to developers coming from the broader Python-on-hardware ecosystem, even though it executes entirely differently.
This page lists every known unsupported feature, explains why it cannot be compiled, and suggests the idiomatic PyMCU alternative where one exists.
Dynamic memory and containers#
Feature |
Why it fails |
Alternative |
|---|---|---|
|
Fixed arrays have no append |
|
|
Hash table requires heap |
|
|
Hash set requires heap |
|
Supported: list[T] (x: list[uint8] = list()) compiles to a bounded bump-allocator
with GC; supports append(), len(), x[i], for v in x:.
bytearray(N) and bytearray(b"...") compile to SRAM uint8[N] arrays.
Fixed-size arrays arr: uint8[N] support both constant- and variable-index access.
Rule of thumb: if the size is not known at compile time, it cannot be compiled.
String operations#
Feature |
Why it fails |
Alternative |
|---|---|---|
|
Requires a heap format buffer |
|
|
Heap strings |
Not available |
|
Runtime string object required |
Use fixed-size buffers |
|
Heap allocation |
Separate |
|
No runtime string object |
Use |
Supported: String literals in flash, raw strings r"\n", uart.println("literal"),
for ch in "ABC": (compile-time unroll), f"text={const}" where all interpolations are
compile-time constants, const[str] runtime subscript (reads byte from flash).
Exception handling#
try / except / raise / finally are supported on AVR targets via avr-libc
setjmp / longjmp. No stack unwinding — the handler is a simple longjmp destination.
try:
if val > 255:
raise ValueError
except ValueError:
handle_error()
finally:
cleanup()
ValueError, TypeError, IndexError, KeyError, and NotImplementedError are builtins
— no import required, exactly like CPython.
Limitations:
Limitation |
Notes |
|---|---|
SRAM cost: ~21 bytes per |
|
|
Only one |
AVR only |
PIC18 and other backends: use return codes or sentinel values instead |
Exception types are integer codes |
Builtins ( |
Prefer return codes for firmware
try / except is valid Python but misaligned with bare-metal best practices. Every try
block spends 21 bytes of SRAM on a jmp_buf even when no exception is raised. On a 2 KB
device this adds up fast.
Because there is only one active jmp_buf at a time (no handler chain), raise and
except must be in the same function. Cross-function propagation is not safe: if an
inner function has its own try block, its jmp_buf overwrites the caller’s, and an
unhandled raise will not reach the outer handler.
The idiomatic alternative is a status return value:
# Idiomatic: zero SRAM overhead, works across any call depth
STATUS_OK: uint8 = 0
STATUS_RANGE: uint8 = 1
def read_sensor() -> uint8:
if adc.read() > 1000:
return STATUS_RANGE
return STATUS_OK
match read_sensor():
case STATUS_OK: ...
case STATUS_RANGE: ...
CompileError — compile-time intrinsic:
raise CompileError("msg") is intercepted by the compiler and aborts compilation with a
CompileError: diagnostic. It never generates any runtime code or longjmp. Used in all
HAL modules to reject unsupported configurations at compile time:
from pymcu.exceptions import CompileError
match __CHIP__.arch:
case "avr":
...
case _:
raise CompileError("SPI not supported on this architecture")
CompileError cannot be caught by try / except — compilation aborts before any binary
is produced.
Unhandled exception output (AVR with UART0):
When a raise has no active except handler, PyMCU prints "E:<TypeName>\r\n" to UART0
(if initialized) then halts with cli; rjmp .-2. Useful for debugging from a serial monitor:
E:ValueError
Only exception types actually raised in the program have their name strings emitted in flash — no overhead for unused exception codes. Chips without UART0 (attiny85 etc.) skip output and go directly to the halt loop.
Supported: assert condition, msg as a compile-time check — a statically false assertion
is a CompileError; a true or runtime assertion is stripped.
Functions and closures#
Feature |
Why it fails |
Alternative |
|---|---|---|
Closures capturing mutable vars |
Closure cell requires heap |
Pass captured values as explicit parameters |
|
Variadic convention needs stack inspection |
Fixed parameter lists |
|
Runtime partial object |
Wrapper |
Higher-order functions (passing functions as values) |
No function pointer type |
|
Unbounded recursion |
Stack overflow on MCU |
Iterative equivalent |
Supported: @inline functions expand at call sites — zero call overhead, zero stack.
Non-@inline functions use a conventional call/ret ABI and can recurse to a fixed depth
(~80 frames on ATmega328P with 2 KB SRAM). lambda x: expr (no closure capture) is inlined
at the call site. nonlocal is supported inside nested @inline functions.
Classes and inheritance#
Feature |
Why it fails |
Alternative |
|---|---|---|
Multiple inheritance / MRO |
C3 linearization is a runtime concept |
Single-level inheritance only |
Runtime polymorphism (vtable dispatch) |
Requires vtable + heap class objects |
Compile-time |
|
No type tags at runtime |
Not available |
|
No runtime string formatting |
|
|
Metaclass + runtime heap |
Manual |
Supported: ZCA @inline classes (zero SRAM), @property / @name.setter,
single-level class inheritance with super(), with obj: context managers
(__enter__/__exit__), @staticmethod, operator dunder methods (__add__, __sub__,
__mul__, __len__, __contains__, __getitem__, __setitem__, all comparison / bitwise
dunders).
Type system limitations#
Feature |
Why it fails |
Alternative |
|---|---|---|
|
Requires float |
Not available |
|
Requires heap |
Not available |
|
Folds to |
Use a sentinel value (e.g. |
|
No heap, no runtime type tag |
Sentinel value pattern |
|
Runtime type tag required |
Separate functions per type |
|
Runtime generics |
Separate |
Note on float: Soft-float (IEEE 754 single-precision) is supported on AVR via a
pure-assembly helper library. Expect ~200-400 cycles per operation. Subnormals are treated as
zero; NaN and Inf propagate correctly.
Pointer arithmetic#
ptr[T] in PyMCU is a compile-time constant address alias, not a runtime pointer.
It is equivalent to a C volatile register macro:
// C: compile-time constant pointer — what ptr[T] models
volatile uint8_t* const PINB = (volatile uint8_t*)0x36;
This means the following operations are not supported:
Operation |
Example |
Why it fails |
|---|---|---|
Pointer advance |
|
|
Variable-index dereference |
|
|
Pointer as function parameter |
|
No |
Pointer difference |
|
Not in IR |
Idiomatic alternative — fixed arrays with variable index:
buf: uint8[16] = [0] * 16
i: uint8 = 0
while i < 16:
buf[i] = compute(i) # compiles to: LDD / STD with Y+offset
i = i + 1
uint8[N] arrays with a runtime index already compile to efficient ld/st with
Y+offset addressing on AVR — no pointer arithmetic needed.
For performance-critical pointer walks in asm: use the Z register (r30:r31)
with ld r24, Z+ / st Z+, r24 for auto-increment through a buffer.
asm("""
ldi r30, lo8(my_buf)
ldi r31, hi8(my_buf)
ldi r18, 16 ; length
_loop:
ld r24, Z+ ; load byte and advance pointer
...
dec r18
brne _loop
""")
---
## Iterators and comprehensions
| Feature | Why it fails | Alternative |
|---|---|---|
| List comprehension over a **runtime** iterable | Length not known at compile time | `for` loop with fixed-size array |
| Dict comprehension | Heap allocation | Not available |
| Set comprehension | Heap allocation | Not available |
| Generator expressions / `yield` | Coroutine frame requires heap | Not available |
| `map()` / `filter()` with runtime iterables | Lazy iterator requires heap | Explicit `for` loop |
**Supported:** `for i in range(N)` (runtime or constant N), `for x in array`,
`for x in [...]`, `for i, x in enumerate(iterable)`, `for x, y in zip(list1, list2)`,
`for x in reversed([...])`, list comprehensions with compile-time constant bounds,
nested list comprehensions, `if`-filtered list comprehensions,
`for pin in [DigitalInOut(p) for p in (...)]` and
`for bit, pin in enumerate([DigitalInOut(p) for p in (...)])` (CT unroll of ZCA instance arrays).
---
## Async and concurrency
| Feature | Why it fails | Alternative |
|---|---|---|
| `async def` / `await` | Async runtime / event loop required | `@interrupt` ISRs + polling loop |
| `asyncio` | Not available | Not available |
| `threading` / `multiprocessing` | OS required | `@interrupt` ISRs |
**Supported:** `@interrupt` decorator for hardware ISRs, `Pin.irq(trigger, handler)` for
external pin interrupts, atomic flag patterns via `GPIOR0`.
:::{admonition} Timer0 and millis / ticks_ms
:class: warning
`millis_init()` (auto-injected when `ticks_ms()` is detected) configures **Timer0** in
normal overflow mode. Do **not** use Timer0 for PWM, CTC, or other purposes when
`ticks_ms()` / `millis()` is active in the same program.
`delay_ms()` and `delay_us()` are unaffected — they use a software busy-loop with no
hardware timer dependency.
:::
---
## Imports and modules
| Feature | Why it fails | Alternative |
|---|---|---|
| Third-party PyPI packages | Only `pymcu` stdlib is compiled | Implement in `pymcu` stdlib or use `@extern` |
| `importlib` / dynamic imports | Runtime module loading | Not available |
| Circular imports | Not supported | Restructure module dependencies |
**Supported:** `import foo`, `from foo import Bar`, `from foo import Bar as B`,
relative imports, multi-module projects, `pymcu` stdlib, `pymcu-circuitpython` and
`pymcu-micropython` compat packages.
---
## Built-ins summary
| Built-in | Status | Notes |
|---|---|---|
| `print(str)` / `print(int)` | ✅ Supported | Routes to UART |
| `range(n)` | ✅ Supported | For-loop bounds; runtime or constant |
| `len(arr)` / `len(b"...")` | ✅ Supported | Compile-time constant fold |
| `abs(x)` | ✅ Supported | Intrinsic |
| `min(a, b)` / `max(a, b)` | ✅ Supported | Intrinsic |
| `sum(iterable)` | ✅ Supported | Compile-time fold or unrolled additions |
| `enumerate(iterable)` | ✅ Supported | Compile-time index counter |
| `zip(a, b)` | ✅ Supported | Compile-time unroll over constant lists |
| `reversed(iterable)` | ✅ Supported | Compile-time reverse unroll |
| `any(iterable)` / `all(iterable)` | ✅ Supported | Compile-time fold |
| `divmod(a, b)` | ✅ Supported | Compile-time or runtime |
| `pow(x, n)` / `x ** n` | ✅ Supported | Compile-time constant fold |
| `hex(n)` / `bin(n)` | ✅ Supported | Compile-time only |
| `str(n)` | ✅ Supported | Compile-time only |
| `ord('A')` / `chr(n)` | ✅ Supported | Compile-time constant only |
| `int.from_bytes(b, e)` | ✅ Supported | Compile-time fold or runtime |
| `sorted()` | ❌ Not supported | No dynamic allocation |
| `map()` / `filter()` | ❌ Not supported | Use explicit `for` loops |
| `input()` | ✅ Supported | `line: bytearray = input("prompt")` — reads until newline from UART; prompt is optional compile-time string; max length is optional integer (default 64); UART preamble auto-injected |
| `open()` / file I/O | ❌ Not supported | No filesystem |
| `exec()` / `eval()` | ❌ Not supported | Interpreter required |
---
## Platform notes (ATmega328P / Arduino Uno)
- **Stack depth:** ~80 nested non-inline calls before overflow (2 KB SRAM, ~16 bytes/frame).
Use `@inline` for leaf helpers.
- **Soft float:** `float` variables and arithmetic are supported via a pure-assembly
soft-float library. No FPU required. ~200-400 cycles per operation.
- **No heap:** every variable must have a size known at compile time.
- **String literals are in flash:** read-only; sent to UART via flash string pool. Cannot be
compared, indexed, or modified at runtime.
- **C/C++ interop:** supported via `@extern` and `[tool.pymcu.ffi]` in `pyproject.toml`.
C sources use `avr-gcc`; C++ sources (`.cpp`/`.cc`/`.cxx`) use `avr-g++`
with `-fno-exceptions -fno-rtti`, enabling use of Arduino libraries.
## Platform notes (RP2040 / Raspberry Pi Pico) — alpha
The RP2040 backend lowers PyMCU's IR to **LLVM IR** (target `thumbv6m-none-eabi`)
rather than emitting assembly directly, so LLVM does register allocation, instruction
selection and optimization. `pymcu build` emits a flat flash image (`firmware.bin`,
with the stage-2 boot loader at offset 0). It is **alpha** and intentionally limited:
- **MVP peripherals only:** GPIO (`pymcu.hal.gpio.Pin`, via single-cycle IO) and
UART0 (`pymcu.hal.uart.UART`, PL011) are supported. SPI, I2C, PWM, ADC, PIO, USB,
timers, EEPROM/flash and the watchdog are **not** wired up on this backend yet.
- **Single core:** only core 0 runs. Dual-core launch and the SIO FIFO are not
exposed.
- **No GC / exceptions / soft-float yet:** `list[T]`, `try/except/raise`, and `float`
arithmetic compile on AVR but are **not supported** on the RP2040 backend — the
codegen rejects the corresponding IR with a clear "not supported yet" error.
Virtual-method dispatch, runtime-indexed arrays and operand-form inline `asm()` are
likewise deferred.
- **Delays:** `delay_ms` / `delay_us` poll the hardware **TIMER** (the
free-running 1 MHz microsecond counter), so timing is accurate on real silicon
regardless of CPU clock and pipeline, not a calibrated busy-loop. In the
emulator the wall-clock measured by `RunMilliseconds` reads the wait slightly
short, because that harness budgets execution by retired instruction count
while the timer advances by elapsed cycles — the firmware delay itself is
exact.
- **UART clock assumption:** the baud divisors assume `clk_peri = 125 MHz`
(`clk_sys` at the pico-sdk default). A configurable clocks HAL is future work.
- **Toolchain:** the backend ships in the `pymcu-arm` package (`pip install pymcu-arm`),
which registers the `rp2040` target. It requires **LLVM** (`opt`, `llc`, `llvm-mc`,
`ld.lld`, `llvm-objcopy`) on the host, provided by the
[`pymcu-arm-toolchain`](https://github.com/PyMCU/pymcu-arm) wheel (analogous to
`pymcu-avr-toolchain`). If the wheel is not available for your platform the toolchain
falls back to a system LLVM (e.g. `brew install llvm lld`).
- **No C/C++ interop (`@extern`) yet** on this backend.