Include/internal/pycore_import.h
cpython 3.14 @ ab2d84fe1023/Include/internal/pycore_import.h
pycore_import.h collects the internal interfaces for CPython's import
machinery that are not exposed through the public Python/import.h API.
It declares the per-interpreter recursive import lock, the low-level
sys.modules dict accessors used inside import.c, the frozen module
registry (_PyImport_FrozenBootstrap and friends), and the extension
module cache that tracks .so-backed modules across sub-interpreters.
The import lock (_PyImport_AcquireLock) is a per-interpreter recursive
mutex. It is taken at the start of PyImport_ImportModuleLevelObject and
released after the module is fully initialized. Recursive imports by the
same thread (which happen constantly during importlib bootstrap) are
allowed: an internal depth counter tracks the recursion level, and the lock
is only released when the depth returns to zero.
The sys.modules dict is stored on PyInterpreterState.modules (declared
in pycore_interp_structs.h). The accessors here (_PyImport_GetModule,
_PyImport_SetModule) wrap the raw dict with the interpreter argument so
that sub-interpreter isolation is maintained: each sub-interpreter has its
own sys.modules dict and cannot see modules cached by another
sub-interpreter.
Map
| Lines | Symbol | Role | gopy |
|---|---|---|---|
| 1-30 | include guard + _PyImport_ImportLock struct | Per-interpreter lock: mutex, thread owner id, level depth counter. | imp/sysmodules.go sysModulesMu |
| 31-80 | _PyImport_AcquireLock / _PyImport_ReleaseLock / _PyImport_IsInitialized | Lock/unlock with recursion depth; initialized predicate. | imp/import.go |
| 81-120 | _PyImport_GetModule / _PyImport_SetModule / _PyImport_RemoveModule | sys.modules dict wrappers taking an PyInterpreterState *. | imp/sysmodules.go GetModule / AddModule / RemoveModule |
| 121-160 | _PyImport_GetModuleAttr / _PyImport_GetModuleAttrString | Fetch an attribute from a named module in sys.modules; used by _io, warnings, etc. | n/a |
| 161-200 | _PyImport_FrozenBootstrap / _PyImport_FrozenStdlib / _PyImport_FrozenTest | Null-terminated arrays of struct _frozen for the three frozen module sets. | imp/frozen.go |
| 201-240 | _PyImport_FindExtensionObject / _PyImport_FixupExtensionObject | Cache and retrieve PyModuleDef-backed .so modules; keyed by (name, path). | n/a (no .so loading in gopy) |
Reading
Import lock protocol (lines 1 to 80)
cpython 3.14 @ ab2d84fe1023/Include/internal/pycore_import.h#L1-80
struct _import_lock {
PyThread_type_lock lock;
unsigned long thread; /* OS thread id of the current owner, or 0 */
int level; /* recursion depth for the current owner */
};
int
_PyImport_AcquireLock(PyInterpreterState *interp)
{
unsigned long me = PyThread_get_thread_ident();
struct _import_lock *import_lock = &interp->imports.lock;
if (import_lock->thread == me) {
/* Recursive acquire by the same thread: just bump the depth. */
import_lock->level++;
return 1;
}
/* Blocking acquire by a different thread. */
if (PyThread_acquire_lock(import_lock->lock, WAIT_LOCK) == PY_LOCK_FAILURE) {
return -1;
}
assert(import_lock->thread == 0 && import_lock->level == 0);
import_lock->thread = me;
import_lock->level = 1;
return 1;
}
int
_PyImport_ReleaseLock(PyInterpreterState *interp)
{
struct _import_lock *import_lock = &interp->imports.lock;
if (import_lock->thread != PyThread_get_thread_ident() || import_lock->level < 1) {
return -1; /* not owner */
}
import_lock->level--;
if (import_lock->level == 0) {
import_lock->thread = 0;
PyThread_release_lock(import_lock->lock);
}
return 0;
}
The import lock prevents two threads from simultaneously initializing the
same module. Without it, a second thread that reaches import foo while
the first thread is mid-way through executing foo.py would see a
partially-initialized module in sys.modules and proceed to use it.
The level counter allows the same thread to re-enter the lock during
bootstrap. The importlib bootstrap itself does import _frozen_importlib
which recursively calls into PyImport_ImportModuleLevelObject. Without
the recursion check, importing any module would deadlock immediately.
In gopy, the imp package uses a sync.RWMutex (sysModulesMu in
imp/sysmodules.go) to serialize writes to sys.modules. There is no
separate import lock struct; Go's sync.RWMutex already permits recursive
read-locks, and the single-interpreter constraint means only one goroutine
runs Python bytecode at a time in v0.12.
sys.modules dict access pattern (lines 81 to 160)
cpython 3.14 @ ab2d84fe1023/Include/internal/pycore_import.h#L81-160
PyObject *
_PyImport_GetModule(PyInterpreterState *interp, PyObject *name)
{
PyObject *modules = interp->modules;
if (modules == NULL) {
return NULL;
}
PyObject *m;
(void)PyDict_GetItemRef(modules, name, &m);
return m; /* new reference, or NULL if not found */
}
int
_PyImport_SetModule(PyInterpreterState *interp, PyObject *name, PyObject *module)
{
PyObject *modules = interp->modules;
if (modules == NULL) {
PyErr_SetString(PyExc_RuntimeError, "no sys.modules");
return -1;
}
return PyObject_SetItem(modules, name, module);
}
Every import path bottlenecks through these two functions rather than
accessing interp->modules directly. The indirection means that a
sub-interpreter can swap out its modules dict (e.g. to run with an
isolated import state) without any call-site changes.
_PyImport_GetModuleAttr and _PyImport_GetModuleAttrString are
convenience wrappers used heavily by the standard library. For example, the
warnings module implementation calls
_PyImport_GetModuleAttrString(interp, "warnings", "_filters_mutated") to
notify filters rather than importing warnings itself, avoiding a circular
import risk.
In gopy, imp/sysmodules.go provides GetModule, AddModule, and
RemoveModule as direct ports, using the package-level sysModules *Dict
rather than interp->modules because gopy has only one interpreter in
v0.12. The vm/eval_import.go IMPORT_NAME arm calls imp.ImportModuleLevel
which checks GetModule as its first cache hit step.
Frozen module registry (lines 161 to 200)
cpython 3.14 @ ab2d84fe1023/Include/internal/pycore_import.h#L161-200
/* Each entry describes one frozen module. */
struct _frozen {
const char *name;
const unsigned char *code; /* marshalled PyCodeObject bytes */
int size;
int is_package;
const char *get_code; /* optional thunk name for lazy loading */
};
/* Three separate arrays, all NULL-terminated. */
extern const struct _frozen _PyImport_FrozenBootstrap[];
extern const struct _frozen _PyImport_FrozenStdlib[];
extern const struct _frozen _PyImport_FrozenTest[];
CPython ships three sets of frozen modules:
_PyImport_FrozenBootstrap: the importlib bootstrap modules (_frozen_importlib,_frozen_importlib_external,zipimport). These are imported before the file-system finder is available._PyImport_FrozenStdlib: selected stdlib modules frozen for startup speed (e.g.abc,codecs,encodings.utf_8)._PyImport_FrozenTest: test helpers frozen only in debug builds.
The import machinery in import.c searches all three arrays in order
during _PyImport_FindFrozenObject. A frozen module is loaded by
unmarshalling the embedded code bytes with PyMarshal_ReadObjectFromString,
then executing the resulting code object with PyImport_ExecCodeModule.
In gopy, imp/frozen.go holds the equivalent FrozenModule registry and
imp/bootstrap.go registers the importlib bootstrap. The frozen module
bytes are stored as Go []byte constants generated by the stdlibinit
package during the MANIFEST.txt-driven build step.
gopy mirror
The gopy import pipeline in imp/ maps to this header as follows:
_PyImport_AcquireLock/_PyImport_ReleaseLock: replaced bysysModulesMu sync.RWMutexinimp/sysmodules.go._PyImport_GetModule/_PyImport_SetModule:GetModule/AddModuleinimp/sysmodules.go._PyImport_FrozenBootstrap:FindFrozeninimp/frozen.go._PyImport_FindExtensionObject: not ported; gopy has no.soloader.
The IMPORT_NAME bytecode arm in vm/eval_import.go mirrors
PyImport_ImportModuleLevelObject (Python/import.c:1561) by checking
the sys.modules cache first, then delegating to imp.ImportModuleLevel.