Skip to content

F12. What ends up on disk - #130

Merged
tamnd merged 1 commit into
mainfrom
f12-what-ends-up-on-disk
Aug 31, 2026
Merged

F12. What ends up on disk#130
tamnd merged 1 commit into
mainfrom
f12-what-ends-up-on-disk

Conversation

@tamnd

@tamnd tamnd commented Aug 31, 2026

Copy link
Copy Markdown
Owner

The last lesson of the front end part, and the twenty sixth overall. F01 to F11 all went one direction: text in, a code object out, eleven stages in between. This one goes the other way, and it is the reason importing a module a second time is fast.

What the lesson does

It compiles a two line file with py_compile, then reads the .pyc back as bytes and takes it apart. Sixteen bytes of header, four fields, four bytes each, and everything after that is one marshalled code object and nothing else.

The header cell prints all four fields next to what they are supposed to match, so the staleness rule is visible rather than described: the mtime and the length in the header are the source file's, and if either drifts the file is thrown away.

Then the magic number, which is the part I did not expect. Only two of its four bytes are the number. The other two are a carriage return and a newline, glued on by PYC_MAGIC_NUMBER_TOKEN. That is a trap on purpose: if something copies a .pyc in text mode on a system that translates line endings, those are the bytes that change, so the file fails the magic check loudly instead of loading and doing something strange three hundred bytes later.

The middle of the lesson is marshal itself. One byte per object, seven low bits holding an ascii letter naming the type, top bit meaning "put this in a numbered table, something later may point at it". A cell prints the first byte of nine different values so the letters are visible. Then the sharing: a 66 character string said twice costs 63 bytes less than saying it twice, because the second one is a five byte back reference, and a nested code object costs the same five.

Type c is sixteen fields in a fixed order, and the order is the entire specification. The lesson writes a fifty line reader for the subset a .pyc uses and points it at the file from the top, then checks the filename, the bytecode and the line table against marshal.loads. Worth noticing where the last two fields are: the line table and the exception table F11 decoded, sitting at the end of every function in the file.

It finishes by writing a .pyc by hand and importing it with SourcelessFileLoader, with no .py anywhere on disk, and then breaking one in each of the two ways. A wrong magic number raises. A wrong timestamp does not raise at all, it just gets compiled again and the file quietly rewritten, and the cell proves that by reading the bytes back afterwards.

Checks

Three cells carry differs or varies notes, because the byte counts move between 3.14 and 3.15 and the timestamps are whenever you ran it. Everything that has to be exact, the four matches lines and the two Trues at the end, says the same on both.

One thing that took a fix: the ('abcd', 'abcd') cell was printing a different first byte on each version, because w_ref refuses to share an object with exactly one reference to it and a bare literal passed straight to dumps has a different refcount in the two runtimes. Binding it to a name first makes it stable, and it also demonstrates the rule the prose describes.

Three new glossary terms: marshal, pyc file, magic number. Six diagrams, .excalidraw and .svg, all under 1100 px. Runs end to end in Pyodide and in Colab. Both READMEs have their rows. just check and just versions are green locally.

This closes the twelve front end lessons on M3, issue #22.

The last lesson of the front end part. Everything from F01 onwards went one
way, text in and a code object out. This goes the other way: the code object
becomes bytes, and those bytes become the same code object again without any
of the eleven stages running.

It compiles a two line file, reads the .pyc back as bytes, decodes the sixteen
byte header field by field, walks the marshalled blob with a fifty line reader
written in the lesson, and then assembles a .pyc by hand and imports it with
no .py anywhere on disk.

Two details worth the trip. Only half the magic number is a number, and the
other half is a carriage return and a newline put there so a text mode copy
corrupts the check rather than the code. And a wrong magic number raises while
a wrong timestamp does not, because one means the bytecode is nonsense and the
other only means the source moved on.

Three new glossary terms: marshal, pyc file, magic number. Six diagrams. Runs
end to end in Pyodide.
@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 mentioned this pull request Aug 31, 2026
5 tasks
@tamnd
tamnd merged commit 856f48d into main Aug 31, 2026
16 checks passed
@tamnd
tamnd deleted the f12-what-ends-up-on-disk branch August 31, 2026 12:19
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