# How Holehe Discovers Modules Dynamically Using pkgutil and importlib

> Holehe dynamically discovers modules using pkgutil and importlib to scan its package at runtime automatically importing submodules and extracting coroutine functions.

- Repository: [Palenath/holehe](https://github.com/megadose/holehe)
- Tags: internals
- Published: 2026-08-28

---

**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`](https://github.com/megadose/holehe/blob/main/holehe/modules/social_media/twitter.py) or [`holehe/modules/shopping/amazon.py`](https://github.com/megadose/holehe/blob/main/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`](https://github.com/megadose/holehe/blob/main/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`](https://github.com/megadose/holehe/blob/main/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.

```python

# 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.

```python

# 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:

```python
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:

```python
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`](https://github.com/megadose/holehe/blob/main/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`](https://github.com/megadose/holehe/blob/main/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`](https://github.com/megadose/holehe/blob/main/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.