The import system

Python · Expert · 16 min

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 pkg first (running its __init__.py), then ask each finder on sys.meta_path for a module spec. The defaults handle built-in modules, frozen modules, and the filesystem via PathFinder, which searches sys.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__)   # SourceFileLoader

Remember

  • Imports check sys.modules, then finders on sys.meta_path locate a spec
  • Modules enter sys.modules before executing — the root of circular-import errors
  • Use python -m for package modules and importlib for dynamic imports

Watch out for

  • Fixing a circular import by reordering lines instead of restructuring the dependency.

Check yourself

  1. Module a does `from b import f` and b does `from a import g` at top level. Why does `import a` fail?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.python.org/3/reference/import.html