The header is 8 bytes: a magic number that identifies the bytecode, and the modification
time of the source file. The code object follows straight after it. Both fields are
written as 4-byte little-endian numbers. import.c writes the magic number,
then a placeholder of 0 for the time, then the code object, and goes back to fill in the
real time last. A file cut off while being written therefore keeps a time of 0, which
will not match the source.
Magic number
Offset 0, 4 bytes: the low two bytes are the number that names the bytecode, and the
high two are always 0D 0A, a carriage return and a line feed. If a file
is ever opened in text mode, those two bytes change and the magic number no longer
matches. CPython compares the whole 4-byte value and recompiles if it differs.
| Magic number | Bytes in the file | Release |
|---|---|---|
| 20121 | 99 4E 0D 0A | 1.5, 1.5.1, 1.5.2 |
| 50428 | FC C4 0D 0A | 1.6 |
| 50823 | 87 C6 0D 0A | 2.0, 2.0.1 |
| 50824 | 88 C6 0D 0A | 2.0 with the -U option, which makes string literals Unicode |
The comment beside the magic number in CPython’s Python/import.c gives
the way these values were chosen: (YEAR-1995), MONTH, DAY. So 20121 is
2, 01, 21, which is 21 January 1997, and 50823 is 23 August 2000. Those are the dates
the numbers encode, not release dates. The sources give no note for each value saying
what changed: the instructions that differ between releases are listed on the
Instructions page.
The -U value is one more than the normal one. import.c in
2.0 adds 1 so that files compiled with the option do not interfere with normal ones.
Source modification time
Offset 4, 4 bytes: the modification time of the .py file that was
compiled, in seconds since 1970. Python reads it and compares it with the source
file’s time. If they are not exactly equal the source is compiled again; the source
being older does not matter. The explorer shows this field as a date and time in
UTC.
File names
The compiled file sits next to the source and takes its name, with one letter added:
foo.py becomes foo.pyc. With the -O option
Python writes and reads foo.pyo instead. The two names hold the same
layout, and the bytecode differs only in what -O leaves out.