skip to content

What are frozen modules in CPython, and why does editing their source change nothing?

level: middleimportance: nice to knowfreq 12%

answer

  1. The import system is itself written in Python
  2. Bytecode embedded in the executable
  3. No file means nothing to validate
  4. The spec origin is not a path
  5. A flag turns it off, not on

basics

~10 s

A frozen module has its compiled bytecode embedded in the python executable, so importing it reads no .py and no .pyc and validates nothing. Editing the matching source changes nothing until -X frozen_modules=off.

solid answer

~50 s

Freezing means the module's marshalled code object is compiled into the interpreter binary at build time. `importlib.machinery.FrozenImporter` sits early in the import path and serves those modules directly, so `abc.__spec__.origin` reads `'frozen'` rather than a filename. Because nothing on disk is consulted, none of the `.pyc` invalidation machinery applies: there is no magic number check against a file, no mtime, no hash — the only way to invalidate a frozen module is to rebuild the interpreter. Python 3.11 extended freezing from the import bootstrap, long frozen out of necessity, to the handful of modules every interpreter start needs anyway, removing real work from each launch. `-X frozen_modules=on|off` controls whether they are used; the default is on for an installed interpreter and off for a locally built one, precisely so that someone editing the stdlib in a checkout sees their edits.

code

pycon · 6 lines
pycon
>>> import abc
>>> abc.__spec__.origin
'frozen'
>>> import json
>>> json.__spec__.origin.endswith('json/__init__.py')
True

go deeper

for a junior

Enough to know that some standard-library modules are compiled into the interpreter itself and are imported without touching any file, so they never appear in a cache directory.

for a middle

Explain that the code object is embedded at build time, that the frozen importer runs before path-based finders, and that no invalidation applies because there is no file in the loop.

for a senior

Recognise the symptom in the wild: an edit to stdlib source with no observable effect is a frozen module, not a stale cache, and -X frozen_modules=off is the confirmation step.

for a principal

Understand it as a startup-cost tradeoff the interpreter build makes on your behalf, and keep it distinct from application bundling, which solves a different problem with different tools.

## Three ways a module can be built into the interpreter It helps to separate three things that all sound like "compiled in". * A **builtin** module is written in C and linked into the executable; `sys.builtin_module_names` lists them and they have no Python source at all. * An **extension** module is C compiled to a shared library loaded at import time. * A **frozen** module is ordinary Python source whose *marshalled code object* was embedded in the executable at build time. It is Python code, and it behaves like any other module once imported — you can inspect it, monkeypatch it, reload considerations aside — but its bytecode came from the binary, not from a file. ## Why CPython freezes anything The import system is written in Python. That is a bootstrap problem: to import the module that implements importing, the interpreter would have to import something. Freezing solves it — the bootstrap import machinery is embedded in the binary and executed directly at startup, before any filesystem-based importer exists. Python 3.11 extended this from the bootstrap to a small set of modules that every interpreter start needs anyway. Each frozen module is a directory scan, an open, a read and an unmarshal that no longer has to happen, which is measurable on a process that starts thousands of times. `importlib.machinery.FrozenImporter` is the finder and loader that serves them, and it is consulted before path-based finders, so a frozen module wins over any file with the same name on `sys.path`. ## Why this leaf cares Everything the bytecode cache does is about deciding whether a file on disk is still authoritative: match the magic number, compare the recorded mtime and size, or re-hash the source. A frozen module has **no disk file in that role at all**. There is no validation step, no `__pycache__` entry, and nothing to invalidate. The code object was fixed when the interpreter was built, and the only thing that changes it is building a new interpreter. That produces a memorable failure: edit the source of a frozen stdlib module in a checkout, restart, and observe nothing change — not because a cache went stale, but because the file you edited was never consulted. Deleting `__pycache__`, touching the file and recompiling all have exactly zero effect, which is why the symptom is so confusing to anyone reasoning about `.pyc` invalidation. ## The escape hatch `-X frozen_modules=on` or `off` decides whether frozen modules are used. The default is **on** for an installed interpreter, where the stdlib is not expected to change under you, and **off** for a locally built one, so that a contributor editing stdlib source sees their edits without rebuilding. Turning it off makes the interpreter fall back to importing those modules from files in the normal way, at a small startup cost. A quick way to tell what you are dealing with is the module's spec. A frozen module reports `origin` as the literal string `'frozen'` rather than a path, and its loader is the frozen importer. Anything whose origin is a filename went through the ordinary source-or-cache path and obeys all the usual invalidation rules. ## What freezing is not It is not a distribution mechanism you reach for casually, and it is not the same as bundling an application into a single executable — that is a packaging concern, and those tools generally embed a zip archive of modules rather than freezing them into the interpreter binary. It is also not an optimization you enable. Frozen modules are already on in an installed interpreter; the flag exists to turn them *off*. A candidate who suggests enabling frozen modules in production for speed has the direction backwards. And it does not make a module immutable at runtime. Once imported, a frozen module is a perfectly ordinary module object in `sys.modules` whose attributes can be read, replaced or patched exactly like any other. Freezing fixes where the bytecode came from, nothing more.

  • How does a frozen module differ from a builtin module?
    A builtin is C code linked into the executable and listed in `sys.builtin_module_names`; it has no Python source. A frozen module is ordinary Python whose compiled code object was embedded in the binary at build time. Both skip the filesystem, but only the frozen one is Python that you could also have imported from a file.
  • Why is the default of -X frozen_modules different for an installed interpreter and a local build?
    An installed interpreter's stdlib is not expected to be edited, so using the embedded bytecode is a free startup win and the default is on. In a checkout someone is very likely editing stdlib source, so the default is off and those modules load from files, making edits visible without rebuilding the interpreter.
  • Does deleting __pycache__ affect a frozen module?
    Not at all. A frozen module's bytecode comes from the executable, so it has no cache entry and no source file consulted at import. None of the invalidation machinery — magic number, mtime and size, or source hash — has anything to check, which is exactly why editing the corresponding source appears to do nothing.

It is the difference between a recipe pinned to the kitchen wall and one printed inside the cookbook's cover: rewriting the pinned copy changes tonight's dinner, rewriting your own notes about the printed one changes nothing until a new edition is published.

saying these in an interview costs you the question

  • Confuses frozen modules with builtin C modules
  • Thinks a frozen module still reads its .py at import
  • Says frozen modules are validated by timestamp like a .pyc
  • Believes -X frozen_modules is a speedup you enable in production
  • Claims freezing makes the module unpatchable at runtime

context