Skip to content

Instantly share code, notes, and snippets.

@devdanzin
Last active July 27, 2026 08:46
Show Gist options
  • Select an option

  • Save devdanzin/85d7c940ad8bb3c068f28c3c21d63970 to your computer and use it in GitHub Desktop.

Select an option

Save devdanzin/85d7c940ad8bb3c068f28c3c21d63970 to your computer and use it in GitHub Desktop.
idlelib #154760: IDLE infrastructure: redirector, file list, debugger, trees

IDLE infrastructure: redirector, file list, debugger, trees

redirector.py, filelist.py, debugger.py, debugger_r.py, tree.py, stackviewer.py, CPython main 993a0c65a7b.

Tracked in the umbrella issue python/cpython#154760 — one of 69 idlelib findings from a two-pass review with code-review-toolkit.

Each numbered heading below is one finding and has its own anchor, so the umbrella table links straight to it. The numbers are row numbers in that table, not identifiers — when referring to one of these, cite #154760 and the row, not the bare number.

Do not start work on a finding until a maintainer has cleared it in #154760. Every row there starts at Under analysis, which means not yet cleared.

AI disclaimer: these reports were drafted with Claude Code. Every cited line was re-read in the tree, every "Verified" block was produced by running the code, and the prior-art search was run against the full python/cpython tracker before publishing.

How to read a finding. Where is the line as of the commit above and will drift. Guarded twin is a place in idlelib that already handles the same situation correctly — it is both the evidence that this is a defect by the project's own standard and the shape of the fix. Prior art records what the tracker already says.


34 — WidgetRedirector swallows every TclError, so text.index() can never raise on any IDLE widget

FIX · reproduced · shape empty-result-conflated-with-absent

Where: redirector.py:116

What happens. except TclError: return "" in WidgetRedirector.dispatch intercepts every subcommand of every percolated Text — i.e. every EditorWindow, PyShell and OutputWindow. Verified on a live widget: a plain Text raises TclError for index('sel.first') while a redirected one returns ''. Callers therefore cannot distinguish 'no selection' from 'the call failed', and three incompatible guard idioms have grown in one file: editor.py:360 tests '' (correct here, wrong on a plain Text); editor.py:558 catches TclError AND tests the value (the correct twin); editor.py:1261 catches TclError only and its own comment promises (None, None) while it measurably returns ('', ''). Five callers survive only because they all happen to write if first and last:.

Verified:

plain Text        index('sel.first') -> TclError
redirected Text   index('sel.first') -> ''      <- every IDLE Text is redirected

Guarded twin. editor.py:558 — catches TclError and tests the returned value, correct on both widget kinds.

Suggested fix. Narrow the except to the operations that legitimately fail, and return a distinguishable sentinel. Note that four search/replace entry points (search.py:35,129, replace.py:28, grep.py:39) work only BECAUSE of the swallow — narrow it without fixing them and Ctrl-F breaks with no selection.

Prior art — no matching report found. Searched the cached python/cpython tracker corpus (1626 topic-IDLE issues + 281 unlabelled idlelib issues + 573 PRs + 86 idle.rst items; 2503 unique) for this defect's terms and for its file/function names. No matching report.

Note. This is the root cause of 0027's neighbourhood: pyshell.py:1016-1017 guards if selfirst is None: # Should not be possible. — the guard can never fire, the comment is right for the wrong reason, and sellast[-1] on '' would raise IndexError.


16 — A failed getcwd() silently yields a relative path used as a window identity key

CONSIDER · confirmed by reading · shape identity-key-from-a-non-artifact-proxy

Where: filelist.py:105-108

What happens. canonize() catches OSError from getcwd() with pass, leaving the filename relative; the result is the identity key for open windows. If the cwd is deleted or unmounted under IDLE, the same file opened once by absolute and once by relative path gets two independent editor windows, and edits in one are silently overwritten by a save from the other.

Guarded twin. iomenu.py:376-378 handles the same getcwd() failure with pwd = '' — also lossy, but only for a dialog's initial directory, not an identity key.

Suggested fix. Fall back to a stable base, or refuse to register the window rather than keying on an ambiguous path.

Prior art — no matching report found. Searched the cached python/cpython tracker corpus (1626 topic-IDLE issues + 281 unlabelled idlelib issues + 573 PRs + 86 idle.rst items; 2503 unique) for this defect's terms and for its file/function names. No matching report.


22 — Debugger.close swallows every failure of quit()

CONSIDER · confirmed by reading · shape except-exception-too-broad

Where: debugger.py:159-162

What happens. The only narrow-body/except Exception/pass combination in non-test idlelib. Debugger.quit is two statements; the realistic failure is TclError from the second when the root is already going away. If set_quit() fails instead, the debugger window closes while the remote debuggee is left un-quit and the user has no indication.

Guarded twin. debugger.py:45-47 catches exactly TclError for the same situation, with an explanatory comment.

Suggested fix. Narrow to except TclError: to match the sibling handler.

Prior art — no matching report found. Searched the cached python/cpython tracker corpus (1626 topic-IDLE issues + 281 unlabelled idlelib issues + 573 PRs + 86 idle.rst items; 2503 unique) for this defect's terms and for its file/function names. No matching report.


25 — Idb.user_exception calls into the GUI unguarded where its twin is guarded

CONSIDER · confirmed by reading · shape fix-not-propagated-to-sibling-path

Where: debugger.py:55-56

What happens. user_exception calls self.gui.interaction bare; the twin user_line wraps the identical call in try/except TclError: pass with the comment 'When closing debugger window with [x]'. Closing the debugger window at the moment an exception is being reported takes the unguarded path.

Guarded twin. debugger.py:44-48 (user_line).

Suggested fix. Apply the same guard the twin uses.

Prior art — no matching report found. Searched the cached python/cpython tracker corpus (1626 topic-IDLE issues + 281 unlabelled idlelib issues + 573 PRs + 86 idle.rst items; 2503 unique) for this defect's terms and for its file/function names. No matching report.

Note. Both twin tests assert only that a Mock recorded its arguments, so neither can observe the difference.


54 — After a Name Conflict the displaced window is marked None instead of re-pointed, so the NEXT collision is silent

FIX · confirmed by reading · shape unchecked-no-op-sentinel

Where: filelist.py:90

What happens. On a filename collision FileList shows the 'Name Conflict' error and sets self.inversedict[conflict] = None. The displaced window is now untracked, so a subsequent collision on the same file finds nothing in inversedict and produces no error at all: two windows edit the same file, each unaware of the other, and whichever saves last wins.

Guarded twin. filelist.py:95, the same function's success path, which re-points self.dict[newkey].

Suggested fix. Re-point the displaced window rather than dropping it.

Prior art — no matching report found. Searched the cached python/cpython tracker corpus (1626 topic-IDLE issues + 281 unlabelled idlelib issues + 573 PRs + 86 idle.rst items; 2503 unique) for this defect's terms and for its file/function names. No matching report.


55 — DictProxy.getitem returns a repr, and only one of its two consumers strips the extra quotes

FIX · confirmed by reading · shape fix-not-propagated-to-sibling-path

Where: debugger_r.py:271

What happens. The RPC layer already repr's the value, so DictProxy.getitem hands back a repr of a repr. debugger.py:572-573 compensates with svalue = svalue[1:-1] under if rpc_client; stackviewer.py does not, so every value shown in the Stack Viewer carries a spurious extra layer of quotes and a string is indistinguishable from a string containing quotes.

Guarded twin. debugger.py:572-573, with the comment explaining exactly why the strip is needed.

Suggested fix. Strip in DictProxy, or apply the same compensation in stackviewer.

Prior art — no matching report found. Searched the cached python/cpython tracker corpus (1626 topic-IDLE issues + 281 unlabelled idlelib issues + 573 PRs + 86 idle.rst items; 2503 unique) for this defect's terms and for its file/function names. No matching report.


56 — Four id()-keyed debugger tables are never cleared, so memory grows with every debugger stop

CONSIDER · confirmed by reading · shape identity-by-id-requires-retention

Where: debugger_r.py:35, debugger_r.py:36, debugger_r.py:37, debugger_r.py:38

What happens. frametable, dicttable, codetable and tracebacktable are module-level dicts keyed by id(), and because id() is reused they must retain every object they have ever seen. Nothing ever removes an entry, so memory grows linearly with the number of debugger stops rather than with live state. Agent-measured: 20,000 stops produced 20,000 entries and took the subprocess from 17MB to 56MB.

Guarded twin. None in-package; the tables have no eviction path at all.

Suggested fix. Clear the tables when the debugger resumes, or key them on a monotonic counter.

Prior art — no matching report found. Searched the cached python/cpython tracker corpus (1626 topic-IDLE issues + 281 unlabelled idlelib issues + 573 PRs + 86 idle.rst items; 2503 unique) for this defect's terms and for its file/function names. No matching report.

Note. The 20k-stop measurement is the agent's; the retention mechanism was verified by reading.


57 — Tree redraw re-binds every node's canvas item and never deletes the bindings

CONSIDER · confirmed by reading · shape per-redraw-binding-never-released

Where: tree.py:234

What happens. _draw_children calls canvas.tag_bind twice per node on every redraw, and the comment on the line above — 'XXX This leaks bindings until canvas is deleted' — has been correct since 1999. Each binding creates a Tcl command that is never deleted and holds a reference to the TreeNode, so every object browsed in a Class Browser, Path Browser or Stack Viewer stays pinned for as long as the window is open, and the Tcl command table grows with every expand/collapse.

Guarded twin. None in idlelib — no sibling does this correctly.

Suggested fix. Delete the old bindings before re-creating them, or bind once at node construction.

Prior art — no matching report found. Searched the cached python/cpython tracker corpus (1626 topic-IDLE issues + 281 unlabelled idlelib issues + 573 PRs + 86 idle.rst items; 2503 unique) for this defect's terms and for its file/function names. No matching report.


70 — Stack Viewer module globals pin every frame of the last exception for the process lifetime

CONSIDER · confirmed by reading · shape fix-not-propagated-to-sibling-path

Where: stackviewer.py:12

What happens. global sc, item, node # For testing. keeps the canvas, the tree item and the root node alive at module scope after the window is closed, so every local variable of every frame of the last traceback stays reachable until another traceback replaces it — including large objects the user expects to have been freed.

Guarded twin. browser.py:98 close() — the structurally identical browser, which destroys both the top and the node. pyshell.py:652 already carries '# XXX Should GC the remote tree'.

Suggested fix. Destroy on close as browser.py does; keep the test hooks on an instance rather than at module scope.

Prior art — no matching report found. Searched the cached python/cpython tracker corpus (1626 topic-IDLE issues + 281 unlabelled idlelib issues + 573 PRs + 86 idle.rst items; 2503 unique) for this defect's terms and for its file/function names. No matching report.


Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment