[Python 3.10 issues] Internal Changes Exposed Bugs (PEP 626 & friends)
By JoeVu, at: Aug. 6, 2025, 3:01 p.m.
Estimated Reading Time: __READING_TIME__ minutes
![[Python 3.10 issues] Internal Changes Exposed Bugs (PEP 626 & friends)](/media/filer_public_thumbnails/filer_public/3b/ec/3bec560d-bee3-4ee5-891b-5f4b2579170a/python_310_issues_internal_changes_exposed_bugs_pep_626__friends.png__1500x900_q85_crop_subsampling-2_upscale.jpg)
![[Python 3.10 issues] Internal Changes Exposed Bugs (PEP 626 & friends)](/media/filer_public_thumbnails/filer_public/3b/ec/3bec560d-bee3-4ee5-891b-5f4b2579170a/python_310_issues_internal_changes_exposed_bugs_pep_626__friends.png__400x240_q85_crop_subsampling-2_upscale.jpg)
Symptoms you might see
-
Debuggers/profilers/tracers behaving oddly (breakpoints miss lines, coverage mismatches).
-
Tools relying on bytecode offsets break (e.g., assumptions about frame.f_lasti).
-
Custom instrumentation that inspects frames/bytecode becomes inaccurate.
Why this happens
Python 3.10 adopted precise line number tables (PEP 626), changing how the interpreter maps bytecode to source lines. As part of this:
-
frame.f_lasti effectively became an instruction offset rather than a raw byte offset (the interpreter switched to wordcode previously; 3.10 tightened semantics). See What’s New in 3.10 → Tracebacks & line numbers and the frame object docs.
-
Line number tables are now more accurate for exceptions and tracing, which is great for users, but breaks tools that depended on old, informal behaviors.
Fix / migration patterns
1) Don’t rely on internal bytecode details. Use supported APIs.
-
Prefer the dis module to reason about instructions/offsets instead of hardcoding offsets.
-
For source locations, rely on frame.f_lineno and code object attributes (co_filename, co_firstlineno, co_positions() in 3.11+) rather than decoding internals yourself. See inspect.
2) Update tracing/debugging logic for PEP 626 semantics.
-
If you implement sys.settrace/sys.setprofile, expect more trace events and more accurate line hops. Review the tracing hooks docs and adjust filters so you don’t over-count coverage or miss branches.
3) Use higher‑level libraries where possible.
-
For code rewriting/analysis, prefer ast/LibCST over bytecode poking. Bytecode is not a stable interface; the AST is.
4) Rebuild & retest C extensions and any frame/bytecode integrations.
-
If a C extension touches frame internals or code objects, recompile on 3.10 and audit against the 3.10 C-API changes.
Example: tracing tool that assumed byte offsets
# Old: comparing raw f_lasti (treated like byte offsets)
def trace(frame, event, arg):
if frame.f_lasti in SOME_BYTE_OFFSETS: # brittle
...
return trace
# New: use dis to map instruction offsets robustly
import dis
def trace(frame, event, arg):
code = frame.f_code
instrs = list(dis.get_instructions(code))
# match by (offset, starts_line) or by opname/name patterns instead of raw numbers
...
return trace
How to avoid pain next time
-
Treat bytecode as unstable. If you must inspect it, go through dis and keep logic resilient to offset shifts.
-
Add CI on new Python versions early (alphas/betas) so tracing/coverage tools surface changes before GA.
-
Prefer AST/source-level instrumentation for portability across versions.