The Import System
Once your project grows past a single file, you'll organize code into packages — directories of modules. Python then needs rules for two things: how an import statement is written (absolute vs relative), and how Python finds the file behind that import (the module search path). This page covers both, plus two problems every Python developer eventually runs into: circular imports and dynamic imports.
Absolute vs relative imports
Consider a small package laid out like this:
mypkg/
__init__.py
utils.py
core.py# mypkg/utils.py
def helper():
return "helper result"Inside core.py, you can reach helper two different ways:
# mypkg/core.py — absolute import
from mypkg.utils import helper
def run():
return helper()# mypkg/core.py — relative import
from .utils import helper
def run():
return helper()Style | Syntax | When to use |
|---|---|---|
Absolute |
| Clear and unambiguous; preferred by PEP 8 for most cases; works from any file regardless of location |
Relative |
| Useful inside large packages to avoid repeating the full package name; only works inside a package, never in a standalone script |
.) means "this package"; two dots (..) means "the parent package". Relative imports only work inside a module that Python recognizes as part of a package — running that file directly as a script (rather than importing it) will raise an error.How Python finds modules: sys.path
When you write import something, Python doesn't search your whole hard drive — it checks a specific, ordered list of locations stored in sys.path. You can inspect it yourself:
import sys
for entry in sys.path:
print(entry)/home/user/myproject /usr/lib/python3.11 /usr/lib/python3.11/lib-dynload /home/user/myproject/venv/lib/python3.11/site-packages
The exact entries vary by machine, but the search order always follows the same general pattern.
Built-in modules — compiled into the Python interpreter itself (e.g.
sys,builtins); these are checked before touching the filesystem at all.The directory containing the script being run (or the current directory in interactive mode) — this is why sibling files in the same folder are importable with no extra setup.
Directories listed in the
PYTHONPATHenvironment variable, if set — searched in the order they appear.Installation-dependent default paths — the standard library location and the
site-packagesdirectory where packages installed via pip live.
The first matching module found along this path wins — which is exactly why naming your own file math.py can shadow the standard library's math module if your directory is searched first.
The role of __init__.py
An __init__.py file inside a directory marks it as a package. Before Python 3.3, this file was required for a directory to be importable as a package; modern Python supports "namespace packages" without it, but including __init__.py is still standard practice.
Beyond just marking a package, __init__.py can contain real code: it runs automatically the first time the package is imported, so it's a common place to set up package-level initialization or to define __all__, a list that controls exactly which names from package import * will expose. (Package structure and __all__ are covered in full depth on the Packages page — this is just the import-system angle.)
a.py import b def use_b(): return b.valueb.py import a value = 42Running
a.py starts loading a, which immediately tries to import b; while loading b, Python hits import a again — but a is still mid-initialization in sys.modules, so depending on what's accessed you get an ImportError or an AttributeError for a name that "should" exist. The usual fixes are: move the import inside the function that needs it (deferring it until call time, after both modules have finished loading), or restructure the code so the shared logic lives in a third module that both import from instead of importing each other.# a.py — fixed by importing inside the function
def use_b():
import b # deferred import, resolved only when use_b() is called
return b.valueDynamic imports with importlib
Sometimes the module name isn't known until runtime — for example, it comes from a config file or user input. The importlib module lets you import by string name instead of writing a literal import statement.
import importlib
module_name = "json"
json_module = importlib.import_module(module_name)
data = json_module.dumps({"status": "ok"})
print(data){"status": "ok"}This is the mechanism behind plugin systems and frameworks that load code by configuration rather than hard-coded imports — use it when the set of modules to import genuinely isn't known ahead of time; for everyday code, a plain import statement is simpler and easier to statically analyze.