Fundamentals 45 min read

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.

Xueersi Online School Tech Team
Xueersi Online School Tech Team
Xueersi Online School Tech Team
Understanding Python's Import System: Concepts, Mechanisms, and Best Practices

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 PyModuleObject

Importing 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   # works

2. 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 Fy

2.2 Relative Import

# package2/module3.py
import module4               # implicit relative import
from . import module4         # explicit relative import
from package2 import module4 # absolute import

The 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, sys

4. 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_VALUE

The 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 spec

5.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 module

Typical 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.

pythonModulespackagesCode LoadingImport SystemPEP
Xueersi Online School Tech Team
Written by

Xueersi Online School Tech Team

The Xueersi Online School Tech Team, dedicated to innovating and promoting internet education technology.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.