The pkgutil module provides utilities for working with Python’s import system. It can list modules available on selected paths, walk subpackages, inspect importers, read resources through loaders, and support legacy namespace-package patterns. It appears in plugin discovery, inspection tools, registries, extensible CLIs, and import diagnostics.
Python imports are dynamic, so discovery requires care. Walking packages can execute initialization code, custom loaders may have their own behavior, and a discovered name is not automatically safe or compatible. Prefer modern importlib APIs when they cover the task, but understand pkgutil because many libraries still rely on it.
List modules with iter_modules
pkgutil.iter_modules() yields information about modules visible on search paths.
import pkgutil
for info in pkgutil.iter_modules():
print(info.name, info.ispkg)
Without an explicit path, it considers top-level modules available in the current environment.
ModuleInfo
Each result contains a finder, a module name, and a package flag.
for info in pkgutil.iter_modules():
print(info.module_finder)
print(info.name)
print(info.ispkg)
The finder may represent a directory, ZIP archive, or custom import implementation.
Discover children of a package
Pass a package’s __path__ and a prefix.
import my_package
import pkgutil
for info in pkgutil.iter_modules(
my_package.__path__,
my_package.__name__ + ".",
):
print(info.name)
The prefix produces complete importable names.
Packages versus modules
ispkg distinguishes a package that may contain children from an ordinary module.
Do not infer this only from file extensions. Frozen modules and custom loaders may not correspond to a normal Python file.
walk_packages
walk_packages() recursively traverses discovered packages.
for info in pkgutil.walk_packages(
my_package.__path__,
my_package.__name__ + ".",
):
print(info.name)
This is more powerful and more invasive than iter_modules().
walk_packages may import packages
To obtain subpackage paths, the function may import packages. Their __init__.py files can register components, read configuration, open connections, or perform other side effects.
Do not recursively walk third-party trees in a privileged process without an isolation plan.
The onerror callback
walk_packages() accepts a callback for import failures.
def on_error(name):
errors.append(name)
for info in pkgutil.walk_packages(
package.__path__,
package.__name__ + ".",
onerror=on_error,
):
process(info)
The callback receives the package name. Record context and decide whether discovery should continue.
Do not hide important failures
An import failure can indicate a missing dependency, platform incompatibility, or initialization bug.
Classify optional plugins separately from required components.
Prefix-based plugin discovery
A simple convention searches for top-level names with a prefix.
plugins = [
info.name
for info in pkgutil.iter_modules()
if info.name.startswith("myapp_plugin_")
]
Discovering a name should not automatically import it without configuration and validation.
Prefer entry points for installed plugins
Entry points let distributions declare plugins without scanning the entire environment.
Modern systems should usually use importlib.metadata.entry_points(). Prefix scanning remains useful for simple or legacy ecosystems.
Import only selected candidates
After discovery, filter candidates by configuration, allowlist, version, and policy.
import importlib
module = importlib.import_module(plugin_name)
Perform imports at a boundary where failures and side effects can be handled.
get_importer
pkgutil.get_importer(path_item) returns the finder associated with one path entry.
importer = pkgutil.get_importer("/opt/app/plugins")
print(importer)
This helps diagnose whether a path is handled as a directory, ZIP, or custom hook.
Importer caches
The import system caches information related to sys.path entries. When paths change, use public importlib invalidation APIs.
Do not modify internal cache dictionaries directly.
iter_importers
iter_importers(fullname="") yields finders that may participate in locating a module.
for finder in pkgutil.iter_importers("my_package.module"):
print(finder)
Determining importers for a submodule can require importing its parent package.
Meta path and path hooks
Python uses sys.meta_path, sys.path_hooks, and importer caches. pkgutil offers convenient access to selected pieces of that system.
Use importlib.abc and importlib.machinery for modern finder and loader implementations.
Read resources with get_data
pkgutil.get_data(package, resource) requests resource bytes through the package loader.
data = pkgutil.get_data(
"my_package",
"data/config.json",
)
The result may be None when the resource is unavailable.
Resources are bytes
Decode text explicitly.
if data is None:
raise FileNotFoundError("resource is missing")
text = data.decode("utf-8")
Validate resource size and format before processing.
Prefer importlib.resources
New code should generally use importlib.resources, which offers a richer API and handles resources that are not ordinary filesystem files.
That module is the next-to-last topic in this batch.
Avoid building paths from __file__
A package may be loaded from a ZIP, frozen executable, or virtual loader. Path(__file__).parent / resource is not universally valid.
Resource APIs abstract the storage backend.
extend_path
extend_path(path, name) supports a legacy model where one package spans several directories.
from pkgutil import extend_path
__path__ = extend_path(__path__, __name__)
This appears in older namespace packages.
Modern namespace packages
PEP 420 supports namespace packages without __init__.py. New projects should prefer that model and correct packaging configuration.
Do not add extend_path by habit.
.pkg files
The legacy mechanism can read .pkg files that extend package paths.
Treat these entries as sensitive configuration because additional paths can change which modules are imported.
Path hijacking
Placing user-writable directories early in sys.path lets an unexpected module shadow a legitimate dependency.
Validate plugin roots, ownership, and permissions before discovery.
Names are not identities
The same module name can resolve to different locations depending on path order.
Record origin, distribution, and version before enabling a plugin.
Distribution names differ from import names
An importable package does not necessarily share the name of its installed distribution.
Use importlib.metadata.packages_distributions() to map top-level modules to distributions.
Virtual environments
Run discovery inside the intended virtual environment. Global Python may expose a completely different module set.
Record sys.executable and sys.path in diagnostics.
ZIP files and zipapps
iter_modules() can work with importers that provide the required enumeration support. Not every custom finder does.
Test discovery inside .pyz applications. See Python zipapp.
Custom finders
For iter_modules() to work with a nonstandard finder, it must implement the expected extension protocol.
Document loader limitations and do not assume every virtual module can be listed.
Discovery does not prove compatibility
A discovered module may require another Python version, operating system, native library, or configuration.
Read metadata and perform controlled validation before enabling it.
Lazy plugin imports
Keeping only names allows imports to be delayed until a feature is used.
This improves startup but moves failures later. Provide a health check for required plugins.
Deterministic ordering
Discovery order can depend on filesystem and finder behavior. Sort by name for reproducible processing.
infos = sorted(
pkgutil.iter_modules(paths),
key=lambda item: item.name,
)
When priority matters, declare it in metadata instead of relying on discovery order.
Duplicate names
Several paths may contain the same module name. Deduplicate carefully and report conflicts.
A clear failure is better than silently choosing a source when origin matters.
Avoid scanning all of sys.path
A broad scan can be slow and include modules unrelated to the application.
Restrict discovery to plugin roots or package namespaces.
Resource limits
Limit entry count, recursion depth, total time, and resource size.
Do not recursively discover modules in arbitrary uploaded directories without controls.
Process isolation
Because walk_packages() may import packages, discover third-party extensions in a separate process with reduced permissions.
A timeout protects the service from an initializer that never returns.
Integration with modulefinder
modulefinder analyzes dependencies referenced by code, while pkgutil enumerates modules offered by importers.
See Python modulefinder.
Integration with importlib.metadata
After discovering a name, retrieve its distribution, version, and entry points from installed metadata.
A plugin should not need to be imported merely to ask which version is installed.
Testing
Test directories, ZIP files, namespace packages, simple modules, packages, duplicates, custom loaders, missing resources, import errors, and several virtual environments.
Use small fixture packages and restore sys.path after each test.
Common mistakes
Common failures include recursively walking packages without considering side effects, scanning the whole environment, trusting filesystem order, using extend_path in new projects, confusing modules with distributions, building resource paths from __file__, and importing every candidate automatically.
Conclusion
pkgutil provides practical tools to enumerate modules, walk packages, inspect importers, and read resources through loaders. Use iter_modules() for bounded discovery and walk_packages() only when importing subpackages is acceptable.
Prefer entry points and modern importlib APIs in new systems, restrict paths, and isolate plugins. Consult the official pkgutil documentation and Python symtable for analyzing names without importing modules.







