Copying an example out takes nothing. Understanding it is what then lets you modify it, fix it and reuse it. And a program is not read like a piece of writing, from the first line to the last: you enter it through landmarks, in an order that is not the order it was written in. This page gives a method for entering an unfamiliar MicroPython program, the language mechanisms that come back from one example to the next, and a worked reading of a program from this wiki, taken exactly as it stands. It closes the MicroPython path: the syntax is assumed to be in place, and what you learn here is how to use it to read.

What is it for?

On a project, most of the code you handle was not written by you: an example found online, a classmate’s script, last year’s project picked up again. Three situations come up constantly, and all three call for reading before writing:

  • “This example does almost what I want, so what do I change?”;
  • “My partner wrote this part and I have to plug mine into it”;
  • “This code worked last week and somebody has changed it.”

Reading is not debugging. Here, no fault is assumed: the program is taken to work, and the aim is to know what it does. When a behaviour is wrong and you are looking for why, that is the other trade (see debugging in MicroPython). The two follow on from each other, since there is no useful debugging of code you have not read.

Where to enter an unfamiliar program

Four stages, in this order. The first reflex to unlearn is starting at the top and working down.

1. Find the boundaries

A MicroPython program has no setup() and no loop(): the file runs from top to bottom, once, and it is the closing while True: that plays the part of the endless loop. The frame is therefore the same three zones, but it is not labelled. It is up to you to see it: the import lines and the object creations at the top stand in for initialisation, and everything indented under while True: is the repeated behaviour.

Look for the while True: first, before reading a single line. Its position cuts the file in two: what sits above it runs once, what sits inside it runs endlessly. Some programs do not have one, for instance a script that writes a file and stops, and that is already a first-order piece of information.

2. Read the header: wired, tuned, remembered

The header usually runs to about fifteen lines and says three distinct things. Learning to tell them apart at a glance saves most of the reading time.

  • The import lines — what the program depends on. machine announces access to the hardware, time some timing, json a save to file, network a Wi-Fi link. The very first line of the file already announces its domain (see [[micropython-modules-en|modules and import]]);
  • the names in capitals — the settings. MicroPython has no constant keyword, so nothing technically stops you changing them, and the capital is a pure convention. It is followed everywhere and reads like a promise: pin numbers, durations, thresholds. This is where you act first when adapting an example;
  • the objects and variables in lower case — the hardware that is wired up (led = Pin(15, Pin.OUT)) and what the program remembers from one pass to the next. The latter are few in number and carry the whole behaviour.

3. Take stock of the states

Take the two or three lower-case variables that are not hardware objects and, for each one, look for every place it is assigned. The search function in Thonny is enough. A typical embedded program has only three to five state variables, and knowing where each one changes is having understood the logic: the rest is only wiring around it.

In MicroPython this stage comes with a bonus: assignments inside a function count only if the function declares global (see below). A state variable written from a function without that declaration does not really move, and spotting the gap is often what explains a mysterious behaviour.

4. Play one pass, just once

Finally, walk through one complete pass of the loop while telling yourself a concrete scenario: “the button is not pressed, 300 ms have gone by since start-up. What is each variable worth, which branch is taken?“. A single pass, played seriously, teaches more than three skim readings.

Tip

The single-sentence test. You have read a program when you can answer two questions without reopening the file: what does this program do, in one sentence? and what changes between two passes of the loop? As long as one of the two resists, the reading is not finished. Conversely, understanding every line is not the aim: you can perfectly well read a program without knowing what one particular module call does. And MicroPython offers a luxury that C++ does not: the REPL lets you test a single line in the meantime.

The MicroPython mechanisms you meet everywhere

The points below are not syntax to learn, since that is already on the path, but reading clues: what the presence of a keyword or of a written form tells you about the program, before you understand any of its detail.

Indentation is the structure

In MicroPython the left indent defines the blocks: there are no braces, and the indentation is not decorative. The consequence for reading is direct and very practical: a block ends where the text comes back to the left, which is visible without reading anything.

if valeur > SEUIL:
    led.on()
    compteur += 1        # inside the if (indented)
print(compteur)          # outside the if (back to the left)

Scanning down the left margin therefore gives you the structure of the program in a few seconds, before any look at the content. It is exactly the opposite of C++, where only the braces count and where the indentation can lie.

Watch out for a less obvious consequence: a deeply indented block signals a stack of conditions, and three levels of indent inside a loop are often a sign that there is some logic to untangle.

The type is not written down: it comes from the assignment

No variable is declared with its type. To know what valeur holds, there is only one way: go back to the place where it is assigned. That is the cost of dynamic typing when reading, and it makes the stocktaking of stage 3 all the more worthwhile.

Two landmarks help: read_u16() always returns an integer from 0 to 65535, ticks_ms() an integer number of milliseconds, and any division with / produces a float, even between two integers. This last point explains a good many unexpected displays (see variables and types).

global: the function writes a shared state

This is the most rewarding reading mechanism in MicroPython, and the one with no direct equivalent in C++. By default, assigning a variable inside a function creates a new, local one, even if an identical name exists above. For a function to really modify the state of the program, it has to announce so:

def tache_led():
    global t_led, etat_led      # without this line, the two assignments
    if ticks_diff(ticks_ms(), t_led) >= 500:   # below would stay local
        t_led = ticks_ms(); etat_led ^= 1
        led.value(etat_led)

Reading reflex: the global line of a function is the exact list of what that function modifies outside itself. It is a statement of intent, free to read, and it tells you before any look at the body which state variables the function touches. A function with no global can only read and compute: it changes nothing.

A compound condition is spelled out in words

The logical operators are written and, or and not, rather than &&, ||, !. As in C++, and binds more tightly than or, so A or B and C reads as “A, or else B together with C”.

if (ticks_diff(ticks_ms(), t_debut) >= DUREE_VERT or
        (demande_pieton and ticks_diff(ticks_ms(), t_debut) >= DUREE_VERT_MIN)):

One point of form specific to the language: the opening bracket lets you break the line in the middle of a condition, and what follows is then indented further so that it does not look like the body of the block. An or at the top level always opens two ways out. Spotting them is understanding the logic of the branch.

with closes by itself

The with block takes charge of releasing the resource: on leaving the block the file is closed again, including when an error occurs part-way through.

with open("mesures.csv", "w") as f:
    f.write(ligne)
# here, the file is already closed again

Reading reflex: the absence of a close() after a with is not an oversight, it is the normal form. Looking for a manual close would be a mistake imported from C++, where closing is precisely the programmer’s job.

try / except: a planned branch, not an accident

A try / except block does not signal fragile code: it describes an anticipated case. The useful reading consists in asking which normal situation of the project produces this exception.

try:
    with open(FICHIER) as f:
        data = json.load(f)
except OSError:
    data = {"demarrages": 0}

Here, OSError is not a fault: it is the first start-up, when the file does not exist yet. The except branch therefore carries the starting value, and reads as a conditional initialisation. The type of exception caught is the main clue — OSError for a missing file or device, ValueError for an impossible conversion.

A worked reading: the three tasks

Here is the method applied end to end to the program from non-blocking programming, taken exactly as it stands. Follow the four stages without looking at the wiki’s own commentary.

The boundaries. The while True: is right at the end of the file and holds nothing but three function calls, without a single line of logic. All of the behaviour is therefore in the three functions above it, and the loop does nothing but chain them endlessly.

The header. Two import lines: machine for Pin and ADC (so hardware: one output, one analogue input, one input), time for ticks_ms and ticks_diff (so timing without blocking). Three hardware objects in lower case — led, capteur, bouton —, then four state variables: t_led, etat_led, t_cap, dernier_btn. Not one capital: the durations of this program are written in place inside the functions, which is precisely the flaw you would fix first if you were adapting it.

The stocktaking of states. This is where the global lines do all the work. tache_led declares global t_led, etat_led, so it touches the date of the last toggle and the state of the LED, and nothing else. tache_capteur declares global t_cap, a single variable, the date of the last reading. tache_bouton declares global dernier_btn, the previous reading of the button. Three functions, three disjoint scopes: none of them can interfere with another one’s variables, and that reads in three lines without examining a single function body.

One pass. Scenario: 120 ms after start-up, the button is not pressed. tache_led() compares ticks_diff(ticks_ms(), t_led) with 500, a value that 120 does not reach, so the LED does not move. tache_capteur() compares the same gap with 100, which 120 does exceed, so the reading is printed and t_cap re-dated. tache_bouton() reads 1 (at rest, PULL_UP holds the pin at 1) and compares it with dernier_btn, which is also 1: no falling edge, nothing happens. The pass ends, and the next one starts again straight away.

The program then sums up in one sentence: three independent activities — blinking, measuring, watching a button — that share the same loop without any of them waiting for the others. What the pass has shown is the central mechanism: each task compares a date with an interval and does nothing most of the time, which leaves the loop free to keep turning.

Getting help without being replaced

Having a program explained to you by a conversational assistant is a legitimate use, on one condition: that what it states is checkable on screen. The criterion is the same as for simulation and does not depend on which way the use runs.

“The function tache_led modifies etat_led” is a checkable statement, since its global line confirms or denies it in three seconds. “This except catches the first start-up case” is checked by deleting the file and running again. Statements of that kind save time without taking anything away from the work: they are hypotheses, you keep the job of validating them, and the REPL is precisely the tool that makes the check immediate.

“This code is correct” is not of that kind. It is the conclusion: the only deliverable of the reading, and the only one that cannot be delegated. An assistant can show you where to look. Deciding that you have understood remains your part.

Common pitfalls

Reading from top to bottom. A program is not a piece of writing: start with the while True:, then go back up to the header to shed light on what you saw there.

Trying to understand everything. An obscure module call does not stop you reading the program. Note it, carry on, come back to it if the question comes up again, and test it at the REPL if it persists.

Forgetting to read the global lines. It is the cheapest and densest information in the file. Skipping it means reading each function in full to find out the same thing.

Looking for a close() after a with. Closing is automatic. A reflex imported from C++, with no object here.

Taking an except for a patch. The branch describes a foreseen case of the project, not badly written code. Asking which normal situation triggers it is the right reading.

Exercises

Exercise 1 — What global announces

Without reading the bodies of the functions, say what each of the three functions in the program above is able to modify. Then say what would happen if the line global t_led, etat_led were removed from tache_led.

Exercise 2 — Three clues, three conclusions

Without looking for the complete program, say what each of these three lines tells you, on its own, about the code it was taken from.

from machine import Pin, I2C
except OSError:
INTERVALLE_MS = 250

Project hook

  • Stage 4 of the proof-of-concept phase — most of the software building blocks of a PoC start from an existing example that gets adapted. The quality of the initial reading decides how much time is lost afterwards.
  • Code review — reading a classmate’s code first calls for being able to read at all. It is the direct prerequisite of the collective routine.

See also