AW Dev Rethought

🌟 The best way to predict the future is to invent it - Alan Kay

🧠 Python DeepCuts — 💡 Import Cache & sys.modules


Description:

One of the most misunderstood behaviours in Python is how imports actually work.

You write:

import math

And it feels like Python loads the module every time.

But in reality, Python loads a module only once, then stores it in a global cache called sys.modules.

This DeepCut explains how that cache works — and why it can lead to subtle bugs in real systems.


🧩 Modules Are Cached in sys.modules

When a module is imported, Python stores it in:

sys.modules
import sys
import math

"math" in sys.modules

This is a dictionary:

{
    "module_name": module_object
}

From that point onward, Python uses this cached object instead of reloading the module.


🧠 Import Happens Only Once

If you import the same module multiple times:

import math
import math

Python does not execute the module again.

Instead:

  • it checks sys.modules
  • finds the existing module
  • returns the same object
id(math) == id(sys.modules["math"])

This is why imports are fast after the first load.


🔄 Module Code Runs Only Once

Top-level code inside a module executes only during the first import.

# demo_module.py
print("Executing module")
import demo_module
import demo_module

Output:

Executing module

Printed only once.

This behaviour is essential for:

  • performance
  • avoiding repeated side effects
  • maintaining module-level state

🧠 Reloading a Module

You can force Python to re-execute a module using:

import importlib
importlib.reload(module)

This reloads the module’s code — but with an important limitation.


⚠️ Reload Does NOT Update Existing References

Consider:

from demo_module import some_function

importlib.reload(demo_module)

Here:

  • demo_module is updated
  • but some_function still points to the old version

This happens because:

  • the reference was copied earlier
  • reload does not rewrite existing references

This is a common source of bugs in:

  • Jupyter notebooks
  • hot-reload systems
  • long-running services

🧬 Circular Imports and Partial Initialisation

When two modules import each other, Python avoids infinite recursion by:

  • inserting the module into sys.modules before execution finishes

This means:

  • other modules may access it
  • even if it’s not fully initialised

This leads to errors like:

  • missing attributes
  • partially defined objects

Understanding this behaviour is key to debugging circular imports.


🔍 sys.modules Is the Source of Truth

Python fully trusts sys.modules.

You can even modify it manually:

import sys

sys.modules["fake_module"] = "Hello"

import fake_module
print(fake_module)

Python does not validate this — it simply returns what’s stored.

This makes sys.modules extremely powerful — and potentially dangerous.


🧠 Why This Matters in Real Systems

Understanding module caching explains many real-world issues:

  • Why changes don’t reflect after reload
  • Why imports behave inconsistently in notebooks
  • Why circular imports break unexpectedly
  • Why shared state exists across modules
  • How plugin systems dynamically load code

This is not just theory — it affects production systems directly.


⚠️ Common Pitfalls

  • Using from module import x with reload
  • Relying on module re-execution
  • Ignoring circular import risks
  • Assuming imports are stateless
  • Modifying sys.modules unintentionally

These issues often appear only in complex or long-running systems.


✅ Key Points

  • Python caches modules in sys.modules
  • Imports reuse the same module object
  • Module code runs only once
  • importlib.reload re-executes code but has limitations
  • Existing references are not updated on reload
  • Circular imports can expose partially initialised modules

The import system is optimised for performance — but requires careful understanding.


Code Snippet:

import sys
import importlib
import math

print("math in cache:", "math" in sys.modules)
print("Module object:", sys.modules["math"])

print(id(math), id(sys.modules["math"]))

# Example with custom module
# Create demo_module.py with:
# print("Module executing")

import demo_module
import demo_module

importlib.reload(demo_module)

from demo_module import some_function
importlib.reload(demo_module)

# some_function still refers to old version

# sys.modules manipulation
sys.modules["fake_module"] = "Hello"
import fake_module
print(fake_module)

Link copied!

Comments

Add Your Comment

Comment Added!