Skip to main content

Python/import.c (part 9)

Source:

cpython 3.14 @ ab2d84fe1023/Python/import.c

This annotation covers the import system's find-and-load path. See python_import8_detail for IMPORT_NAME, IMPORT_FROM, sys.modules caching, and relative imports.

Map

LinesSymbolRole
1-80_find_and_loadOuter entry: check sys.modules, call _find_and_load_unlocked
81-180_find_and_load_unlockedCheck for circular imports, call _find_spec
181-280_find_specTry each sys.meta_path finder
281-380Import lockPer-module lock prevents duplicate initialization
381-600_load_unlockedCall spec.loader.exec_module

Reading

_find_and_load

# CPython: Lib/importlib/_bootstrap.py:1240 _find_and_load
def _find_and_load(name, import_):
module = sys.modules.get(name, _NEEDS_LOADING)
if module is _NEEDS_LOADING:
return _find_and_load_unlocked(name, import_)
if module is None:
message = f'import of {name} halted; use of sys.modules[{name!r}] = None'
raise ModuleNotFoundError(message)
_lock_unlock_module(name)
return module

sys.modules[name] = None is a way to block an import (useful for mocking). The _NEEDS_LOADING sentinel distinguishes "not yet imported" from "explicitly blocked".

_find_spec

# CPython: Lib/importlib/_bootstrap.py:1200 _find_spec
def _find_spec(name, path, target=None):
meta_path = sys.meta_path
if meta_path == []:
raise ImportError('sys.meta_path is empty')
for finder in meta_path:
with _ImportLockContext():
try:
find_spec = finder.find_spec
except AttributeError:
continue
spec = find_spec(name, path, target)
if spec is not None:
return spec
return None

sys.meta_path is tried in order. Built-in finders: BuiltinImporter (C extensions built into the interpreter), FrozenImporter (frozen stdlib modules), PathFinder (searches sys.path). A None spec from a finder means "I don't handle this module".

Import lock

// CPython: Python/import.c:340 _get_module_lock
static _PyRecursiveMutex *
_get_module_lock(PyInterpreterState *interp, PyObject *name)
{
/* Get or create a per-module lock */
PyObject *locks = interp->imports.locks;
PyObject *lock = PyDict_GetItemWithError(locks, name);
if (lock == NULL) {
lock = _PyThread_allocate_lock();
PyDict_SetItem(locks, name, lock);
}
return (_PyRecursiveMutex *)lock;
}

Each module name gets its own recursive lock. This allows the importing thread to re-enter the same module (circular imports: module A imports B, B imports A; A's partial module is returned). A second thread trying to import the same module blocks until the first thread finishes.

_load_unlocked

# CPython: Lib/importlib/_bootstrap.py:1120 _load_unlocked
def _load_unlocked(spec):
module = module_from_spec(spec)
sys.modules[spec.name] = module # Install BEFORE exec_module
try:
spec.loader.exec_module(module)
except BaseException:
try:
del sys.modules[spec.name]
except KeyError:
pass
raise
return sys.modules[spec.name]

The module is installed in sys.modules before exec_module runs. This handles circular imports: if exec_module triggers another import of the same module, the partial module is returned from sys.modules. If exec_module raises, the module is removed.

gopy notes

_find_and_load is in vm/eval_import.go as importFindAndLoad. _find_spec iterates objects.SysMetaPath. The import lock is a per-module sync.Mutex in a sync.Map. _load_unlocked calls objects.LoaderExecModule.