How to Dynamically Load All Holehe Modules in Python

TLDR: Holehe discovers and loads every checker module at runtime by walking the holehe.modules package using pkgutil.walk_packages and importlib.import_module.

The open-source OSINT tool Holehe (megadose/holehe) searches for accounts linked to email addresses across hundreds of platforms. To maintain its extensible plugin architecture without hard-coding service names or import statements, the project dynamically loads all checker modules at runtime from the holehe/modules directory. This design allows developers to add new service checkers simply by dropping a Python file into the appropriate sub-package without modifying the core loading logic.

How Holehe Discovers Modules Dynamically

Instead of static imports, Holehe implements a dynamic module discovery system in holehe/core.py. The mechanism relies on Python's built-in pkgutil and importlib libraries to traverse the package structure, import every module found, and extract the specific checking functions used to verify email existence on various platforms.

Step 1: Recursively Import Submodules with import_submodules()

In holehe/core.py, lines 37-47, the import_submodules() function implements the core discovery logic.

import importlib
import pkgutil

def import_submodules(package, recursive=True):
    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

How it works:

  • Package traversal: pkgutil.walk_packages(package.__path__) iterates over every file in the package directory (e.g., amazon.py, spotify.py).
  • Dynamic import: Each discovered module is imported using importlib.import_module(full_name), where full_name is the fully-qualified name (e.g., holehe.modules.shopping.amazon).
  • Recursive loading: When recursive=True and the item is a package (is_pkg), the function calls itself to handle nested subdirectories.

The function returns a dictionary mapping fully-qualified module names to their imported module objects.

Step 2: Extract Checker Functions with get_functions()

After importing the modules, Holehe must extract the actual callable functions that perform email checks. The get_functions() method in holehe/core.py, lines 50-63, handles this extraction.

def get_functions(modules, args=None):
    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 modules that require password recovery

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

Key details:

  • Filtering logic: The condition len(module.split(".")) > 3 skips the base package itself, targeting only actual checker modules nested within sub-packages.
  • Function retrieval: The code accesses the checking function via modu.__dict__[site], where site is the final component of the module path (e.g., "amazon").
  • Optional filtering: If args.nopasswordrecovery is specified, the loader excludes modules requiring password recovery by checking string representations.

The function returns a list of callable objects ready to be invoked with email addresses.

Putting It Together: Loading and Executing Modules

In the main execution flow of holehe/core.py, lines 105-108, these utilities combine to produce a runnable list of modules.


# https://github.com/megadose/holehe/blob/master/holehe/core.py#L105-L108

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

Execution flow:

  1. Discovery: import_submodules("holehe.modules") returns a dictionary of all modules (e.g., {"holehe.modules.shopping.amazon": <module>, ...}).
  2. Extraction: get_functions(modules, args) processes this dictionary to extract the specific checking functions.
  3. Invocation: Each extracted function in websites is scheduled for concurrent execution against the target email address.

Practical Implementation Examples

Example 1: Listing All Available Modules

This standalone script demonstrates how to dynamically load all Holehe modules and print their names without executing checks.

import importlib
import pkgutil

# Load all sub-modules of holehe.modules

def import_submodules(package, recursive=True):
    if isinstance(package, str):
        package = importlib.import_module(package)
    results = {}
    for _, name, is_pkg in pkgutil.walk_packages(package.__path__):
        full_name = f"{package.__name__}.{name}"
        results[full_name] = importlib.import_module(full_name)
        if recursive and is_pkg:
            results.update(import_submodules(full_name))
    return results

def get_functions(modules):
    funcs = []
    for fqname, mod in modules.items():
        if len(fqname.split(".")) > 3:          # skip the package itself

            func_name = fqname.split(".")[-1]   # e.g. "amazon"

            funcs.append((func_name, getattr(mod, func_name)))
    return funcs

if __name__ == "__main__":
    all_modules = import_submodules("holehe.modules")
    checks = get_functions(all_modules)

    for name, fn in checks:
        print(f"Loaded module: {name}  ->  {fn}")

Example 2: Running Checks with Holehe's Helpers

This example uses Holehe's own utilities to run a complete check on a single email address.

import httpx
import trio
from holehe.core import import_submodules, get_functions, launch_module

async def run_one(email):
    client = httpx.AsyncClient()
    modules = import_submodules("holehe.modules")
    checks = get_functions(modules)          # list of checker functions

    results = []

    async with trio.open_nursery() as nursery:
        for check in checks:
            # each `check` is a callable that expects (email, client, out)

            nursery.start_soon(launch_module, check[1], email, client, results)

    await client.aclose()
    return results

# Example usage

if __name__ == "__main__":
    email = "example@example.com"
    trio.run(run_one, email)

Both snippets illustrate how to dynamically discover, import, and invoke every Holehe module without hard-coding module names or maintaining static import lists.

Key Source Files and Architecture

File Role
holehe/core.py Implements import_submodules() for discovery and get_functions() for callable extraction.
holehe/modules/__init__.py Package marker enabling pkgutil.walk_packages traversal.
holehe/modules/*/*.py Individual checker modules (e.g., shopping/amazon.py) containing the functions discovered at runtime.

This architecture creates a true plugin system where adding holehe/modules/social/newservice.py automatically makes the service available to Holehe scans without touching the loader code.

Summary

  • Dynamic discovery: Holehe uses pkgutil.walk_packages() in holehe/core.py to find all Python files under holehe.modules at runtime.
  • Programmatic import: The import_submodules() function leverages importlib.import_module() to load discovered modules into memory without hard-coded import statements.
  • Function extraction: get_functions() filters the loaded modules and extracts the specific checking callables by inspecting module dictionaries.
  • Zero-configuration extension: New service checkers become automatically available simply by placing a Python file in the holehe/modules directory structure.

Frequently Asked Questions

What is the advantage of dynamically loading Holehe modules?

Dynamic loading eliminates the need to maintain a static list of supported services in the core code. According to the megadose/holehe source code, this allows the tool to support hundreds of platforms while keeping the codebase maintainable—developers add new checkers by creating files rather than modifying import blocks.

How does Holehe handle nested package structures?

The import_submodules() function accepts a recursive=True parameter (default enabled) that calls itself recursively when pkgutil.walk_packages identifies a subdirectory (is_pkg=True). This mechanism discovers modules at any depth within holehe/modules/ (e.g., holehe/modules/shopping/amazon.py).

What is the difference between import_submodules() and get_functions()?

import_submodules(package) returns a dictionary mapping fully-qualified module names to imported module objects (e.g., {"holehe.modules.music.spotify": <module>}). get_functions(modules) consumes this dictionary and returns a list of the actual callable functions defined within those modules (e.g., [<function amazon>, <function spotify>]).

Can I filter which modules are loaded at runtime?

Yes. The get_functions() signature accepts an optional args parameter. When provided with args.nopasswordrecovery=True, the function filters out modules requiring password recovery by inspecting string representations of the module contents, as implemented in holehe/core.py lines 57-60.

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 →