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 8 – Files, storage and data formats

Files, safe writes and power cuts

Save settings and logs on a MicroPython board so that a power cut never leaves them half-written: temporary files and rename, checked records and fewer erases.

  • Intermediate
  • 20 minutes
  • Examples run with Python 3.14.8 and Pyodide 314.0.7
  • By MySmartCoPilot
Free core, Pro depth: 7 of 20 minutes free
Free: A save in place can leave a file half-written · Write a temporary file, then rename it over the old one · Check yourself. With a pass: 3 in-depth parts and 3 interview answers, about 13 minutes.

What you will learn

  • Write a configuration file so that a power cut leaves either the old or the new version
  • Recover at boot from every state that an interrupted save can leave behind
  • Detect torn records in an append-only log with a length and a CRC
  • Prove a storage format safe by replaying every point where a cut can stop a save
  • Limit flash wear by batching writes, with the erases counted

Before you start

What you’ll learn · Before you start

What you will learn

  • Write a configuration file so that a power cut leaves either the old or the new version
  • Recover at boot from every state that an interrupted save can leave behind
  • Detect torn records in an append-only log with a length and a CRC
  • Prove a storage format safe by replaying every point where a cut can stop a save
  • Limit flash wear by batching writes, with the erases counted

Before you start

On this page

A Pico saves its settings with open('config.json', 'w'), and one day the sensor it reads during the save stops answering: the file is left with the first 32 bytes of the new settings, and at the next boot json.loads() raises ValueError and the board starts from its defaults. On FAT, a power cut during the save can break the file the same way.

Safe writes come down to two patterns: write a new version of a file beside the old one and rename it into place, and give every record of a log its length and a checksum. Both can be proved by trying every point where power cuts could stop a save, and checking what is left.

A save in place can leave a file half-written

open(path, 'w') empties the file before the first byte of the new settings is written, so files saved this way pass through states that hold no valid settings. If an error stops the save before its last write, the file keeps only what was written so far. This program saves twice; during the second save, reading the sensor’s calibration fails:

A save in place, stopped halfway by an error Python · inplace/save_in_place.py
"""A configuration saved in place, and what an error halfway through the save leaves behind.

open(path, "w") empties the file at once. When the save stops before its last write,
leaving the with block closes the file, and the part already written is what remains.
"""
import json

DEFAULTS = {"ssid": "", "interval": 60, "offset": 0}


def read_offset(sensor_ok):
    """Stands in for reading the sensor's calibration over I2C."""
    if not sensor_ok:
        raise OSError(5, "EIO")  # the sensor stopped answering
    return -12


def save_in_place(path, ssid, interval, sensor_ok=True):
    with open(path, "w") as f:
        f.write('{"ssid": %s, ' % json.dumps(ssid))
        f.write('"interval": %d, ' % interval)
        f.write('"offset": %d}' % read_offset(sensor_ok))


def load(path):
    try:
        with open(path) as f:
            return json.loads(f.read())
    except OSError:
        print("  no file -> defaults")
    except ValueError:
        print("  not valid JSON -> defaults")
    return dict(DEFAULTS)


save_in_place("config.json", "lab", 10)
print("save 1:", load("config.json"))

try:
    save_in_place("config.json", "lab", 30, sensor_ok=False)
except OSError as e:
    print("save 2 stopped: OSError", e.args[0])
with open("config.json") as f:
    print("config.json now holds %r" % f.read())
print("next boot:", load("config.json"))

Output

save 1: {'ssid': 'lab', 'interval': 10, 'offset': -12}
save 2 stopped: OSError 5
config.json now holds '{"ssid": "lab", "interval": 30, '
  not valid JSON -> defaults
next boot: {'ssid': '', 'interval': 60, 'offset': 0}

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

The with block did what it should and closed the file when the exception left it, and closing is what made those 32 bytes the file. On LittleFS: A small filesystem designed for flash: it writes changes to free blocks, commits them in small checked logs and spreads erases over the blocks. The rp2 and esp32 ports of MicroPython format their flash with LittleFS v2. More in “Filesystems: LittleFS, FAT and vfs.mount”, whose documentation says a file’s changes are committed only when it is closed or synced, a power cut in the middle of the writes keeps the old version. An error inside the with block, or a flush() halfway through, commits the half-written file all the same. FAT has no such commit, so a power cut during the save can leave the file without its settings, or damage the filesystem itself, as MicroPython’s documentation warns.

Calling flush() to make a save safer

On LittleFS, f.flush() commits the file as it is at that moment, so a flush halfway through a save makes half a file permanent. Leave the commit to close(), once every byte has been written.

Checkpoint

Check: What a save in place can leave

  1. Question 1 of 2 On LittleFS, a save in place writes half of config.json inside a with block, then raises OSError. The board resets. What does config.json hold?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The half that was written, because leaving the with block closed the file, and closing commits it

    The exception left the with block, the block closed the file, and the close committed what was there: half the new settings, which json.loads() refuses at the next boot.

  2. Question 2 of 2 On LittleFS, power fails in the middle of a save in place, between two calls of f.write(), with no flush() before. What does config.json hold at the next boot?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The old settings, because the new writes were never committed

    LittleFS turns this one case into a safe one: without a commit, the old version stays. On FAT, which has no commit, the same cut can leave the file without its settings, and on either filesystem an error or a flush() commits half a file.

Write a temporary file, then rename it over the old one

The safe pattern never touches config.json until the new version is complete. Write the new settings to config.json.tmp, close it, and only then rename it over config.json with os.rename(). LittleFS’s documentation states that rename is atomic: Done in one step that cannot be seen half-done: after a power cut, an atomic rename has either happened completely or not at all., even if power is lost, so after a cut config.json is the old file or the new one. MicroPython’s FAT driver renames in two steps when the target exists: it removes config.json, then renames the temporary file, so a cut between them leaves only config.json.tmp, complete. Step through a save and the boot that follows it:

Interactive · stage by stage A save that a power cut cannot break, and the boot that finishes it Step through one save and the next boot. The arrows at the side are the ways a boot falls back.
A save that a power cut cannot break, and the boot that finishes it, step 8 of 8. Showing Write config.json.tmp, Close it, Rename over config.json, Boot: load config.json, Use it; delete the .tmp, Else load the .tmp, Else use the defaults. In focus: tmp-bad, Else use the defaults.a cut keeps the old filemissing or not JSONmissing or not JSONWrite config.json.tmpthe new settings, in fullClose itLittleFS commits the file hereRename over config.jsonone atomic step on LittleFSBoot: load config.jsonread it and json.loads()Use it; delete the .tmpa leftover is a cut saveElse load the .tmpwhole if the rename was cutElse use the defaultsand log that both failed

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

Every step as text
  1. Write beside it. save() writes the whole new settings to config.json.tmp, in one open() and one close(). config.json is not touched yet, so it still holds the old settings.
  2. A cut while writing. If power fails now, config.json is whole and old. The temporary file may be empty or half-written, and the next boot deletes it.
  3. Close the new file. Closing commits config.json.tmp on LittleFS. From here the new settings exist in full, in a file of their own.
  4. Switch in one step. os.rename() puts the new file in place of the old. LittleFS does it atomically, even if power is lost, so config.json is the old file or the new one. MicroPython's FAT driver first removes config.json, then renames.
  5. Boot. At the next boot, load() first reads config.json. If it is there and is valid JSON, it is the old version or the new one, never a mix.
  6. A leftover .tmp. A config.json.tmp beside a good config.json belongs to a save that never reached its rename. load() deletes it and keeps the version that is there.
  7. The rename was cut. On FAT, a cut between the removal and the rename leaves no config.json, but a whole config.json.tmp. load() reads it and renames it into place, finishing the save.
  8. Nothing to load. With neither file valid, as on the very first boot, load() returns the defaults, and the app should log it, because on a board in service it means something else went wrong.

The program below is that pattern, with a replay of one save on a model of a filesystem without a commit, which keeps every byte as soon as it is written. It stops the save at every point a cut could, then runs the boot code on what is left:

The safe save, cut at every point and booted Python · store/config_store.py
"""A configuration saved safely: write config.tmp, close it, rename it over config.json.

save() and load() are what a board runs. The replay models a filesystem without a commit,
which applies each operation in order and keeps every byte as soon as it is written: it stops
one save at every point a power cut could, then boots, that is, runs load() on what is left.
"""
import json
import os

DEFAULTS = {"ssid": "", "interval": 60}


def save(cfg, path="config.json"):
    tmp = path + ".tmp"
    with open(tmp, "w") as f:
        f.write(json.dumps(cfg))
    os.rename(tmp, path)  # one atomic step on LittleFS


def read(name):
    try:
        with open(name) as f:
            return json.loads(f.read())
    except (OSError, ValueError):
        return None


def remove(name):
    try:
        os.remove(name)
    except OSError:
        pass


def load(path="config.json"):
    tmp = path + ".tmp"
    cfg = read(path)
    if cfg is not None:
        remove(tmp)  # a save that did not finish: keep the old version
        return cfg
    cfg = read(tmp)
    if cfg is not None:
        os.rename(tmp, path)  # the save stopped after its write: finish it
        return cfg
    remove(tmp)
    return dict(DEFAULTS)


# ---- the replay ------------------------------------------------------------------
OLD = {"ssid": "lab", "interval": 10}
NEW = {"ssid": "lab", "interval": 30}
J, T = "config.json", "config.json.tmp"
data = json.dumps(NEW)

WAYS = {
    "in place": [("open_w", J), ("write", J)],
    "tmp, then rename": [("open_w", T), ("write", T), ("rename",)],
    "tmp, rename on FAT": [("open_w", T), ("write", T), ("remove", J), ("rename",)],
}


def states(steps):
    """Every state a cut can leave: after each step, and after each byte of a write."""
    fs = {J: json.dumps(OLD)}
    yield dict(fs)
    for step in steps:
        if step[0] == "write":
            for k in range(len(data)):
                yield dict(fs, **{step[1]: data[:k]})
            fs[step[1]] = data
        elif step[0] == "open_w":
            fs[step[1]] = ""
        elif step[0] == "remove":
            del fs[step[1]]
        else:  # rename T over J
            fs[J] = fs.pop(T)
        yield dict(fs)


print("one save cut at every point, then load() at boot")
print("%-20s %6s %4s %4s %7s" % ("", "cuts", "old", "new", "broken"))
for way, steps in WAYS.items():
    count = {"old": 0, "new": 0, "broken": 0}
    for fs in states(steps):
        for name in (J, T):
            remove(name)
        for name, text in fs.items():
            with open(name, "w") as f:
                f.write(text)
        cfg = load()
        count["old" if cfg == OLD else "new" if cfg == NEW else "broken"] += 1
    print("%-20s %6d %4d %4d %7d" % (way, sum(count.values()), count["old"], count["new"], count["broken"]))

Output

one save cut at every point, then load() at boot
                       cuts  old  new  broken
in place                 34    1    1      32
tmp, then rename         35   34    1       0
tmp, rename on FAT       36   34    2       0

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

Every state the pattern can leave, 35 with LittleFS’s rename and 36 with FAT’s, loads the old settings or the new ones, while the save in place breaks 32 of its 34. The new settings win once the rename has run, and on FAT also when only the temporary file is left, which the boot code renames into place.

Key idea

Never write over the only good copy: write the new version beside it, complete it, switch to it in one atomic step, and let the boot code finish or discard a switch that was cut.

Other files need more than this pattern. A log only grows, so each record carries its length and a CRC-32, and the reader stops at a torn record: A record that a power cut or a reset stopped partway through its write, so that only its first bytes reached the storage. A reader recognises it by a length that runs past the end of the data or a checksum that does not match.. A setting kept in raw flash, without any filesystem, needs two blocks that take turns, so that one always holds a whole copy. Each erase wears a flash block a little, so settings saved every second wear the flash out. Batching the writes in RAM cuts the erases. A replay of every cut point, like the one above, proves such designs safe.

Checkpoint

Check: The temporary file and the rename

  1. Question 1 of 2 On a FAT filesystem, power fails after MicroPython's rename removed config.json and before it renamed config.json.tmp. What does the lesson's load() do at the next boot?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: It finds no config.json, loads the whole config.json.tmp and renames it into place

    MicroPython's FAT driver renames over an existing file in two steps, removal and rename. The second boot rule, load the temporary file when config.json is missing or broken, closes that window.

  2. Question 2 of 2 At boot, config.json loads and a config.json.tmp lies beside it. What should load() do with the temporary file?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Delete it, because it belongs to a save that never reached its rename

    A valid config.json wins. The temporary file is from a save that was cut before its rename, so the board carries on with the version that was in force, and the leftover goes.

Check yourself

The questions below cover saves in place and the temporary-file pattern, and explain every choice.

Practice that adapts to you

Practise this lesson

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

  1. Question 1 of 6 What does open('config.json', 'w') do to an existing config.json?

    Choose one answer.

    Show the answer: Question 1 of 6

    Answer: It empties it, before anything new is written

    The file is empty from the moment it is opened with w. Every save in place therefore passes through states in which the settings are missing or incomplete.

  2. Question 2 of 6 A save writes config.json in four parts and calls f.flush() after each, to be safe. On LittleFS, power fails after the second flush. What does the next boot find?

    Choose one answer.

    Show the answer: Question 2 of 6

    Answer: The first two parts, which the second flush committed as the file

    A flush during a save turns a cut that LittleFS would have survived into half a file. Write everything, then close once.

  3. Question 3 of 6 In the lesson's replay, how many of the 34 states that a cut can leave during a save in place loaded neither the old settings nor the new ones?

    Type a number.

    Show the answer: Question 3 of 6

    Answer: 32 states

    Only the state before the save, the old file, and the state after it, the new file, loaded. Every other state held an empty or partly written config.json.

  4. Question 4 of 6 Put the steps of a safe save in order.

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

    Show the answer: Question 4 of 6

    Answer:

    1. Write the new settings to config.json.tmp
    2. Close config.json.tmp
    3. Rename config.json.tmp over config.json

    The new file must be complete and committed, written and closed, before the rename switches to it in one step.

  5. Question 5 of 6 At boot, config.json is missing and config.json.tmp holds valid JSON. What does the lesson's load() do?

    Choose one answer.

    Show the answer: Question 5 of 6

    Answer: It renames config.json.tmp to config.json and returns its settings

    Finishing the save is safe because the temporary file was closed before the rename started. This case arises on FAT, whose rename removes the old file first, and on any board's very first save.

  6. Question 6 of 6 A teammate's save runs os.remove("config.json"), then writes a new config.json. Which state can a cut leave that the lesson's pattern never does?

    Choose one answer.

    Show the answer: Question 6 of 6

    Answer: No valid settings on the flash at all

    The pattern's rule is to make the new version complete before the old one goes. Removing first breaks it, and so does FAT's own rename, which is why the boot rules finish a rename that was cut.

Logs that stop cleanly at a torn record Pro: opens with a Pro, Premium or Ultimate pass

A log is appended to, never replaced, so the rename pattern does not fit it. Each record below carries its length and a CRC-32, which lets a reader tell a whole record from one a power cut tore, and lets the boot repair the log before it appends again.

In this part:

  • A record format with a length and a CRC-32, cut at every byte
  • Repair before the next append, or new records are lost

1 diagram · 1 runnable example (Python) · about 5 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Proving a save safe by replaying every cut Pro: opens with a Pro, Premium or Ultimate pass

A save is safe when every state a power cut can leave behind loads the old version or the new one. This part builds that check for a setting kept in raw flash, without any filesystem, and uses it to prove an A/B design that never loses a setting.

In this part:

  • Where a cut can stop a save in raw flash
  • The invariant that makes the A/B store safe

1 diagram · 1 runnable example (Python) · about 5 minutes

Opening this part with your pass…Pro part. It opens with the same pass as the part above.

Flash wear, counted: batching writes Pro: opens with a Pro, Premium or Ultimate pass

Every erase wears a block a little, and a board that saves every second erases blocks tens of thousands of times a day. This part counts the erases of four designs, turns them into days of life, and ends with an exercise that builds the two-block log of the count.

In this part:

  • Erases per day, four designs counted
  • Batching, and what it costs
  • Exercise: a two-block log that passes the replay
  • A two-block log in flash that survives every power cut
    • Results of the sample tests

1 runnable example (Python) · 1 exercise with sample tests · about 6 minutes

Opening this part with your pass…Pro part. It opens with the same pass as the part above.

Interview questions

  1. Fresher · Warm-up

    Why does a board write new settings to a temporary file and rename it, instead of rewriting config.json in place?

    Opening config.json with 'w' empties it at once, so until the last byte is written the file holds no valid settings. An error during the save, a flush() halfway through or, on FAT, a power cut leaves it incomplete, and the next boot falls back to its defaults. Writing the new version to config.json.tmp leaves the old file untouched until the new one is complete and closed; os.rename() then switches to it, which LittleFS does atomically, even through a power loss. At boot a valid config.json is used and a leftover temporary file deleted; if config.json is missing or broken, a valid temporary file is renamed into place, which also covers FAT, where MicroPython's rename removes the old file first. Every state a cut can leave then loads the old settings or the new ones.

  2. Mid to senior

    After a site's power outage, 3 of 200 Pico-based devices came back with factory settings: their config.json was empty or half-written. The save uses with open('config.json', 'w') and json.dump(). How could that happen on LittleFS, and what do you change in the code and in the tests?

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

  3. Mid to senior

    A Pico logs a 32-byte reading every second by appending it to a file on LittleFS and closing the file each time. Will the flash last the planned five years? How do you estimate it, and what would you change?

    Model answer, Pro. First step and follow-ups

  4. Fresher to mid

    After a power cut, a device's append-only log ends in half a record. Since then it has logged a reading every minute, but the export, which parses the log from the start, gets nothing new from it. What happened, and how do the writer and the reader fix it?

    Model answer, Pro. First step and follow-ups

Flashcards · 7 cards

Recall it, then check

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

  1. Why can open('config.json', 'w') leave a board without valid settings?

    It empties the file before the first byte is written, so an error halfway through the save, or a power cut on FAT, leaves only what was written so far.

  2. When does LittleFS commit a file's changes?

    When the file is closed or synced. A power cut halfway keeps the old version, but a close() after an error, or a flush() halfway, commits the half-written file.

  3. What is the safe way to save a settings file?

    Write config.json.tmp, close it, then os.rename() it over config.json. LittleFS renames in one atomic step, even through a power cut.

  4. How does MicroPython's FAT driver rename over an existing file, and what must the boot code do?

    It removes config.json, then renames the temporary file, so a cut between the two leaves only config.json.tmp, complete. The boot code renames a valid .tmp into place.

  5. What does the boot code do with config.json and a leftover config.json.tmp?

    A valid config.json wins and the .tmp is deleted; a missing or broken config.json is replaced by a valid .tmp; with neither, the board uses its defaults.

  6. Why not call flush() in the middle of a save on LittleFS?

    flush() commits the file as it is at that moment, so half a file becomes permanent. Leave the commit to close(), once every byte has been written.

  7. What does each record of an append-only log on flash carry?

    Its length and a CRC-32, so that a reader recognises a torn record left by a cut and stops there.

Key takeaways

  • open(path, 'w') empties a file at once, so a save stopped by an error, or by a power cut on FAT, leaves it without valid settings, and on LittleFS closing or flushing commits whatever is there.
  • Safe writes put the new version in config.json.tmp, close it and rename it over config.json, which LittleFS does in one atomic step, even through power cuts.
  • At boot, a valid config.json wins and a leftover .tmp is deleted; a missing or broken config.json is replaced by a valid .tmp, which covers FAT’s two-step rename; with neither, the board uses its defaults.

Terms in this lesson

atomic
Done in one step that cannot be seen half-done: after a power cut, an atomic rename has either happened completely or not at all.
torn record
A record that a power cut or a reset stopped partway through its write, so that only its first bytes reached the storage. A reader recognises it by a length that runs past the end of the data or a checksum that does not match.
wear levelling
Spreading the erases of a flash device over many blocks, so that no block reaches its rated number of erases long before the others. LittleFS does it for the blocks it allocates.

References

Related tools

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.