How Holehe Discovers Modules Dynamically Using pkgutil and importlib

Holehe discovers its service modules dynamically by leveraging Python's pkgutil.walk_packages and importlib.import_module to recursively scan the holehe.modules package at runtime, automatically importing every submodule and extracting coroutine functions without maintaining a central registry.

Holehe, the open-source email enumeration tool maintained by megadose/holehe, probes dozens of online services without hard-coding each check. Its architecture relies on a dynamic module discovery system that automatically detects new service modules dropped into the codebase. Understanding how Holehe discovers modules dynamically reveals a clean, zero-maintenance approach to plugin architecture using only standard library tools.

The Package Structure and Naming Convention

All service checks live under the holehe.modules package, following a strict convention that enables automatic discovery. Individual services are organized by category—such as holehe/modules/social_media/twitter.py or holehe/modules/shopping/amazon.py—where each module defines a single coroutine function named identically to the file.

This design eliminates the need for a central registry or configuration file. When you add a new service, you simply create a Python file under holehe/modules/ with a function matching the filename. The discovery mechanism in holehe/core.py handles the rest.

Recursive Module Import with pkgutil

The heart of Holehe's discovery logic resides in the import_submodules function within holehe/core.py. This utility uses pkgutil.walk_packages to traverse the filesystem (or zip imports) and yields every module found under the package path.


# holehe/core.py

def import_submodules(package, recursive=True):
    """Get all the holehe submodules"""
    if isinstance(package, str):
        package = importlib.import_module(package)
    results = {}
    for loader, name, is_pkg in pkgutil.walk_packages(package.__path__):
        full_name = package.__name__ + '.' + name
        results[full_name] = importlib.import_module(full_name)
        if recursive and is_pkg:
            results.update(import_submodules(full_name))
    return results

The function first converts a string package name to a module object using importlib.import_module. It then iterates through pkgutil.walk_packages, which inspects package.__path__ and returns tuples containing the loader, module name, and a boolean indicating if the item is a package. Each discovered module is imported immediately via importlib.import_module, executing the module's code and bringing its public objects into memory.

Filtering and Extracting Service Functions

After importing all modules, Holehe must extract only the callable service checks. The get_functions function filters the imported modules based on their fully-qualified names and extracts the specific coroutine function that performs the email check.


# holehe/core.py

def get_functions(modules, args=None):
    """Transform the modules objects to functions"""
    websites = []
    for module in modules:
        if len(module.split(".")) > 3:
            modu = modules[module]
            site = module.split(".")[-1]
            if args is not None and args.nopasswordrecovery:
                # filter out password-recovery-only services

                if "adobe" not in str(modu.__dict__[site]) and ... :
                    websites.append(modu.__dict__[site])
            else:
                websites.append(modu.__dict__[site])
    return websites

The logic len(module.split(".")) > 3 ensures the code only processes modules at the correct depth within the package hierarchy, skipping the base holehe.modules package itself. It then accesses modu.__dict__[site], where site is the final component of the module path (e.g., "twitter"), retrieving the coroutine function defined in that file. The function also supports filtering based on command-line arguments, such as excluding password-recovery-only services when --nopasswordrecovery is specified.

From Discovery to Execution

In the maincore function, Holehe orchestrates the discovery and execution flow. After parsing arguments, it calls the discovery utilities to build a list of callable functions:

modules = import_submodules("holehe.modules")
websites = get_functions(modules, args)

The resulting websites list contains coroutine functions ready for execution. Holehe then passes these to a Trio nursery, launching each service check concurrently:

for website in websites:
    nursery.start_soon(launch_module, website, email, client, out)

This approach means adding a new service requires zero changes to the core logic. Dropping a properly named .py file into the modules directory automatically includes it in the next run.

Benefits of Dynamic Discovery

Zero-maintenance registry. Because the filesystem itself serves as the registry, developers never need to update a master list or configuration file when adding services. The import_submodules function always reflects the current state of the holehe/modules/ directory.

Module isolation. Each service check lives in its own file, preventing namespace collisions and making the codebase easier to maintain. Dependencies and service-specific logic remain encapsulated within individual modules.

Native concurrency support. By returning coroutine functions rather than classes or callbacks, Holehe seamlessly integrates with Trio's structured concurrency model. The discovery system yields functions that can be immediately scheduled as concurrent tasks, drastically reducing total execution time when checking multiple services.

Summary

  • Holehe uses pkgutil.walk_packages in holehe/core.py to scan the holehe.modules package tree at runtime.
  • The import_submodules function recursively imports every module using importlib.import_module.
  • get_functions filters imports by path depth and extracts coroutine functions matching the filename.
  • New services require only a new Python file in the modules directory—no registry updates needed.
  • The resulting function list feeds directly into Trio's nursery for concurrent execution.

Frequently Asked Questions

How does Holehe find new modules without maintaining a registry?

Holehe treats the filesystem as the registry. The import_submodules function uses pkgutil.walk_packages to inspect the holehe.modules package path at runtime, automatically importing any Python files it finds. This eliminates the need for manual registration lists or configuration files.

What happens if I add a new Python file to holehe/modules/?

If you add a file like holehe/modules/social_media/newservice.py containing a coroutine function named newservice, Holehe will automatically import it on the next execution. The get_functions filter will detect the module, extract the function, and include it in the list of websites to check against the target email.

Why does Holehe use pkgutil instead of hardcoded imports?

Using pkgutil.walk_packages allows Holehe to scale to hundreds of services without bloating the core codebase with import statements. This dynamic approach keeps the tool modular and ensures that contributors can add services by creating new files without modifying holehe/core.py.

How does Holehe handle nested packages within the modules directory?

The import_submodules function accepts a recursive=True parameter. When it encounters a subpackage (indicated by is_pkg being True), it calls itself recursively to walk that subpackage's path. This allows Holehe to organize services into categories like social_media or shopping while still discovering all nested modules.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →