MicroPython Basics
MicroPython is a lean, efficient implementation of Python 3 designed to run on microcontrollers with as little as 256 KB of flash and 16 KB of RAM. It brings Python's readable syntax to ESP32, Raspberry Pi Pico, STM32, and dozens of other boards.
MicroPython vs CPython (Standard Python)
| Feature | MicroPython | CPython (Standard Python) |
|---|---|---|
| Target | Microcontrollers | Desktop/Server |
| RAM | 16 KB – 520 KB | Gigabytes |
| Flash | 256 KB – 4 MB | Gigabytes (disk) |
| Startup time | < 100 ms | 30–100 ms |
| Hardware access | machine module | OS-level (GPIO libs) |
| Standard library | Subset (uos, utime) | Full |
| Float | Single precision (32-bit) | Double precision (64-bit) |
| f-strings | Supported (v1.19+) | Fully supported |
asyncio | uasyncio (subset) | Full asyncio |
The REPL — Interactive Python Shell
The REPL (Read-Eval-Print Loop) gives you a live Python prompt directly on the device over USB serial.
# Connect via Thonny or: screen /dev/ttyUSB0 115200
# Try these in the REPL:
>>> print("Hello from ESP32!")
Hello from ESP32!
>>> import machine
>>> machine.freq()
240000000 # 240 MHz
>>> import gc
>>> gc.mem_free()
208976 # ~204 KB free RAM
>>> import os
>>> os.uname()
(sysname='esp32', nodename='esp32', release='1.22.0',
version='v1.22.0 on 2024-01-05', machine='ESP32 module')
>>> 2 ** 32
4294967296 # Python integers, not C int — no overflow!
# List files on the device
>>> import os
>>> os.listdir()
['boot.py', 'main.py', 'config.py']Core Modules
machine — Hardware Control
from machine import Pin, ADC, PWM, I2C, SPI, UART, Timer, WDT, RTC, deepsleep
# GPIO
led = Pin(2, Pin.OUT)
btn = Pin(0, Pin.IN, Pin.PULL_UP)
# ADC
adc = ADC(Pin(34))
adc.atten(ADC.ATTN_11DB)
# PWM
pwm = PWM(Pin(15), freq=50)
pwm.duty(512) # 50% duty cycle
# Timer (calls a function periodically)
def tick(timer):
led.toggle()
timer = Timer(0)
timer.init(period=500, mode=Timer.PERIODIC, callback=tick)
# LED now blinks at 1 Hz without blocking main code
# Watchdog Timer (resets the device if not fed regularly)
wdt = WDT(timeout=5000) # 5 second timeout
while True:
# Do work...
wdt.feed() # Reset watchdog. If this isn't called, device resets.utime — Time Functions
import utime
# Current time (seconds since 2000-01-01 in MicroPython)
print(utime.time()) # e.g. 749289600
# High-resolution timing
start = utime.ticks_ms() # Milliseconds
# ... do some work ...
elapsed = utime.ticks_diff(utime.ticks_ms(), start)
print(f"Took: {elapsed} ms")
# Microsecond precision
start_us = utime.ticks_us()
utime.sleep_us(100) # Sleep 100 microseconds
elapsed_us = utime.ticks_diff(utime.ticks_us(), start_us)
print(f"Elapsed: {elapsed_us} µs")
# Sleep functions
utime.sleep(1) # 1 second
utime.sleep_ms(500) # 500 milliseconds
utime.sleep_us(1000) # 1000 microsecondsnetwork — WiFi
import network
import utime
def connect_wifi(ssid, password, timeout=15):
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
if wlan.isconnected():
print(f"Already connected: {wlan.ifconfig()[0]}")
return True
wlan.connect(ssid, password)
deadline = utime.time() + timeout
while not wlan.isconnected():
if utime.time() > deadline:
print(f"Connection timeout after {timeout}s")
return False
print(".", end="")
utime.sleep(0.5)
ip, mask, gateway, dns = wlan.ifconfig()
rssi = wlan.status('rssi')
print(f"\nIP: {ip} RSSI: {rssi} dBm")
return True
connect_wifi("MySSID", "MyPassword")File System
MicroPython has a built-in file system (FAT or LittleFS) stored in flash.
import os
# File system operations
print(os.listdir('/')) # ['boot.py', 'main.py', 'lib']
print(os.listdir('/lib')) # ['dht.py', 'mqtt.py']
os.mkdir('/data') # Create directory
os.rename('/data', '/logs') # Rename
os.remove('/logs/old.txt') # Delete file
os.rmdir('/logs') # Remove directory (must be empty)
# Get filesystem stats (total, free bytes)
stat = os.statvfs('/')
total_bytes = stat[0] * stat[2]
free_bytes = stat[0] * stat[3]
print(f"Flash: {free_bytes/1024:.1f} KB free of {total_bytes/1024:.1f} KB")
# Reading and writing files
with open('/data/sensor.csv', 'a') as f:
f.write("2024-01-15,24.5,61.2\n")
with open('/data/sensor.csv', 'r') as f:
for line in f:
print(line.strip())Memory Management
RAM is the most precious resource on a microcontroller.
import gc
# Check memory
print(f"Free: {gc.mem_free():,} bytes")
print(f"Alloc: {gc.mem_alloc():,} bytes")
# Force garbage collection
gc.collect()
print(f"After GC: {gc.mem_free():,} bytes free")
# Memory-efficient patterns
# BAD: builds a list in memory then iterates
data = [i * 2 for i in range(1000)]
for item in data:
pass
# GOOD: generator — processes one item at a time
def double_gen(n):
for i in range(n):
yield i * 2
for item in double_gen(1000): # Only one value in memory at a time
pass
# BAD: string concatenation (creates new strings repeatedly)
msg = ""
for i in range(100):
msg += str(i) + ","
# GOOD: join (builds once)
msg = ",".join(str(i) for i in range(100))
# Check allocation after operations
gc.collect()
print(f"Final free RAM: {gc.mem_free():,} bytes")boot.py and main.py
MicroPython runs two files on startup in this order:
# boot.py — runs first, hardware initialization
# Keep it minimal. Errors here are hard to debug.
import machine
import gc
# Increase heap for complex apps
gc.enable()
# Optionally reduce frequency to save power during init
# machine.freq(80_000_000) # 80 MHz during boot
print("Boot complete")
# main.py — your application code
# This is where your app logic lives
import dht
from machine import Pin
import utime
from config import WIFI_SSID, WIFI_PASSWORD, MQTT_BROKER
# ... app code ...
def main():
sensor = dht.DHT22(Pin(4))
while True:
sensor.measure()
print(f"T={sensor.temperature()} H={sensor.humidity()}")
utime.sleep(5)
if __name__ == '__main__':
main()Key MicroPython Differences from CPython
# 1. No `time` module — use `utime`
import utime # Not: import time
utime.sleep(1)
# 2. Reduced standard library — use 'u' prefixed versions
import ujson # Faster, smaller than json
import uos # Subset of os
import ure # Subset of re (regex)
# 3. No `threading` — use async or hardware timers
import uasyncio
async def blink():
led = Pin(2, Pin.OUT)
while True:
led.toggle()
await uasyncio.sleep_ms(500)
async def main():
uasyncio.create_task(blink())
# ... other tasks ...
await uasyncio.sleep(60)
uasyncio.run(main())
# 4. Single precision floats (not double)
import math
print(math.pi) # 3.141593 (not 3.141592653589793)
# 5. Limited f-string support in older versions
# Use format() if targeting older firmware
temp = 24.5
old_way = "Temp: {:.1f} C".format(temp)
new_way = f"Temp: {temp:.1f} C" # Works in MicroPython 1.19+
# 6. `const()` optimization — avoids allocating an object
from micropython import const
MAX_RETRIES = const(5) # Compiled to integer constant, saves RAMInstalling Libraries (upip / mip)
# MicroPython 1.19+ — use mip (micropython package manager)
# Run in REPL while connected to WiFi:
import mip
mip.install("umqtt.simple") # MQTT client
mip.install("dht") # DHT sensor driver
mip.install("bme280") # BME280 driver
# Manual installation: copy .py files to /lib/ on the device
# via Thonny file manager or ampy tool (pip install adafruit-ampy)Summary
- MicroPython is Python 3 running on microcontrollers with kilobytes of RAM
- The REPL gives you an interactive Python prompt over USB serial for rapid testing
- The
machinemodule provides hardware control: GPIO, ADC, PWM, I2C, SPI, UART, Timer utimeprovides timing functions; always useticks_diff()for elapsed time (handles overflow)boot.pyruns first (hardware init),main.pyruns your application- MicroPython has a smaller standard library, single-precision floats, and no threading (use
uasyncio) - RAM management matters — use generators, call
gc.collect(), and useconst()for fixed values
Next, we'll look at Arduino programming for cases where you need C/C++ performance and the Arduino library ecosystem.