This PEP proposes adding a mapping from each bytecode instruction to the startand end column offsets of the line that generated them as well as the end linenumber. This data will be used to improve tracebacks displayed by the CPythoninterpreter in order to improve the debugging experience. The PEP also proposesadding APIs that allow other tools (such as coverage analysis tools, profilers,tracers, debuggers) to consume this information from code objects.
The primary motivation for this PEP is to improve the feedback presented aboutthe location of errors to aid with debugging.
Python currently keeps a mapping of bytecode to line numbers from compilation.The interpreter uses this mapping to point to the source line associated withan error. While this line-level granularity for instructions is useful, asingle line of Python code can compile into dozens of bytecode operationsmaking it hard to track which part of the line caused the error.
Consider the following line of Python code:
x['a']['b']['c']['d']=1
If any of the values in the dictionaries areNone, the error shown is:
Traceback(mostrecentcalllast):File"test.py",line2,in<module>x['a']['b']['c']['d']=1TypeError:'NoneType'objectisnotsubscriptable
From the traceback, it is impossible to determine which one of the dictionarieshad theNone element that caused the error. Users often have to attach adebugger or split up their expression to track down the problem.
However, if the interpreter had a mapping of bytecode to column offsets as wellas line numbers, it could helpfully display:
Traceback(mostrecentcalllast):File"test.py",line2,in<module>x['a']['b']['c']['d']=1~~~~~~~~~~~^^^^^TypeError:'NoneType'objectisnotsubscriptable
indicating to the user that the objectx['a']['b'] must have beenNone.This highlighting will occur for every frame in the traceback. For instance, ifa similar error is part of a complex function call chain, the traceback woulddisplay the code associated to the current instruction in every frame:
Traceback(mostrecentcalllast):File"test.py",line14,in<module>lel3(x)^^^^^^^File"test.py",line12,inlel3returnlel2(x)/23^^^^^^^File"test.py",line9,inlel2return25+lel(x)+lel(x)^^^^^^File"test.py",line6,inlelreturn1+foo(a,b,c=x['z']['x']['y']['z']['y'],d=e)~~~~~~~~~~~~~~~~^^^^^TypeError:'NoneType'objectisnotsubscriptable
This problem presents itself in the following situations.
Traceback(mostrecentcalllast):File"test.py",line19,in<module>foo(a.name,b.name,c.name)AttributeError:'NoneType'objecthasnoattribute'name'
With the improvements in this PEP this would show:
Traceback(mostrecentcalllast):File"test.py",line17,in<module>foo(a.name,b.name,c.name)^^^^^^AttributeError:'NoneType'objecthasnoattribute'name'
Traceback(mostrecentcalllast):File"test.py",line1,in<module>x=(a+b)@(c+d)ValueError:operandscouldnotbebroadcasttogetherwithshapes(1,2)(2,3)
There is no clear indication as to which operation failed, was it the additionon the left, the right or the matrix multiplication in the middle? With thisPEP the new error message would look like:
Traceback(mostrecentcalllast):File"test.py",line1,in<module>x=(a+b)@(c+d)~~^~~ValueError:operandscouldnotbebroadcasttogetherwithshapes(1,2)(2,3)
Giving a much clearer and easier to debug error message.
Debugging aside, this extra information would also be useful for codecoverage tools, enabling them to measure expression-level coverage instead ofjust line-level coverage. For instance, given the following line:
x=foo()ifbar()elsebaz()
coverage, profile or state analysis tools will highlight the full line in bothbranches, making it impossible to differentiate what branch was taken. This isa known problem inpycoverage.
Similar efforts to this PEP have taken place in other languages such as Java inthe form ofJEP358.NullPointerExceptions in Java were similarly nebulous whenit came to lines with complicated expressions. ANullPointerException wouldprovide very little aid in finding the root cause of an error. Theimplementation for JEP358 is fairly complex, requiring walking back through thebytecode by using a control flow graph analyzer and decompilation techniques torecover the source code that led to the null pointer. Although the complexityof this solution is high and requires maintenance for the decompiler every timeJava bytecode is changed, this improvement was deemed to be worth it for theextra information provided forjust one exception type.
In order to identify the range of source code being executed when exceptionsare raised, this proposal requires adding new data for every bytecodeinstruction. This will have an impact on the size ofpyc files on disk andthe size of code objects in memory. The authors of this proposal have chosenthe data types in a way that tries to minimize this impact. The proposedoverhead is storing twouint8_t (one for the start offset and one for theend offset) and the end line information for every bytecode instruction (inthe same encoded fashion as the start line is stored currently).
As an illustrative example to gauge the impact of this change, we havecalculated that including the start and end offsets will increase the size ofthe standard library’s pyc files by 22% (6MB) from 28.4MB to 34.7MB. Theoverhead in memory usage will be the same (assuming thefull standard libraryis loaded into the same program). We believe that this is a very acceptablenumber since the order of magnitude of the overhead is very small, especiallyconsidering the storage size and memory capabilities of modern computers.Additionally, in general the memory size of a Python program is not dominatedby code objects. To check this assumption we have executed the test suite ofseveral popular PyPI projects (including NumPy, pytest, Django and Cython) aswell as several applications (Black, pylint, mypy executed over either mypy orthe standard library) and we found that code objects represent normally 3-6% ofthe average memory size of the program.
We understand that the extra cost of this information may not be acceptable forsome users, so we propose an opt-out mechanism which will cause generated codeobjects to not have the extra information while also allowing pyc files to notinclude the extra information.
In order to have enough information to correctly resolve the locationwithin a given line where an error was raised, a map linking bytecodeinstructions to column offsets (start and end offset) and end line numbersis needed. This is similar in fashion to how line numbers are currently linkedto bytecode instructions.
The following changes will be performed as part of the implementation ofthis PEP:
co_positions that will return a sequence offour-element tuples containing the full location of every instruction(including start line, end line, start column offset and end column offset)orNone if the code object was created without the offset information.intPyCode_Addr2Location(PyCodeObject*co,intaddrq,int*start_line,int*start_column,int*end_line,int*end_column)
will be added so the end line, the start column offsets and the end columnoffset can be obtained given the index of a bytecode instruction. Thisfunction will set the values to 0 if the information is not available.
The internal storage, compression and encoding of the information is left as animplementation detail and can be changed at any point as long as the public APIremains unchanged.
These offsets are propagated by the compiler from the ones stored currently inall AST nodes. The output of the public APIs (co_positions andPyCode_Addr2Location)that deal with these attributes use 0-indexed offsets (just like the AST nodes), but the underlyingimplementation is free to represent the actual data in whatever form they choose to be most efficient.The error code regarding information not available isNone for theco_positions() API,and-1 for thePyCode_Addr2Location API. The availability of the information highly dependson whether the offsets fall under the range, as well as the runtime flags for the interpreterconfiguration.
The AST nodes useint types to store these values. The current implementation, however,utilizesuint8_t types as an implementation detail to minimize storage impact. This decisionallows offsets to go from 0 to 255, while offsets bigger than these values will be treated asmissing (returning-1 on thePyCode_Addr2Location andNone API in theco_positions() API).
As specified previously, the underlying storage of the offsets should beconsidered an implementation detail, as the public APIs to obtain this valueswill return either Cint types or Pythonint objects, which allows toimplement better compression/encoding in the future if bigger ranges would needto be supported. This PEP proposes to start with this simpler version anddefer improvements to future work.
When displaying tracebacks, the default exception hook will be modified toquery this information from the code objects and use it to display a sequenceof carets for every displayed line in the traceback if the information isavailable. For instance:
File"test.py",line6,inlelreturn1+foo(a,b,c=x['z']['x']['y']['z']['y'],d=e)~~~~~~~~~~~~~~~~^^^^^TypeError:'NoneType'objectisnotsubscriptable
When displaying tracebacks, instruction offsets will be taken from thetraceback objects. This makes highlighting exceptions that are re-raised worknaturally without the need to store the new information in the stack. Forexample, for this code:
deffoo(x):1+1/0+2defbar(x):try:1+foo(x)+foo(x)exceptExceptionase:raiseValueError("oh no!")fromebar(bar(bar(2)))
The printed traceback would look like this:
Traceback(mostrecentcalllast):File"test.py",line6,inbar1+foo(x)+foo(x)^^^^^^File"test.py",line2,infoo1+1/0+2~^~ZeroDivisionError:divisionbyzeroTheaboveexceptionwasthedirectcauseofthefollowingexception:Traceback(mostrecentcalllast):File"test.py",line10,in<module>bar(bar(bar(2)))^^^^^^File"test.py",line8,inbarraiseValueError("oh no!")frome^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ValueError:ohno
While this code:
deffoo(x):1+1/0+2defbar(x):try:1+foo(x)+foo(x)exceptException:raisebar(bar(bar(2)))
Will be displayed as:
Traceback(mostrecentcalllast):File"test.py",line10,in<module>bar(bar(bar(2)))^^^^^^File"test.py",line6,inbar1+foo(x)+foo(x)^^^^^^File"test.py",line2,infoo1+1/0+2~^~ZeroDivisionError:divisionbyzero
Maintaining the current behavior, only a single line will be displayedin tracebacks. For instructions that span multiple lines (the end offsetand the start offset belong to different lines), the end line number mustbe inspected to know if the end offset applies to the same line as thestarting offset.
To offer an opt-out mechanism for those users that care about thestorage and memory overhead and to allow third party tools and otherprograms that are currently parsing tracebacks to catch up the followingmethods will be provided to deactivate this feature:
PYTHONNODEBUGRANGES.python-Xno_debug_ranges.If any of these methods are used, the Python compiler willnot populatecode objects with the new information (None will be used instead) and anyunmarshalled code objects that contain the extra information will have it strippedaway and replaced withNone). Additionally, the traceback machinery will notshow the extended location information even if the information was present.This method allows users to:
pyc files by using one of the two methods when said filesare created.pyc files if those were created withthe extra information in the first place.Doing this has avery small performance hit as the interpreter state needsto be fetched when code objects are created to look up the configuration.Creating code objects is not a performance sensitive operation so this shouldnot be a concern.
The change is fully backwards compatible.
A reference implementation can be found in theimplementation fork.
It has been proposed to use a single caret instead of highlighting the fullrange when reporting errors as a way to simplify the feature. We have decidedto not go this route for the following reasons:
something=foo(a,b,c)ifbar(a,b,c)elseother(b,c,d)
tools (such as coverage reporters) want to be able to highlight the totality of the callthat is covered by the executed bytecode (let’s sayfoo(a,b,c)) and not just a singlecharacter. Even if is technically possible to re-parse and re-tokenize the source codeto re-construct the information, it is not possible to do this reliably and wouldresult in a much worse user experience.
Having a configure flag to opt out of the overhead even when executing Pythonin non-optimized mode may sound desirable, but it may cause problems whenreading pyc files that were created with a version of the interpreter that wasnot compiled with the flag activated. This can lead to crashes that would bevery difficult to debug for regular users and will make different pyc filesincompatible between each other. As this pyc could be shipped as part oflibraries or applications without the original source, it is also not alwayspossible to force recompilation of said pyc files. For these reasons we havedecided to use the -O flag to opt-out of this behaviour.
One potential solution to reduce the memory usage of this feature is to notload the column information from the pyc file when code is imported. Only if anuncaught exception bubbles up or if a call to the C-API functions is made willthe column information be loaded from the pyc file. This is similar to how weonly read source lines to display them in the traceback when an exceptionbubbles up. While this would indeed lower memory usage, it also results in afar more complex implementation requiring changes to the importing machinery toselectively ignore a part of the code object. We consider this an interestingavenue to explore but ultimately we think is out of the scope for this particularPEP. It also means that column information will not be available if the user isnot using pyc files or for code objects created dynamically at runtime.
Although it would be possible to implement some form of compression over thepyc files and the new data in code objects, we believe that this is out of thescope of this proposal due to its larger impact (in the case of pyc files) andthe fact that we expect column offsets to not compress well due to the lack ofpatterns in them (in case of the new data in code objects).
Thanks to Carl Friedrich Bolz-Tereick for showing an initial prototype of thisidea for the Pypy interpreter and for the helpful discussion.
This document is placed in the public domain or under theCC0-1.0-Universal license, whichever is more permissive.
Source:https://github.com/python/peps/blob/main/peps/pep-0657.rst
Last modified:2025-11-07 04:32:09 GMT