datarekha

What are .pyc files and what role does Python bytecode play?

The short answer

When Python imports a module, it compiles the source to platform-independent bytecode and caches it in a .pyc file inside __pycache__. On subsequent imports the cached bytecode is loaded directly if the source is unchanged, skipping the parse-and-compile step. Bytecode is not machine code — it is still interpreted by the CPython virtual machine.

How to think about it

This question is really about everything that happens between you saving a .py file and the CPU running it. There are two layers to the answer: the compilation step that turns source into bytecode, and the caching step — the .pyc file — that avoids redoing that compilation on every import.

The compilation pipeline

Each time CPython imports a module, your source runs through four stages:

source.py
   │  Tokeniser  →  tokens (keywords, names, literals, operators)
   │  Parser     →  AST (abstract syntax tree)
   │  Compiler   →  bytecode (a code object)
   ▼  CPython VM →  executes the instructions

The compiler’s output — a marshalled code object — is what gets saved to __pycache__/source.cpython-312.pyc. The interpreter version is baked into the filename, so caches from different Python releases never collide.

What bytecode looks like

dis lets you see it:

import dis

def add(a, b):
    return a + b

dis.dis(add)
# RESUME          0
# LOAD_FAST       0 (a)
# LOAD_FAST       1 (b)
# BINARY_OP       0 (+)
# RETURN_VALUE

These are stack-machine instructions, run by the CPython virtual machine — not native machine code. That’s the whole reason Python is slower than C in a tight loop yet fast enough for almost everything real: there’s an interpreter standing between the bytecode and the CPU. (The exact opcodes shift between releases — this is recent-3.x output — which is precisely why the cache is version-stamped.)

What’s inside a .pyc

A .pyc is a small binary header followed by that marshalled code object:

bytes 0–3    magic number   (changes whenever a release changes bytecode)
bytes 4–7    bit field      (0 = timestamp-based, 1 = hash-based invalidation)
bytes 8–15   source mtime + size   (or a hash, if hash-based)
bytes 16+    the marshalled code object

The magic number is the guard that stops a 3.11 interpreter from loading a 3.12 .pyc by accident.

Invalidation and source-free deploys

By default CPython regenerates the .pyc when the source’s mtime or size changes; you can switch to content-hash invalidation with --check-hash-based-pycs always. And you can ship only .pyc files — the interpreter will run them — pre-compiling a whole tree with:

python -m compileall src/

Just don’t mistake that for security: bytecode decompiles trivially. It only prevents accidental edits on a production host.

Keep practising

All Python questions

Explore further

Skip to content