In March 2026, an AI-assisted audit swept roughly 94,000 lines of CPython's pure-Python standard library (asyncio, email, xml, http, multiprocessing, concurrent.futures, importlib, logging, typing, argparse, inspect, subprocess, dataclasses, pickle, tarfile, zipfile, shutil, tempfile, pdb, contextlib, configparser) and produced 68 candidate findings — mostly overly-broad except clauses, silently swallowed errors, and a handful of outright crashes. None of it was ever independently re-verified or filed as GitHub issues.
In July 2026, every one of those 68 candidates was re-verified from scratch: re-read against current CPython main (commit b4662e8e3b60, 2026-07-15), reproduced where practical with an actually-executed script, and checked against GitHub's issue/PR history to see whether it was already known. 38 candidates survived as genuine, unfiled bugs — the rest turned out to be already fixed, already tracked, or (most commonly) intentional/documented behavior the original audit misread. That ~44% false-positive rate is itself worth internalizing: audit output is a candidate list, not a findings list, and this catalog only contains what independently cleared both bars.
This document collects those 38 confirmed, unfiled, reproducible issues, organized by module, each with the current file:line, a plain-language description, a working reproducer (actually run — output included), and a suggested fix direction. Three appendices cover what didn't make the cut, for transparency and because the "why not" is itself instructive.
Intended use: a seed list of "good first issue" candidates for people learning to contribute to CPython — small, self-contained, well-scoped bugs in pure-Python code, each with enough detail that a newcomer can go straight from this document to a PR without re-deriving the analysis.
- Re-check the tracker. This catalog is a snapshot as of 2026-07-15. Do one more
gh search issues --repo python/cpython "<keywords>"before starting work — someone may have filed or fixed it since. - Read the surrounding code yourself. Line numbers will drift as
mainmoves; use them as a starting point, not gospel. - Discuss before large changes. Anything touching public behavior (not just an internal
exceptclause) may warrant a quick issue discussion before a PR, per the devguide. - Some of these are one-line fixes; some are "figure out the right design" fixes. They're flagged as such below — pick according to the contributor's experience level.
- Location:
Lib/typing.py:413-418(function_tp_cache) - Problem:
_tp_cachewraps a function withfunctools.lru_cacheand falls back to an uncached call when arguments are unhashable — detected viaexcept TypeError. But that sameexceptalso catches aTypeErrorthe wrapped function legitimately raises for unrelated reasons, silently calling the function a second time (with side effects) before finally letting the error propagate. - Reproducer (verified):
import typing calls = [] @typing._tp_cache def f(x): calls.append(x) if x == "bad": raise TypeError("real bug inside f, unrelated to hashability") return x * 2 f("bad") print(calls) # ['bad', 'bad'] — f() ran twice
- Suggested fix: Narrow the fallback trigger to unhashable-argument errors specifically (e.g. inspect
e.argsfor "unhashable type", or pre-check hashability before the cached call) so a genuineTypeErrorfrom the wrapped function isn't treated as a cache-miss signal. - Difficulty: Small, self-contained.
- Location:
Lib/typing.py:1875(function_get_protocol_attrs) - Problem: Collects a
Protocolclass's declared members by readingbase.__annotations__for each class in the MRO, guarded byexcept Exception. Meant to fall back gracefully when a class lacks the attribute, but also silently swallows genuine bugs (e.g. a broken metaclass raising something other thanAttributeError), producing an incomplete/wrong attribute set with no error. - Reproducer (verified):
import typing class BrokenMeta(type): def __getattribute__(cls, name): if name == '__annotations__': raise RuntimeError("simulated broken metaclass") return super().__getattribute__(name) class Base(metaclass=BrokenMeta): pass print(typing._get_protocol_attrs(Base)) # set() — RuntimeError silently swallowed
- Suggested fix: Narrow to
except AttributeError. - Difficulty: Small, self-contained.
- Location:
Lib/typing.py:1265-1272 - Problem: After instantiating via a generic alias (e.g.
List[int]()), tries to stampresult.__orig_class__ = selffor introspection, wrapped inexcept Exception: passto tolerate__slots__-style rejection. Also hides genuine bugs in a custom__setattr__. - Reproducer (verified):
import typing class Broken: def __setattr__(self, name, value): if name == '__orig_class__': raise RuntimeError("simulated bug, unrelated to 'cannot set attribute'") object.__setattr__(self, name, value) alias = typing._GenericAlias(Broken, (int,)) result = alias() # succeeds silently despite the RuntimeError
- Suggested fix: Narrow to
except (AttributeError, TypeError)(the documented cases per the adjacent comment). - Difficulty: Small, self-contained.
- Location:
Lib/typing.py:2122-2126(function_proto_hook, installed as__subclasshook__for runtime-checkable protocols) - Problem: Structurally identical to #2 but in the code path used by
issubclass()/isinstance()checks against protocols. A broken metaclass/descriptor here gets silently absorbed during a subclass check instead of propagating. - Reproducer (verified): calling
MyProto.__subclasshook__(OtherProto)whereOtherProto's metaclass raisesRuntimeErroron__annotations__access returnsNotImplementedwith no error, instead of surfacing theRuntimeError. - Suggested fix: Narrow to
except AttributeError, same as #2. - Difficulty: Small, self-contained. Note: fixing #2, #3, and #4 together (they're the same anti-pattern in three places) makes a reasonable single PR.
- Location:
Lib/typing.py:754, 778-781(functionLiteral) - Problem:
Literal[...]deduplicates its arguments via a helper that needs hashability; wrapped inexcept TypeError: pass. If any argument is unhashable (e.g. adict), dedup is abandoned for the entireLiteral, so duplicate unhashable values survive while duplicate hashable values elsewhere are always removed — inconsistent behavior depending purely on hashability. - Reproducer (verified):
import typing print(typing.Literal[1, 1, 2].__args__) # (1, 2) — deduplicated d = {"a": 1} print(typing.Literal[d, d].__args__) # ({'a': 1}, {'a': 1}) — NOT deduplicated
- Suggested fix: Call the existing
_deduplicate(..., unhashable_fallback=True)(already used elsewhere, e.g. forAnnotated) instead of the bare call. - Difficulty: Small, self-contained — the fix is already implemented elsewhere in the same file.
- Location:
Lib/argparse.py:2887-2893 - Problem:
except (AttributeError, OSError): passaround the write call used byprint_help()/print_usage()/error(). Passing a non-file-like object (or a broken stream) asfile=causes the expected output to vanish with zero error, making the bug very hard to notice. - Reproducer (verified):
import argparse p = argparse.ArgumentParser() result = p.print_help(file="not_a_file") print("print_help returned:", result) # None — no help text, no error
- Suggested fix: Remove
AttributeErrorfrom the catch (wrong type entirely shouldn't be silently accepted); narrowOSErrorhandling to closed-stream cases specifically if that was the original intent. - Difficulty: Small, self-contained.
(No standalone valid finding for inspect.py in this catalog — two candidates from the original audit were disputed as intentional/documented; see Appendix C.)
- Location:
Lib/subprocess.py:2165-2194(POSIXPopen._internal_poll) - Problem: Polls child status via
os.waitpid; special-casesECHILDand the__del__-triggered case, but any other unexpectedOSErroris caught and discarded —self.returncodestaysNoneforever, silently masking a real failure instead of surfacing it. - Reproducer (verified):
import subprocess, errno from unittest import mock p = subprocess.Popen(["sleep", "0.2"]) with mock.patch("os.waitpid", lambda pid, flags: (_ for _ in ()).throw(OSError(errno.EINVAL, "simulated"))): print(p.poll()) # None — unexpected OSError silently absorbed
- Suggested fix: Re-raise (or at least log) when
e.errno != ECHILDand_deadstate is None. - Difficulty: Small, self-contained.
- Location:
Lib/subprocess.py:145-155 - Problem:
if self.returncode and self.returncode < 0:treatsNoneas falsy, routing to a%d-formatting branch that can't formatNone— sostr(err)itself raisesTypeError, a particularly bad failure mode for an exception's own string representation. - Reproducer (verified):
import subprocess err = subprocess.CalledProcessError(None, "cmd") str(err) # TypeError: %d format: a real number is required, not NoneType
- Suggested fix: Handle
returncode is Noneexplicitly before the%dbranch. - Difficulty: Small, self-contained. Note: a recent, unrelated PR (#151486 / gh-151485, merged 2026-06-24) touched this same
__str__method for a different bug (command quoting) — worth reading for context, but it doesn't fix this.
- Location:
Lib/subprocess.py:2238-2263 - Problem: Uses
kqueue()as a fast-path child-exit detector; ifkq.control()raises anyOSError, the method just returnsFalse(falls back to normal polling — no state corruption, but a genuinely unexpected error likeEBADFis treated identically to "no event yet," with no visibility for debugging). - Reproducer: verified against a local dev build with kqueue mocked (kqueue is unavailable on Linux) — an injected
EBADFwas silently swallowed and treated as a normal "no event" case. - Suggested fix: At minimum, add a comment explaining why any
OSErroris considered safe here (mirroring an existing comment a few lines above for a related catch). - Difficulty: Small, low-severity polish.
- Location:
Lib/tarfile.py:1680-1685(TarInfo._apply_pax_info) - Problem: An invalid (non-numeric)
size/uid/gid/mtimevalue in a PAX extended header is silently replaced with0onValueError.size=0truncates the extracted file to nothing;uid=0means root ownership. Security-relevant for archives from untrusted sources. - Reproducer (verified): built a tar with a real 100-byte member but a PAX header
sizeof"not-a-number"; on extraction,member.size == 0and the extracted content is empty — the 100 bytes are silently discarded. - Suggested fix: Raise
InvalidHeaderError(consistent with other malformed-PAX-header handling elsewhere in the same file) instead of defaulting to0. - Difficulty: Small, self-contained, but touches security-sensitive code — read carefully before submitting.
12. shutil.move: partial copy left at destination if unlink(src) fails after a successful cross-device copy
- Location:
Lib/shutil.py:930-951 - Problem: Cross-filesystem
move()copies then deletes the source. If the copy succeeds butos.unlink(src)fails (e.g. permission denied), the function raises but leaves the file at both locations, with no cleanup of the destination copy attempted. - Reproducer (verified): monkeypatched
os.renameto raise (simulatingEXDEV) andos.unlinkto raisePermissionErroronly for the source path; after the "failed"move(), bothsrcanddstexist with the file's content. - Suggested fix: Wrap
os.unlink(src)in its own try/except and, on failure, remove the just-created destination copy before re-raising (or clearly document the caveat). - Difficulty: Small-medium — needs care around partial-failure semantics.
13. shutil.move: overly broad except OSError triggers the copy-then-delete fallback for non-EXDEV errors
- Location:
Lib/shutil.py:930-931 - Problem: The fallback is meant to trigger only for cross-device moves (
errno.EXDEV), but catches bareOSError, which also matches same-filesystem permission errors and other unrelated failures — masking what should be a direct, clear error. - Reproducer (verified): mocked
os.renameto raisePermissionError(errno.EACCES, ...)(explicitly notEXDEV); the copy-then-delete fallback still ran. - Suggested fix: Check
err.errno == errno.EXDEVbefore falling back; re-raise directly for otherOSErrorsubtypes. - Difficulty: Small, self-contained. Pairs naturally with #12 (same function, same PR is reasonable).
- Location:
Lib/shutil.py:609-614 - Problem: After copying directory contents,
copystat(src, dst)failures are dropped entirely (not even appended to theerrorslist) if the exception has awinerrorattribute — a guard broad enough to swallow any Windows API error undercopystat, not just the one specific "access time on directory" limitation the adjacent comment describes. - Reproducer: Windows-specific; not reproducible on Linux — verified via source reading only.
- Suggested fix: Narrow the guard to the specific Windows error code(s) actually expected, rather than any
winerror-bearing exception. - Difficulty: Small, but needs a Windows environment to verify — good for a contributor with Windows access.
15. pickle._instantiate: broken exception chaining passes a traceback object as a constructor argument
- Location:
Lib/pickle.py:1623-1630(_Unpickler._instantiate) - Problem: When
klass(*args)raisesTypeErrorduring old-protocol unpickling, the code doesraise TypeError(msg, err.__traceback__)— passing the traceback object as a second positional constructor argument, not attaching it via.with_traceback()orraise ... from err. The result:e.argsliterally contains a traceback object, pollutingstr(exception), with no real exception chaining. - Reproducer (verified):
# via pickle._Unpickler()._instantiate(Bad, (1,2,3)) where Bad.__init__ raises TypeError # e.args == ('in constructor for Bad: original failure', <traceback object at 0x...>) # e.__cause__ is None — no proper chaining occurred
- Suggested fix:
raise TypeError("in constructor for %s: %s" % (klass.__name__, str(err))) from err. - Difficulty: Small, self-contained.
- Location:
Lib/http/cookiejar.py:1620-1634(CookieJar.make_cookies) - Problem: All
Set-Cookie/Set-Cookie2values from one response are parsed together; if any single cookie trips an exception not already handled by the (fairly extensive) per-attribute parsing, the surroundingexcept Exceptiondiscards the whole batch — including cookies that parsed fine — and returns[]. AUserWarningis emitted (so it's not fully invisible), but a legitimate session/auth cookie can vanish just because it was batched alongside one malformed one. - Reproducer (verified): a response with two
Set-Cookieheaders — a validsession_token=SECRET123; Path=/and a malformedbogus=1; Max-Age=<400-digit number>(triggersOverflowErrordeep inside cookie construction) — resulted injar.make_cookies(...)returning[], dropping the valid session cookie too. - Suggested fix: Catch exceptions per-cookie inside the parsing loop (skip just the bad one) instead of wrapping the whole batch.
- Difficulty: Medium — touches cookie-parsing control flow, worth care with existing tests.
- Location:
Lib/http/client.py:396-399(HTTPResponse.begin) - Problem:
except ValueError: self.length = Nonearoundint(length)— a non-numericContent-Lengthis silently treated as "unknown length" (falls back to read-until-close framing) with no warning, even though RFC 7230 §3.3.3 calls a malformedContent-Lengthan unrecoverable framing error. - Reproducer (verified): local HTTP server sending
Content-Length: not-a-numberwithConnection: close; client silently proceeds withself.length = None, no warning or exception. - Suggested fix: Emit a
warnings.warn()when this happens, or document the leniency explicitly. - Difficulty: Small, self-contained.
- Location:
Lib/multiprocessing/resource_tracker.py:480-481 - Problem: When leaked semaphores/shared-memory are detected at shutdown, the descriptive
warnings.warn(...)call is itself wrapped inexcept Exception: pass. Under-W error/PYTHONWARNINGS=error(which converts the warning into a raised exception), the message about what leaked is completely swallowed — only the numericexit_code=1survives. - Reproducer (verified): under
python3 -W error, the warning-to-exception conversion is silently caught, leaving onlyexit_code: 1with no printed message. - Suggested fix: Don't blanket-catch around the
warnings.warn()call itself; if protection is needed, fall back tosys.stderr.writeon failure. - Difficulty: Small, self-contained.
- Location:
Lib/multiprocessing/managers.py:885-890 - Problem: When a proxy's finalizer fails to tell the manager to decrement a referent's refcount (e.g. connection error), the failure is only logged via
util.debug(...), which is a complete no-op unless multiprocessing's internal logger has been explicitly enabled. By default: nothing is printed, and the manager-side object leaks invisibly. - Suggested fix: Escalate to
util.info(still gated but a step up), or add an optional failure callback. - Difficulty: Small, self-contained.
20. multiprocessing.util._run_after_forkers: exceptions are completely invisible by default, not just low-priority
- Location:
Lib/multiprocessing/util.py:237-240 - Problem: After-fork callback failures (which can mean child-process state — locks, RNG state — was never properly reinitialized) go through
util.info(...), which — likeutil.debugabove — is a no-op unless the internal logger is enabled. By default, a corrupted-child-state scenario produces zero output anywhere, and the child continues running as if nothing happened. - Reproducer (verified): registered an after-fork hook raising
RuntimeError; with default settings, no output at all and the child exits 0; withmultiprocessing.util.log_to_stderr(logging.INFO)enabled, the message appears — confirming both the mechanism and its default invisibility. - Suggested fix: Use
sys.stderr.write/warnings.warnas a fallback when the internal logger is unset. - Difficulty: Small-medium — changes default-visible behavior, worth discussing scope first.
- Location:
Lib/multiprocessing/forkserver.py:409-425 - Problem: The forkserver's per-request child process sets
code = 1before running the request handler; on any exception, the traceback is printed butcodeis never adjusted, soos._exit(code)always reports exit status1regardless of what actually failed — all failure types become indistinguishable via exit code alone. - Suggested fix: Set distinct exit codes for different exception categories, or otherwise encode something about the failure type (as
resource_tracker.pyalready does with codes 1/2/3). - Difficulty: Small, low severity — the real traceback is still printed to stderr, only the exit-code granularity is lost.
(A note on scope: this module got the deepest cross-referencing against GitHub history of any bucket — several of the following patterns have very recent sibling fixes at closely related call sites, which is strong evidence maintainers consider this bug class real and worth fixing. Those precedents are cited inline; read the linked PRs before starting work, they're the best model for the expected fix shape.)
- Location:
Lib/asyncio/selector_events.py:220-266 - Problem: If
protocol_factory()or transport creation raises while accepting an incoming connection, the exception is only reported viacall_exception_handlerwhenself._debugis true. In production (the default), the connection is dropped with zero notification anywhere. - Reproducer (verified): a server with a
protocol_factorythat raisesRuntimeError, a real client connecting: withdebug=False,exception_handlercalls = 0; withdebug=True, 1 call with a clear message. (No fd leak either way — the accepted socket is refcounted away via__del__.) - Suggested fix: Call
self.call_exception_handler(context)unconditionally, dropping theif self._debug:guard. - Precedent: gh-153133 / PR #153134 (merged 2026-07-08, one week before this catalog's verification date) fixed the closely related client-side connect path (
asyncio.create_connection()leaking a socket if transport creation fails) with the same "wrap in try/except, clean up, re-raise/report" shape. The accept-side sibling described here has not been filed. - Difficulty: Small, self-contained, well-precedented.
23. shutdown_asyncgens checks Exception, not BaseException — drops custom BaseException subclasses from gather()
- Location:
Lib/asyncio/base_events.py:576-593 - Problem: Uses
tasks.gather(..., return_exceptions=True), then only reports a result viacall_exception_handlerifisinstance(result, Exception). Butasyncio.Taskgenuinely can and does return rawBaseExceptioninstances fromgather(return_exceptions=True)(e.g. a customBaseExceptionsubclass raised from an async generator'sfinally, liketrio.Cancelled-style signals). Those are silently discarded — no logging at all, unconditionally (not even debug-gated). - Reproducer (verified): an async generator whose
finallyraises a customclass MyFatalSignal(BaseException); afterawait loop.shutdown_asyncgens(),exception_handlercalls = 0. - Suggested fix: Change
isinstance(result, Exception)toisinstance(result, BaseException). - Difficulty: Small, self-contained, one-character-class fix — probably the cleanest/safest pick in this whole catalog.
- Location:
Lib/asyncio/base_events.py:625-633 - Problem: Runs in a background thread; if
self._default_executor.shutdown(wait=True)raises and the loop happens to already be closed at that moment, the exception is discarded entirely — no logging, and the associated future is left permanently unresolved (not even cancelled). - Reproducer (verified): replaced
loop._default_executorwith a fake object whoseshutdown()raises, andloop.is_closedto returnTrue; afterloop._do_shutdown(future),exception_handlercalls = 0,futurehas no exception set and isn't done. - Suggested fix: Call
logger.error(...)directly (bypassing the loop, which may be closed) when the exception can't be delivered via the future. - Difficulty: Small — narrow race condition, but reproducible and clearly unhandled.
25. _call_connection_lost's finally block can shadow the original connection_lost() exception (6 call sites, 4 files)
- Locations:
Lib/asyncio/selector_events.py:909-919(_SelectorTransport)Lib/asyncio/selector_events.py:1209-1220(_SelectorDatagramTransport)Lib/asyncio/unix_events.py:620-625(_UnixReadPipeTransport)Lib/asyncio/unix_events.py:825-830(_UnixWritePipeTransport)Lib/asyncio/proactor_events.py:155-172(_ProactorBasePipeTransport— the riskiest of the six, since it callssock.shutdown(), not justclose())Lib/asyncio/base_subprocess.py:275-284(BaseSubprocessTransport)
- Problem: Each site does
try: protocol.connection_lost(exc) finally: <cleanup>. Ifconnection_lost()raises and cleanup also raises, Python's own semantics mean the cleanup exception is what propagates, with the original attached only as__context__— demoted, not lost outright, but easy to miss in a busy log. - Reproducer (verified): with
connection_lost()raisingValueError("ORIGINAL")and cleanup (sock.close()) also raising, the exception that reachescall_exception_handleris the cleanup exception;e.__context__correctly holds the original — confirmed by direct inspection. - Important context: this is not fully silent even today —
_call_connection_lostruns vialoop.call_soon(...), andevents.Handle._runreports any propagating exception viacall_exception_handlerunconditionally, regardless ofdebug. The bug is specifically that a cleanup-time failure can replace the original in that report, not that nothing is reported at all. - Suggested fix: Wrap cleanup steps so a failure there doesn't discard/replace an in-flight exception — e.g.
try/exceptaround cleanup withlogger.exception(...)on failure, then explicit re-raise of the original. - Precedent: gh-145541 / PR #145554 (merged 2026-03-09/10, backported to 3.13/3.14/3.15) fixed a narrower symptom at the
base_subprocess.pysite specifically (a double-set_resultInvalidStateError) — confirms this exact method is an actively maintained hotspot, but that fix didn't address the general shadowing hazard. gh-149388 / PR #149518 (merged 2026-05-09, backported) fixed the Windows-only half of theproactor_events.pysite (madePipeHandle.close()idempotent) — the PR author's own words confirm "the first, legitimate close still propagates the OSError," i.e. the general hazard (including the POSIXsock.shutdown()call this catalog entry describes) remains open. - Difficulty: Medium — six sites, one shared pattern; a good "understand the pattern, fix it everywhere" issue for a contributor ready to touch multiple files in one PR (or split into six small ones).
26. StreamReaderProtocol.connection_made: a synchronous callback that raises leaves the transport open forever
- Location:
Lib/asyncio/streams.py:180-242 - Problem: If
client_connected_cbpassed tostart_server()is a plain (non-coroutine) function and it raises synchronously, the exception is reported viacall_exception_handler(throughHandle._run, unconditionally) — but unlike the coroutine-callback branch a few lines below, the transport is never closed. The server-side connection is left open/hanging indefinitely. - Reproducer (verified):
asyncio.start_server(sync_cb, ...)wheresync_cbraisesRuntimeError,debug=False:exception_handlerfires once with a clear message, buttransport.is_closing()isFalseafterward — the connection stays open. - Suggested fix: Wrap the synchronous call the same way the coroutine branch already is: on failure, call
call_exception_handler(already happening via the generic handler) andtransport.close(). - Precedent: gh-110894 / PR #111601 (merged Nov 2023) fixed exactly this for the coroutine branch, explicitly scoped as
if coroutines.iscoroutine(res):— the plain-functionelsebranch was left with no equivalent cleanup. This entry is the direct, well-precedented completion of that fix. - Difficulty: Small — extend an existing, already-merged fix pattern to one more branch of the same function.
27. importlib._bootstrap._set_lazy_attributes: downgrades any error, including SystemError/MemoryError, to a filtered-by-default ImportWarning
- Location:
Lib/importlib/_bootstrap.py:1311-1316 - Problem: Part of the new PEP 810 explicit-lazy-imports machinery (added 2026, via gh-142349). After loading any module,
_imp._set_lazy_attributes(module, name)is called insidetry/except Exception, converting any failure into anImportWarning. SinceImportWarningis filtered out by Python's default warning configuration, this makes even a genuinely serious internal error (a bug in the brand-new lazy-attributes code,MemoryError, etc.) invisible on every single import, by default. - Reproducer (verified): on a dev build with PEP 810, monkeypatched
_imp._set_lazy_attributesto raiseSystemError, with the standard default warning filters installed: importing a module produced zero output. Withwarnings.simplefilter("always"), the same error surfaced as anImportWarning— confirming it's real but normally invisible. - Suggested fix: Only downgrade genuinely expected/benign failures to
ImportWarning; let serious exceptions (MemoryError,SystemError) propagate or at minimum log visibly. - Difficulty: Small-medium — new code (2026), no prior discussion found, good candidate to raise with whoever's actively working on PEP 810.
28. SysLogHandler leaves a closed socket in a truthy state after a failed connection, defeating the reconnect check
- Location:
Lib/logging/handlers.py:906-928(_connect_unixsocket) - Problem: On connection failure,
self.socket.close()is called butself.socketis never reset toNone.emit()'s reconnect check isif not self.socket: self.createSocket()— a closed-but-non-None socket object is still truthy, so the check wrongly concludes the socket is fine and skips reconnecting. - Reproducer (verified):
SysLogHandler(address="/tmp/nonexistent_syslog_socket_xyz")— after the failed connect,self.socketis a closed socket object (fileno() == -1, still truthy), and a subsequent.send()raisesOSError: Bad file descriptor. - Suggested fix: Set
self.socket = Nonein bothexcept OSError:blocks in_connect_unixsocket(). - Difficulty: Small, self-contained.
- Location:
Lib/logging/config.py:133, 155, 159, 161 - Problem: INI-based handler/formatter setup
eval()s the rawargs=/kwargs=/defaults=strings from the config file with no surrounding try/except infileConfig()itself (unlikeDictConfigurator, whose methods wrap failures with a clearValueError('Unable to configure handler %r' % name) from e). A malformed value surfaces as a bareSyntaxErrorpointing at<string>, with no mention of which handler/formatter section is the culprit. - Reproducer (verified): an INI config with an unclosed paren in a handler's
args=;fileConfig()raisesSyntaxError: '(' was never closedthrougheval(), with no indication which section caused it. - Suggested fix: Wrap the
eval()calls with a try/except that re-raises including the section/option name, mirroring the patternDictConfiguratoralready uses. - Precedent: gh-103606 (filed and fixed by logging maintainer vsajip himself) improved a different unhelpful-
fileConfig-error scenario (missing file), confirming this general category of improvement is welcomed upstream. - Difficulty: Small-medium.
- Location:
Lib/importlib/_bootstrap_external.py:1469(inside awhile True:loop, backing the newPathEntryFinder.discover()API, added 2026 via gh-139899) - Problem: Scans a directory via
os.scandir(); anyOSErrorfromnext()or from an entry'sis_dir()/is_file()is caught and ignored without breaking the loop. For a transient single-entry failure this is reasonable (skip and continue). But if the underlying condition is persistent — e.g. an NFS "stale file handle," a disconnected removable volume — every subsequentnext()raises the same error forever, and the loop never terminates: nobreak, no retry limit. - Reproducer (verified): patched
os.scandirto return an iterator whose__next__unconditionally raisesOSErrorforever;finder._find_children()under a 10-secondtimeoutwas killed (exit 124) — confirmed genuine hang. A companion test with a single transient injected error correctly skipped it and returned all real entries, confirming the "skip one bad entry" case works — it's specifically the persistent-failure case that hangs. - Suggested fix: Add a
break(or a bounded retry counter) in theexcept OSError:branch. - Difficulty: Small, but a real hang bug (not just silent data loss) — worth flagging prominently. New code (2026), no existing discussion found.
31. HTTPHandler.emit() never checks the HTTP response status — a rejected log is indistinguishable from a delivered one
- Location:
Lib/logging/handlers.py:1357(insideemit(),Lib/logging/handlers.py:1318) - Problem: Sends the log record over HTTP and calls
h.getresponse()(necessary to keep the connection state clean) but never inspectsresponse.status. A 4xx/5xx rejection from the receiving server looks identical to success;handleError()is never invoked. - Reproducer (verified): a local
http.server.HTTPServerthat always returns HTTP 500, pointed to by anHTTPHandler; logging a record produced empty stderr despite the explicit rejection. - Suggested fix: Check
response.statusand route non-2xx responses throughself.handleError(record). - Difficulty: Small, self-contained.
- Location:
Lib/logging/handlers.py:1118-1147 - Problem: Opens an SMTP connection;
smtp.quit()is only called as the last statement in thetryblock, afterehlo()/starttls()/login()/send_message(). If any of those steps raises (e.g.SMTPAuthenticationError), execution jumps toexcept Exception: self.handleError(record)andsmtp.quit()/smtp.close()is never called — the connection leaks. - Reproducer (verified): mocked
smtplib.SMTPto raiseSMTPRecipientsRefusedfromsend_message(); confirmedquit()/close()were never called (the connection was left open, even though the failure itself was visibly reported viahandleError()). - Suggested fix:
try/finally: smtp.quit()(or.close()on failure) around the post-connect logic. - Difficulty: Small, self-contained.
33. xml.sax.expatreader: bare except: during external-entity resolution discards the real error — has its own FIXME comment
- Location:
Lib/xml/sax/expatreader.py:426-428(ExpatParser.external_entity_ref) - Problem: Any exception during external-entity processing — including one raised inside a user's own SAX content-handler callback — is caught by a bare
except: return 0. Expat then reports a genericSAXParseException: "error in processing external entity reference"to the caller; the original exception's type, message, and traceback are gone. The code has a literal comment acknowledging this:# FIXME: save error info here? - Reproducer (verified): a content handler that raises
KeyboardInterruptwhile processing an element sourced from an external entity results in a genericSAXParseException, not the originalKeyboardInterrupt. - Suggested fix: Catch a narrower exception set (or at minimum re-raise
BaseExceptionsubclasses), and/or preservesys.exc_info()/ useraise ... from errso the real cause is surfaced. - Difficulty: Small-medium — the fix itself is small, but improving error propagation through the Expat C-extension boundary needs care.
34. email._header_value_parser: one bare except: pass, an outlier against 31 other call sites in the same file
- Location:
Lib/email/_header_value_parser.py:2538-2541(insideget_parameter, guardingget_extended_attrtext) - Problem: Every other call site in this file (31 of them) catches the specific
errors.HeaderParseError; this one bareexcept: passcatches everything. Currently benign (the guarded call only raisesHeaderParseErrorin practice) but silently swallows anything a future refactor might raise here instead. - Reproducer (verified):
Parser(policy=policy.default).parsestr('Content-Type: application/x-stuff; title*0*="us-ascii\'en\'This%20is%20"\n\n')exercises this exact branch and produces the expectedInvalidHeaderDefect. - Suggested fix:
except errors.HeaderParseError:to match the other 31 sites. - Difficulty: Trivial — smallest fix in this whole catalog.
34b. Bonus, discovered during verification of #34's neighbor: headerregistry.UnstructuredHeader.parse never propagates defects recorded on the underlying parse tree
- Location:
Lib/email/headerregistry.py—UnstructuredHeader.parse(compareAddressHeader.parse,MIMEVersionHeader.parse,ContentTransferEncodingHeader.parse, which all copyparse_tree.all_defectsintokwds['defects']) - Problem: Not part of the original audit — found while verifying the base64-decode-defect finding below (#35). Parsing a header like
Subject: =?utf-8?b?AAAAA?=(base64 payload with an unrecoverable length error) records anInvalidBase64LengthDefecton the innerEncodedWordparse-tree node — butmsg['Subject'].defectscomes back()(empty), even thoughtok.all_defectson the parse tree shows the defect really was recorded.UnstructuredHeader.parsesimply never copies it up, unlike its sibling*Header.parseimplementations. - Suggested fix: Have
UnstructuredHeader.parsecopyparse_tree.all_defectsintokwds['defects'], matching the other header-type parsers. - Difficulty: Small-medium — needs to check whether
Unstructuredheaders were deliberately excluded from defect-copying for a reason, or whether this is a plain oversight.
35. email._encoded_words.decode_b: caller decodes still-base64-encoded bytes as if they were real content (only the "no timeout" half is a bug — see below)
- Location:
Lib/email/_encoded_words.py:130-136 - (Verification note: this was flagged DISPUTED by the review — the behavior is a deliberate, discussed design decision from
bpo-27397/#71584/PR #7583 (2018), where a maintainer explicitly chose "return the undecoded payload" over raising, per the email module's stated philosophy of never raising on parse and always recording a defect instead. Not included as an actionable item — listed here only because it led directly to the real, distinct bug #34b above. See Appendix C for the full reasoning.)
- Location:
Lib/xml/sax/saxutils.py:338-368 - Problem: A public utility function that opens a local file or URL and stores the handle on the returned
InputSource, with nothing about the return value (no context manager, no owning close) guaranteeing the handle gets closed if the caller errors out before consuming it. - Reproducer (verified): obtained an
InputSourcefrom a real temp file viaprepare_input_source, thendeld it and forcedgc.collect()— the file was still open (ResourceWarning: unclosed file), only cleaned up via GC finalization, not deterministically. - Context:
expatreader.py's own top-levelparse()was separately hardened against this (bpo-30264, "ExpatParser closes the source on error") — but that fix lives in the caller, not inprepare_input_sourceitself, so any other caller of this public function remains exposed. - Suggested fix: Document that callers must close the returned stream, and/or apply the same guard
expatreader.parse()uses to other stdlib callers (e.g.xml.dom.xmlbuilder.DOMBuilder.parse). - Difficulty: Medium — public API, resource-lifecycle change needs care.
- Location:
Lib/xml/dom/pulldom.py:318-326(parse()),204-278(DOMEventStreamclass) - Problem: The file is only closed automatically when the caller reads all the way to EOF via
getEvent(). A very commonpulldomusage pattern — callexpandNode()on the first interesting node, then discard the stream — never triggers that close.DOMEventStreamhas noclose(), no__del__, and no__enter__/__exit__. - Reproducer (verified):
events = pulldom.parse(path); next(events)(one event, stream not exhausted), thendel events; gc.collect()— file remained open with aResourceWarning; confirmed noclose/__del__/__exit__exist on the class at all. - Suggested fix: Add
close()(closingself.stream/self.parser) and context-manager support toDOMEventStream, matching the patternElementTree.iterparse()already has (close()added in gh-69893, aResourceWarningadded in gh-140601). - Difficulty: Small-medium, well-precedented by the
iterparse()fix.
- Location:
Lib/xml/dom/xmlbuilder.py:212-245(DOMEntityResolver.resolveEntity/_get_opener),190-194(DOMBuilder.parse) - Problem: Both places fetch a URL (from potentially attacker-supplied XML content) via
urlopen-family calls with notimeout=anywhere in the file. A slow or hanging remote server hangs the parse indefinitely. - Suggested fix: Add a
timeout=parameter (with a sane default) to theurlopen/opener calls. - Note: the broader "should external entities auto-fetch URLs at all" question is a longstanding, documented, module-wide XML tradeoff (see CPython's
xml-securitydocs section, which recommendsdefusedxmlfor untrusted input) — not something this entry is claiming is novel. Only the missing timeout specifically is unaddressed anywhere. - Difficulty: Small, self-contained — scope it to just the timeout, not a redesign of entity resolution.
39. xml.etree.ElementTree.XMLPullParser.feed(): parse errors are queued silently and easy to lose entirely
- Location:
Lib/xml/etree/ElementTree.py:1316-1324 - Problem:
feed()catchesSyntaxErrorfrom the underlying parser and appends it to an internal events queue instead of raising;feed()itself always returnsNone. If the caller only callsfeed()in a loop (a legitimate, common streaming pattern) and never callsread_events()— or stops consuming before reaching the queued error — the parse failure is invisible with no other observable effect. - Reproducer (verified):
parser.feed(b"<root>&bogus_undefined_entity;</root>")returns normally with no exception; theParseErrorsits in the internal queue, invisible unless later drained. Also observed: a subsequentclose()can raise a differentParseError(different line/column) than the one actually queued first — the original error's info can be lost entirely even by callers who do eventually callclose(). - Suggested fix: Document the queuing behavior prominently (currently undocumented for
feed()), and/or expose a cheap "has an error been queued" check that doesn't require drainingread_events(). - Difficulty: Medium — behavioral/API question in a widely-used module, worth discussing scope before implementing.
40. xml.dom.domreg.getDOMImplementation: fallback discovery masks genuine bugs, not just "not installed"
- Location:
Lib/xml/dom/domreg.py:73-76 - Problem: When called with no explicit implementation name, tries each well-known implementation (
minidom,4DOM) in turn underexcept Exception: continue(comment: "typically ImportError, or AttributeError"). A genuine bug in a candidate'sgetDOMImplementation()factory (e.g. a realAttributeErrorfrom a typo) is caught identically to "this implementation isn't available" and silently skipped. - Reproducer (verified): registered a fake well-known implementation whose
getDOMImplementation()deliberately raisesAttributeErrorto simulate a real bug (not a missing-moduleImportError); the fallback loop silently moved on tominidomwith no trace of the failure. - Suggested fix: Catch
(ImportError, AttributeError)specifically, matching what the comment already says is "typical." - Difficulty: Trivial. Note: low real-world impact since
well_known_implementationsis a tiny, fixed, mostly-dead-code dict — good for a first-time contributor precisely because it's low-stakes.
| Original finding | Existing issue/PR |
|---|---|
multiprocessing/queues.py: feeder thread silently drops unpicklable objects on put() |
python/cpython#84376 (open since 2020-04-05, migrated from bpo-40195) |
| Original finding | Fix |
|---|---|
dataclasses.py: _update_func_cell_for__class__ crashes on an empty closure cell |
gh-148947 / PR #148948, merged 2026-04-25 |
These looked plausible in the original AI-generated audit but turned out, on direct verification, to be either (a) code that already handles the case correctly (often via a bare raise/re-raise the audit missed), (b) explicitly documented, intended behavior, or (c) a previously-litigated design decision a CPython maintainer already reviewed and declined to change. Roughly 30 of the original 68 candidates fell into this bucket — a useful reminder that "an AI found this" is a starting point for investigation, not a finding.
tarfile.pybareexcept:(×2) — both end in an unconditionalraise; nothing is swallowed.configparser.read()silently skips unreadable files — explicitly documented inconfigparser.rst: "designed so you can specify an iterable of potential configuration file locations."tarfile.next()'sraise e—raise edoes not lose the traceback in Python 3 (verified empirically); one branch structurally requiresraise eover bareraise.zipfile.ZipExtFile'sexcept AttributeError: passaroundseekable()— deliberately added inbpo-22908to support duck-typed file objects without.seekable().tarfile.gettarinfo's uid/gid-name fallback to''— only the cosmetic name field is affected; numeric uid/gid (what actually matters for extraction) is unaffected. Matches GNU tar behavior.zipfile.testzip()returning a filename instead of raising — the documented API contract, unchanged since introduction.tarfileextraction errors invisible atdebug=0— matches documenteddebug/errorlevelsemantics exactly.multiprocessing/managers.pyshutdown "leaves zombie processes" — false:gh-91231added a terminate/kill fallback that reliably reaps the process regardless.multiprocessing/resource_tracker.pybareexcept:"causing leaks to go undetected" — doesn't match current code; the only bare-except sites either re-raise or don't gate leak detection, which runs unconditionally in afinallyblock.multiprocessing/managers.py serve_client"hangs forever on dead connection" — false: the actual handler explicitly closes the connection and exits the serving thread.multiprocessing/forkserver.pybareexcept:— re-raises; the original report itself flagged this as likely a non-issue, confirmed.multiprocessing/pool.pyresult/task handlers "silently drop" failures — false in both cases: failures are explicitly forwarded to the caller via theAsyncResultmechanism; the onlyexcept KeyError: passpresent guards an already-completed-job case, not real data loss.multiprocessing/managers.pyproxy__str__fallback — the method's own docstring documents the fallback-on-failure contract explicitly.http/server.py'ssuppress(Exception)— narrowly scoped to onesetsockoptcall with an explanatory comment for expected IPv4-only failure.multiprocessing/popen_forkserver.py's hardcoded255— a defensible "abnormal termination" sentinel, only used when the real exit code is structurally unavailable.asyncio._fatal_errordiscardingOSErrorunlessdebug=True— independently reported and closed as working-as-intended twice (gh-109215, gh-68765); the application-facingconnection_lost(exc)callback still receives the real exception regardless of debug mode.asyncio's tworaise OSError(...) from Nonesites — the identical idiom exists deliberately inLib/socket.py; no diagnostic information is actually lost since the original error is embedded in the new message.logging.handlers.SocketHandler"silently drops" records — explicitly documented in both the code and the official docs; extensively discussed and declined by the logging maintainer in bpo-40352/gh-84532.logging.configlistener'sdictConfig→fileConfigcascading fallback — the core mechanism was reviewed and kept by the maintainer in gh-127567; practical impact is also overstated (FileHandlerself-heals,StreamHandlerisn't even touched in testing).importlib._bootstrap_external.pypyc validation swallowingImportError/EOFError— this is the bytecode-cache invalidation mechanism, working exactly as designed; the sourceless-loader variant (no fallback available) correctly does not swallow the same errors, confirming the scoping is deliberate.importlib._bootstrap_external.py _fix_up_module'sexcept Exception: pass— explicitly cross-referenced in its own comment to two sibling call sites with the same documented policy.logging.LogRecord'sexcept Exception: passforprocessName/taskName— carries an explicit in-code justification dating to a 2010-era bug (issue 8200) about multiprocessing import-order races; this is on the hottest possible code path (every log call).importlib._bootstrap.py _lock_unlock_module's deadlock tolerance — the standard, decades-old, well-known "accept a partially-initialized module rather than deadlock" circular-import policy, cross-referenced elsewhere in the same file.inspect._findclassraising on nested qualnames — real quirk, but its only caller (getdoc()) has wrapped it in a guard since 2020; no crash actually reaches any public API.inspect.classify_class_attrs's broadexcept Exception— explicitly documented in the function's own docstring, with a comment referencing a specific ~20-year-old bug (bug #1785) that motivated the deliberately broad catch.typing.overload'sexcept AttributeError: # Not a normal function; ignore.— deliberately added alongsideget_overloads(), per its own comment.xml.dom.expatbuilder's bareexcept:— ends in an unconditionalraise;KeyboardInterruptpropagates correctly (verified by injection).email._encoded_words.decode_b's "inconsistent return type" — a deliberate, discussed design choice frombpo-27397(2018): the email module's philosophy is to never raise on parse, only record defects. (This investigation did surface a real, different, unfiled bug as a side effect — see #34b above.)
inspect.getblock's possibly-unbound_tokenafter certainSyntaxErrors (Lib/inspect.py:1138-1155). The code pattern (_, *_token_info = _tokenusing aforloop variable that might never have been bound) would genuinely crash withUnboundLocalErrorif reached — but extensive fuzzing against the current C-accelerated tokenizer could not find any input that actually reaches this branch: most malformed-syntax inputs raisetokenize.TokenError(whichgetblock'sexcept SyntaxErrordoesn't even catch), and the oneSyntaxErrorsubtype that can occur (IndentationError) is intercepted earlier. May be dead defensive code from a 2023 fix (gh-105013) that's no longer reachable given later tokenizer changes — or there may be a triggering input that wasn't tried. Someone with deeper tokenizer expertise should take a second look before filing.email.message.get_content_charset()— the original audit's claim ("returnsfailobjon an encoding-lookup error") does not match actual behavior: verified that on a bad RFC 2231charset*=name, the function instead returns the raw, still-undecoded payload string as if it were a valid charset name (aLookupErroratLib/email/message.py:945-946, not thefailobjbranch at950-951, which is a separate and clearly-intentional "must be pure ASCII" guard). There may be a real bug here, but it needs to be re-described accurately around the actual mechanism before it's actionable.
Thanks for the great catalog -- I used #2/#4 (
except Exceptionin_get_protocol_attrs/_proto_hook) for my first real contribution attempt.I filed an issue and PR, and got feedback from a typing module maintainer (JelleZijlstra) that this is intentional. Annotations can execute arbitrary code, so
__annotations__access can legitimately raise more than justAttributeError/NameError, and theFORWARDREFformat is the safe fallback for those cases.I confirmed this myself: a function call inside an annotation raising
RuntimeError/TypeErrorstill falls back cleanly throughFORWARDREF, since it never evaluates the expression. Narrowing toexcept (AttributeError, NameError)breaks that.Probably worth moving #2 and #4 to Appendix C. Linking the issue/PR for reference: