PyMCU vs Arduino#
Arduino is the dominant platform for beginner and hobbyist embedded development. This page shows how PyMCU compares — what works the same, what works differently, and where PyMCU goes beyond what Arduino offers.
The pitch#
Arduino made microcontrollers accessible with a simplified C++ API. PyMCU does the same for Python: write familiar Python syntax, get the same tight native code you’d get from C.
```cpp void setup() { pinMode(13, OUTPUT); }
void loop() { digitalWrite(13, HIGH); delay(500); digitalWrite(13, LOW); delay(500); } ```
PyMCU (Python)
```python from pymcu.hal.gpio import Pin from pymcu.time import delay_ms
def main(): led = Pin(“PB5”, Pin.OUT) # D13 on Uno while True: led.toggle() delay_ms(500) ```
Both compile to the same 124-byte AVR assembly.
---
## Function equivalents
| Arduino function | PyMCU equivalent | Notes |
|------------------|-----------------|-------|
| `pinMode(n, OUTPUT)` | `Pin("PB5", Pin.OUT)` | Pin name is compile-time typed |
| `digitalWrite(n, HIGH)` | `led.high()` | Inlines to a single `SBI` |
| `digitalRead(n)` | `led.value` | Single `SBIS`/`SBIC` |
| `analogRead(n)` | `AnalogPin(n).read()` | 10-bit, same as Arduino |
| `analogWrite(n, v)` | `PWM("PD6", v).start()` | Hardware PWM channels only |
| `delay(ms)` | `delay_ms(ms)` | Exact same semantics |
| `millis()` | `millis()` | From `pymcu.time` |
| `micros()` | `micros()` | From `pymcu.time` |
| `tone(pin, freq)` | `tone(freq)` | Hardware toggle on OC2A (D11) |
| `noTone(pin)` | `noTone()` | |
| `map(x, ...)` | `map_range(x, ...)` | From `pymcu.math` |
| `constrain(x, lo, hi)` | `constrain(x, lo, hi)` | From `pymcu.math` |
| `random(n)` | `random(n)` | From `pymcu.random` |
| `randomSeed(s)` | `randomSeed(s)` | From `pymcu.random` |
| `attachInterrupt(pin, fn, FALLING)` | `pin.irq(fn, Pin.IRQ_FALLING)` | |
| `Serial.begin(baud)` | `UART(baud)` | |
| `Serial.print(x)` | `uart.write_str(x)` | |
| `Serial.println(x)` | `uart.println(x)` | |
| `EEPROM.read(addr)` | `eeprom.read(addr)` | |
| `EEPROM.write(addr, v)` | `eeprom.write(addr, v)` | |
| `Wire.begin()` | `I2C(0)` | master mode |
| `Wire.beginTransmission(addr)` + `Wire.write(b)` | `i2c.write_to(addr, buf, n)` | |
| `SPI.begin()` + `SPI.transfer(b)` | `SPI(SPI.CONTROLLER).transfer(b)` | |
| `Servo.write(degrees)` | `Servo("PB1").write(degrees)` | Timer1, D9 or D10 |
### MicroPython / CircuitPython compat layer
If you already know MicroPython, import the compat shim and use the API you know:
```python
from machine import Pin, UART, SPI, I2C, ADC, Timer, PWM
import utime
The compat layer is a thin compile-time alias — it produces identical machine code to the native PyMCU HAL.
What PyMCU adds#
1. Static typing catches errors at compile time#
duty: uint8 = read_sensor() # uint8 — compiler enforces the type
pwm.set_duty(duty) # no runtime type check needed
Arduino has no type checking. A silent integer truncation in C++ can cause subtle hardware bugs that are nearly impossible to find with a serial monitor.
2. Source-level debugger#
PyMCU ships a full VS Code DAP debugger — set breakpoints directly on Python source lines, step through code, inspect all 32 registers and SRAM in real time.
Arduino’s only debugging tool is Serial.print().
3. CPU profiler with flamegraphs#
pymcu profile --ms 5000 -o profile.speedscope.json
Drag profile.speedscope.json onto speedscope.app to see
exactly where your firmware spends CPU time, down to the individual function call.
Works with multi-task RTOS firmware — each task gets its own profile tab.
No equivalent exists for Arduino.
4. RTOS in Python#
from rtos import add_task, start_scheduler, Priority
add_task(sensor_task, Priority.HIGH) # 50% CPU
add_task(display_task, Priority.NORMAL) # 25% CPU
add_task(blink_task, Priority.IDLE) # 12.5% CPU
start_scheduler() # preemptive, 1 ms tick
A production-grade preemptive RTOS in pure Python, compiled to tight AVR assembly. See the rtos-multitask example.
5. Zero-cost abstractions#
Every HAL method is @inline — no function call overhead, no virtual dispatch.
The compiler folds chip-dispatch match blocks at compile time and emits exactly
the instructions for the selected hardware.
A led.toggle() call on ATmega328P compiles to a single SBIS PORTB,5 / RJMP +2 / SBI PORTB,5 / RJMP +2 / CBI PORTB,5 sequence — the same as hand-written assembly.
6. C interop via @extern#
from pymcu.ffi import extern
@extern("crc8_maxim", header="crc.h")
def crc8(data: ptr[uint8], len: uint8) -> uint8: ...
Call any C library function directly from Python. PyMCU handles the calling convention; no wrapper glue code needed.
What Arduino still has#
Arduino advantage |
Notes |
|---|---|
Library ecosystem |
4000+ libraries; PyMCU has a growing stdlib + FFI for C libs |
More boards |
ESP32, STM32, RP2040, SAMD, nRF52 (PyMCU: AVR + PIC + RISC-V, ARM coming) |
Arduino IDE simplicity |
PyMCU uses VS Code; more powerful but higher initial setup |
Community / tutorials |
Vast corpus of Arduino examples |
Code size comparison#
Compiling blink (toggle D13, delay 500 ms) for Arduino Uno (16 MHz):
Toolchain |
Flash |
SRAM |
|---|---|---|
Arduino (avr-gcc -Os) |
~928 bytes |
9 bytes |
PyMCU |
124 bytes |
0 bytes |
PyMCU eliminates the Arduino runtime overhead (init code, millis ISR,
main() wrapper, Serial globals) that is linked in even for simple programs.
Summary#
PyMCU is the right choice when you want:
Python syntax with zero runtime overhead
Static typing to catch mistakes at compile time
Professional tooling: VS Code debugger, Speedscope profiler
RTOS without a heavy C library
Minimal flash footprint on constrained chips (ATtiny, ATmega48)
Arduino is the right choice when you need:
The vast existing library ecosystem
Boards beyond AVR (ESP32, STM32, RP2040)
A simpler zero-config IDE