[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)
[Python 3.10 issues] Internal Changes Exposed Bugs (PEP 626 & friends)

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.

 

Tag list:

Subscribe

Subscribe to our newsletter and never miss out lastest news.