How Python Imports Work: Module Execution Lifecycle
When a Python script executes an import statement, the
runtime initiates a rigorous, multi-stage lifecycle to resolve, load,
and initialize the requested code before binding it to the current
scope. This article details the exact execution lifecycle of a Python
module during import, tracking the process from the initial lookup in
sys.modules, through finder and loader hooks governed by
PEP 451, to bytecode compilation, isolated execution, and namespace
binding.
1. The Cache Check
(sys.modules)
Before performing any filesystem or network operations, Python checks
the global cache located at sys.modules.
- Cache Hit: If the module name exists in
sys.modules, Python immediately returns the cached module object. Execution skips the search, load, and initialization steps entirely. - Cache Miss: If the module is not found, or if its
entry is explicitly set to
None(indicating a cached negative lookup), Python initiates the search phase.
2. The Search Phase
(Finders and sys.meta_path)
Python delegates the task of locating the module to a sequence of
"finders" registered in the sys.meta_path list. By default,
this list contains:
BuiltinImporter: Searches for modules compiled directly into the Python interpreter (e.g.,sys,builtins).FrozenImporter: Searches for frozen modules compiled into bytecode and bundled with the binary.PathFinder: Searches the filesystem using paths listed insys.pathand any package__path__attributes.
Each finder implements the
find_spec(fullname, path, target=None) method. The runtime
iterates through sys.meta_path sequentially. The first
finder that recognizes the module returns a ModuleSpec
object (PEP 451), which encapsulates all metadata required to load the
module, such as its loader, origin, submodule search locations, and
caching flags. If no finder returns a valid specification, Python raises
a ModuleNotFoundError.
3. Module Creation and Cache Reservation
Once a ModuleSpec is obtained, the associated loader
(found on spec.loader) manages the creation and population
of the module object.
- Instantiation: The loader invokes
spec.loader.create_module(spec). If this returnsNone, Python falls back to default creation semantics viatypes.ModuleType(spec.name). - Boilerplate Attributes: Python sets core attributes
on the module object based on the spec, including
__name__,__file__,__cached__,__doc__,__loader__,__package__, and__spec__. - Early Insertion into
sys.modules: Crucially, Python adds the freshly allocated, unexecuted module tosys.modulesbefore running its code. This step prevents infinite recursion in circular import scenarios (Module AimportsModule B, which importsModule A).
4. Code Loading and Compilation
If the module is backed by source code on disk (standard
.py files managed by SourceFileLoader):
- Bytecode Lookup: Python checks for a valid,
precompiled
.pycfile in the__pycache__directory matching the current interpreter version and source file timestamp/hash. - Compilation: If the cache is missing or stale, the source code is read, parsed into an Abstract Syntax Tree (AST), and compiled into a Python code object.
- Bytecode Write: If permissions allow, the newly
compiled bytecode is written to
__pycache__asynchronously or immediately for future use.
5. Module Execution
(exec_module)
With an empty module object registered in sys.modules
and a valid code object ready, the loader executes the module:
- The runtime calls
spec.loader.exec_module(module). - The code object is evaluated inside the module’s dictionary
namespace (
module.__dict__). - Every top-level statement executes sequentially:
- Class and function definitions are evaluated, creating code objects and assigning them to module-level names.
- Top-level variable assignments populate
module.__dict__. - Nested
importstatements trigger recursive executions of this entire lifecycle. - Any arbitrary code (e.g.,
print(), loops, network calls) executes immediately.
If an unhandled exception occurs during exec_module, the
import aborts. Python removes the incompletely initialized module from
sys.modules to prevent a broken state from persisting, and
the exception bubbles up to the caller.
6. Name Binding
After successful execution, control returns to the scope where the import statement originated. The runtime binds the loaded module or its attributes to the local namespace:
import foo: Binds the identifierfooto the loaded module object.import foo.bar: Loadsfoo, loadsfoo.bar, setsbaras an attribute onfoo, and bindsfooto the local namespace.from foo import bar: Loadsfoo, retrieves the attributebarfromfoo.__dict__, and bindsbardirectly into the current scope.import foo as baz: Binds the modulefooto the identifierbaz.