Skip to content

How Python's rglob Silently Loses Files, and Why macOS Makes It Worse

A Python service I work on did something that shouldn't be possible. It wrote a directory full of files, turned around to upload them, and found nothing. The files were right there — ls saw them from another terminal.

The upload logged success. Zero files. No exception. Digging into why took me through three separate layers of the stack — none of which would tell me what actually happened — and a paper trail going back to 2015 that almost nobody has read.

Nico Robin standing before the massive red Road Poneglyph inside the Whale Tree of Zou, dwarfed by a wall of ancient glyphs

The record of every one of these problems has been sitting in public bug trackers and mailing lists for years — some of it for two decades. This post is the story of the bug, the archaeology of who reported what and when, and the fix we shipped while the deeper layers stay unfixed.1

If this corner of the stack is new to you, here's the short version of each layer, in the order the post walks them:

  1. Python's rglob hides errors. It walks a folder tree and returns every file it finds — and if the operating system refuses or fails anywhere along the walk, it silently skips instead of complaining. A folder it couldn't read looks exactly like a folder with nothing in it, and there is no way to find out which one you got.
  2. The operating system never promised your listing was fresh. A listing that races concurrent writes is allowed to miss the newest files — on every system — and on macOS the filesystem adds behaviour Apple has never documented in either direction.
  3. The obvious repair tool doesn't do what its name says. fsync, the call that's supposed to force pending writes all the way down to disk, only goes halfway on macOS — and it was never designed to freshen directory listings in the first place.

Each section below expands one layer, with receipts. When the jargon gets thick, look for the collapsible "in plain English" boxes — they're there so you can keep up without leaving the page. And if you're here because you have this bug right now, jump straight to the fix.

A Program That Succeeds at Doing Nothing

The service writes a batch of parquet files (the columnar data files pipelines pass around) into a directory, then hands that directory to an uploader. The uploader does what every Python tutorial would tell you to do:

discovered = [p for p in src.rglob("*") if p.is_file()]
results = [upload(p) for p in discovered]
reason = "uploaded" if discovered else "skipped:nothing_to_upload"

pathlib is the modern, blessed way to walk a directory. On a quiet machine this always works. In production — Linux containers on ARM, like almost everyone's production — it intermittently returned an empty iterator for a directory that had files in it. An os.listdir in the same process, microseconds later, saw them fine.

The insidious part is what happens next: nothing. The caller sees an empty list and proceeds. Logs say the upload completed. The next pipeline stage treats the empty result as a legitimate zero-row dataset. A program that crashes gets fixed the same day; a program that silently does nothing can lie to you for months.

So I did the obvious thing and tried to reproduce it locally — on an Apple Silicon MacBook, because that's what dev laptops are now. It reproduced. Then it kept reproducing in ways production had never shown: wider windows, weirder timings, listings that disagreed with each other from one call to the next. The repro environment was lying in more ways than the production one.

It turned out three independently-true facts were involved. The first fires on every platform and is the heart of the production bug. The second is allowed everywhere but gets dramatically worse on a Mac. The third is purely a macOS problem — and it matters because a Mac is where you'll be standing when you try to debug this. None of the three is a bug in the villain sense. All of them are documented — if you know where to dig.

Layer One — pathlib Swallows Errors, and Has Since 2015

rglob works by walking into a folder, asking the operating system what's inside, and repeating that for every subfolder it finds. At any step of that walk, the OS can say no: you don't have permission, the thing it was reading vanished a moment ago, a read transiently failed. In Python, those refusals arrive as an OSError exception.

Here's the design decision at the heart of this post: when that happens inside glob() or rglob(), pathlib catches the error and just keeps going. No exception, no warning, no hook to find out it happened. Whatever it was scanning at that moment quietly drops out of the results. So the output of rglob cannot distinguish "this directory is empty" from "this directory has a thousand files I wasn't allowed to see."

In plain English — what 'swallowing an error' looks like

Deep inside pathlib's directory walk sits the moral equivalent of this:

try:
    entries = os.scandir(directory)   # ask the OS what's inside
except OSError:
    pass                              # couldn't read it? pretend it was empty

If the read fails, pretend it succeeded and found nothing. There's no log, no flag, no exception for the caller to catch — a failed read and a genuinely empty folder produce the identical return value. That's the property this whole post hangs on: not just that errors are dropped, but that their absence is unobservable.

The paper trail on this is remarkable, and nobody has ever written it up as a story. It starts in May 2015, when a bug tracker user named Gregorio filed cpython#68308 — asking, reasonably, that rglob stop crashing on unreadable directories and skip them the way os.walk does. Core developer Serhiy Storchaka was the lone dissent, proposing a bash-failglob-style option to surface errors instead. That idea was deflected to "a different issue" which was never filed.

The suppression patch (by Ulrich Petri) sat unreviewed for months, until Guido van Rossum personally hit the bug in January 2016: "I'm just going to commit this." Two days later he noticed his own fix made results depend on directory scan order — "dependent on the ordering of the names returned by listdir(), which is just wrong" — re-patched it, and closed with a line that reads differently a decade later: "Should be fixed for real now."

It wasn't. In 2019, Thierry Parmentelat filed cpython#83075 with strace output (a tool that records every call a program makes to the OS) showing that a single symlink into an unreadable directory made glob() silently drop other, perfectly readable files. Matt Wozniski reduced it to a five-command repro, and Pablo Galindo root-caused it as a regression from a performance rewrite. Nobody had noticed — because the failure mode is silence, exactly as Guido's warning predicted.

The endgame is the telling part. In 2021, cpython#90208 asked for an opt-in flag to surface the errors. It was resolved in 2023 by pathlib's maintainer Barney Gale going the other way: suppressing all OSError below the top-level path. That shipped in Python 3.13, whose docs now state that errors are "suppressed in many cases, but not all." And in March 2026, Jakub Kuczys demonstrated that list(Path('/root').glob('*')) returns [] with zero indication anything went wrong — and filed cpython#146646 as a docs-only issue, writing: "Based on #68308 (and just the fact that it has stayed unchanged for years), I assume this is intentional." Eleven years in, the behaviour's age had become the argument for keeping it.

What makes this sting is that CPython knows how to do better — it just didn't do it here. os.walk has taken an onerror callback since forever. Path.walk, added in 3.12, takes on_error. POSIX find prints to stderr and exits nonzero when it can't read a directory. glob and rglob give you nothing. "Errors should never pass silently. Unless explicitly silenced" is in the Zen of Python; there is no way to explicitly un-silence these.

The most rigorous Python codebases already know. Black recurses with iterdir and handles errors itself. pytest's internal visit() helper walks with os.scandir. mypy's file cache catches OSError and re-raises it. Ruff walks the tree in Rust.

Trivia

Ruff has a lint rule, PTH207, that tells you to replace glob.glob with Path.glob because pathlib is more idiomatic. The migration it recommends moves you from an API that suppresses most errors to one that, since 3.13, suppresses all of them below the root. The linter whose own authors bypass pathlib for traversal nudges everyone else toward it.

Hot Take

Documenting a footgun is not fixing a footgun. The 3.13 docs note and the 2026 glob-module note upgraded a decade-old surprise from "undocumented behaviour" to "intended behaviour" without adding a single way to opt out. That's the cheapest possible resolution each time, and it's now load-bearing: any future fix would be a documented-behaviour break.

Now the honest question, because you should ask it: did suppression actually cause our production incident? I can't prove it — and that is precisely the indictment. If an error fired during those listings, rglob ate it. If the kernel simply returned a stale view, rglob passed it along. The return value is identical either way: []. The API doesn't just fail — it destroys the evidence of how it failed, which means the first question of any incident review ("what actually happened?") is unanswerable by design.

What I can offer is the one measurement we did get. While reproducing locally, rglob would come back empty while os.scandir — the lower-level directory-scanning call that pathlib itself is built on — returned the full set of files when called at essentially the same instant. Two readers of the same kernel state disagreeing is what turns pathlib from bystander into suspect. Though even here, honesty: the two calls are microseconds apart, and we could never fully separate "pathlib dropped entries" from "the view changed between the calls."

Switching to os.scandir closes the silence class entirely — errors propagate, so at minimum you learn which failure you have. But it can't conjure files the operating system didn't report. That's the next layer down.

Layer Two — The Filesystem Never Promised You a Fresh Listing

Here's the part I would have called a kernel bug, before I read the spec. Whenever any program lists a folder — ls, Finder, Python's os.listdir, pathlib's walk — it ultimately goes through one low-level operation: readdir, the operating system's "tell me what's in this directory" call. POSIX, the standard that defines how that call must behave on Unix-like systems, says something remarkable in the readdir specification: "If a file is removed from or added to the directory after the most recent call to opendir() or rewinddir(), whether a subsequent call to readdir() returns an entry for that file is unspecified."

Unspecified. A listing that races concurrent writes is allowed to miss files, by design, on every POSIX system. But read the clause carefully, because it's easy to apply too broadly — I did at first: it covers files added while the directory is being read. Our production pipeline was, as far as we could reconstruct, sequential — write, close, then list. POSIX has no carve-out for that. A filesystem that returns a stale view after a completed write-and-close isn't exercising the spec's freedom; something else is going on.

So here is the honest statement about production, on Linux: we never definitively identified the mechanism. The candidates: the container filesystem (production runs in containers, and overlay filesystems have a documented history of readdir caching quirks); writes that weren't as complete as the code believed; or an error that fired and was eaten — and Layer One explains why that last one can never be ruled out. The listing was wrong, the API was silent about why, and the crime scene was already clean by the time anyone looked.

The local repro on my Mac is where things got measurably stranger — and that's an APFS question. First, the mental model that makes any of this thinkable: creating a file is really two writes — the file's bytes, and the folder's record that the name exists — and nothing guarantees they become visible at the same instant. APFS is the copy-on-write filesystem on every modern Mac, and the honest answer to what it does to directory listings under pressure is that nobody outside Apple knows.

In plain English — a new file is two writes, not one

Creating a file changes two separate things: the file's contents (the bytes you wrote) and the folder's own records saying "a file with this name now exists here" — the metadata. They are not committed at the same instant. APFS batches its metadata updates for speed using a copy-on-write structure: it builds a new version of the folder's records off to the side and atomically swaps a pointer when it commits. Until that swap happens, both of these can be true at once: the file's bytes are fully written and you can open() it by name, and a directory listing still reads the old records and doesn't show it. "The file exists" and "the folder doesn't list it" are not contradictions — they're two different questions asked of two different data structures.

I want to be careful here, because this is the least-documented layer of the three. Apple's APFS Reference describes the copy-on-write B-tree design across 181 pages and does not mention readdir once. There is no published contract for when a freshly created file becomes visible to directory enumeration. What I saw while reproducing locally — files that stat (the "does this exact path exist?" check) and open could see by name while a concurrent recursive listing came back without them, under burst writes on an Apple Silicon laptop — is consistent with metadata commits lagging data writes, but I can't point you at an Apple document that says so, because there is no Apple document that says anything about it at all. The silence is the finding.

What is on the record is enough to stop giving APFS the benefit of the doubt — with precision about what it shows. In 2020, Giovanni Pizzi of the AiiDA project filed bpo-41291: a delete-versus-open race, where a file being replaced while another process opens it yields empty reads and st_ino == 0 (an inode number of zero — the file's on-disk identity, reading back as "nothing"). That is not a listing bug, and I'm not claiming it as one — but it is the closest thing on the public record to an Apple-acknowledged APFS race of any kind. CPython's macOS maintainer Ronald Oussoren reproduced it in pure C and bisected it by filesystem: fires on APFS, not on HFS+. His conclusion: "This appears to be a bug in the APFS filesystem implementation." Reported to Apple as FB8009608. No public resolution.

Two years earlier, Gregory Szorc had profiled Firefox builds on macOS and found readdir on APFS serializing on a global kernel lock — the machine spending ~75% of its CPU time in the kernel just listing directories. A performance finding, not a correctness one — but the same neglected code path, and the same ending: a radar filed, a partial improvement shipped without comment, no acknowledgement.

One precision note, because it matters for anyone debugging their own version of this: several distinct mechanisms produce the "listing lies" symptom, and they are not the same bug. A writer in another process racing your listing is POSIX-unspecified territory. Files changing while you're mid-walk is a TOCTOU problem — time-of-check to time-of-use, meaning the world changed between when you looked and when you acted. bpo-41291 is an apparent APFS defect. iCloud and Dropbox "dataless" files break globbing in their own special way. And a file you wrote but never flushed is a you problem. The fix depends on which one you have — and this is where I have to practice what I'm preaching about evidence: production, on Linux, remains formally unsolved; the mechanism candidates above are indistinguishable after the fact. The local Mac repro showed listing inconsistency we could measure but never fully attribute. When your listing API is silent, every one of these mechanisms produces the identical observable: [].

So the instinctive fix is: force the pending metadata down before listing — sync the directory. Hold that instinct, because it's about to get complicated twice over: the syncing tool doesn't mean what its name says on a Mac, and it was never a listing-freshness tool anywhere.

Layer Three — fsync Is a Polite Request on macOS

On Linux, fsync(fd) — the fd is a file descriptor, the numeric handle the OS gives you for anything you've opened, folders included — means: this data is on durable storage before the call returns. On macOS, fsync(fd) means: this data has been handed to the drive, which may keep it in a volatile cache and write it whenever it feels like it. Apple's own fsync man page has said so for over twenty years, and it's blunt about the consequences: "This is not a theoretical edge case."

In plain English — the three stops between your program and the disk

When you write a file, the bytes make three stops: your operating system's memory (fast, lost if the OS crashes), the drive's own internal cache (fast, lost if power cuts out), and finally the durable flash storage (survives anything short of hardware failure). fsync is the call that's supposed to mean "push my data all the way down, then tell me." On Linux it pushes through all three stops. On macOS it stops at the drive's cache — so data that fsync just claimed was safe can still be eaten by a power cut. The macOS call that pushes all the way is fcntl(fd, F_FULLFSYNC), and it's dramatically slower — which is exactly why it isn't the default, and exactly why almost nothing uses it.

The picture is worth having in your head — three stops, and the two systems disagree about which stop "safe" means:

graph LR
    P["Your program<br/>write()"] --> K["Stop 1<br/>OS memory<br/>lost on OS crash"]
    K --> D["Stop 2<br/>drive cache<br/>lost on power cut"]
    D --> F["Stop 3<br/>durable flash<br/>survives power loss"]

fsync on Linux pushes your data to stop 3. fsync on macOS stops at 2 and reports success anyway. The real flush is a separate call, fcntl(fd, F_FULLFSYNC), which pushes to stop 3 and has existed since Mac OS X 10.3 in 2003. The canonical explanation came from Apple filesystem engineer Dominic Giampaolo on the PostgreSQL mailing list in 2005 — he'd verified drive-cache behaviour with a logic analyzer on the ATA cable, and described the full flush as "a heavy handed operation... But in an app like a database, it is essential." The database world quietly listened: SQLite added PRAGMA fullfsync in 2004, PostgreSQL grew fsync_writethrough. Everyone who truly needed durability opted in twenty years ago, which is precisely why there was never enough pressure to change the default.

The rest of us found out in February 2022, when Asahi Linux's Hector Martin measured an M1's SSD doing ~46 write operations per second with real flushes versus ~40,000 without and called it benchmark cheating. Michael Tsai's roundup of the ensuing fight is the fairest record — including the counter-voices: most non-Apple consumer drives simply lie about flushes, and when Russ Bishop power-loss-tested four NVMe SSDs, half of them lost data they had claimed to flush. Alex Miller's "Darwin's Deceptive Durability" distilled it for database people into three deadpan headings: fsync does not fsync, O_DSYNC does not O_DSYNC, SQLite is not durable.

This genre has a famous Linux chapter too — fsyncgate, 2018: PostgreSQL's Craig Ringer discovered that when Linux fsync fails, the kernel silently discards the data and the next call reports success. His report is dry understatement at its finest — "The write never made it to disk, but we completed the checkpoint, and merrily carried on our way. Whoops, data loss." Jonathan Corbet's LWN writeup and Dan Luu's verbatim archive tell it properly, and a later USENIX study found no major application handles fsync failure correctly even now. The takeaway for this post: durability primitives whose names promise more than they deliver are a genre, not a macOS quirk.

Now the caveat this section has been building to — and it cuts against our own fix, so it stays in. fsync is a durability tool: it controls what survives a power cut. It is not, on any system I can find documentation for, a visibility tool — nothing in POSIX or Apple's docs says that syncing a directory's file descriptor refreshes what the next readdir returns on a running machine. On Linux it has no defined effect on our problem at all. On macOS, the theory that F_FULLFSYNC forces APFS to commit the metadata transaction that feeds enumeration is exactly that — a theory, resting on the same undocumented behaviour as Layer Two. We put the barrier in our fix anyway: it costs 5–20ms on NVMe, and if the theory is right, it tightens the window. But I won't pretend we proved it does anything. Keep that skepticism handy for the next section.

How Three Half-Truths Compound

Any one of these alone is trivia. Stacked, they turn the friendliest file API in Python into silent data loss. Read the diagram top to bottom — this is the order it happens in at runtime:

sequenceDiagram
    participant W as Writer
    participant K as Kernel / filesystem
    participant R as Reader (rglob)
    W->>K: write() + close() burst
    Note over K: directory view can lag or err<br/>(mechanism varies by OS)
    R->>K: readdir(parent)
    K-->>R: empty or partial view
    Note over R: and IF any error fired mid-walk,<br/>pathlib swallowed it — unknowable
    R-->>R: returns [] — looks like success
Layer What it's supposed to guarantee What it actually guarantees
Path.rglob Returns every file in the tree Returns whatever readdir delivered, silently dropping anything that errored — and never telling you whether anything did
readdir The current contents of the directory Under concurrent writes: unspecified by POSIX. After sequential writes: should be current — but APFS's behaviour is undocumented and container filesystems have their own quirks
fsync(dir_fd) Pending metadata committed to durable storage Kernel buffers only on macOS (F_FULLFSYNC for the drive) — and on no system is it a documented listing-freshness barrier

In production on Linux we can name the symptom but not the mechanism — the listing was wrong and the API was silent about why. On a Mac dev box, two more undocumented layers join in, which is why the bug was easier to reproduce at my desk than it ever was to catch in the logs. The window is tens of milliseconds. The probability per call is tiny. The probability across a multi-hour pipeline run writing thousands of files is not — and every miss looks exactly like a legitimately empty directory.

The reframe that finally made the fix obvious: a directory listing is not a query. It's a measurement. It has noise, it has a sampling window, and if you need it to be right, you have to engineer for that the way you would for any other measurement.

What We Shipped

First: do you even need this? Only if your code lists a directory in the same breath as writing into it — write-then-immediately-list, or listing while writers may still be active. A static folder read at startup is fine with plain rglob. If that's you, close this tab guilt-free.

For everyone else, four moves, each honest about what it does and doesn't buy:

  1. Replace rglob with an os.scandir recursion that raises on error. The one provable fix: silence becomes a loud failure, so you always know which kind of empty you got.
  2. Retry briefly with backoff. The unglamorous move that, in truth, probably does most of the work.
  3. Nudge the directory metadata before each attemptF_FULLFSYNC on macOS, os.fsync elsewhere. This is the theory-not-proof part flagged above: cheap, harmless, unproven.
  4. A tripwire and a loud log. Two same-instant top-level listings that disagree mean the directory is visibly in flux; if it never settles, operators hear about it instead of nobody.

The block below is complete — imports, exception, logging — paste it and it runs:

import fcntl
import logging
import os
import sys
import time
from pathlib import Path

logger = logging.getLogger(__name__)

# In the stdlib on macOS builds of Python; the raw value elsewhere is inert.
F_FULLFSYNC = getattr(fcntl, "F_FULLFSYNC", 51)


class StorageListingError(Exception):
    """A directory listing failed — surfaced instead of swallowed."""


def _flush_directory_metadata(path: Path) -> None:
    """Nudge the kernel to commit pending directory metadata.

    fsync is a durability primitive; treating it as a listing-freshness
    barrier is a theory, not a documented contract. Kept because it is
    cheap and cannot make things worse."""
    fd = os.open(path, os.O_DIRECTORY)
    try:
        if sys.platform == "darwin":
            try:
                fcntl.fcntl(fd, F_FULLFSYNC)
            except OSError:
                os.fsync(fd)  # e.g. network/virtual filesystems
        else:
            os.fsync(fd)
    finally:
        os.close(fd)


def _scandir_recursive(path: Path) -> list[Path]:
    """Like Path.rglob('*') for files, except errors raise instead of
    silently shrinking the result."""
    out: list[Path] = []
    try:
        with os.scandir(path) as it:
            for entry in it:
                if entry.is_dir(follow_symlinks=False):
                    out.extend(_scandir_recursive(Path(entry.path)))
                elif entry.is_file(follow_symlinks=False):
                    out.append(Path(entry.path))
    except OSError as exc:
        raise StorageListingError(f"could not list {path}") from exc
    return out


def safe_list_directory(path: Path, max_attempts: int = 3) -> list[Path]:
    """Recursive file listing, hardened for directories written moments ago."""
    backoffs = (0.0, 0.05, 0.10)
    last: list[Path] = []
    for attempt in range(max_attempts):
        if attempt:
            time.sleep(backoffs[min(attempt, len(backoffs) - 1)])
        _flush_directory_metadata(path)
        listing = _scandir_recursive(path)
        # Tripwire, not proof: two top-level listings taken back-to-back
        # that DISAGREE mean the directory is visibly in flux. Agreement
        # does not prove the recursive listing is complete.
        if set(os.listdir(path)) == {p.name for p in path.iterdir()}:
            return listing
        last = listing
    logger.warning(
        "directory listing never converged: path=%s attempts=%d",
        path, max_attempts,
    )
    return last

It drops in at every rglob call site with one deliberate behavioural change: where rglob silently returned partial results, this raises. That is the point — if your caller can't handle a StorageListingError, it was already mishandling the invisible version of the same event.

Which parts can I defend, and how hard? The scandir-and-raise part: fully — it provably closes the silence class. The retry: empirically — it's the standard answer to transient inconsistency, whatever the mechanism. The flush: it's the belt-and-braces theory flagged in Layer Three; we never ran the ablation that would separate its contribution from the retry's, and I'd genuinely love to see someone do it. The tripwire: it detects churn between two instants, not stable staleness — a steadily wrong view sails through it, which is why the warning log exists. The whole thing is probabilistic, not deterministic. We tightened the window; we did not close it. The Darwin branch exists because dev machines live on Macs even though production is Linux; the cost on a Mac is 5–20ms of F_FULLFSYNC per attempt, far cheaper on Linux, and negligible against upload latencies either way.

The Fixes That Don't Trust Listings

Closing the window for real means restructuring so the race can't exist, and every mature system that has faced this converged on one of three shapes — none of which trust a directory listing:

  • Sentinel markers. The writer touches a marker file last, after everything is flushed; readers refuse to list until the marker exists. Hadoop's _SUCCESS file, carried forward by Spark, Hive, and Iceberg.
  • Atomic rename. Write into a staging directory, flush, then rename it into place — POSIX guarantees observers see the old state or the new state, never a partial one. Maildir has done this since 1995; Git's index.lock is the same idea.
  • Kernel events instead of listings. Subscribe to FSEvents/inotify before writing and build the file set from events. Watchman, Buck2, and Mercurial's fsmonitor live here.

There's an optimistic precedent for the "or the platform just fixes it" ending, too. S3's LIST calls were eventually consistent for fourteen years, and it bit production hard enough that Netflix built s3mper — Daniel Weeks's 2014 writeup could be this post's abstract: "there is no indication that a problem occurred... if only a small portion of input is missing the results will appear convincing." Hadoop built S3Guard for the same reason. Then in December 2020, AWS made S3 strongly consistent for everyone, retroactively, for free — Werner Vogels's engineering post-mortem notes the formal verification took more work than the implementation. The S3Guard docs now just say: delete the DynamoDB table.

Sit with the irony for a second: an object store on the other side of the planet now gives you stronger listing guarantees than the filesystem on the laptop you're reading this on.

The People Who Dug First

Almost nothing in this post is my discovery. The record was public the whole time — it just took a production incident to make me read it. Credit where it belongs:

  • The pathlib thread: Gregorio (the 2015 report), Serhiy Storchaka (the road not taken), Ulrich Petri (the patch), Guido van Rossum (the warning that came true), Thierry Parmentelat and Matt Wozniski (the strace proof), Pablo Galindo (the regression fix), Barney Gale (3.13's resolution), and Jakub Kuczys (the 2026 issue that finally got it documented).
  • The Darwin durability thread: Dominic Giampaolo (the 2005 explanation that still holds), Hector Martin (the numbers), Michael Tsai (the fairest record of the fight), Alex Miller (the essay to read first), and Howard Oakley (the calm explainer).
  • The fsyncgate thread: Craig Ringer (the discovery), Jonathan Corbet (the no-villains account), Dan Luu (the archive, and the whole "files are hard" genre), Thomas Munro and Andres Freund (the fix), and Ted Ts'o (the kernel's side of the story, which deserves to be heard).
  • The APFS thread: Giovanni Pizzi and Ronald Oussoren (bpo-41291 — the one Apple-acknowledged APFS race, a delete-vs-open one rather than a listing one), and Gregory Szorc (the readdir profiling). This thread is the shortest because almost nobody has dug here. That should probably change.

If you take one action from this post, make it structural: treat directory listings as measurements, surface your errors, and when correctness matters, publish atomically or use a marker. The standard library teaches you to trust the listing. Production says don't.


The scholars of Ohara, with the Buster Call bearing down on them, threw their books into the lake so the knowledge would outlive the island. Twenty years later it was all still there, waiting for someone willing to dive.

The knowledge about this bug was never hidden either. It's been sitting in open issue trackers, mailing list archives, and man pages since before some of the people hitting it learned Python — eleven years of it in one CPython thread alone. Nobody burned the library. We just stopped reading the poneglyphs.

The lake at Ohara filled with the books the scholars saved from the burning Tree of Knowledge


  1. Intro and closing images are from the One Piece anime via the One Piece Wiki. Attribution and credits — please contact if anything needs updating.