Python Basics: Discover Local Module Names with pkgutil
A diagnostic tool can list candidate modules without importing each one. pkgutil.iter_modules inspects a search path and reports names plus a package flag. A fixture containing import-time exceptions demonstrates the boundary between discovery and execution.
Prepare a scratch directory
Tested on Linux with CPython 3.12.14. You need the named runtime, Bash, a text editor, and the standard mktemp, rm, and rmdir utilities. Run Python without -O so the assertions remain enabled. Any generated fixture files live inside an automatically cleaned temporary directory.
Keep using the same terminal. Stop if setup fails, before saving the source:
start_dir="$PWD"
work_dir="$(mktemp -d)" || exit 1
cd -- "$work_dir" || exit 1Run the example
Save this as demonstrate-pkgutil.py:
import pkgutil
import sys
from pathlib import Path
from tempfile import TemporaryDirectory
with TemporaryDirectory() as directory:
root = Path(directory)
(root / "example_leaf.py").write_text(
'raise RuntimeError("do not import")\n', encoding="utf-8")
package = root / "example_group"
package.mkdir()
(package / "__init__.py").write_text(
'raise RuntimeError("do not import package")\n', encoding="utf-8")
(package / "child.py").write_text("VALUE = 3\n", encoding="utf-8")
found = sorted((item.name, item.ispkg) for item in pkgutil.iter_modules([directory]))
assert found == [("example_group", True), ("example_leaf", False)]
for name, is_package in found:
print(name, "package=" + str(is_package))
assert "example_leaf" not in sys.modules
assert "example_group" not in sys.modules
children = list(pkgutil.iter_modules([str(package)], prefix="example_group."))
assert [(item.name, item.ispkg) for item in children] == [("example_group.child", False)]
print("Explicit child path:", children[0].name)
print("Fixture modules imported:", False)Run it:
python3 -B demonstrate-pkgutil.pyObserved output:
example_group package=True
example_leaf package=False
Explicit child path: example_group.child
Fixture modules imported: FalseRead the result
The first call inspects only the supplied top-level directory, so it finds the package and leaf module but not the child inside the package. The second call supplies the child directory directly. Its prefix changes the reported name; it does not import the parent. Neither deliberate import-time exception runs, and the module-cache assertions verify the fixture names were not imported.
Apply it carefully
iter_modules is shallow. walk_packages is recursive but needs to import packages to discover their paths, so it has different side effects. Discovery also depends on importer support; it is not an exhaustive inventory of everything importable, especially with namespace packages or custom import hooks. Do not treat a discovered name as permission to load untrusted code.
The official reference describes the API and its options. The checks above cover the specific fixture, not every possible input or environment.
Clean up
Remove the source and then the empty practice directory:
rm -- demonstrate-pkgutil.py
cd -- "$start_dir" || exit 1
rmdir -- "$work_dir"
unset start_dir work_dirIf the directory is not empty, inspect the remaining files rather than deleting unknown contents.
Acknowledgements
This post was created with assistance from AI.