Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

MicroPython for Microcontrollers  Module 1 – Getting started: boards, firmware, the REPL and mpremote

boot.py, main.py and a layout that survives resets

The MicroPython boot sequence from _boot.py to the REPL, a thin main.py with the app in /lib, what soft and hard resets keep, and a main.py that recovers.

  • Beginner
  • 15 minutes
  • Examples run with Python 3.14.8 and Pyodide 314.0.7
  • By MySmartCoPilot
Free lesson, Pro answers: 15 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Describe the boot sequence, _boot.py, boot.py, main.py and the REPL, and when main.py is skipped
  • Organise a project with a thin main.py and its modules in /lib, and predict which file an import finds
  • Distinguish what a soft reset and a hard reset keep, and which calls cause each
  • Write a main.py that recovers from a crashing app without locking you out of the board

Before you start

What you’ll learn · Before you start

What you will learn

  • Describe the boot sequence, _boot.py, boot.py, main.py and the REPL, and when main.py is skipped
  • Organise a project with a thin main.py and its modules in /lib, and predict which file an import finds
  • Distinguish what a soft reset and a hard reset keep, and which calls cause each
  • Write a main.py that recovers from a crashing app without locking you out of the board

Before you start

On this page

Save a main.py that crashes on its first line and then resets the board, and the board starts again, crashes and resets, over and over, dropping off your computer’s USB each time. Every MicroPython board starts the same way after a reset, and two files you control, boot.py and main.py, decide whether that start leads to a working app, a prompt or a loop.

This lesson follows the start of a board step by step, lays out a project so that a reset never runs stale code, compares what soft and hard resets keep, and ends with a main.py that cannot lock you out, whatever the app does.

Every reset runs the same start: boot.py, main.py, then the REPL

Copy a main.py that prints hello to a Pico, open its REPL and press Ctrl-D: hello appears, and only then the prompt. Whatever starts the board, a hard reset: A complete reset of the microcontroller, as at power-on: all hardware state is cleared, except that the RTC may keep its time. machine.reset(), the reset button and a watchdog cause one. such as power-on or a soft reset: A restart of the MicroPython interpreter without resetting the chip: Python objects, imports and most peripheral set-up are cleared, then the boot files run again. Ctrl-D at an empty prompt does one. such as Ctrl-D, the firmware runs the same sequence. Step through it:

Interactive · stage by stage What a board runs after every reset Step through the start of a Pico. The arrows at the side are the two ways the firmware skips main.py.
What a board runs after every reset, step 7 of 7. Showing A hard or soft reset, _boot.py, boot.py, USB starts, main.py, The REPL. In focus: The REPL.an error skips main.pyraw-REPL reset skips itA hard or soft resetpower, Ctrl-D, sys.exit()_boot.pyfrozen into the firmwareboot.pyone-off set-up; must endUSB startsafter boot.py on the rp2 portmain.pyyour app; may loop foreverThe REPLwith the globals of both

The drawing shows the whole scene; its 7 steps are listed below.

Every step as text
  1. The reset. Power-on, the reset button, machine.reset(), a watchdog or waking from deep sleep cause a hard reset; Ctrl-D, machine.soft_reset() and sys.exit() a soft one. Both lead into the same sequence.
  2. _boot.py. First the firmware runs _boot.py, a script frozen into it. On the rp2 port it mounts the flash filesystem at /, and formats it first on a new board.
  3. boot.py. Then boot.py, if the filesystem has one, for one-off set-up such as the network. It must end, because the start goes on only when it does.
  4. USB starts late. On the rp2 port, as on every port with machine.USBDevice, USB starts only now, after boot.py, so what boot.py prints may not reach your terminal.
  5. Skipped main.py. If boot.py raised an exception, the firmware skips main.py and starts the REPL. A soft reset from the raw REPL skips it too, which is how mpremote reaches a board whose main.py never returns.
  6. main.py. Otherwise main.py runs, and every global that boot.py set is still set. It may loop forever; Ctrl-C on the serial port stops it with KeyboardInterrupt.
  7. The REPL. When main.py ends, or crashes with an exception it does not catch, the REPL starts, with every global of boot.py and main.py still set.

boot.py is for set-up that must happen before anything else and then finish, such as configuring hardware that starts with a different setting. The documentation adds a warning for some ports, the rp2 port among them: USB starts only after boot.py exits, so what it prints may not reach your terminal. Many projects need no boot.py at all and put the set-up at the top of main.py. main.py is the app. It may loop forever, and both files share one set of globals, which is easy to forget when a name in main.py seems to come from nowhere.

The next program models three starts of a board by the rules of the rp2 port’s main loop: after a normal reset, after a boot.py that raises, and after a soft reset from the raw REPL: The REPL's mode for programs, entered with Ctrl-A: it echoes nothing, runs the code it received when Ctrl-D arrives, and frames every reply with OK and two 0x04 bytes. More in “Install firmware and use the REPL”, the way mpremote resets a board:

Three starts of a board, by the rules of its main loop Python · boot/boot_sim.py
"""The start of a board, three times, by the rules of the rp2 port's main loop (ports/rp2/main.c).

After the frozen _boot.py the firmware runs boot.py, then main.py, all in one set of globals. main.py runs only when
boot.py raised no error and the board is in the friendly REPL; a soft reset from the raw REPL skips it. Here the two
files are strings and exec() stands in for the firmware's runner.
"""

FILES = {
    "boot.py": "wifi_ssid = 'lab-net'\nprint('boot.py ran')\n",
    "main.py": "print('main.py ran; wifi_ssid is', wifi_ssid)\n",
}


def run_file(name, files, scope):
    """Run one file in the board's globals; return True if it raised no exception (a missing file counts as one)."""
    if name not in files:
        return True
    try:
        exec(files[name], scope)
        return True
    except Exception as e:
        print(f"  {name} raised {e!r}")
        return False


def start(files, raw_repl=False):
    scope = {"__name__": "__main__"}
    boot_ok = run_file("boot.py", files, scope)
    if boot_ok and not raw_repl:
        run_file("main.py", files, scope)
    else:
        print("  main.py skipped")
    shared = sorted(k for k in scope if not k.startswith("__"))
    print(f"  REPL starts, with the globals {shared}")


print("1. A reset:")
start(FILES)
print("2. A reset after a broken boot.py:")
start({**FILES, "boot.py": "raise OSError('SD card not found')\n"})
print("3. A soft reset from the raw REPL, as mpremote does:")
start(FILES, raw_repl=True)

Output

1. A reset:
boot.py ran
main.py ran; wifi_ssid is lab-net
  REPL starts, with the globals ['wifi_ssid']
2. A reset after a broken boot.py:
  boot.py raised OSError('SD card not found')
  main.py skipped
  REPL starts, with the globals []
3. A soft reset from the raw REPL, as mpremote does:
boot.py ran
  main.py skipped
  REPL starts, with the globals ['wifi_ssid']

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 boot_sim.py

The second start is the trap: an error in boot.py costs you main.py, without any message of its own beyond the traceback. The third explains a puzzle from the previous lesson: mpremote soft-reset resets in raw mode, so your new main.py does not start. To watch it start, reset with mpremote reset, or press Ctrl-D in mpremote repl, which soft-resets from the friendly REPL.

A boot.py that never ends

A while True: loop in boot.py keeps the start from ever reaching main.py, and on the rp2 port USB never starts either, so the board looks dead. Put loops in main.py, where Ctrl-C can stop them, and keep boot.py short enough to finish.

Checkpoint

Check: The boot sequence

  1. Question 1 of 2 In which of these starts does the firmware skip main.py? Choose every one.

    Choose every answer that is right.

    Show the answer: Question 1 of 2

    Answer:

    • mpremote soft-reset the board, which it does from the raw REPL
    • boot.py raised an exception

    The rp2 and esp32 main loops run main.py only when boot.py succeeded and the board is in the friendly REPL. The boot_sim.py program showed both skips.

  2. Question 2 of 2 boot.py sets wifi_ssid = 'lab-net', and main.py prints wifi_ssid without defining it. What happens at the next start?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: main.py prints lab-net

    Both files run in one global context, and the REPL that follows keeps it too. That is convenient, and also the reason a name in main.py can seem to come from nowhere.

A thin main.py, with the app’s modules in /lib

The reset and boot page suggests a main.py that only imports the app and starts it, import my_app and my_app.main(). The app itself lives in modules, and those belong in /lib. On the Pico, sys.path is ['', '.frozen', '/lib'], from MicroPython’s runtime and the rp2 port’s main loop: the current folder first, which is / after a normal start, then the frozen module: A module compiled into the firmware image when the firmware is built. On most ports its bytecode runs straight from flash, so it takes no RAM for the code. More in “What MicroPython is and where it runs” inside the firmware, then /lib. In each folder name.py is tried before name.mpy, and the first match wins. The next program applies those rules to a board with a tidy /lib and one stray file at the root:

Which file each import finds on a Pico Python · imports/which_module.py
"""Which file an import finds on a Pico, by the rules MicroPython documents.

sys.path on the rp2 port is ['', '.frozen', '/lib']: the current folder (/ after a normal boot), then the modules
frozen into the firmware, then /lib. In each folder name.py is tried before name.mpy, and the first match wins.
"""

SYS_PATH = ["", ".frozen", "/lib"]
# Frozen into the RPI_PICO firmware by its manifest (ports/rp2/boards/manifest.py).
FROZEN = {"asyncio", "dht", "ds18x20", "neopixel", "onewire"}
# The board's files in this example: a tidy /lib, plus a stray old sensor.py left at the root.
BOARD = {
    "/main.py", "/sensor.py",
    "/lib/app.py", "/lib/sensor.py", "/lib/neopixel.py",
    "/lib/config.py", "/lib/config.mpy", "/lib/units.mpy",
}


def find(name, cwd="/"):
    """The file that `import name` loads, or None for an ImportError."""
    for entry in SYS_PATH:
        if entry == ".frozen":
            if name in FROZEN:
                return f"{name}, frozen into the firmware"
            continue
        folder = cwd if entry == "" else entry
        for ext in (".py", ".mpy"):
            path = folder.rstrip("/") + "/" + name + ext
            if path in BOARD:
                return path
    return None


for name in ["app", "sensor", "neopixel", "config", "units", "logging"]:
    print(f"import {name:<9} -> {find(name) or 'ImportError: no module named ' + repr(name)}")

Output

import app       -> /lib/app.py
import sensor    -> /sensor.py
import neopixel  -> neopixel, frozen into the firmware
import config    -> /lib/config.py
import units     -> /lib/units.mpy
import logging   -> ImportError: no module named 'logging'

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 which_module.py

Two results surprise. import sensor finds the old /sensor.py at the root, because the current folder comes before /lib, so the new /lib/sensor.py is never used. And import neopixel never reaches /lib/neopixel.py, because the Pico firmware freezes a neopixel module of its own, and .frozen comes before /lib.

Names that nothing else uses

Give your modules names that no built-in or frozen module has: help('modules') lists both. lab_neopixel.py is found, while neopixel.py in /lib never is on a Pico.

A layout that avoids both traps puts nothing but the boot files at the root:

Path on the board What it holds
/main.py A few lines: the recovery check of the last section, then the app’s start
/boot.py Optional: set-up that must run first and finish
/lib/app.py The app, with a main() function
/lib/... Your other modules, and packages that mip installs there
/data/ Files the app writes, such as logs and settings, kept apart from code

With the root down to the two boot files and folders, nothing there can shadow /lib, and replacing the app means replacing files in /lib while main.py stays the same.

Keep main.py itself as source. The reset and boot page mentions precompiled main.mpy and boot.mpy files, but the main loops of the rp2 and esp32 ports in 1.29.0 look for boot.py and main.py by those names only, so a main.mpy on the filesystem never runs at a reset. Precompile the modules in /lib that main.py imports instead.

Key idea

Keep the root down to boot.py and main.py and every module in /lib, under a name of its own: the current folder and the frozen modules come before /lib in sys.path.

Checkpoint

Check: A thin main.py, modules in /lib

  1. Question 1 of 2 A Pico holds an old /sensor.py at the root and a new /lib/sensor.py. Which does import sensor load after a normal start?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: /sensor.py, because the current folder, /, comes first in sys.path

    sys.path on the Pico is ['', '.frozen', '/lib']. Remove the stray /sensor.py, or keep the root down to boot.py and main.py, so that /lib is the only home of your modules.

  2. Question 2 of 2 You write your own neopixel.py and copy it to /lib on a Pico. What does import neopixel load?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The neopixel module frozen into the firmware, because .frozen comes before /lib

    Give your module a name of its own, such as lab_neopixel.py. help('modules') lists the built-in and frozen modules to avoid.

Soft and hard resets keep different things

The two kinds of reset run the same sequence, but they start from different states. The reset and boot documentation lists what each leaves behind:

What After a soft reset After a hard reset
Python variables, objects and imported modules Cleared Cleared
Most peripherals set up through machine Reset, except pin modes on most ports; Pin.irq() always reset Reset
Open files and sockets Closed; the filesystem is left valid Gone with the reset
Wi-Fi and other network connections Still up, with their IP address Down; the app connects again
The USB serial REPL Continues, as if nothing happened Drops off and comes back
The RTC’s time Kept May be kept, depending on the port
What starts one Ctrl-D, machine.soft_reset(), sys.exit() Power-on, a reset button, machine.reset(), a watchdog, waking from deep sleep

machine.reset_cause() returns the cause of the last reset that the port can tell, for example machine.PWRON_RESET or machine.WDT_RESET, so an app can log why it restarted. Ports tell different things: the esp32 port returns machine.SOFT_RESET after a soft reset, while the rp2 port knows only those two causes, so after Ctrl-D a Pico still reports the hard reset that came before.

The difference between the two resets matters in tests: a board that passes after a power cycle can fail after Ctrl-D, because a pin still drives the value the last run left on it, or because a network connection the app expects to open is already open.

So the kind of reset before a test decides what the test proves. A test that only ever follows a power cycle never meets a pin that the previous run left high, or a Wi-Fi connection that is already up. Run start-up checks after both kinds of reset, and after the raw-mode soft reset that mpremote does, before you rely on them.

sys.exit() resets since 1.24

Since MicroPython 1.24, an unhandled SystemExit on a board, which sys.exit() raises, ends the script with a soft reset instead of leaving you at the REPL. On the unix and windows ports it still ends the program, as in CPython.

Checkpoint

Check: What soft and hard resets keep

  1. Question 1 of 2 Which of these are still true after a soft reset? Choose every one.

    Choose every answer that is right.

    Show the answer: Question 1 of 2

    Answer:

    • A pin set as an output is still an output, on most ports
    • The Wi-Fi connection is still up, with its IP address

    A soft reset clears Python and most peripheral set-up, but keeps network connections, pin modes on most ports, the REPL and the RTC's time.

  2. Question 2 of 2 On a Pico running 1.29.0, what does an unhandled sys.exit() in main.py do?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: It soft-resets the board

    The 1.24 release changed sys.exit() and raise SystemExit to trigger a soft reset, for consistency with the unix port, which exits completely on SystemExit.

A recovery path keeps a crashing app from locking you out

The reset and boot page shows a main.py that runs the app inside try, prints any exception with sys.print_exception() and then calls machine.reset(). That is a good start, because a board in the field should restart rather than sit at a prompt nobody reads. It has one weakness: an app that fails at every start makes the board reset forever, and a hard reset takes USB down each time, so a terminal loses the board at every reset. The next program models such a board: five starts under that main.py, and five under one that counts its restarts in a small file:

An app that fails at every start, under two main.py files Python · crash-loop/crash_loop.py
"""Five boots of a board whose app always fails at start, under two versions of main.py.

The first is the pattern of MicroPython's reset and boot page: print the error, then machine.reset(). The second
counts its restarts in a small file that survives a reset, resets at most three times and then stays at the REPL.
Here the board is simulated: Reset stands for machine.reset(), and the dict `flash` for the file in its flash.
"""


class Reset(Exception):
    """Stands for machine.reset(): this boot ends, and the next one starts from the top."""


def app_main():
    raise OSError("sensor not found on the I2C bus")


def main_reset_always(flash):
    try:
        app_main()
    except Exception as e:
        print("  Fatal error in main:", repr(e))
    raise Reset


def main_bounded(flash, limit=3):
    try:
        app_main()
        flash["restarts"] = 0
    except Exception as e:
        print("  crash:", repr(e))
        if flash["restarts"] < limit:
            flash["restarts"] += 1
            print(f"  restart {flash['restarts']} of {limit}")
            raise Reset
        print("  too many restarts: staying at the REPL")


for main in (main_reset_always, main_bounded):
    print(main.__name__)
    flash = {"restarts": 0}
    for boot in range(1, 6):
        try:
            main(flash)
            print(f"boot {boot}: main.py ended, the REPL starts")
            break
        except Reset:
            print(f"boot {boot}: reset")
    print()

Output

main_reset_always
  Fatal error in main: OSError('sensor not found on the I2C bus')
boot 1: reset
  Fatal error in main: OSError('sensor not found on the I2C bus')
boot 2: reset
  Fatal error in main: OSError('sensor not found on the I2C bus')
boot 3: reset
  Fatal error in main: OSError('sensor not found on the I2C bus')
boot 4: reset
  Fatal error in main: OSError('sensor not found on the I2C bus')
boot 5: reset

main_bounded
  crash: OSError('sensor not found on the I2C bus')
  restart 1 of 3
boot 1: reset
  crash: OSError('sensor not found on the I2C bus')
  restart 2 of 3
boot 2: reset
  crash: OSError('sensor not found on the I2C bus')
  restart 3 of 3
boot 3: reset
  crash: OSError('sensor not found on the I2C bus')
  too many restarts: staying at the REPL
boot 4: main.py ended, the REPL starts

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 crash_loop.py

The first main.py never stops resetting. The second resets three times, which gives a passing fault, such as a sensor that needed a moment, its chances, and then stays at the REPL with the error printed. Step through the decisions of that second main.py, with a recovery button checked before anything else:

Interactive · step by step The main.py that cannot lock you out Step through one boot of a board whose app fails. The dashed arrow is the next boot after a reset.
The main.py that cannot lock you out, step 7 of 7. Showing Recovery button held?, Stay at the REPL, Run app.main(), Log the crash, Restarts left? (3 of 3 used), machine.reset(). In focus: Stay at the REPL.not heldheldan exceptionyesnone leftnext bootRecovery button held?checked firstStay at the REPLfix or remove filesRun app.main()inside try and exceptLog the crashsys.print_exceptionRestarts left?3 of 3 usedmachine.reset()one restart morenone left

The drawing shows the whole scene; its 7 steps are listed below.

Every step as text
  1. The button first. At every boot, main.py first reads a recovery button. Held down, it skips the app, and the board waits at the REPL, where you can fix or remove the app's files.
  2. Run the app. Not held, main.py runs app.main() inside try and except Exception. KeyboardInterrupt is not a subclass of Exception, so Ctrl-C still stops the app and reaches the REPL.
  3. The app fails. The app raises OSError, say a sensor missing from the I2C bus. main.py catches it and prints it with sys.print_exception, and can append it to a log file for later.
  4. Count the restarts. Then it reads how many restarts it has made in a row, a count kept in a small file in flash, which survives the reset that follows.
  5. Restart, while it may. With restarts left, it adds one to the count and calls machine.reset(). A passing fault, a sensor that needed a moment, gets another chance at the next boot.
  6. The next boot. The board starts again from the top, with the button check, and the count is still in its file.
  7. Stop, when none are left. After the third restart the count is used up, so main.py returns instead of resetting. The board stays at the REPL with the error printed, instead of resetting every few seconds.

The button is your own safe mode: A start that skips the app, and on some ports boot.py and main.py as well, and goes straight to the REPL, so that a board whose code fails at start can be repaired.. Some ports offer one of their own, a start that skips boot.py and main.py, but the reset and boot page lists only port-specific instructions, and for the rp2 and esp32 ports those are factory resets that erase everything. A button you check yourself costs one pin and keeps your files.

If a board is already soft-bricked: A board that is not damaged but no longer reaches a usable prompt at start, for example because boot.py never ends or the code takes over the REPL's serial port., try the gentle ways first. mpremote stops a running main.py with Ctrl-C and resets in raw mode, which skips it, so mpremote rm :main.py works on a board whose main.py loops or hangs; in a reset loop it may take a few tries to catch the board between two resets. At a prompt, import os and os.remove('main.py') do the same. Only when no prompt can be reached does the factory reset remain: erase the whole flash, install the firmware again and copy your files back.

Catching everything in main.py

except: with nothing after it, or except BaseException, also catches KeyboardInterrupt, so Ctrl-C can no longer stop the app and you lose the easiest way back to the prompt. Catch Exception: in Python’s exception hierarchy KeyboardInterrupt is not one of its subclasses.

Checkpoint

Check: A main.py that recovers

  1. Question 1 of 2 An app fails at every start under the documentation's main.py, which prints the error and then calls machine.reset(). What does the board do?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: It resets over and over, losing its USB connection at every reset

    crash_loop.py showed five resets in five starts. Counting restarts in a file and stopping after three turns the loop into a board that waits at the REPL with the error printed.

  2. Question 2 of 2 Why should main.py catch Exception instead of using a bare except:?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: A bare except also catches KeyboardInterrupt, so Ctrl-C could no longer stop the app

    The reset and boot page's own comment says that a non-Exception error such as KeyboardInterrupt drops to the REPL. Keep that door open by catching Exception.

Practise: write the supervisor

Write the function that the second main.py calls at every start: the button check, the app inside try, the log line, and a restart count that stops after three. The sample tests use a fake board that records what it is asked to do:

Exercise · Medium · Python

A main.py supervisor that cannot lock you out

Write supervise(app, log, reset, button_pressed, state, max_restarts=3) in supervisor.py, the function a main.py calls once per boot. app is the app's main function, log(text) records a line, reset() restarts the board (on a board, machine.reset()), button_pressed() tells whether the recovery button is held, and state is a dictionary with the key "restarts" that survives a reset (on a board, a small file). Return what happened:

  1. If button_pressed() is true, do not start the app: log "safe mode: app not started" and return "safe".
  2. Run app(). If it returns, set state["restarts"] to 0 and return "ended".
  3. If it raises an Exception, log "crash: <type>: <message>", for example "crash: OSError: no sensor". Then, if state["restarts"] is below max_restarts, add 1 to it, call reset() and return "reset"; otherwise log "too many restarts: staying at the REPL" and return "stopped" without calling reset().
  4. Let KeyboardInterrupt through, so that Ctrl-C still reaches the REPL: it is not an Exception.

Starter code · supervisor.py

def supervise(app, log, reset, button_pressed, state, max_restarts=3):
    """Run app once for this boot; return "safe", "ended", "reset" or "stopped"."""
    app()
    return "ended"
The sample tests · test_supervisor.py
from supervisor import supervise


class Board:
    """Records what supervise asked the board to do."""

    def __init__(self, held=False):
        self.lines = []
        self.resets = 0
        self.held = held

    def log(self, text):
        self.lines.append(text)

    def reset(self):
        self.resets += 1

    def button(self):
        return self.held


def failing():
    raise OSError("no sensor")


def test_safe_mode():
    """starts nothing while the button is held"""
    board, ran = Board(held=True), []
    result = supervise(lambda: ran.append(1), board.log, board.reset, board.button, {"restarts": 0})
    assert result == "safe" and ran == [] and board.resets == 0
    assert board.lines == ["safe mode: app not started"]


def test_app_ends():
    """clears the restart count when the app returns"""
    board, state = Board(), {"restarts": 2}
    assert supervise(lambda: None, board.log, board.reset, board.button, state) == "ended"
    assert state["restarts"] == 0 and board.resets == 0


def test_crash_resets():
    """logs a crash and resets while restarts are left"""
    board, state = Board(), {"restarts": 0}
    assert supervise(failing, board.log, board.reset, board.button, state) == "reset"
    assert board.lines == ["crash: OSError: no sensor"]
    assert board.resets == 1 and state["restarts"] == 1


def test_stops_at_limit():
    """stays at the REPL once max_restarts is used up"""
    board, state = Board(), {"restarts": 3}
    assert supervise(failing, board.log, board.reset, board.button, state, max_restarts=3) == "stopped"
    assert board.resets == 0 and state["restarts"] == 3
    assert board.lines == ["crash: OSError: no sensor", "too many restarts: staying at the REPL"]


def test_ctrl_c_reaches_the_repl():
    """lets KeyboardInterrupt through"""
    board = Board()

    def interrupted():
        raise KeyboardInterrupt

    try:
        supervise(interrupted, board.log, board.reset, board.button, {"restarts": 0})
    except KeyboardInterrupt:
        assert board.resets == 0
        return
    raise AssertionError("KeyboardInterrupt was swallowed")
A hint

Check the button before anything else and return early. Then wrap only the call of app() in try/except Exception as e: KeyboardInterrupt is not a subclass of Exception, so it passes through by itself. The log line is "crash: " + type(e).__name__ + ": " + str(e).

The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Then try the self-check on the start sequence, the layout, the two resets and recovery. It picks the topic where your answers have been weakest, and each answer you pick is followed by its reason:

Practice that adapts to you

Practise this lesson

14 questions about 4 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 14 Put the steps of a normal start in order.

    Give each item its position, from 1 (first).

    Show the answer: Question 1 of 14

    Answer:

    1. The frozen _boot.py mounts the filesystem
    2. boot.py runs
    3. main.py runs
    4. The REPL starts

    _boot.py prepares the filesystem, boot.py does one-off set-up, main.py is the app, and the REPL starts when main.py ends or crashes.

  2. Question 2 of 14 On a Pico, boot.py prints starting, but you never see it in your terminal, while main.py's prints appear. Why?

    Choose one answer.

    Show the answer: Question 2 of 14

    Answer: On the rp2 port USB starts only after boot.py ends, so its output is printed before the terminal can receive it

    Keep boot.py quiet and short, or log to a file. Output from main.py appears, because USB is up by then.

  3. Question 3 of 14 You copied a new main.py with mpremote and ran mpremote soft-reset, yet main.py did not start. What starts it?

    Choose one answer.

    Show the answer: Question 3 of 14

    Answer: mpremote reset, or Ctrl-D at the friendly REPL of mpremote repl

    The raw-mode reset that mpremote uses is what lets it reach any board, and it is also why it never starts main.py. Reset from the friendly REPL, or hard-reset, to see the real start.

  4. Question 4 of 14 What is sys.path on a Pico after a normal start?

    Choose one answer.

    Show the answer: Question 4 of 14

    Answer: ['', '.frozen', '/lib']

    The order decides every import: a file in the current folder wins, then a frozen module, then /lib.

  5. Question 5 of 14 /lib holds both config.py and config.mpy. Which does import config load?

    Choose one answer.

    Show the answer: Question 5 of 14

    Answer: config.py, because in each folder name.py is tried before name.mpy

    After precompiling a module, remove its .py file, or the board keeps compiling the source and the .mpy is never used.

  6. Question 6 of 14 You precompile main.py to main.mpy with mpy-cross, copy main.mpy to a Pico and remove main.py. What happens at the next reset?

    Choose one answer.

    Show the answer: Question 6 of 14

    Answer: main.mpy does not run; after boot.py the board goes straight to the REPL

    The start runs boot.py and main.py by those names. Keep main.py as a few lines of source and precompile the modules in /lib that it imports, where the import rules find the .mpy files.

  7. Question 7 of 14 Why keep main.py down to a few lines that import the app from /lib and start it?

    Choose one answer.

    Show the answer: Question 7 of 14

    Answer: The app can be replaced, tested and precompiled as modules, while the start and its recovery check stay the same

    The reset and boot page suggests exactly this: a simple entry point that imports the app, which keeps the structure clear and makes it easy to switch between apps.

  8. Question 8 of 14 Which function tells your app whether this start came from power-on or from a watchdog?

    Choose one answer.

    Show the answer: Question 8 of 14

    Answer: machine.reset_cause()

    Logging machine.reset_cause() at each start tells you later whether a board restarted after a power-on or because of the watchdog. The esp32 port also reports deep sleep and soft resets; the rp2 port knows only those two causes.

  9. Question 9 of 14 Which of these cause a hard reset? Choose every one.

    Choose every answer that is right.

    Show the answer: Question 9 of 14

    Answer:

    • A watchdog that is not fed in time
    • Waking from deep sleep, on most ports
    • machine.reset()

    Hard resets come from the hardware or from machine.reset(); soft resets come from Ctrl-D, machine.soft_reset() and sys.exit().

  10. Question 10 of 14 An LED that the app switches on during its run should be off at start. After a power cycle it is; after Ctrl-D it is still on. Why?

    Choose one answer.

    Show the answer: Question 10 of 14

    Answer: A soft reset leaves pin modes and levels as they were on most ports, so the pin still drives the LED

    A power cycle resets the pin hardware, and a soft reset does not. An app that sets each pin to its starting value at start behaves the same after both.

  11. Question 11 of 14 Which MicroPython function prints a caught exception with its traceback?

    Choose one answer.

    Show the answer: Question 11 of 14

    Answer: sys.print_exception(e)

    sys.print_exception() is MicroPython's short form of CPython's traceback printing. Pass an open file as its second argument to log the traceback.

  12. Question 12 of 14 Why does the recoverable main.py keep its restart count in a small file, and not in a global variable?

    Choose one answer.

    Show the answer: Question 12 of 14

    Answer: The reset clears every Python variable, while a file in flash survives it

    crash_loop.py kept the count in a dict that stood for such a file. A real main.py reads it at start, adds one before each reset, and clears it once the app has run well.

  13. Question 13 of 14 A board's main.py hangs in a loop, and you want to delete it from your computer. Which command does it?

    Choose one answer.

    Show the answer: Question 13 of 14

    Answer: mpremote rm :main.py

    The raw-mode reset that skips main.py is exactly what makes this repair possible. At the board's own prompt, import os and os.remove('main.py') do the same.

  14. Question 14 of 14 Your ESP32 product must be repairable in the field without erasing its flash. What do you build in?

    Choose one answer.

    Show the answer: Question 14 of 14

    Answer: A recovery button that main.py reads before it starts the app, and a limit on restarts

    Your own safe mode costs one pin and a few lines of main.py, and it works the same on every port.

Interview questions

Warm-up (fresher): What belongs in boot.py, and what belongs in main.py?

boot.py runs first after every reset and is for set-up that must happen before anything else and then end: setting up a piece of hardware, or on some boards the network. It must not loop, because the start goes on only after it ends, and on ports such as rp2 even USB starts only after boot.py, so its prints may not be seen. An exception in boot.py also makes the firmware skip main.py. main.py is the app: it runs next, may loop forever, and shares its globals with boot.py. A good main.py stays short, imports the app from /lib and starts it inside a try block, and many projects need no boot.py at all.

Two longer interview questions of this module start from this lesson:

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Fresher to mid

    After an update, a board in the field restarts every few seconds and you cannot get a prompt on it. How do you recover it without losing its data, and how should its main.py have been written so that a failing app can never lock you out?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    Your hardware test passes after a power cycle but fails after Ctrl-D: a relay that must start off is on, and the app's Wi-Fi set-up, written for a fresh board, misbehaves because the board is still connected. Explain what a soft reset keeps and clears, and how you make the start behave the same after every kind of reset.

    Model answer, Pro. First step and follow-ups

Flashcards · 8 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. In what order does a MicroPython board start after any reset?

    The frozen _boot.py, then boot.py, then main.py, then the REPL. An error in boot.py, or a soft reset from the raw REPL as mpremote does it, skips main.py.

  2. Why must boot.py never loop forever?

    The start reaches main.py only after boot.py ends, and on the rp2 port USB starts only after boot.py exits, so the board looks dead. Loops belong in main.py, where Ctrl-C can stop them.

  3. What is sys.path on a Pico, and which file wins an import?

    ['', '.frozen', '/lib']: the current folder, then the frozen modules, then /lib, with name.py tried before name.mpy in each. A stray file at the root, or a frozen module, with that name wins over your file in /lib.

  4. Does a main.mpy run at reset on the rp2 and esp32 ports of 1.29.0?

    No: their main loops look for boot.py and main.py by those names only. Keep main.py as source, and precompile the modules in /lib that it imports.

  5. What does a soft reset keep that a hard reset loses?

    Network connections with their IP address, the USB serial REPL, pin modes on most ports and the RTC's time. Both kinds clear Python objects and imported modules.

  6. What does sys.exit() do on a board since MicroPython 1.24?

    An unhandled SystemExit ends the script with a soft reset instead of leaving you at the REPL. On the unix and windows ports it still ends the program, as in CPython.

  7. Why should main.py catch Exception rather than everything?

    A bare except: or except BaseException also catches KeyboardInterrupt, so Ctrl-C can no longer stop the app. KeyboardInterrupt is not a subclass of Exception.

  8. How do you remove a main.py that loops or hangs?

    mpremote rm :main.py: mpremote stops the program with Ctrl-C and soft-resets in raw mode, which skips main.py. In a reset loop it may take a few tries to catch the board between two resets.

Key takeaways

  • After every reset the firmware runs _boot.py, boot.py and main.py, then the REPL; an error in boot.py and a soft reset from the raw REPL both skip main.py.
  • On the Pico, sys.path is the current folder, then .frozen, then /lib, so a stray root file or a frozen module of the same name wins over your file in /lib.
  • A soft reset clears Python and most peripherals but keeps network connections, pin modes and the REPL; a hard reset clears almost everything, and machine.reset_cause() reports why the board last reset, as far as its port can tell.
  • A main.py that checks a recovery button, logs crashes and limits its restarts with a count in flash can never lock you out of the board.

Terms in this lesson

hard reset
A complete reset of the microcontroller, as at power-on: all hardware state is cleared, except that the RTC may keep its time. machine.reset(), the reset button and a watchdog cause one.
safe mode
A start that skips the app, and on some ports boot.py and main.py as well, and goes straight to the REPL, so that a board whose code fails at start can be repaired.
soft reset
A restart of the MicroPython interpreter without resetting the chip: Python objects, imports and most peripheral set-up are cleared, then the boot files run again. Ctrl-D at an empty prompt does one.
soft-bricked
A board that is not damaged but no longer reaches a usable prompt at start, for example because boot.py never ends or the code takes over the REPL's serial port.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.