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.