Python's import system, explained
import looks trivial until a circular import or a "module not found" error stops you cold.
Underneath, importing is a well-defined process: find the module, load and execute it once,
cache it, and bind a name. Knowing the steps makes import problems easy to diagnose.
What import actually does
The first time you import a module, Python finds it, executes its top-level code once, builds a module object, caches it, and binds a name in your namespace. Re-importing reuses the cache — the code does not run again.
# greet.py
print("greet module loading")
GREETING = "hello"
# main.py
import greet # prints "greet module loading"
import greet # prints nothing — already cached
print(greet.GREETING)
That "runs once" rule is why putting executable side effects at module top level can
surprise you, and why the if __name__ == "__main__" guard exists.
sys.modules is the cache
Imported modules are stored in the sys.modules dict, keyed by name. Python checks it
before searching the filesystem, which is what makes repeat imports instant.
import sys
import json
"json" in sys.modules # True
sys.modules["json"] # <module 'json' ...>
You can even force a reload with importlib.reload, but in normal code the cache is exactly
what you want.
sys.path: where Python looks
To find a module, Python searches the directories in sys.path, in order: the script's
directory (or cwd), then PYTHONPATH entries, then installed/standard-library locations.
import sys
for p in sys.path:
print(p) # first match wins
This is why a local file named random.py can shadow the standard library — your
directory is searched first. Naming files after stdlib modules is a classic self-inflicted
bug.
Absolute vs relative imports
Absolute imports name the full path from a top-level package and are the recommended default. Relative imports use leading dots to reference the current package, handy inside a package.
# absolute — clear and unambiguous
from myapp.utils.text import slugify
# relative — . is current package, .. is parent
from .text import slugify
from ..models import User
Relative imports only work inside a package (a module that was imported as part of one), not in a script run directly — a frequent source of "attempted relative import" errors.
import vs from import
import module binds the module name; from module import name binds the name directly.
The latter is convenient but copies a reference at import time, so rebinding the original
later won't be seen.
import math
math.pi # access through the module
from math import pi, sqrt # bind names directly
sqrt(16)
from math import sqrt as square_root # alias to avoid clashes
Avoid from module import * outside the REPL — it hides where names come from and can
clobber locals.
Circular imports
If module A imports B while B imports A, one of them runs into a half-initialised module and some names won't exist yet. The fixes: restructure to remove the cycle, move the import inside the function that needs it (deferring it past module load), or import the module rather than its names.
# Instead of a top-level "from b import thing" that cycles:
def do_work():
from b import thing # imported lazily, after both modules finish loading
return thing()
Recap
Importing finds, executes once, caches, and binds: top-level module code runs a single
time, and sys.modules caches the result so re-imports are instant. Modules are located by
searching sys.path in order (your directory first — beware shadowing stdlib names).
Prefer absolute imports; relative (dotted) imports work only inside packages. from X import name binds names directly but copies references. Break circular imports by
restructuring or deferring the import inside a function.