Python Module 18 – Advanced Python: data model, metaprogramming and internals
Inside the import system
Follow a Python import through sys.modules, finders, specs and loaders, import plugins by name with importlib, and read package data and installed versions.
What you will learn
- Trace an import through sys.modules, the finders on sys.meta_path and the loader that runs the module
- Read a module's spec with importlib.util.find_spec() and say what its loader and origin mean
- Import plugins by name with importlib.import_module() and report each failure without stopping
- Write a meta path finder that serves or blocks modules, and place it on sys.meta_path
- Read data files and installed versions with importlib.resources and importlib.metadata
Before you start
What you’ll learn · Before you start
What you will learn
- Trace an import through sys.modules, the finders on sys.meta_path and the loader that runs the module
- Read a module's spec with importlib.util.find_spec() and say what its loader and origin mean
- Import plugins by name with importlib.import_module() and report each failure without stopping
- Write a meta path finder that serves or blocks modules, and place it on sys.meta_path
- Read data files and installed versions with importlib.resources and importlib.metadata
Before you start
On this page
The first import greeting in a program goes through a cache, a list of finders and a loader before the module’s
first line runs; the second import greeting is a single dictionary lookup. Knowing those steps explains why a module
imported twice runs once, why a plugin system can load the wrong file, and what each kind of import error means.
Below you will watch the finders being asked, read the specs of five modules, and build a plugin loader that reports each broken plugin instead of stopping at the first.
An import looks in sys.modules before it searches anywhere
import greeting first looks for the name in sys.modules, the module cache: The dictionary sys.modules, which maps the name of every module a program has imported to its module object. An import looks there first, which is why a module's file runs only once per program. More in “Modules, imports and __name__”. Only
on a miss does Python search: it asks each finder: An object the import system asks to find a module by name. It answers with a module spec that says how to load the module, or with None when the module is not one it knows; the finders on sys.meta_path are asked in order. on sys.meta_path: The list of finders that an import asks, in order, for a module that is not in sys.modules yet. By default it holds one finder for built-in modules, one for frozen modules and one for the folders of sys.path.
in turn, and the first one that knows the module returns a module spec: A ModuleSpec object: what a finder knows about a module, such as its name, its loader, where it comes from (origin) and, for a package, where its submodules are. A module keeps its spec as __spec__. naming a
loader: The object that turns a module spec into a working module: the import system calls its create_module() to make the module object and its exec_module() to run the module's code in it.. The loader creates the module, the module is added to sys.modules, and only then
does its code run. Step through a first and a second import:
The drawing shows the whole scene; its 8 steps are listed below.
Every step as text
- Look in the cache. The first import greeting looks up the name in sys.modules. It is not there yet, so the search begins.
- The finders, in order. Python asks each finder on sys.meta_path in turn. The Watcher of main.py is first and prints that it was asked, then returns None, so the next finder is asked.
- The path finder knows it. The path finder searches the folders of sys.path and finds greeting.py beside main.py. It returns a spec whose loader is a SourceFileLoader.
- An empty module. The loader makes an empty module object, and the import system sets its __name__, __file__ and __spec__ from the spec.
- Registered first. The module goes into sys.modules before any of its code runs, so a module that imports greeting back meets this half-made module instead of loading a second copy.
- The code runs. exec_module() runs greeting.py in the new module's namespace. That printed the line "greeting.py runs" and defined hello().
- The name is bound. The import statement binds the name greeting in main.py to the module, and the first import is done.
- The second import. The second import greeting finds the module in sys.modules and binds the name at once. No finder is asked and greeting.py does not run again, which is why main.py printed nothing between its two headings.
The program puts a finder of its own at the front of sys.meta_path, one that only reports each name it is asked for:
steps/main.py
import sys
class Watcher:
"""A finder that only reports the names it is asked for."""
def find_spec(self, name, path, target=None):
print(f" finders asked for {name!r}")
return None # not mine: the next finder on sys.meta_path answers
sys.meta_path.insert(0, Watcher())
print("first import:")
import greeting
print("second import:")
import greeting # noqa: F811
print("cached:", sys.modules["greeting"] is greeting)
print(greeting.hello("Asha")) Output
first import: finders asked for 'greeting' greeting.py runs second import: cached: True Hello, Asha
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 main.py
steps/greeting.py
print(" greeting.py runs")
def hello(name):
return f"Hello, {name}" Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
The finder was asked once and greeting.py ran once; the second import found the module in sys.modules and asked
nobody. Because the module is in the cache before its code runs, a circular import: Two or more modules that import each other. It fails when one of them asks for a name that the other has not defined yet, because that module is still running its first lines. More in “Modules, imports and __name__”
meets a half-built module instead of loading a second copy. And if the module’s code raises, the import system takes
it out of sys.modules again, so a later import starts afresh.
Checkpoint
Check: The steps of an import
A finder returns a spec, and a loader runs the module
importlib.util.find_spec(name) asks the same finders without importing the module and returns the spec, or None
when no finder knows the name; for a dotted name such as json.decoder it does import the parent package first. A spec records what the import system needs: the module’s name, its loader, its
origin (a file, built-in or frozen) and, for a package, where its submodules are. By default sys.meta_path
holds three finders: one for modules built into the interpreter, one for frozen modules and one that searches the
folders of sys.path.
specs/specs.py
import importlib.util
import pathlib
for name in ["json", "sys", "__hello__", "ledger", "no_such_module"]:
spec = importlib.util.find_spec(name) # finds the module, imports nothing
if spec is None:
print(f"{name}: no spec, so importing it raises ModuleNotFoundError")
continue
loader = spec.loader if isinstance(spec.loader, type) else type(spec.loader)
origin = pathlib.Path(spec.origin).name if spec.has_location else spec.origin
kind = "package" if spec.submodule_search_locations is not None else "module"
print(f"{name}: {kind}, loader {loader.__name__}, origin {origin}") Output
json: package, loader SourceFileLoader, origin __init__.py sys: module, loader BuiltinImporter, origin built-in __hello__: module, loader FrozenImporter, origin frozen ledger: module, loader SourceFileLoader, origin ledger.py no_such_module: no spec, so importing it raises ModuleNotFoundError
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 specs.py
specs/ledger.py
BALANCE = 0 Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Version note
In the browser, Python’s standard library is kept in one zip file, so json is found there and loaded by
zipimporter, the loader for modules inside zip archives. The steps of the import are the same; only the loader
differs.
In your browser, Pyodide 314.0.7 (CPython 3.14.2) prints:
json: package, loader zipimporter, origin __init__.py sys: module, loader BuiltinImporter, origin built-in __hello__: module, loader FrozenImporter, origin frozen ledger: module, loader SourceFileLoader, origin ledger.py no_such_module: no spec, so importing it raises ModuleNotFoundError
sys is compiled into the interpreter and __hello__ is a frozen module, whose code is stored inside the interpreter
too, so neither has a file. json is a package and ledger is the file beside the script. A module that has been
imported keeps its spec as module.__spec__, and its __file__, __loader__ and __package__ are set from it.
Checkpoint
Check: Module specs and loaders
Importing a module whose name is known only at run time
A plugin system reads module names from a setting, which the import statement cannot use, because its names are
written in the code. importlib.import_module(name) takes the name as a string and returns that module, for a dotted
name the submodule itself, where __import__() would return the top-level package. This loader imports four plugins
and keeps going when one fails:
plugins/main.py
import importlib
def load_plugins(names):
"""Imports each plugin by name; one that fails is reported, not fatal."""
loaded, errors = {}, {}
for name in names:
try:
loaded[name] = importlib.import_module(name)
except ModuleNotFoundError as error:
if error.name == name:
errors[name] = "no such module"
else: # the plugin exists, but something it imports does not
errors[name] = f"needs {error.name}"
except Exception as error: # the plugin's own code failed
errors[name] = f"{type(error).__name__}: {error}"
return loaded, errors
names = ["plugins.csv_export", "plugins.pdf_export", "plugins.broken", "plugins.xml_export"]
loaded, errors = load_plugins(names)
for name, module in loaded.items():
print(f"loaded {name}: {module.FORMAT}")
for name, reason in errors.items():
print(f"failed {name}: {reason}") Output
loaded plugins.csv_export: CSV failed plugins.pdf_export: needs fancy_pdf failed plugins.broken: ValueError: invalid literal for int() with base 10: 'A4' failed plugins.xml_export: no such module
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 main.py
plugins/__init__.py · plugins/plugins/__init__.py
"""Export plugins: each module of this package adds one export format.""" plugins/plugins/csv_export.py
FORMAT = "CSV" plugins/plugins/pdf_export.py
import fancy_pdf # a library this machine does not have
FORMAT = "PDF" plugins/plugins/broken.py
FORMAT = "XLSX"
PAGE_SIZE = int("A4") # a bug in the plugin itself Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
The three failures are three different problems. plugins.xml_export does not exist, plugins.pdf_export exists but
imports fancy_pdf, which this machine lacks, and plugins.broken runs and raises a ValueError of its own. The
name of a ModuleNotFoundError tells the first two apart: it is the plugin’s own name only when the plugin itself
is missing.
Reporting every ImportError as a missing plugin
except ImportError: print(name, "not installed") reports a plugin that is installed but needs another library as
not installed, and sends someone to install what is already there. Compare error.name with the name you imported.
A loader that tells the cases apart turns a vague import error into the right fix: correct the plugin’s name, install the library it needs, or repair the plugin’s own code.
Key idea
Every import asks sys.modules first, then the finders for a spec, then the spec’s loader, and the step that failed
says what to fix.
Checkpoint
Check: Importing by name
Practise: a plugin loader, then check yourself
The exercise asks for the loader of this lesson in the shape a plugin system needs, with the three kinds of failure told apart. Its sample tests write their plugins to a temporary folder and run in your browser.
Exercise · Medium · Python
A plugin loader that reports every failure
load_plugins(names) in loader.py imports each module named in the list names and returns two dictionaries, (loaded, errors), in the order of names:
loadedmaps each name that imported to its module. For a dotted name such as"tools.text"that is the moduletools.textitself, not the packagetools.errorsmaps each name that failed to a short reason:"missing"when that module does not exist,"needs <module>"when it exists but a module it imports does not, and the class name of the exception, such as"ValueError", for any other error the plugin's own code raises.
One failing plugin never stops the others. The tests write their plugins to a temporary folder first.
Starter code · loader.py
def load_plugins(names):
"""Imports each named module and returns (loaded, errors), never stopping at a failure."""
loaded, errors = {}, {}
for name in names:
try:
loaded[name] = __import__(name)
except ImportError:
errors[name] = "missing"
return loaded, errors The sample tests · test_loader.py
import importlib
import sys
import tempfile
from pathlib import Path
from loader import load_plugins
FOLDER = Path(tempfile.mkdtemp())
FILES = {
"plug_a.py": "NAME = 'a'\n",
"plug_b.py": "NAME = 'b'\n",
"plug_needs.py": "import plug_library_not_here\nNAME = 'needs'\n",
"plug_broken.py": "NAME = 'broken'\nraise ValueError('bad setting')\n",
"plug_pkg/__init__.py": "",
"plug_pkg/part.py": "NAME = 'part'\n",
}
for file, text in FILES.items():
(FOLDER / file).parent.mkdir(parents=True, exist_ok=True)
(FOLDER / file).write_text(text)
sys.path.insert(0, str(FOLDER))
importlib.invalidate_caches()
def test_loads_good_plugins():
"""plugins that import are returned as modules, with no errors"""
loaded, errors = load_plugins(["plug_a", "plug_b"])
assert [m.NAME for m in loaded.values()] == ["a", "b"]
assert errors == {}
def test_dotted_name_gives_the_module_itself():
"""a dotted name returns the submodule, not its package"""
loaded, _ = load_plugins(["plug_pkg.part"])
assert loaded["plug_pkg.part"].__name__ == "plug_pkg.part"
def test_missing_plugin():
"""a module that does not exist is reported as missing"""
assert load_plugins(["plug_not_written"]) == ({}, {"plug_not_written": "missing"})
def test_missing_dependency():
"""a plugin that imports a missing module names that module"""
_, errors = load_plugins(["plug_needs"])
assert errors == {"plug_needs": "needs plug_library_not_here"}
def test_plugin_raises():
"""an exception from the plugin's own code is reported by its class name"""
_, errors = load_plugins(["plug_broken"])
assert errors == {"plug_broken": "ValueError"}
def test_one_failure_does_not_stop_the_others():
"""every name is tried, in order, whatever fails before it"""
loaded, errors = load_plugins(["plug_broken", "plug_a", "plug_not_written", "plug_b"])
assert list(loaded) == ["plug_a", "plug_b"]
assert list(errors) == ["plug_broken", "plug_not_written"] A hint
__import__("tools.text") returns the package tools, which is why the documentation points to importlib.import_module() for imports by name. To tell a missing plugin from a plugin with a missing dependency, look at the name attribute of the ModuleNotFoundError: which module could not be found?
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Then check the import steps, specs and imports by name: every choice says why it is right or what it misses, and a missed idea is asked again.
Practice that adapts to you
Practise this lesson
9 questions about 11 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Finders and loaders of your own, and loading a file by its path Pro: opens with a Pro, Premium or Ultimate pass
The import system is open: a finder of your own on sys.meta_path can serve modules from anywhere, and its loader
decides how their code runs. This part builds a finder that serves modules from a dictionary, shows that its place
on the list decides which module wins, and reproduces what breaks when a file is loaded by path without registering
it.
In this part:
- A meta path finder that serves modules from a dictionary of source text
- The first finder in the meta path wins: a dictionary module and a file of one name
- Loading a file by its path, and the pickle that fails without sys.modules
- Lazy loading with LazyLoader, and the error that moves to the first use
- Exercise: a finder that makes a module look missing
- A finder that makes a module look missing
- Results of the sample tests
4 runnable examples (Python, greet.py, lazy_import.py, load_by_path.py, order.py, plugins/invoice.py, report.py) · 1 exercise with sample tests · about 9 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Reloading, package data and installed metadata, each failure reproduced Pro: opens with a Pro, Premium or Ultimate pass
Three parts of importlib solve problems that file paths and restarts cannot. This part reproduces what
importlib.reload() leaves behind, reads a data file that sits inside a package even when the package is in a zip
file, and finds installed versions and plugins through their metadata.
In this part:
- What importlib.reload() keeps: old objects beside new code, reproduced
- Rules that change without a restart: reload checked data, not code
- Package data that works from a folder and from a zip: importlib.resources
- Installed versions and plugins from metadata, with a .dist-info written by hand
4 runnable examples (Python, csv_export.py, demo_plugins/__init__.py, installed.py, json_export.py, read_units.py, units/__init__.py, units/factors.json) · about 7 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Interview questions
-
Mid to senior
After a deploy, a worker still behaves as if reports.py were the old version: a function added in the new version is missing, although the new file is on disk. How do you find which code the worker is running and why, and how do you stop it happening again?
-
Mid to senior
Your team wants pricing rules, written as a Python module, to change in a long-running service without a restart, by calling importlib.reload() when the file changes. What breaks, and what would you build instead?
-
Mid to senior
A command-line tool takes 1.8 seconds before it prints --help, and users notice. How do you find which imports cost the time, and what would you change, on Python 3.14 and on 3.15?
Key takeaways
- Every import asks
sys.modulesfirst, so a module’s code runs once per program, and the module is in the cache before its code runs. - On a miss, the finders on
sys.meta_pathare asked in order for a spec, and the spec’s loader creates and runs the module. importlib.util.find_spec()shows where a module would come from without importing it.importlib.import_module()imports by name, and thenameof aModuleNotFoundErrorsays which module was missing.
Terms in this lesson
- finder
- An object the import system asks to find a module by name. It answers with a module spec that says how to load the module, or with None when the module is not one it knows; the finders on sys.meta_path are asked in order.
- loader
- The object that turns a module spec into a working module: the import system calls its create_module() to make the module object and its exec_module() to run the module's code in it.
- module spec
- A ModuleSpec object: what a finder knows about a module, such as its name, its loader, where it comes from (origin) and, for a package, where its submodules are. A module keeps its spec as __spec__.
- sys.meta_path
- The list of finders that an import asks, in order, for a module that is not in sys.modules yet. By default it holds one finder for built-in modules, one for frozen modules and one for the folders of sys.path.
References
- The Python Language Reference: The import system (Python Software Foundation)
- importlib: The implementation of import (Python Software Foundation)
- importlib.reload() (Python Software Foundation)
- importlib.util.find_spec() (Python Software Foundation)
- importlib.machinery.ModuleSpec (Python Software Foundation)
- importlib.resources: Package resource reading, opening and access (Python Software Foundation)
- importlib.metadata: Accessing package metadata (Python Software Foundation)
- sys.meta_path (Python Software Foundation)
- sys.modules (Python Software Foundation)
- zipimport: Import modules from Zip archives (Python Software Foundation)
- Command line and environment: -X importtime (Python Software Foundation)
- PEP 451: A ModuleSpec Type for the Import System (Python Software Foundation)
- PEP 810: Explicit lazy imports (Python Software Foundation)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress