skip to content

What does CPython store in `__pycache__`, and why does it not make startup free?

level: middleimportance: should knowfreq 28%

answer

  1. Compilation cached, execution never
  2. One directory beside the source files
  3. Filenames carry the interpreter version tag
  4. Timestamp and size validate it by default
  5. Frozen modules skip the filesystem entirely

basics

~20 s

It stores compiled bytecode for the source files beside it, named like mod.cpython-314.pyc, so later runs skip parsing and compiling. It caches compilation only — every process still executes each module body, which is usually the larger cost.

solid answer

~40 s

On the first import of a source module, CPython compiles it and writes the bytecode into a `__pycache__` directory next to the source, tagged with the interpreter version (`mod.cpython-314.pyc` on 3.14). Later runs validate that file — by default against the source's timestamp and size — and load the bytecode instead of recompiling. That saves parsing and compilation, which is real but usually the smaller half of import cost: the module body still executes in full in every process, so a warm cache does not make imports free. Set `sys.dont_write_bytecode`, `-B` or `PYTHONDONTWRITEBYTECODE` to suppress writing, and `PYTHONPYCACHEPREFIX` to redirect the cache when the source tree is read-only. Separately, the interpreter freezes a set of startup stdlib modules into the binary, so they load with no filesystem access at all.

code

python · 11 lines
python
import importlib.machinery
import importlib.util
import sys

for name in ("os", "abc", "decimal"):
    module = __import__(name)
    frozen = module.__spec__.loader is importlib.machinery.FrozenImporter
    print(name, "frozen:", frozen, "| cached:", getattr(module, "__cached__", None))

print(importlib.util.cache_from_source("tm_index.py"))
print("dont_write_bytecode:", sys.dont_write_bytecode)

go deeper

for a junior

Recognise the directory and what it is for: compiled bytecode for the source files next to it, written automatically, safe to delete, and never something to commit to version control.

for a middle

Explain the mechanics — version-tagged filenames, timestamp-and-size validation by default, and the key distinction that compilation is cached across runs while body execution is paid by every process.

for a senior

Bring the deployment angle: a read-only source tree silently recompiles on every start, so compile at build time or redirect the cache, and pick hash-based invalidation where timestamps cannot be trusted.

for a principal

Own the build-and-image policy — where bytecode is produced, which invalidation mode an immutable image uses, and the reasoning that caching compilation buys a fixed amount while dependency breadth is the cost that keeps growing.

### What is in the directory When a source module is imported and no valid cached bytecode exists, CPython compiles the source and writes the result to `__pycache__/<name>.cpython-<major><minor>.pyc` beside the source file — `__pycache__/tm_index.cpython-314.pyc` on Python 3.14. The layout and the version tag come from PEP 3147 (Python 3.2); the tag is what lets several interpreter versions share a checkout without fighting over one `.pyc`. `importlib.util.cache_from_source` computes the path for you, and `importlib.util.source_from_cache` goes the other way. A `.pyc` holds a header plus the marshalled code object for the module. It is not machine code and not a result — it is the compiled instruction stream that the module body would have produced anyway. ### How the cache is validated By default a `.pyc` records the source's modification time and size; on import, CPython stats the source and recompiles if either differs. This is fast but timestamp-dependent, which is awkward for reproducible builds and for checkouts where mtimes are not meaningful. PEP 552 (Python 3.7) added **hash-based** `.pyc` files, selectable through `py_compile.PycInvalidationMode`: a *checked* hash-based file makes the interpreter hash the source and compare, while an *unchecked* one is trusted without any source check — the right choice for an immutable deployment image, where nothing can change and you want to skip the check entirely. ### What it saves, and what it does not Import cost is two things: **compilation** (source text → bytecode) and **execution** (running the module body). `__pycache__` caches the first across runs. The second happens in **every process, every time**, and in most real programs it is the larger share once the cache is warm — building tables, defining classes, running decorators, and recursively importing dependencies. This is why "the second run is faster" is only a first-run story, and why deleting `__pycache__` is never a fix for anything performance-related: it makes exactly one run slower and changes nothing afterwards. ### When the cache is absent or unwritable If the directory cannot be written — a read-only image, a directory owned by another user — imports still succeed, but the source is recompiled **on every run**, silently. In a container that starts a fresh process per request, that is a permanent tax nobody sees in a local test. The fixes are to compile the tree at build time so the caches ship warm, or to point `PYTHONPYCACHEPREFIX` (equivalently `sys.pycache_prefix`) at a writable location so caches land in a parallel tree. To suppress caching deliberately — a scratch directory, a security-sensitive path — use the `-B` flag, `PYTHONDONTWRITEBYTECODE`, or set `sys.dont_write_bytecode` at runtime. Note it suppresses *writing*, not *reading*: an existing valid `.pyc` is still used. ### Frozen and built-in modules Two other categories skip the filesystem dance altogether. * **Built-in modules** are compiled into the interpreter binary as C code and listed in `sys.builtin_module_names`. There is no Python source to find or compile. * **Frozen modules** are Python modules whose marshalled bytecode is embedded in the binary. The import bootstrap itself has always been frozen — it must be, since the import system cannot import itself — and Python 3.11 substantially expanded the set to cover the stdlib modules needed during startup, with a measurable improvement to interpreter start time. Frozen modules are loaded by `importlib.machinery.FrozenImporter`, need no `stat` calls and no path search, and have no `__pycache__` entry at all. You can see which is which by checking a module's spec loader, and the effect is the same in spirit as a warm cache — compilation already done — with the extra win of no filesystem lookups. ### What a `.pyc` is not It is not faster-running code: the bytecode loaded from a cache file is exactly what compiling the source would have produced, so execution speed after import is identical either way. It is not a distribution format that hides source, since bytecode is trivially disassembled and is tied to one interpreter version by that filename tag. And it is not portable across versions — which is the point of the tag: a single checkout used by two interpreter versions gets two `.pyc` files in the same directory and neither invalidates the other. ### The practical summary A warm `__pycache__` removes compilation from steady-state startup, and that is worth having: ship it warm in images, keep it writable in development, and use hash-based invalidation when timestamps cannot be trusted. But if startup is still slow with a warm cache, the cost is body execution and dependency breadth, and no amount of bytecode caching will touch it.

  • A container's source directory is read-only. What happens to `__pycache__`?
    Nothing fails — imports still work — but the cache cannot be written, so every process recompiles the source from scratch. The cost is invisible locally and permanent in production. Compile the tree at build time so the `.pyc` files ship in the image, or point `PYTHONPYCACHEPREFIX` at a writable directory so caches land in a parallel tree.
  • Why would you choose hash-based `.pyc` files over the default?
    Because timestamp-and-size validation depends on mtimes, which are unreliable in reproducible builds, in checkouts restored from an archive, and across image layers. A checked hash-based file compares a hash of the source instead; an unchecked one skips validation entirely, which is the fastest and correct choice for an immutable image where the source cannot change.
  • Why is the import bootstrap frozen into the interpreter binary?
    Because the import system cannot import itself — something has to load the machinery before any path-based import can work, so its bytecode is embedded in the binary and loaded without touching the filesystem. Python 3.11 extended the same treatment to the stdlib modules needed during startup, cutting path searches and stat calls from interpreter start.

A .pyc is a saved translation of a script, not a recording of the performance: the interpreter still performs the whole play in every process, it just no longer has to translate the script first.

saying these in an interview costs you the question

  • Thinks a .pyc means the module body is not executed
  • Suggests deleting __pycache__ to speed a program up
  • Believes .pyc files contain machine code
  • Assumes an unwritable cache directory breaks imports
  • Cannot say what validates a cached .pyc by default
  • Claims a warm cache makes repeated imports free

context