Python pkgutil: Discover Packages

Published on: August 27, 2026
Reading time: 6 minutes
A developer typing code on a laptop with a Python book beside in an office.

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.

Share:

Facebook
WhatsApp
Twitter
LinkedIn

Article content

    Related articles

    Close-up view of a computer screen displaying code in a software development environment.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python runpy: Execute Modules and Scripts

    Learn Python runpy to execute modules and scripts, control __main__, run_path, alter_sys, namespaces, testing, and process isolation.

    Ler mais

    Tempo de leitura: 6 minutos
    27/08/2026
    Colorful stacked shipping containers at Hamburg port, showcasing global trade and logistics.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python modulefinder: Discover Imports

    Learn Python modulefinder to discover imports, transitive dependencies, missing modules, paths, plugins, and limitations of static analysis.

    Ler mais

    Tempo de leitura: 8 minutos
    27/08/2026
    Detailed view of code and file structure in a software development environment.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python py_compile: Compile One File

    Learn Python py_compile to compile one file, control .pyc output, logical filenames, optimization, hash invalidation, and build errors.

    Ler mais

    Tempo de leitura: 7 minutos
    27/08/2026
    A developer typing code on a laptop with a Python book beside in an office.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python compileall: Generate .pyc Bytecode

    Learn Python compileall to generate .pyc files, validate syntax, compile in parallel, control optimization, paths, and reproducible builds.

    Ler mais

    Tempo de leitura: 7 minutos
    27/08/2026
    Vivid close-up of code on a computer screen showcasing programming details.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python codeop: Compile Interactive Input

    Learn Python codeop to detect complete, incomplete, or invalid commands, build REPLs, and preserve __future__ flags safely per session.

    Ler mais

    Tempo de leitura: 6 minutos
    27/08/2026
    A developer typing code on a laptop with a Python book beside in an office.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python linecache: Read Source Lines

    Learn Python linecache to retrieve source lines, refresh cached files, support tracebacks and loaders, preserve indentation, and secure paths.

    Ler mais

    Tempo de leitura: 7 minutos
    27/08/2026