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.

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:

  1. BuiltinImporter: Searches for modules compiled directly into the Python interpreter (e.g., sys, builtins).
  2. FrozenImporter: Searches for frozen modules compiled into bytecode and bundled with the binary.
  3. PathFinder: Searches the filesystem using paths listed in sys.path and 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.

  1. Instantiation: The loader invokes spec.loader.create_module(spec). If this returns None, Python falls back to default creation semantics via types.ModuleType(spec.name).
  2. Boilerplate Attributes: Python sets core attributes on the module object based on the spec, including __name__, __file__, __cached__, __doc__, __loader__, __package__, and __spec__.
  3. Early Insertion into sys.modules: Crucially, Python adds the freshly allocated, unexecuted module to sys.modules before running its code. This step prevents infinite recursion in circular import scenarios (Module A imports Module B, which imports Module A).

4. Code Loading and Compilation

If the module is backed by source code on disk (standard .py files managed by SourceFileLoader):

  1. Bytecode Lookup: Python checks for a valid, precompiled .pyc file in the __pycache__ directory matching the current interpreter version and source file timestamp/hash.
  2. 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.
  3. 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:

  1. The runtime calls spec.loader.exec_module(module).
  2. The code object is evaluated inside the module’s dictionary namespace (module.__dict__).
  3. 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 import statements 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: