Import errors, circular imports and plugin loading are recurring problems in large codebases.
import pkg.mod does roughly this:
- check `sys.modules`, the cache of already-imported modules; if present, use it (this is why module code runs once)
- otherwise import the parent package
pkgfirst (running its__init__.py), then ask each finder onsys.meta_pathfor a module spec. The defaults handle built-in modules, frozen modules, and the filesystem viaPathFinder, which searchessys.path(or the parent package's__path__) - create the module object, insert it into `sys.modules`, then have the spec's loader execute the code in the module's namespace
- bind the name in the importing scope
Inserting into sys.modules before executing explains circular imports: if a imports b and b imports a at top level, b receives the partially initialised a, and from a import thing fails if thing isn't defined yet. Fixes: move shared code to a third module, import the module rather than names (import a and use a.thing later), or import inside the function.
Source files are compiled to bytecode and cached as __pycache__/mod.cpython-313.pyc, invalidated by source timestamp (or hash). The tag shows caches are per interpreter version.
A directory without __init__.py on sys.path becomes a namespace package, which can span several directories — handy for plugins, confusing when accidental. python -m pkg.tool runs a module with its package context, so relative imports work, unlike python pkg/tool.py.
Programmatic tools live in importlib: import_module(name) for dynamic imports, importlib.util.find_spec, importlib.resources for data files, importlib.metadata for installed distributions and entry points. A module-level __getattr__ (PEP 562) enables lazy attributes and deprecation shims. importlib.reload re-executes a module but doesn't update objects already imported elsewhere.
Example
import importlib
import importlib.util
import sys
import tempfile
from pathlib import Path
spec = importlib.util.find_spec("json")
print(spec.name, Path(spec.origin).name) # json __init__.py
pkg_dir = Path(tempfile.mkdtemp())
(pkg_dir / "settings.py").write_text(
"import warnings\n"
"TIMEOUT = 30\n"
"def __getattr__(name):\n"
" if name == 'OLD_TIMEOUT':\n"
" warnings.warn('use TIMEOUT', DeprecationWarning)\n"
" return TIMEOUT\n"
" raise AttributeError(name)\n")
sys.path.insert(0, str(pkg_dir))
settings = importlib.import_module("settings")
print(settings.OLD_TIMEOUT) # 30 (via module __getattr__)
print(importlib.import_module("settings") is sys.modules["settings"]) # True
print(type(settings.__spec__.loader).__name__) # SourceFileLoaderRemember
- Imports check
sys.modules, then finders onsys.meta_pathlocate a spec - Modules enter
sys.modulesbefore executing — the root of circular-import errors - Use
python -mfor package modules andimportlibfor dynamic imports
Watch out for
- Fixing a circular import by reordering lines instead of restructuring the dependency.