Skip to content

MicroPython — Python Programming for Microcontrollers Guide

DodaTech Updated 2026-06-24 6 min read

In this tutorial, you'll learn about MicroPython. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.

MicroPython is a lean and efficient implementation of Python 3 designed to run on microcontrollers and constrained systems, bringing Python's readability and rapid development to IoT devices with as little as 256KB of flash and 16KB of RAM.

Why MicroPython Matters

Writing firmware in C/C++ requires managing memory manually, understanding linker scripts, and waiting through compile-flash-test cycles. MicroPython allows you to interact with hardware through a live REPL (Read-Eval-Print Loop) — you type code and see results immediately. Change a pin assignment? Edit the file on the device's filesystem and reset. No cross-compiler, no IDE, no flashing toolchain. This speed of iteration makes MicroPython ideal for prototyping, education, and low-complexity deployments. The Raspberry Pi Pico ships with MicroPython support as a first-class option. ESP32, STM32, nRF52840, and hundreds of other boards run MicroPython. DodaZIP's internal prototyping lab uses MicroPython for quick validation of sensor integrations before committing to C firmware.

Plain-Language Explanation

Imagine writing Arduino code. You write a complete program, compile it, upload it to the board, and wait for it to reboot. If something is wrong, you edit, recompile, and re-upload. This is like writing a letter, mailing it, and waiting for a reply to see if you made a typo.

MicroPython is like having a conversation. You connect to the board over USB, and it presents a Python prompt (>>>). Type import machine and press enter — instant result. Blink a pin: machine.Pin(2, machine.Pin.OUT).value(1) — the LED turns on immediately. When your code works, save it as main.py on the board. On reset, it runs automatically. Writing IoT firmware feels like writing a Python script.

graph TD
    subgraph "MicroPython Development Flow"
        PC[Computer] -->|USB Serial| REPL[MicroPython REPL
>> prompt] PC -->|File Upload| FS[Flash Filesystem
main.py, boot.py] REPL -->|Interactive| Test[Test GPIO / I2C / SPI] FS -->|Auto-run on boot| App[Application Code] App -->|WiFi| Net[Network
MQTT / HTTP] App -->|GPIO| Sensors[Sensors] App -->|GPIO| Actuators[Actuators] end style REPL fill:#27ae60,color:#fff style FS fill:#3498db,color:#fff style App fill:#e67e22,color:#fff

Getting Started

Flash MicroPython to the board using esptool (ESP32) or the official UF2 bootloader (Raspberry Pi Pico):

# ESP32: Erase and flash MicroPython
pip install esptool
esptool.py --port /dev/ttyUSB0 erase_flash
esptool.py --port /dev/ttyUSB0 --baud 460800 write_flash -z 0x1000 esp32-20230426-v1.20.0.bin
# Raspberry Pi Pico: Download UF2 file, copy to Pico when in bootloader mode

Connect via serial terminal:

screen /dev/ttyUSB0 115200
# Or: picocom /dev/ttyUSB0 -b115200

GPIO Control

MicroPython's machine module provides hardware control:

import machine
import time

# Setup LED pin
led = machine.Pin(2, machine.Pin.OUT)

# Blink pattern
while True:
    led.value(1)
    time.sleep(0.5)
    led.value(0)
    time.sleep(0.5)

# Read button input
button = machine.Pin(0, machine.Pin.IN, machine.Pin.PULL_UP)
print(f"Button state: {button.value()}")

# PWM (dim LED)
pwm = machine.PWM(machine.Pin(2), freq=1000)
for duty in range(0, 1024, 16):
    pwm.duty(duty)
    time.sleep(0.05)

Expected REPL output:

Button state: 1
(LED fades smoothly from off to full brightness)

I2C Sensor Reading

Connect a BME280 temperature/humidity/pressure sensor over I2C:

import machine
import time

i2c = machine.I2C(0, scl=machine.Pin(22), sda=machine.Pin(21), freq=400000)
print(f"I2C devices: {i2c.scan()}")

# BME280 typically at address 0x76
BME280_ADDR = 0x76

def read_bme280():
    # Read calibration data
    calib = i2c.readfrom_mem(BME280_ADDR, 0x88, 26)
    dig_T1 = calib[0] | (calib[1] << 8)

    # Read temperature
    data = i2c.readfrom_mem(BME280_ADDR, 0xFA, 3)
    adc_T = (data[0] << 12) | (data[1] << 4) | (data[2] >> 4)

    # Compensate temperature
    var1 = ((adc_T >> 3) - (dig_T1 << 1)) * 5
    temperature = var1 / 100.0
    return temperature

while True:
    temp = read_bme280()
    print(f"Temperature: {temp:.1f}°C")
    time.sleep(5)

Expected output:

I2C devices: [118]
Temperature: 24.3°C
Temperature: 24.5°C
Temperature: 24.4°C

WiFi and MQTT

Connecting the ESP32 to WiFi and publishing over MQTT:

import network
import time
from umqtt.simple import MQTTClient

# WiFi connection
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
wlan.connect("YourNetwork", "YourPassword")

while not wlan.isconnected():
    time.sleep(0.5)
    print("Connecting...")

print(f"Connected: {wlan.ifconfig()}")

# MQTT publish
client = MQTTClient("esp32-sensor", "broker.dodatech.com")
client.connect()
print("MQTT connected")

import machine
adc = machine.ADC(machine.Pin(34))
adc.atten(machine.ADC.ATTN_11DB)

while True:
    light_level = adc.read()
    client.publish(b"sensors/light", str(light_level).encode())
    print(f"Published: {light_level}")
    time.sleep(10)

Expected output:

Connecting...
Connected: ('192.168.1.42', '255.255.255.0', '192.168.1.1', '8.8.8.8')
MQTT connected
Published: 2048
Published: 1987
Published: 2102

Filesystem and Boot Scripts

MicroPython mounts an internal flash filesystem. Create main.py to run on boot:

# boot.py — runs first on boot
import network
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
wlan.connect("YourNetwork", "YourPassword")

# main.py — runs after boot.py
import time
import machine

led = machine.Pin(2, machine.Pin.OUT)

while True:
    led.value(not led.value())
    time.sleep(1)

Use ampy or rshell to upload files:

# Install ampy
pip install adafruit-ampy

# Upload files
ampy --port /dev/ttyUSB0 put boot.py
ampy --port /dev/ttyUSB0 put main.py

# List files on device
ampy --port /dev/ttyUSB0 ls

# Run a script without saving
ampy --port /dev/ttyUSB0 run test.py

Classes and Modules

MicroPython supports Python classes — organize your code into modules:

# sensor.py — saved on the device
import machine

class BME280:
    def __init__(self, i2c, addr=0x76):
        self.i2c = i2c
        self.addr = addr
        self._load_calibration()

    def _load_calibration(self):
        calib = self.i2c.readfrom_mem(self.addr, 0x88, 26)
        self.dig_T1 = calib[0] | (calib[1] << 8)

    def read_temperature(self) -> float:
        data = self.i2c.readfrom_mem(self.addr, 0xFA, 3)
        adc = (data[0] << 12) | (data[1] << 4) | (data[2] >> 4)
        var = ((adc >> 3) - (self.dig_T1 << 1)) * 5
        return var / 100.0

# Usage in main.py
import machine
from sensor import BME280

i2c = machine.I2C(0, scl=machine.Pin(22), sda=machine.Pin(21))
bme = BME280(i2c)
print(f"Temperature: {bme.read_temperature():.1f}°C")

Common Mistakes

  1. Memory exhaustion: MicroPython has limited RAM. Avoid allocating large lists or strings in loops. Use generators and pre-allocate buffers for network operations.

  2. WiFi reconnection not handled: WiFi disconnections are common. Always check wlan.isconnected() before publishing and reconnect if needed.

  3. Floating-point on non-FP hardware: Some microcontrollers lack hardware FPU. Use integer math and fixed-point scaling where possible.

  4. Infinite loops without yield: A tight loop blocks the scheduler (if using _thread) and prevents WiFi maintenance. Add time.sleep_ms(0) to yield control.

  5. Not deep sleeping: MicroPython doesn't natively support ESP32 deep sleep in all builds. Verify your firmware build includes deep sleep support before deploying battery devices.

Practice Questions

  1. What is the difference between MicroPython and regular Python? MicroPython is Python 3.4-compatible with limited standard library. It lacks os, subprocess, and numpy but includes machine and network modules for hardware access.

  2. How does the REPL help with debugging? The REPL allows interactive testing of hardware commands without compile-flash cycles. You can probe GPIO pins, read I2C registers, and test logic incrementally.

  3. What is the purpose of boot.py vs main.py? boot.py runs first for one-time setup (WiFi, pin config). main.py runs next as the main application loop. This separation allows fixing a broken main.py from the REPL.

  4. Why might MicroPython be unsuitable for production IoT? Memory constraints, lack of hard real-time guarantees, and higher power consumption compared to optimized C firmware limit MicroPython to prototyping and moderate-complexity applications.

  5. How do you recover a MicroPython board with a corrupted filesystem? Flash the MicroPython firmware again with esptool. This erases and rewrites the flash including the filesystem, restoring the REPL.

Mini Project

Build a MicroPython web server on ESP32:

import network
import socket
import machine
import time

wlan = network.WLAN(network.STA_IF)
wlan.active(True)
wlan.connect("YourNetwork", "YourPassword")
while not wlan.isconnected():
    time.sleep(0.5)

adc = machine.ADC(machine.Pin(34))
adc.atten(machine.ADC.ATTN_11DB)

addr = socket.getaddrinfo('0.0.0.0', 80)[0][-1]
server = socket.socket()
server.bind(addr)
server.listen(1)
print(f"Server at http://{wlan.ifconfig()[0]}/")

while True:
    conn, _ = server.accept()
    request = conn.recv(1024)
    light = adc.read()
    response = f"""HTTP/1.0 200 OK\r\nContent-Type: text/html\r\n\r\n
<!DOCTYPE html>
<html><body>
<h1>ESP32 Light Sensor</h1>
<p>Value: <strong>{light}</strong></p>
<p>Time: {time.localtime()}</p>
</body></html>"""
    conn.send(response.encode())
    conn.close()

Open a browser to the ESP32's IP address. The page refreshes with each request showing the current light sensor reading.

Cross-References

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro