Language Overview#
PyMCU is a compiler, not an interpreter#
This distinction shapes every design decision in PyMCU.
MicroPython / CircuitPython |
PyMCU |
|
|---|---|---|
Python runtime on MCU |
Yes — ~256 KB interpreter |
No |
Bytecode executed at runtime |
Yes |
No |
Types resolved |
At runtime |
At compile time |
Heap allocation |
Yes (garbage collected) |
No |
Flash usage |
256 KB + your code |
Your code only |
Execution speed |
Bytecode (slow) |
Native machine code |
When you run pymcu build, the compiler (running on your PC) reads your Python source,
type-checks it, and emits native AVR assembly for the ATmega328P. The MCU never sees Python
syntax — only machine instructions.
Supported Python subset#
PyMCU accepts a statically-typed, allocation-free subset of Python 3. The guiding rule is:
If the size and type of every value is not known at compile time, it cannot be compiled.
Supported statements#
if / elif / else, while, for, match / case, def, class, return, pass,
import, from … import, global, with, assert (compile-time),
try / except / finally, raise.
raise CompileError("msg") aborts compilation with a diagnostic — it never generates runtime
code and cannot be caught by try / except.
Supported expressions#
Arithmetic (+ - * / % //), comparison, bitwise, logical (and/or/not),
ternary, augmented assignment, type casts (uint8(x)), abs, min, max, len,
walrus (:=), tuple literals and unpacking, member access, array indexing, list
comprehensions (compile-time constant bounds only).
MCU-specific extensions#
uint8,int8,uint16,int16,uint32,int32primitive typesptr[T]— memory-mapped I/O pointerconst[T]— compile-time constant enforcementasm("instr")— inline assembly with register constraints@inline— zero-cost function expansion@interrupt(vector)— hardware ISR handler__CHIP__/__FREQ__— conditional compilation by chip name and clock frequency
Static typing is mandatory#
Every variable must carry a type annotation at its first assignment. The compiler does not infer types — the annotation drives register width, instruction selection, and memory layout.
# Correct — annotation at first assignment
count: uint16 = 0
flag: uint8 = 0
# Correct — built-in int maps to int16, no import required
n: int = read_sensor()
# CompileError — no annotation
x = 42
The built-in int maps to int16 and requires no import. All other sized types come from
pymcu.types:
from pymcu.types import uint8, uint16, uint32, int8, int16, int32, ptr, const
No heap — everything is stack or global#
PyMCU targets microcontrollers with 32 bytes to 8 KB of SRAM. There is no malloc, no
garbage collector, and no heap. Every value must have a fixed size known at compile time:
# OK — size is known at compile time
buf: uint8[8] = [0] * 8
# CompileError — dynamic size
n = get_size()
buf: uint8[n] = [0] * n # cannot allocate variable-size array
Container types that require heap allocation (list.append, dict, set) are not available.
See Language Limitations for the full list with alternatives.
Module system#
PyMCU compiles multi-file projects. Imports resolve to PyMCU stdlib modules or your own source files:
from pymcu.hal.gpio import Pin # HAL module
from pymcu.types import uint8 # type definitions
from sensors import read_dht11 # your own module in src/
Third-party PyPI packages cannot be used — only the pymcu stdlib and modules you write
yourself (in PyMCU-compatible Python) are compiled.
Entry point#
PyMCU supports two entry-point styles:
Explicit def main(): (recommended):
def main():
led = Pin("PB5", Pin.OUT)
while True:
led.toggle()
delay_ms(500)
Top-level script style (MicroPython / CircuitPython compatible):
led = Pin("PB5", Pin.OUT)
while True:
led.toggle()
delay_ms(500)
The compiler synthesizes a main entry point from top-level executable statements
automatically.
Conditional compilation#
__CHIP__ and __FREQ__ are compile-time string/integer constants. Dead branches are
eliminated before code generation — no runtime cost:
if __CHIP__.arch == "avr":
asm("nop") # only emitted; dead branches removed at compile time