Bug report
Bug description:
bdb.Bdb.clear_all_file_breaks(filename) iterates over a list while mutating
it via Breakpoint.deleteMe(), which removes elements from the same list.
When two or more breakpoints exist at the same (filename, lineno), this
classic iterate-and-delete pattern skips alternate elements, leaving orphan
Breakpoint instances in Breakpoint.bplist and Breakpoint.bpbynumber.
The same module already uses the defensive slice-copy pattern in
clear_break() (the sibling function), so this is a clear local
inconsistency, not a contractual choice.
Internal asymmetry (smoking gun):
In Lib/bdb.py:
clear_break() — defensive copy:
for bp in Breakpoint.bplist[filename, lineno][:]:
bp.deleteMe()
clear_all_file_breaks() — no copy:
for line in self.breaks[filename]:
blist = Breakpoint.bplist[filename, line]
for bp in blist: # iterating raw list
bp.deleteMe() # mutates blist
Both functions delete breakpoints via the same deleteMe() mechanism, which
removes from Breakpoint.bplist[(filename, lineno)]. The asymmetric defense
between sibling functions is the strongest evidence this is an oversight
rather than a deliberate design choice.
Reproducer:
import bdb
class MyDbg(bdb.Bdb): pass
dbg = MyDbg()
# Three breakpoints at the same (file, line)
dbg.set_break(__file__, 10)
dbg.set_break(__file__, 10)
dbg.set_break(__file__, 10)
# Snapshot before clearing
key = (bdb.canonic(__file__), 10)
before = list(bdb.Breakpoint.bplist.get(key, []))
print(f"Before: {len(before)} breakpoints in Breakpoint.bplist[{key}]")
# Clear all in this file
dbg.clear_all_file_breaks(bdb.canonic(__file__))
# Snapshot after clearing
after = list(bdb.Breakpoint.bplist.get(key, []))
print(f"After: {len(after)} breakpoints in Breakpoint.bplist[{key}]")
print(f"Orphans in Breakpoint.bpbynumber: "
f"{[n for n, b in enumerate(bdb.Breakpoint.bpbynumber) if b is not None]}")
Expected behavior:
Before: 3 breakpoints in Breakpoint.bplist[...]
After: 0 breakpoints in Breakpoint.bplist[...]
Orphans in Breakpoint.bpbynumber: []
Actual behavior (Python 3.14.3):
Before: 3 breakpoints in Breakpoint.bplist[...]
After: 1 breakpoints in Breakpoint.bplist[...] ← orphan remains
Orphans in Breakpoint.bpbynumber: [<some index>] ← orphan remains
For N breakpoints at the same (file, line), floor(N/2) orphans remain.
Suggested fix (one-character diff):
--- a/Lib/bdb.py
+++ b/Lib/bdb.py
@@ -755,7 +755,7 @@
if filename not in self.breaks:
return 'There are no breakpoints in %s' % filename
for line in self.breaks[filename]:
blist = Breakpoint.bplist[filename, line]
- for bp in blist:
+ for bp in blist[:]:
bp.deleteMe()
del self.breaks[filename]
This matches the existing pattern at clear_break() in the same file.
Suggested test (Lib/test/test_bdb.py):
def test_clear_all_file_breaks_with_multiple_bps_same_line(self):
"""Regression test: clear_all_file_breaks must remove all breakpoints,
even when multiple breakpoints share the same (file, line)."""
dbg = bdb.Bdb()
src = bdb.canonic(__file__)
dbg.set_break(src, 10)
dbg.set_break(src, 10)
dbg.set_break(src, 10)
self.assertEqual(len(bdb.Breakpoint.bplist[(src, 10)]), 3)
dbg.clear_all_file_breaks(src)
self.assertNotIn((src, 10), bdb.Breakpoint.bplist)
Versions:
- Reproduced on Python 3.14.3 (Windows, MINGW64).
- Reading source confirms identical pattern in
main and 3.13 branches.
- Behavior identical regardless of whether the breakpoints are set with
conditions, ignore counts, or via different Bdb subclasses.
Related (not duplicate):
Issue #54770 (bpo-10561, fixed 2010) covers a different scenario: the
clear bpnumber pdb command potentially deleting more than one breakpoint.
That issue addresses clear_bpbynumber / breakpoint identity. The bug
reported here is in clear_all_file_breaks and concerns iterate-and-delete
on the shared Breakpoint.bplist list. No prior issue or PR specifically
targets this asymmetry between clear_break (defensive) and
clear_all_file_breaks (vulnerable).
Linked PRs
Bug report
Bug description:
bdb.Bdb.clear_all_file_breaks(filename)iterates over a list while mutatingit via
Breakpoint.deleteMe(), which removes elements from the same list.When two or more breakpoints exist at the same
(filename, lineno), thisclassic iterate-and-delete pattern skips alternate elements, leaving orphan
Breakpointinstances inBreakpoint.bplistandBreakpoint.bpbynumber.The same module already uses the defensive slice-copy pattern in
clear_break()(the sibling function), so this is a clear localinconsistency, not a contractual choice.
Internal asymmetry (smoking gun):
In
Lib/bdb.py:clear_break()— defensive copy:clear_all_file_breaks()— no copy:Both functions delete breakpoints via the same
deleteMe()mechanism, whichremoves from
Breakpoint.bplist[(filename, lineno)]. The asymmetric defensebetween sibling functions is the strongest evidence this is an oversight
rather than a deliberate design choice.
Reproducer:
Expected behavior:
Actual behavior (Python 3.14.3):
For N breakpoints at the same
(file, line),floor(N/2)orphans remain.Suggested fix (one-character diff):
This matches the existing pattern at
clear_break()in the same file.Suggested test (
Lib/test/test_bdb.py):Versions:
mainand 3.13 branches.conditions, ignore counts, or via different
Bdbsubclasses.Related (not duplicate):
Issue #54770 (bpo-10561, fixed 2010) covers a different scenario: the
clear bpnumberpdb command potentially deleting more than one breakpoint.That issue addresses
clear_bpbynumber/ breakpoint identity. The bugreported here is in
clear_all_file_breaksand concerns iterate-and-deleteon the shared
Breakpoint.bplistlist. No prior issue or PR specificallytargets this asymmetry between
clear_break(defensive) andclear_all_file_breaks(vulnerable).Linked PRs
Bdb.clear_all_file_breaks#149039