Python Imports Execute a Cached Module Graph
How sys.modules, package initialization, from-import bindings, cycles, and reload shape import behaviour.
An import statement is both a name-binding operation and a request to locate, create, and possibly execute a module. Keeping those phases separate explains why repeated imports are cheap and why circular imports can expose partially initialised state.
The cache is populated before execution finishes
For a normal source module, the import machinery checks sys.modules, finds a module specification, creates a module object, places it in sys.modules, and asks the loader to execute it. Inserting the object before execution prevents recursive imports from creating a second copy.
If module execution fails, the failing entry is normally removed from sys.modules. Modules successfully loaded as side effects can remain cached.
import sys
import reports
assert sys.modules['reports'] is reports
The cache key is the fully qualified module name. Importing the same file under different names through path manipulation can create distinct module objects and duplicate global state.
Import forms bind different local names
import settings binds the module object. from settings import timeout looks up an attribute and binds that object directly in the importing namespace.
from settings import timeout
import settings
settings.timeout = 30
print(timeout)
The from binding is live: because it remembers the source module and attribute name, the print observes 30 after reassignment. It behaves like an ES module import rather than an ordinary Python assignment.
Cycles expose incomplete namespaces
If a imports b while b imports a, the second import finds a in sys.modules. That object may not yet contain names assigned later in a.py. Accessing one too early raises an error describing a partially initialised module.
Cycles are easier to tolerate when modules define functions and classes before doing cross-module work, or when an import is moved inside a function that runs after startup. A shared dependency module is often clearer than carefully arranging side effects.
Package __init__.py participates in the graph. Importing package.child first initialises package, then loads the child and normally binds it as an attribute of the parent package.
Reload is not a clean restart
importlib.reload(module) creates a new module object, replaces the entry in sys.modules, and executes code into a fresh namespace. Existing references to the old module continue pointing at the old object, while later imports receive the new one.
Reload can be useful in interactive development, but application correctness should not depend on it resetting all state. Explicit state containers and restartable processes provide clearer lifecycle boundaries.
Imports are predictable when modules minimise top-level side effects, use stable qualified names, and expose functions that perform work after the dependency graph has finished initialising.