DEV Community

Cover image for How Python's Import System Works and Why It Matters for Debugging
Ameer Abdullah
Ameer Abdullah

Posted on

How Python's Import System Works and Why It Matters for Debugging

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:

  1. Python checks sys.modules for mymodule. If found, returns the cached module object immediately.

  2. If not found, Python locates the module file.

  3. Python creates a new module object and adds it to sys.modules under the module name.

  4. Python executes the module file's code in the new module's namespace.

  5. The name mymodule in 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)
Enter fullscreen mode Exit fullscreen mode

Output:

True
True
Enter fullscreen mode Exit fullscreen mode

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}")
Enter fullscreen mode Exit fullscreen mode
# main.py
import config
import config  # second import
print(config.DEBUG)
Enter fullscreen mode Exit fullscreen mode

Output:

config module loading
DEBUG is True
True
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode
# module_b.py
print("loading module_b")
from module_a import a_function

def b_function():
    return "from b"
Enter fullscreen mode Exit fullscreen mode

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}")
Enter fullscreen mode Exit fullscreen mode

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)