Understanding Python's Import System: Concepts, Mechanisms, and Best Practices
This article provides an in‑depth exploration of Python’s import system, covering basic concepts such as modules and packages, absolute and relative imports, the underlying C implementation, the role of importlib, finders and loaders, and practical guidelines for structuring imports in real projects.
For every Python developer, the import keyword is familiar, yet its inner workings are often overlooked. This article walks through the entire import system, from high‑level concepts to low‑level C implementation, and offers practical advice for clean import usage.
1. What Can Be Imported? – Basic Concepts
In Python, everything is an object, but the two most common importable entities are modules and packages . Both are represented internally by PyModuleObject instances of type PyModule_Type .
// Objects/moduleobject.c
PyTypeObject PyModule_Type = {
PyVarObject_HEAD_INIT(&PyType_Type, 0)
"module", /* tp_name */
sizeof(PyModuleObject), /* tp_basicsize */
// ...
};
// Python's
corresponds to PyModule_Type
// Imported module objects correspond to PyModuleObjectImporting a module or a package yields the same kind of object:
import os
import pandas
print(os) # module 'os' from 'C:\python38\lib\os.py'
print(pandas) # module 'pandas' from 'C:\python38\lib\site-packages\pandas\__init__.py'
print(type(os)) # class 'module'
print(type(pandas)) # class 'module'1.1 Modules
A module is the smallest unit of Python code – a .py , compiled .pyc/.pyo , extension .pyd , or .pyw file.
1.2 Packages
Packages are directories containing multiple modules. Regular packages contain an __init__.py file; since Python 3.3, PEP 420 introduced Namespace Packages , which omit __init__.py and have different runtime attributes ( __path__ , __loader__ , etc.).
project/
├── foo-package/
│ └── spam/blah.py
└── bar-package/
└── spam/grok.py
# Both directories provide a "spam" namespace without __init__.py
import sys
sys.path.extend(['foo-package', 'bar-package'])
import spam.blah # works
import spam.grok # works2. Import Styles – Absolute vs. Relative
Python 2.6 used relative imports by default; from Python 3 onward, absolute imports are the default. Absolute imports specify the full package path, while relative imports use leading dots to indicate the current package hierarchy.
2.1 Absolute Import
from package1 import module1
from package1.module2 import Fx
from package2 import Cx
from package2.subpackage1.module5 import Fy2.2 Relative Import
# package2/module3.py
import module4 # implicit relative import
from . import module4 # explicit relative import
from package2 import module4 # absolute importThe dot ( . ) denotes the current package; .. moves one level up, and so on. Implicit relative imports are discouraged (PEP 328) in favor of explicit imports.
3. Normalising Imports – PEP 8 Guidelines
Key recommendations from PEP 8 include:
One import per line.
Place all imports at the top of the file.
Prefer absolute imports for readability.
Avoid wildcard imports ( from module import * ).
Group imports: standard library, third‑party, then local imports, separated by blank lines.
# Recommended style
import os
import sys
from subprocess import Popen, PIPE
# Not recommended
import os, sys4. What the import Keyword Actually Does
When the interpreter encounters an import statement, it executes the IMPORT_NAME bytecode, which ultimately calls the built‑in __import__ function (or a fast‑path C routine).
0 LOAD_CONST 0 (0)
2 LOAD_CONST 1 (None)
4 IMPORT_NAME 0 (os)
6 STORE_NAME 0 (os)
10 RETURN_VALUEThe C implementation in ceval.c shows the call chain:
case TARGET(IMPORT_NAME): {
PyObject *name = GETITEM(names, oparg);
PyObject *fromlist = POP();
PyObject *level = TOP();
PyObject *res = import_name(tstate, f, name, fromlist, level);
SET_TOP(res);
...
}import_name either calls the fast‑path PyImport_ImportModuleLevelObject or invokes the overridden __import__ function.
4.1 Native Import Path
static PyObject *
import_name(PyThreadState *tstate, PyFrameObject *f,
PyObject *name, PyObject *fromlist, PyObject *level)
{
// ... retrieve __import__ and call it if overloaded
if (import_func == tstate->interp->import_func) {
int ilevel = _PyLong_AsInt(level);
return PyImport_ImportModuleLevelObject(name, f->f_globals,
f->f_locals ? f->f_locals : Py_None,
fromlist, ilevel);
}
// otherwise call the overridden __import__
...
}4.2 __import__ Path
static PyObject *
builtin___import__(PyObject *self, PyObject *args, PyObject *kwds)
{
// parse arguments
...
return PyImport_ImportModuleLevelObject(name, globals, locals,
fromlist, level);
}5. The Search Phase – Finders
The import system first looks in sys.modules . If the module is not cached, it iterates over sys.meta_path , a list of finder objects ( BuiltinImporter , FrozenImporter , PathFinder ).
sys.meta_path
# [
# class '_frozen_importlib.BuiltinImporter',
# class '_frozen_importlib.FrozenImporter',
# class '_frozen_importlib_external.PathFinder'
# ]Each finder implements find_spec(name, path, target) and returns a ModuleSpec . For example, BuiltinImporter.find_spec checks _imp.is_builtin(name) and returns spec_from_loader(name, BuiltinImporter, origin='built-in') if true.
5.1 PathFinder
PathFinder searches sys.path (or a package’s __path__ ) using sys.path_hooks and a cache sys.path_importer_cache . It delegates to FileFinder for filesystem entries.
class PathFinder:
@classmethod
def find_spec(cls, fullname, path=None, target=None):
if path is None:
path = sys.path
spec = cls._get_spec(fullname, path, target)
return spec5.2 FileFinder
FileFinder looks for files with known suffixes ( .py , .pyc , extension modules) and creates a loader via spec_from_file_location .
def _get_supported_file_loaders():
extensions = ExtensionFileLoader, _imp.extension_suffixes()
source = SourceFileLoader, SOURCE_SUFFIXES
bytecode = SourcelessFileLoader, BYTECODE_SUFFIXES
return [extensions, source, bytecode]6. The Load Phase – Loaders
Given a ModuleSpec , the import system calls _load_unlocked(spec) . If the loader provides exec_module , it creates a new module object, inserts it into sys.modules , and executes the loader’s exec_module method.
def _load_unlocked(spec):
if spec.loader is not None:
if not hasattr(spec.loader, 'exec_module'):
return _load_backward_compatible(spec)
module = module_from_spec(spec)
sys.modules[spec.name] = module
try:
spec.loader.exec_module(module)
except Exception:
del sys.modules[spec.name]
raise
return moduleTypical loaders:
ExtensionFileLoader : loads compiled shared libraries via dlopen / LoadLibrary and calls the module’s PyInit_modulename .
SourcelessFileLoader : reads .pyc , unmarshals the code object, and executes it.
SourceFileLoader : compiles .py to a code object before execution.
7. Import Tricks and Advanced Usage
Advanced techniques include dynamically modifying sys.path or sys.meta_path to add custom finders (e.g., loading modules from a remote source), using import hooks, and building plugin systems that reload modules via importlib.reload() .
# Dynamically add a directory to the search path
import sys
sys.path.append('/my/custom/dir')
# Register a custom finder
import importlib.abc, importlib.util
class RemoteFinder(importlib.abc.MetaPathFinder):
def find_spec(self, fullname, path, target=None):
# custom logic to fetch module code from a server
...
sys.meta_path.insert(0, RemoteFinder())By understanding the full import pipeline—from the high‑level import statement down to the C‑level bytecode and the interplay of finders and loaders—developers can write clearer code, debug import‑related issues, and extend Python’s import machinery for sophisticated applications.
Xueersi Online School Tech Team
The Xueersi Online School Tech Team, dedicated to innovating and promoting internet education technology.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.