Skip to content

F10: inside a code object - #128

Merged
tamnd merged 1 commit into
mainfrom
f10-inside-a-code-object
Aug 31, 2026
Merged

F10: inside a code object#128
tamnd merged 1 commit into
mainfrom
f10-inside-a-code-object

Conversation

@tamnd

@tamnd tamnd commented Aug 31, 2026

Copy link
Copy Markdown
Owner

F09 ended with a code object falling out of the assembler. This one opens it up.

The hook is that a code object holds no values. Not one. It has the instructions, the literals, the names to look up later and the list of slots a frame will need, and every actual value your program computes lives somewhere else. That is what makes one code object able to serve every call, and it is the thing to have straight before F13 builds a frame.

What is in it

It starts by compiling a small file with a class, a method and a generator in it, then walking co_consts recursively. A nested definition is not a separate thing the compiler hands back alongside the module, it is stored as a constant of the enclosing code object, sitting next to the numbers and the strings. Printing the tree makes that concrete in about four lines.

The part that earns the lesson is localsplus. co_varnames, co_cellvars and co_freevars read like three tuples and are three filtered views of one array, and for def outer(a, b=2, *args, c, **kw) with a nested function reading a, the array has seven slots and the three tuples add up to eight. a is tagged both local and cell, get_localsplus_names tests one bit at a time, and so a comes out of two of the filters. The lesson enumerates the real array with _varname_from_oparg and prints which tuples each slot turns up in, which makes the overcount obvious rather than surprising.

That tag is also the answer to a question a reader has probably had for a while, which is why dis sometimes shows two instructions before the first line of the function. MAKE_CELL 0 has to replace the value in slot 0 with a cell holding it before anything can read it, so it runs before your code does.

Equality

code_richcompare compares the name, the argument counts, the flags, the first line number, the constants, the names, the side tables, and the bytecode through _Py_GetBaseCodeUnit, which is the unspecialized form. It never compares co_filename. So the same source compiled from /one/place.py and from /somewhere/else.py gives two code objects that are equal and hash the same, and the filename is carried for tracebacks rather than for identity.

The specialization half is nicer than it sounds. Run a function five thousand times, and it still equals a fresh compile, and co_code is byte for byte identical, and _co_code_adaptive underneath it is not. Three lines, and the reader has seen the quiet copy the interpreter has been writing into the whole time.

Then replace, which gives a new object rather than changing this one, and the AttributeError you get for assigning to co_name, printed rather than described.

The usual

Eighteen cells, six diagrams, six claims each with a runnable cell under it and no unobservable ones, eight citations into Include/cpython/code.h, Include/internal/pycore_code.h, Objects/codeobject.c and Python/assemble.c. No new glossary terms: code object, frame, cell and the rest were all already in there. One version note, the usual one saying which interpreter you are on. just check passes and the notebook runs top to bottom on Pyodide as well as on 3.14 and 3.15.

F11 takes the two side tables this one only names, the line table and the exception table.

What compiling a file actually hands you. The lesson walks a module down
to a method through co_consts, because a nested definition is stored as a
constant next to the numbers and strings, and then takes the record apart.

The part worth the lesson is localsplus. co_varnames, co_cellvars and
co_freevars look like three separate tuples and are three views of one
array, filtered by tag bytes, and a parameter a nested function reads is
tagged both local and cell so it appears in two of them. Seven slots come
out of _varname_from_oparg and the tuples add up to eight. That is also
why a closure's first instruction runs before line one: MAKE_CELL has to
put the cell in the slot before anything can read it.

Then co_flags one bit at a time, then equality, which compares the
bytecode in its unspecialized form and never compares the filename. Two
compiles of the same source from two different files are equal and hash
the same, and a function run five thousand times still equals a fresh
compile while _co_code_adaptive underneath it does not. Then replace and
the AttributeError you get for trying to assign.

Eighteen cells, six diagrams, six claims each with a cell under it, eight
citations into Include/cpython/code.h, Objects/codeobject.c and
Python/assemble.c.
@tamnd tamnd added kind/lesson A chapter: prose, notebook, experiments, boss fight area/frontend Tokenizer, PEG parser, AST, symbol table, codegen, code objects labels Aug 31, 2026
@tamnd
tamnd merged commit 8cc7919 into main Aug 31, 2026
16 checks passed
@tamnd
tamnd deleted the f10-inside-a-code-object branch August 31, 2026 11:42
@tamnd tamnd mentioned this pull request Aug 31, 2026
5 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/frontend Tokenizer, PEG parser, AST, symbol table, codegen, code objects kind/lesson A chapter: prose, notebook, experiments, boss fight

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant