How does Python's import system work, and what happens when you import a module?
When you execute import module, Python follows this process:
Check
sys.modulescache: If the module was already imported, return the cached version. This prevents re-execution and ensures singletons.Find the module: Search
sys.path(list of directories) and finders insys.meta_pathfor the module.Load the module: Use the appropriate loader (file-based, built-in, frozen, etc.) to read the module's code.
Create module object: Create a new
types.ModuleTypeobject.Add to
sys.modules: Cache the module object BEFORE executing the code (important for circular imports).Execute module code: Run the module's top-level code in the module's namespace.
Bind the name: Bind the module name in the importing module's namespace.
The search order in sys.path is: current directory, PYTHONPATH environment variable, standard library, site-packages (third-party).
import foo:
1. if 'foo' in sys.modules: return cached
2. Find foo.py in sys.path
3. Create module object
4. sys.modules['foo'] = module (before execution!)
5. Execute foo.py code in module namespace
6. Bind name: caller.foo = module
Import variants:
import foo -> foo.bar()
from foo import bar -> bar()
import foo as f -> f.bar()
from foo import * -> bar() (uses __all__)
import sys
# sys.modules is the cache of all imported modules
print('os' in sys.modules) # True (already imported by Python)
print('json' in sys.modules) # May be False
import json
print('json' in sys.modules) # True (now cached)
# Importing again returns the cached version
import json as j1
import json as j2
print(j1 is j2) # True -- same object
# sys.path controls where Python looks for modules
print(sys.path[:3]) # ['', '/usr/lib/python3.x', ...]
# Import variants
import os # import module
from os.path import join # import specific name
import os.path as osp # import with alias
# Module attributes
print(json.__name__) # 'json'
print(json.__file__) # '/usr/lib/python3.x/json/__init__.py'
print(json.__package__) # 'json'
# Force reimport (for development/testing)
import importlib
importlib.reload(json) # re-executes the module code
# Module is executed only once
# Create a test to demonstrate:
import types
test_module = types.ModuleType('test_mod')
exec('print("Module executed!"); VALUE = 42', test_module.__dict__)
sys.modules['test_mod'] = test_module
import test_mod # does NOT print again -- cached
print(test_mod.VALUE) # 42
# Cleanup
del sys.modules['test_mod']
# Import time side effects
# When you import, ALL top-level code in the module runs:
# - Function/class definitions (fast)
# - Variable assignments (fast)
# - Print statements, API calls, DB connections (slow, side effects!)
# Best practice: put side effects inside if __name__ == '__main__'
- •Modules are cached in sys.modules -- imported only once
- •sys.path controls the search order for modules
- •Module code is executed on first import (top-level statements run)
- •importlib.reload() forces re-execution of module code
- •Modifying a module after import doesn't require reimport (it's the same object)
- •from module import x copies the reference -- reassigning x doesn't affect the module
- •Circular imports work because sys.modules is populated before execution completes
- •sys.path can be modified at runtime to add custom import locations