Modules and the Import System
Modules and the Import System
After reading this chapter, you will master Python’s module architecture, trace the import resolution pipeline through sys.modules and sys.path, dynamically load modules using importlib, and diagnose and resolve circular import deadlocks.
Mental model
In Python, a module is simply a .py source file executed as a distinct namespace object (types.ModuleType). When an import foo statement executes, CPython traverses a four-stage pipeline:
import foo
│
▼
[ 1. Check sys.modules Cache ]
│
├─ If cached ───────────────▶ Return existing module reference immediately
│
▼ (Not cached)
[ 2. Finders (sys.meta_path) ] ──▶ Search sys.path directories for 'foo.py'
│
▼ (Found file)
[ 3. Loaders (FileLoader) ] ──▶ Compile bytecode, allocate empty ModuleType
│
▼
[ 4. Execute Module Code ] ──▶ Run top-level statements, populate __dict__,
cache in sys.modules, bind name in caller
Because modules are cached in sys.modules upon first import, subsequent imports of the same module across threads or files return the cached instance with zero disk I/O or recompilation.
Minimal example
Save as import_internals.py:
# import_internals.py
import importlib
import sys
from types import ModuleType
def inspect_import_cache(module_name: str) -> None:
print(f"Is '{module_name}' cached in sys.modules? {module_name in sys.modules}")
def dynamically_load_module(module_name: str) -> ModuleType:
"""Programmatically import a module by string name using importlib."""
print(f"\nDynamically loading '{module_name}' via importlib...")
mod = importlib.import_module(module_name)
print(f"Module file path : {getattr(mod, '__file__', 'built-in')}")
print(f"Module docstring : {mod.__doc__[:40] if mod.__doc__ else 'None'}...")
return mod
def main() -> None:
inspect_import_cache("json")
# Dynamic import
json_mod = dynamically_load_module("json")
inspect_import_cache("json")
# Use dynamically loaded module
serialized = json_mod.dumps({"status": "healthy", "code": 200})
print(f"Serialized output: {serialized}")
if __name__ == "__main__":
main()Run via uv run python import_internals.py:
Is 'json' cached in sys.modules? False
Dynamically loading 'json' via importlib...
Module file path : .../lib/python3.14/json/__init__.py
Module docstring : JSON (JavaScript Object Notation) <http:...
Is 'json' cached in sys.modules? True
Serialized output: {"status": "healthy", "code": 200}
Worked examples
Case 1: Dynamic Driver Registry with importlib
In cloud automation and backend platforms, you frequently need to load storage, networking, or database drivers specified by external configuration strings:
# driver_loader.py
import importlib
from typing import Any
class DriverLoader:
@staticmethod
def load_formatter(format_name: str) -> Any:
# Standard library formatters mapping
known_modules = {
"json": "json",
"csv": "csv",
}
if format_name not in known_modules:
raise ValueError(f"Unsupported driver: {format_name}")
mod = importlib.import_module(known_modules[format_name])
return mod
if __name__ == "__main__":
loader = DriverLoader()
driver = loader.load_formatter("json")
data = {"metric": "cpu", "value": 85.5}
print(f"Driver loaded dynamically: {driver.__name__}")
print(f"Formatted: {driver.dumps(data)}")Run:
uv run python driver_loader.pyOutput:
Driver loaded dynamically: json
Formatted: {"metric": "cpu", "value": 85.5}
Case 2: Diagnosing and Breaking Circular Imports
A circular import occurs when Module A imports Module B at top-level, but Module B imports Module A before Module A has finished executing its definition pass.
Circular Import Deadlock:
module_a.py ──(imports)──▶ module_b.py
▲ │
│ ▼
└────────(imports)──────────┘
When Python hits from module_a import Service inside module_b, module_a is still being executed; Service does not yet exist in module_a.__dict__, raising: ImportError: cannot import name 'Service' from partially initialized module 'module_a'.
The Fixes:
- Move shared domain types/interfaces to a third, leaf module (
types.pyormodels.py) imported by both. - Use local imports: Import the dependency inside the specific function that uses it, rather than at module top-level.
- Use
typing.TYPE_CHECKING: If the import is only needed for type annotations, gate it behindif TYPE_CHECKING:.
# circular_fix_pattern.py
from typing import TYPE_CHECKING
if TYPE_CHECKING:
# Only imported during static analysis (mypy/pyright), NEVER at runtime!
from collections.abc import Sequence
def process_batch(items: "Sequence[str]") -> int:
return len(items)
if __name__ == "__main__":
print("Function defined cleanly without circular runtime dependency.")
print("Result:", process_batch(["task-1", "task-2"]))Run:
uv run python circular_fix_pattern.pyOutput:
Function defined cleanly without circular runtime dependency.
Result: 2
Case 3: The if __name__ == "__main__": Boundary
When a Python file is invoked directly (python script.py), CPython assigns its __name__ attribute to "__main__". When imported by another file (import script), __name__ is set to the module’s file stem ("script").
# dual_mode_module.py
def calculate_throughput(bytes_transferred: int, duration_seconds: float) -> float:
"""Pure library function: usable when imported."""
if duration_seconds <= 0:
raise ValueError("Duration must be greater than zero.")
return bytes_transferred / duration_seconds
def _cli_entrypoint() -> None:
"""CLI testing routine: runs ONLY when executed directly."""
sample_bytes = 104_857_600 # 100 MB
duration = 2.5
rate_mb = calculate_throughput(sample_bytes, duration) / (1024 * 1024)
print(f"[CLI Runner] Transfer rate: {rate_mb:.2f} MB/s")
if __name__ == "__main__":
_cli_entrypoint()Run:
uv run python dual_mode_module.pyOutput:
[CLI Runner] Transfer rate: 40.00 MB/s
When another module runs from dual_mode_module import calculate_throughput, _cli_entrypoint() will not execute.
Pitfalls
Pitfall 1: Standard Library Shadowing
Naming a local script after a standard library module (e.g. creating math.py, random.py, test.py, or json.py in your project folder) breaks the import system:
Your Directory:
├── math.py (Your script)
└── test_math.py (import math -> loads YOUR math.py instead of CPython's!)
Because CPython checks the current working directory first in sys.path[0], your local file shadows the standard library module, causing cryptic AttributeError: module 'math' has no attribute 'sqrt' errors.
Exercises
- Print all paths in
sys.pathand inspect where Python looks for modules on your machine. - Write a function that accepts a module name as a string, checks whether it is already loaded in
sys.modules, and reloads it usingimportlib.reload()if present. - Create a simulated two-module circular import and resolve it using local function-level imports.
- Use
__all__to restrict the symbols exported when a consumer executesfrom my_module import *.
Further reading
- Python Documentation: The Import System (
sys.meta_path, Finders, Loaders). - Python Standard Library:
importlibandsys.modules. - PEP 302: New Import Hooks.