Skip to content

Umbrella issue: idlelib analysis with code-review-toolkit #154760

Description

@devdanzin

Bug report

Bug description:

A two-pass review of Lib/idlelib turned up 69 findings — 37 FIX (a wrong result, silent data loss, or an unreachable feature) and 32 CONSIDER. Every one carries a file:line, a concrete failure scenario, and — where idlelib already handles the same situation correctly somewhere else — the guarded twin that shows it is a defect by the project's own standard and suggests the fix.

I'm filing them under one umbrella so they can be picked off individually rather than flooding the tracker.

Please don't start work on a row until a maintainer has cleared it. These are a reviewer's findings, not triaged bugs: some will be intentional behaviour, some not worth the churn, and some wrong. Every row starts at Under analysis, which means not yet cleared — do not work on it. A maintainer moving a row off Under analysis is what opens it up; a PR against an uncleared row may be wasted effort, and several of these touch code where the current behaviour is load-bearing.

To take a cleared one: open a normal issue or PR for it and drop a comment here with the link — I'll put it in the Status column. If any row is a duplicate, a non-bug, or intentional, say so and I'll annotate it.

Found with code-review-toolkit. Reports were drafted with AI assistance (Claude Code) and then re-verified by hand — see Disclosure at the end.

Scope and verification

  • Everything below is against main at 993a0c65a7b (2026-07-27). The review itself was carried out on the 3.14 branch at 6080c866096; every finding was then re-checked against main by comparing the enclosing function (46 byte-identical, 13 changed and read individually, 11 module-level checked by hand), and every line number here is main's. Line numbers drift; the cited function and mechanism will not.
  • 18 findings were reproduced against a running interpreter (real Tk where the widget matters); the rest were confirmed by reading the code and its tests. Each gist entry says which. The reproductions were re-run on a release build of main at the commit above.
  • python -m test test_idle passes on this commit (305 run, 83 skipped headless). Every finding below is one the suite cannot see — and four of them (26, 71, 72, 73) are about why.
  • Prior art was checked for all 73 findings against the whole tracker: 1626 topic-IDLE issues, 281 unlabelled issues mentioning idlelib, 573 PRs and 86 idle.rst items (2503 unique). Three were already reported and have been removed from this report — they are listed at the bottom so nobody re-derives them. Where an open issue covers the area without covering the defect, the table names it.

Three systemic roots

These are worth reading first: each explains several of the individual rows below, and fixing the root is a smaller change than fixing its instances one at a time.

  • 31 — Five independent hand-written recognizers for Python syntax with no shared source of truth
    idlelib answers “is this Python syntax?” in five hand-written places with no shared definition, and new syntax reaches some and not others. colorizer is consistently the one that gets updated — four of the rows below are drift between it and a sibling.
  • 32 — The -n execution backend is missing five cross-cutting protections the RPC backend has
    use_subprocess selects between two interchangeable execution backends, and every cross-cutting protection lives inside the RPC one rather than in a shared driver. SystemExit is the sharpest cell and the two are opposites: the RPC backend contains it and returns to the prompt, while -n asks “Do you want to exit altogether?” with default="yes" and raises out of mainloop(), so unsaved editor buffers get no save prompt. Ctrl-C, Restart and the recursion headroom are missing from -n too.
  • 34 — WidgetRedirector swallows every TclError, so text.index() can never raise on any IDLE widget
    except TclError: return "" in WidgetRedirector.dispatch intercepts every subcommand of every percolated Text — which is every EditorWindow, PyShell and OutputWindow. On the widgets idlelib actually uses, text.index() therefore never raises and never returns None, callers cannot tell “no selection” from “the call failed”, and three incompatible guard idioms have grown in one file.

Start here

Lowest effort to confirm, highest cost if left alone — all silent:

  • 35 — Format Paragraph silently deletes characters from every non-comment line of a selection (format.py:59)
  • 36 — A literal % in any setting strips every open window's keybindings and leaves them off (config.py:48)
  • 27 — Copy-with-prompts drops the last line whenever the selection ends at column 10, 20, 100... (pyshell.py:1034)
  • 60 — Saving a file whose coding cookie cannot encode its text writes a BOM and keeps the cookie (iomenu.py:324-326)
  • 51 — restore_file_breaks appends without clearing, so Save As carries the old file's breakpoints over (pyshell.py:268)
  • 59 — _study1 has no f-string state, so a PEP 701 multi-line f-string is read as three statements (pyparse.py:245)
  • 43 — Three keysyms shipped in every keyset cannot be spelled by the key-configuration dialog (config_key.py:15)
  • 34 — WidgetRedirector swallows every TclError, so text.index() can never raise on any IDLE widget (redirector.py:116)

Findings

The # column is a row number in this issue, not an identifier. Please cite this issue plus the row (“#‹this issue› row 35”) rather than the bare number on its own — a bare number does not autolink and does not lead anyone back here.

Status: Under analysis = awaiting a maintainer's call, not cleared for work · #N = filed, in progress · #N FIXED = filed and fixed · won't fix / intentional = closed by a maintainer.

Prior art names an existing issue where one is relevant: from = this is residue of that merged change; see = that issue covers the area but not this defect, so a comment there may be better than a new issue.

Shell, the -n backend, and breakpoints (15)

pyshell.py, run.py, rpc.pyfull reports

# Sev Location Finding Prior art Status
2 FIX pyshell.py:499-540 One subprocess-accept timeout wedges IDLE permanently Under analysis
8 FIX run.py:635-639,695 Stack Viewer reads an attribute only runcode() ever creates Under analysis
27 FIX pyshell.py:1034 Copy-with-prompts drops the last line whenever the selection ends at column 10, 20, 100... from #95511 Under analysis
28 FIX pyshell.py:1016 get_prompt_text zips a computed line range against actual lines from #95191 Under analysis
32 FIX pyshell.py:792-803 The -n execution backend is missing five cross-cutting protections the RPC backend has Under analysis
33 FIX pyshell.py:680 Under idle -n, file is silently IDLE's own path instead of the script's from #77165 Under analysis
51 FIX pyshell.py:268 restore_file_breaks appends without clearing, so Save As carries the old file's breakpoints over Under analysis
52 FIX pyshell.py:244 Breakpoints are keyed by the raw filename while windows are keyed by the normalized one Under analysis
53 FIX pyshell.py:259 breakpoints.lst uses = as its delimiter, which is legal in a path Under analysis
12 CONSIDER run.py:190-197 The subprocess's last-resort handler can itself raise NameError Under analysis
13 CONSIDER rpc.py:358-361 Every OSError on recv becomes 'the peer hung up' Under analysis
14 CONSIDER pyshell.py:804-821 endexecuting() is skipped if showtraceback() fails Under analysis
15 CONSIDER rpc.py:258-260 Remote internal errors are reported only at a permanently-disabled debug level Under analysis
18 CONSIDER rpc.py:130-141 Reconnect resets some per-connection state but not the packet read buffer Under analysis
21 CONSIDER pyshell.py:564-574 terminate_subprocess catches every OSError though its comment names one Under analysis

Configuration and the Settings dialog (9)

config.py, configdialog.py, config_key.pyfull reports

# Sev Location Finding Prior art Status
1 FIX config.py:136-140 A failed config save deletes the user's config file Under analysis
36 FIX config.py:48 A literal % in any setting strips every open window's keybindings and leaves them off Under analysis
43 FIX config_key.py:15 Three keysyms shipped in every keyset cannot be spelled by the key-configuration dialog Under analysis
48 FIX config.py:244 An invalid value in the DEFAULT config is swallowed where the user-config clause warns from #60715 Under analysis
49 FIX config.py:605 The keybinding collision test compares whole lists, so a partial overlap is undetected and silent Under analysis
61 FIX config.py:756 Help sources are numbered on write and sorted as strings on read, so entry 10 sorts before entry 2 see #71652 Under analysis
62 FIX config_key.py:227 An advanced key binding stored as a Tk event sequence is read back as a whitespace-separated list see #55646, #152747 Under analysis
50 CONSIDER config.py:199 The whole IDLE profile location is a function of the launch directory when HOME is unusable see #60066, #52478 Under analysis
69 CONSIDER parenmatch.py:53 Two extensions' in-code fallback defaults disagree with the values actually shipped Under analysis

Editor: formatting, undo, search/replace, save and run (14)

format.py, undo.py, replace.py, searchengine.py, iomenu.py, runscript.py, zoomheight.pyfull reports

# Sev Location Finding Prior art Status
3 FIX replace.py:226-229 Replace's NULL-guard tests the call receiver instead of the match Under analysis
5 FIX searchengine.py:146-151 Forward search abandons the rest of a line after a zero-width match Under analysis
9 FIX zoomheight.py:66-105 A failed window probe leaves the user's window stuck maximized from #81220 Under analysis
35 FIX format.py:59 Format Paragraph silently deletes characters from every non-comment line of a selection see #143774, #143775 Under analysis
45 FIX replace.py:161 undo_block_start/stop is unpaired on the exception path in four places Under analysis
60 FIX iomenu.py:324-326 Saving a file whose coding cookie cannot encode its text writes a BOM and keeps the cookie Under analysis
67 FIX runscript.py:189 Run Module ignores the return of io.save(), so a failed save runs the old code from disk Under analysis
11 CONSIDER iomenu.py:360-375 A bad print command orphans the user's unsaved source in /tmp Under analysis
19 CONSIDER replace.py:172-188 Replace All's final selection uses the pre-replacement span Under analysis
20 CONSIDER searchengine.py:85-89 getprog catches only re.PatternError, so OverflowError escapes to every caller Under analysis
24 CONSIDER iomenu.py:253,261 Recent-files list is updated outside the success guard Under analysis
46 CONSIDER undo.py:104 undo_block_stop does not guard the sentinel its own twin guards Under analysis
64 CONSIDER format.py:340 Untabify rewrites the whole line where its tabify mirror rewrites only the indent Under analysis
65 CONSIDER undo.py:294 DeleteCommand.undo reinserts text without tags where the Insert mirror round-trips them Under analysis

The five hand-written syntax recognizers (7)

pyparse.py, hyperparser.py, codecontext.py, colorizer.pyfull reports

# Sev Location Finding Prior art Status
30 FIX hyperparser.py:298 HyperParser's string-prefix scanner never learned f or t from #139742, #139756 Under analysis
47 FIX codecontext.py:22 codecontext's BLOCKOPENERS lacks match/case, so those blocks never appear in Code Context from #77745, #25851 Under analysis
59 FIX pyparse.py:245 _study1 has no f-string state, so a PEP 701 multi-line f-string is read as three statements see #77061 Under analysis
63 FIX hyperparser.py:137-140 Bracket matching checks the bracket type on the backward scan only see #124451 Under analysis
17 CONSIDER pyparse.py:44-48 A comment with no space after # is invisible to HyperParser Under analysis
31 CONSIDER hyperparser.py:298 Five independent hand-written recognizers for Python syntax with no shared source of truth Under analysis
58 CONSIDER pyparse.py:21 _synchre matches def but cannot match async def, making indent analysis quadratic in async modules see #77061 Under analysis

Completions and calltips (5)

autocomplete*.py, calltip.pyfull reports

# Sev Location Finding Prior art Status
4 FIX autocomplete.py:117,134 Autocomplete's mode filter never applies to ATTRS because ATTRS == 0 Under analysis
6 FIX calltip.py:189 get_argspec guards two of its three touches of a user object Under analysis
10 FIX autocomplete_w.py:238 A failed is_active check permanently disables the completion window's Configure handler from #26404 Under analysis
29 FIX autocomplete_w.py:363-368 and binds tighter than or, so the modifier guard covers one completion mode of two Under analysis
68 FIX autocomplete_w.py:454-491 The completion popup teardown is unguarded where the calltip's identical teardown guards every step Under analysis

Infrastructure: redirector, file list, debugger, trees (9)

redirector.py, filelist.py, debugger*.py, tree.py, stackviewer.pyfull reports

# Sev Location Finding Prior art Status
34 FIX redirector.py:116 WidgetRedirector swallows every TclError, so text.index() can never raise on any IDLE widget Under analysis
54 FIX filelist.py:90 After a Name Conflict the displaced window is marked None instead of re-pointed, so the NEXT collision is silent Under analysis
55 FIX debugger_r.py:271 DictProxy.getitem returns a repr, and only one of its two consumers strips the extra quotes Under analysis
16 CONSIDER filelist.py:105-108 A failed getcwd() silently yields a relative path used as a window identity key Under analysis
22 CONSIDER debugger.py:159-162 Debugger.close swallows every failure of quit() Under analysis
25 CONSIDER debugger.py:55-56 Idb.user_exception calls into the GUI unguarded where its twin is guarded Under analysis
56 CONSIDER debugger_r.py:35 Four id()-keyed debugger tables are never cleared, so memory grows with every debugger stop Under analysis
57 CONSIDER tree.py:234 Tree redraw re-binds every node's canvas item and never deletes the bindings Under analysis
70 CONSIDER stackviewer.py:12 Stack Viewer module globals pin every frame of the last exception for the process lifetime Under analysis

Documentation and the generated in-app help (6)

Doc/library/idle.rst, help.py, help.html, config-main.deffull reports

# Sev Location Finding Prior art Status
37 FIX Doc/library/idle.rst:731-732 The documented sys.argv[0] contract for idle -r is wrong, and idle -h contradicts it correctly Under analysis
38 FIX Doc/library/idle.rst:752 The network diagnostic in the docs names a port IDLE has never listened on Under analysis
39 CONSIDER help.py:251 The in-app help is generated correctly, so every documentation error is faithfully shipped Under analysis
40 CONSIDER Doc/library/idle.rst Documentation still names UI that was renamed or split two releases ago Under analysis
41 CONSIDER config-main.def:48 Four config options are fully supported and completely invisible, and one has no consumer Under analysis
44 CONSIDER mainmenu.py:80 Three virtual events are declared but unreachable, one of them from a live menu Under analysis

Test-suite integrity (4)

idle_test/full reports

# Sev Location Finding Prior art Status
26 CONSIDER idle_test/test_autocomplete.py:241 Five tests in the suite cannot fail Under analysis
71 CONSIDER idle_test/mock_idle.py:48 The shared test stubs cannot express the conditions the guards under test exist for Under analysis
72 CONSIDER idle_test/test_searchengine.py:279 A test class freezes text.index to a constant, leaving seven mutations of live code undetected Under analysis
73 CONSIDER idle_test/test_undo.py Four test files exit 0 even when their tests fail Under analysis

Excluded — please don't pursue these

Found by the review and then dropped — three because they are already reported, one because main already fixed it. Listed only so the same ground isn't covered twice:

Finding Why it is not in the tables
Custom run arguments are corrupted by a Tcl list round-trip (query.py:352,368-376) already reported: #93016
A bare except guards a four-link attribute chain (autocomplete.py:173-175) already reported: #59518
<<python-context-help>> is offered in Settings, bound by nothing, and has never had a handler (config.py:614) already reported: #88740
Configure IDLE → Keys raises KeyError for any third-party extension that ships disabled (config.py:486 on 3.14) fixed on main by gh-89520 / GH-28713GetExtensionKeys now uses current_keyset.get(event, None). Still present on the 3.14 branch; a backport may be wanted.

Disclosure & caveats

  • The reports were drafted with AI assistance (Claude Code); each gist carries an explicit disclaimer. Before publishing, every cited line was re-read in the tree at the stated commit, and every "Verified" transcript was produced by running the code.
  • That re-verification changed three findings, which is the reason it is worth mentioning: 59 was upgraded from an argument to a reproduction by a differential its author had not run; a fourth claim (about how many test files return 0 on failure) was wrong by an order of magnitude and is corrected in 73; and two line numbers were off.
  • 33 is deliberate in part. The commit that introduced the -n exclusion says so in its own message. What is reported is only the part that commit did not consider: under -n the name is not absent, it is IDLE's own entry point.
  • Severities are the reviewer's, not a maintainer's. CONSIDER items in particular are judgement calls and several may be intentional; say so and I'll annotate them.

Each finding is also classified against a catalogue of recurring Python bug shapes; the shape name in each gist entry (zip-truncates-on-length-mismatch, per-line-property-derived-from-the-aggregate, …) is what let the review hunt siblings of a confirmed defect rather than stop at the first instance. Several rows here were found that way.

CPython versions tested on:

CPython main branch

Operating systems tested on:

Linux

Metadata

Metadata

Assignees

Labels

stdlibStandard Library Python modules in the Lib/ directorytopic-IDLEtype-bugAn unexpected behavior, bug, or error

Projects

Status
No status

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions