In August 2025 I ran into this in a unittest.mock test:
AttributeError: 'assert_called_twice_with' is not a valid assertion. Use a spec for the mock if 'assert_called_twice_with' is meant to be an attribute.. Did you mean: 'assert_called_once_with'?
Two periods before “Did you mean.” Tiny, harmless, ugly. I found gh-137716 on the CPython tracker and decided to fix it myself.
The diff in the end was 117 lines added and 51 removed across three files. Lib/traceback.py, Lib/test/test_traceback.py, and a NEWS entry. The PR is gh-138111. Nine months later it is still labelled awaiting merge and stale, and most of this post is really about that timeline rather than the patch.
What the bug actually was
Permalink to “What the bug actually was”The “Did you mean” suggestions for AttributeError, NameError, ImportError, and ModuleNotFoundError are computed in traceback.py by a function originally named _compute_suggestion_message. The suggestion gets appended to the original error message:
# original
return f"{msg}. Did you mean: '{suggestion}'?"If msg already ended in a period (or ? or !), you got the double punctuation. Easy. Strip the trailing terminator before concatenating:
TERMINATORS = (".", "?", "!")
def _suggestion_message(msg, suggestion):
if msg.endswith(TERMINATORS):
msg = msg[:-1]
return f"{msg}. Did you mean: '{suggestion}'?"Five lines. I expected the PR to be a one-day round trip. It was not.
What the PR became
Permalink to “What the PR became”Once I was inside traceback.py, the reviewers started pointing out adjacent things that needed fixing under the same umbrella, and the PR just kept growing. The same function was duplicated in two places, one for AttributeError and one for NameError, so I consolidated them. ModuleNotFoundError had its own special path that detected stdlib names and suggested an import, sitting outside the suggestion code path, and that got folded in. Non-ASCII suggestion names (yes, you can have non-ASCII identifiers in Python) had a separate code path that didn’t go through the new function, that got fixed too. The “site initialization disabled” message had also lost its virtual environment hint somewhere along the way, so I added that back.
The original five-line fix turned into a refactor of the whole suggestion pipeline. Which is normal. CPython reviewers are conservative for good reasons. Anything in traceback.py runs every time a user sees an error, which is to say, constantly. Touching it means earning the right to touch it, which usually means cleaning up around the change.
Things I learned by reading CPython
Permalink to “Things I learned by reading CPython”The best reason to do this kind of contribution, honestly, is that you have to read the code. A few things I didn’t know before.
Suggestion computation is opt-in. traceback.py will only compute “Did you mean” if the interpreter is running with the suggestion machinery available. There is a fast path for environments where the cost matters (-X disable_suggestions, embedded interpreters). I had just assumed it was always on.
Non-ASCII identifier handling is everywhere. You cannot assume a suggestion name is plain ASCII, the function that scores suggestions uses a Levenshtein variant that has to be careful with surrogate pairs. I learned this from a reviewer comment, not from the docs.
The NEWS entry is part of the PR. Every behaviour change in CPython requires a Misc/NEWS.d/next/ entry, generated by blurb. The format is rigid, the filename is templated (YYYY-MM-DD-HH-MM-SS.gh-issue-NNNNN.RANDOM.rst), and the body is one sentence aimed at end users. I had to redo mine three times before it was acceptable.
The contribution timeline
Permalink to “The contribution timeline”Aug 2025: issue filed, PR opened, first review round
Aug 2025: refactor: rename, consolidate
Aug 2025: first batch of tests added
Sep 2025: refactor based on review
Oct-Dec 2025: four merges from main; no review
Feb 2026: refactor: consolidate ModuleNotFoundError, non-ASCII fix
May 2026: still labelled "awaiting merge" and "stale"
There is a stretch of three months in there where the PR just sat untouched by reviewers and I rebased four times to keep it mergeable. CPython has a small core team, an enormous queue, and a triage system that is pretty honest about its limits. stale is a label, not an insult. The right response is patience and rebases, not pinging people.
If you are thinking about a CPython contribution, calibrate your expectations by the merge time, not by how big or small the change is. A five-line fix can easily take a year. That is the cost of caring about a 35-year-old codebase that runs everywhere.
Was it worth it
Permalink to “Was it worth it”Yes, for reasons that have not very much to do with the patch landing. I read more of traceback.py than I would have otherwise, and I now actually know how Python’s error messages are assembled, which has been useful in unrelated debugging work several times. I also have a real example of how CPython review works to point juniors at when they ask me whether they should contribute to open source. The honest answer is “yes, but pick a small change and prepare to wait.” And the PR is on my resume. It is one line, and people remember it.
If it ever merges, that is a bonus. If it does not, I still got the things above. Filing the issue and opening the PR are most of the value, and they take a weekend.