Module caching, execution order, and circular imports explained by tracing what actually happens.
How Python's Import System Works and Why It Matters for Debugging
The import system is one of the least understood parts of Python and one of the most practically important for debugging production issues. Circular import errors, unexpected code execution, and module state bugs all stem from not understanding what happens when Python encounters an import statement.
What Happens on the First Import
When Python executes import mymodule for the first time:
Python checks
sys.modulesformymodule. If found, returns the cached module object immediately.If not found, Python locates the module file.
Python creates a new module object and adds it to
sys.modulesunder the module name.Python executes the module file's code in the new module's namespace.
The name
mymodulein the importing module is bound to the module object.
Step 3 happens before step 4. This is critical for understanding circular imports.
The Module Cache
import sys
import os
print("os" in sys.modules)
print(sys.modules["os"] is os)
Output:
True
True
Every imported module is cached in sys.modules. Subsequent imports return the cached object without re-executing the module code.
Module-Level Code Executes on Import
# config.py
print("config module loading")
DEBUG = True
print(f"DEBUG is {DEBUG}")
# main.py
import config
import config # second import
print(config.DEBUG)
Output:
config module loading
DEBUG is True
True
The print statements in config.py run exactly once — when the module is first imported. The second import returns the cached module object without re-executing the code.
Circular Import Behavior
# module_a.py
print("loading module_a")
from module_b import b_function
def a_function():
return "from a"
# module_b.py
print("loading module_b")
from module_a import a_function
def b_function():
return "from b"
When you import module_a, Python starts executing it, encounters from module_b import b_function, starts executing module_b, which tries to import a_function from module_a. But module_a is already in sys.modules (added in step 3 before its code finished executing in step 4). The partially initialized module_a is returned, and a_function does not exist yet, causing an ImportError.
The fix is to restructure to avoid circular dependencies, use lazy imports inside functions, or use import module instead of from module import name.
The __name__ Pattern
# utility.py
def calculate():
return 42
print(f"This module's name is: {__name__}")
if __name__ == "__main__":
result = calculate()
print(f"Result: {result}")
When run directly: __name__ is "__main__", both prints execute. When imported: __name__ is "utility", only the first print executes, the if block is skipped.
This is how Python distinguishes between running a file as a script and importing it as a module. It is tested in interviews specifically because many candidates use this pattern without understanding what it actually does. Practice predicting outputs like these at PyCodeIt. Now 2.0 version available. Go and check now!
Top comments (0)