# How holehe Dynamically Discovers and Loads Modules at Runtime

> Discover how holehe dynamically discovers and loads modules at runtime. Learn about its package scanning, importlib usage, and function retrieval for efficient module integration.

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

---

**holehe dynamically discovers its service checkers by recursively scanning the `holehe.modules` package using `pkgutil.walk_packages` and `importlib`, then converts the imported modules into callable functions via `get_functions`.**

The [holehe](https://github.com/megadose/holehe) email exposure testing tool supports hundreds of services without hard-coding each integration. Instead, it leverages Python's dynamic import capabilities to load modules at runtime. This architecture allows developers to add new service checkers simply by placing Python files in the appropriate directory, eliminating the need for manual registration.

## The Core Discovery Mechanism

The dynamic loading process centers on the [`import_submodules`](https://github.com/megadose/holehe/blob/master/holehe/core.py#L37) function defined in [`holehe/core.py`](https://github.com/megadose/holehe/blob/main/holehe/core.py). This function serves as the entry point for discovering all available service modules under the `holehe.modules` namespace.

### Recursive Package Scanning with pkgutil

The function utilizes **`pkgutil.walk_packages`** to iterate over every directory and Python file within the target package's `__path__`. This standard library utility yields a tuple containing the loader, module name, and a boolean indicating whether the entry is a subpackage.

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

```

### Dynamic Module Import with importlib

For each entry discovered by `walk_packages`, the function constructs the fully qualified module name (e.g., `holehe.modules.social_media.twitter`) and imports it using **`importlib.import_module`**. When `recursive=True` and the entry is a package (directory), the function calls itself to descend into subdirectories, ensuring nested modules like `holehe.modules.shopping.amazon` are loaded alongside top-level modules.

## Converting Modules to Callable Checkers

Discovery alone does not make the modules usable. Immediately after calling `import_submodules`, the code passes the results dictionary to **`get_functions`**, which filters and extracts the actual checker functions.

The transformation logic inspects each imported module and retains only objects whose dotted name depth exceeds three levels (e.g., `holehe.modules.social_media.twitter.twitter`). This filtering ensures only the specific service checkers are retained, excluding intermediate package markers.

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

```

The resulting `websites` list contains callable functions that accept an email address, an HTTP client, and an output list—the standard signature for all holehe service checkers.

## Practical Usage Examples

### Listing All Discovered Modules

You can programmatically inspect which modules holehe has loaded by invoking `import_submodules` directly:

```python
from holehe.core import import_submodules

all_modules = import_submodules("holehe.modules")
print(f"Loaded {len(all_modules)} modules")
print(list(all_modules.keys())[:5])

# Output includes: ['holehe.modules.social_media.twitter', ...]

```

### Manually Executing a Specific Checker

Once imported, individual checkers can be accessed by their fully qualified path and executed with an async HTTP client:

```python
import httpx
import trio
from holehe.core import import_submodules

async def check_twitter(email):
    modules = import_submodules("holehe.modules")
    twitter_func = modules['holehe.modules.social_media.twitter'].twitter
    
    async with httpx.AsyncClient(timeout=10) as client:
        out = []
        await twitter_func(email, client, out)
        return out

results = trio.run(check_twitter, "test@example.com")

```

### Adding a Custom Service Module

The dynamic architecture enables zero-configuration extension. To add support for a new service:

1. Create a new Python file at [`holehe/modules/category/mynewservice.py`](https://github.com/megadose/holehe/blob/main/holehe/modules/category/mynewservice.py)
2. Define an async function matching the signature: `async def mynewservice(email, client, out):`
3. Restart holehe—the `import_submodules` call will automatically discover and load the new module on the next execution cycle

No changes to [`core.py`](https://github.com/megadose/holehe/blob/main/core.py) or any central registry are required.

## Key Source Files

The module discovery system is implemented across three primary locations:

- **[[`holehe/core.py`](https://github.com/megadose/holehe/blob/main/holehe/core.py)](https://github.com/megadose/holehe/blob/master/holehe/core.py)** – Contains the [`import_submodules`](https://github.com/megadose/holehe/blob/master/holehe/core.py#L37) function and `get_functions` logic that orchestrates runtime loading.

- **[[`holehe/modules/__init__.py`](https://github.com/megadose/holehe/blob/main/holehe/modules/__init__.py)](https://github.com/megadose/holehe/blob/master/holehe/modules/__init__.py)** – Marks the `modules` directory as a Python package, establishing the root namespace for all service checkers.

- **[[`holehe/modules/social_media/twitter.py`](https://github.com/megadose/holehe/blob/main/holehe/modules/social_media/twitter.py)](https://github.com/megadose/holehe/blob/master/holehe/modules/social_media/twitter.py)** – Representative example of a service module that gets dynamically imported. Each similar file under `holehe/modules/` follows this pattern.

## Summary

- **Dynamic Discovery:** holehe uses [`import_submodules`](https://github.com/megadose/holehe/blob/master/holehe/core.py#L37) to scan the `holehe.modules` package tree at runtime.
- **Standard Library Tools:** The implementation relies on **`pkgutil.walk_packages`** for directory traversal and **`importlib.import_module`** for actual imports.
- **Recursive Loading:** Subpackages are handled recursively, ensuring all nested service categories are discovered regardless of directory depth.
- **Function Extraction:** `get_functions` transforms the raw module dictionary into a list of callable checker functions by filtering for specific naming patterns.
- **Zero-Config Extensibility:** Adding new services requires only placing a Python file in the correct directory; the discovery mechanism handles registration automatically.

## Frequently Asked Questions

### How does holehe find new modules without updating a configuration file?

holehe treats the `holehe/modules/` directory as a live package tree. The [`import_submodules`](https://github.com/megadose/holehe/blob/master/holehe/core.py#L37) function scans this tree on every execution using `pkgutil.walk_packages`, meaning any new `.py` file added to the directory structure is automatically imported on the next run without requiring manual registration.

### What Python standard library modules does holehe use for dynamic loading?

The discovery mechanism relies on two specific standard library modules: **`pkgutil`** (specifically `walk_packages`) to iterate over package contents, and **`importlib`** (specifically `import_module`) to programmatically import discovered modules by their fully qualified names.

### Can I disable recursive module loading in holehe?

The [`import_submodules`](https://github.com/megadose/holehe/blob/master/holehe/core.py#L37) function accepts a `recursive` parameter that defaults to `True`. Setting this to `False` would restrict loading to only the immediate children of the specified package, though the standard holehe execution always uses the recursive default to ensure all subcategories (like `social_media`, `shopping`, etc.) are loaded.

### Why does holehe use dotted name depth filtering in get_functions?

The `get_functions` utility filters imported objects to include only those with a name depth greater than three (e.g., `holehe.modules.category.service`). This ensures that intermediate package directories and the root [`__init__.py`](https://github.com/megadose/holehe/blob/main/__init__.py) files are excluded from the final callable list, retaining only the actual service checker functions that implement the email verification logic.