How to Import and Run Holehe Modules Dynamically in Python: A Developer's Guide

Dynamic module loading in Holehe relies on three core utilities in holehe/core.py — import_submodules(), get_functions(), and launch_module() — which together enable runtime discovery and execution of any email-checking service without CLI invocation.

The Holehe open-source project by megadose organizes its 100+ service-specific email checks as individual Python functions scattered across the holehe.modules package tree. Rather than hardcoding imports, the library provides clean abstractions for loading these checks on demand. This guide explains how to import and run Holehe modules dynamically in Python, whether you need to target a single service, execute checks in bulk, or embed Holehe capabilities into a larger application.


Understanding Holehe's Dynamic Import Architecture

Holehe's runtime flexibility stems from three carefully designed utilities in holehe/core.py. Understanding each helps you choose the right approach for your use case.

The Core Utilities Explained

Utility Purpose Source Location
import_submodules(package, recursive=True) Walks the package tree using pkgutil.walk_packages and importlib.import_module, returning a dictionary mapping full module names to loaded module objects [holehe/core.py lines 37-47](https://github.com/megadose/holehe/blob/master/holehe/core.py#L37-L47)
get_functions(modules, args=None) Extracts actual check functions from imported modules, filters by CLI flags like --no-password-recovery, returns list of callables [holehe/core.py lines 50-63](https://github.com/megadose/holehe/blob/master/holehe/core.py#L50-L63)
launch_module(module, email, client, out) Executes a single check inside async context, normalizes exceptions into consistent result dictionaries [holehe/core.py lines 66-78](https://github.com/megadose/holehe/blob/master/holehe/core.py#L66-L78)

These utilities let you programmatically replicate the CLI's discovery and execution logic without shelling out to subprocess.


Method 1: Dynamic Import of All Modules with Specific Targeting

The most common pattern mirrors Holehe's CLI behavior: load everything, then select specific checks to run.

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

async def run_twitter_check(email: str):
    # 1. Import every submodule under holehe.modules

    modules = import_submodules("holehe.modules")
    
    # 2. Build callable list using same logic as CLI

    funcs = get_functions(modules)

    # 3. Find Twitter check by function name

    twitter_fn = next(fn for fn in funcs if fn.__name__ == "twitter")
    
    # 4. Prepare async client and result container

    client = httpx.AsyncClient()
    out = []
    
    # 5. Execute within Trio nursery (matches CLI concurrency model)

    async with trio.open_nursery() as nursery:
        nursery.start_soon(launch_module, twitter_fn, email, client, out)

    await client.aclose()
    return out[0]  # Single result dict with 'exists', 'emailrecovery', etc.

# Execute

result = trio.run(run_twitter_check, "test@example.com")
print(result)

This approach guarantees you have access to all available checks while maintaining precise control over which services to query.


Method 2: Direct Single-Module Import Without Discovery Overhead

When you know exactly which service to check, bypass discovery entirely using Python's standard importlib.

import importlib
import httpx
import asyncio

# 1. Load specific module by dotted path

mod = importlib.import_module("holehe.modules.social_media.twitter")

# 2. Extract check function (naming convention: function matches module name)

twitter_check = getattr(mod, "twitter")

async def check_email(email: str):
    async with httpx.AsyncClient() as client:
        out = []
        # Signature: (email, client, out) — consistent across all modules

        await twitter_check(email, client, out)
        return out[0]

# Run synchronously

result = asyncio.run(check("test@example.com"))
print(result)

Performance note: This eliminates pkgutil scanning overhead and imports only the dependencies required for Twitter's specific check.


Method 3: Bulk Execution of All Available Checks

Reproduce the full CLI behavior programmatically by running every discovered module concurrently.

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

async def run_all_checks(email: str):
    # Discover and import all modules

    modules = import_submodules("holehe.modules")
    
    # Extract all callable check functions

    funcs = get_functions(modules)
    
    # Setup shared client and results container

    client = httpx.AsyncClient()
    out = []

    # Launch all checks concurrently in Trio nursery

    async with trio.open_nursery() as nursery:
        for fn in funcs:
            nursery.start_soon(launch_module, fn, email, client, out)

    await client.aclose()
    return out  # List of result dicts from all services

# Execute full scan

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

# Process results

for r in results:
    status = "FOUND" if r.get("exists") else "not found"
    print(f"{r['domain']}: {status}")

The get_functions() utility automatically respects CLI-equivalent filtering. Pass args with appropriate flags to exclude password recovery checks if needed.


Key File Reference for Dynamic Module Loading

Understanding the project structure helps debug import issues and extend functionality:

File Role in Dynamic Execution
holehe/core.py Central orchestrator containing import_submodules(), get_functions(), and launch_module()
holehe/modules/**/__init__.py Package markers enabling Python's package discovery
holehe/modules/social_media/twitter.py Example check file exposing def twitter(email, client, out)
holehe/localuseragent.py Default User-Agent string for HTTP clients
holehe/instruments.py Progress bar instrumentation (CLI-only, safe to ignore for programmatic use)

All check functions share an identical signature: async def servicename(email: str, client: httpx.AsyncClient, out: list) -> None. Results are appended to out as dictionaries containing keys like name, domain, exists, emailrecovery, phoneNumber, and others.


Handling Async Contexts: Trio vs asyncio

Holehe's source code uses Trio for structured concurrency. The launch_module() function is designed for Trio nurseries. However, since individual check functions are plain async callables, you can adapt them to asyncio with wrapper code:

import asyncio
import httpx

# Import a check directly

from holehe.modules.social_media.twitter import twitter

async def asyncio_wrapper(email: str):
    async with httpx.AsyncClient() as client:
        out = []
        await twitter(email, client, out)
        return out[0]

# Using standard asyncio instead of Trio

result = asyncio.run(asyncio_wrapper("test@example.com"))

For production use, prefer Trio to match Holehe's underlying design and avoid potential compatibility edges.


Summary

  • Use import_submodules("holehe.modules") to discover and load all service checks at runtime without hardcoded imports.

  • Use get_functions() to extract callable check functions from the module dictionary, with automatic CLI flag support.

  • Use launch_module() for exception-safe execution within Trio's concurrency model, or call check functions directly with standard asyncio if preferred.

  • Import individual modules via importlib.import_module() when targeting known services and minimizing overhead.

  • All check functions follow the signature (email, client, out) and mutate the out list with result dictionaries.


Frequently Asked Questions

Can I run Holehe checks without installing the full package?

No. The checks depend on Holehe's internal utilities and shared HTTP patterns defined in holehe/core.py. While you could theoretically vendor individual check files, you'd need to replicate client configuration and result normalization manually.

Does dynamic importing affect performance compared to static imports?

Initial discovery with import_submodules() adds overhead proportional to the package tree size (~100+ modules). For single-check scenarios, use direct importlib.import_module() targeting specific paths like "holehe.modules.social_media.twitter" to minimize startup time.

Can I filter which modules to load dynamically?

Yes. The get_functions() function accepts an args parameter matching CLI arguments. Pass a namespace object with no_password_recovery=True to exclude checks that attempt email recovery flows. You can also post-filter the returned function list by inspecting __module__ or __name__ attributes.

What happens if a check module throws an exception?

The launch_module() wrapper catches all exceptions and converts them to standardized result dictionaries with exists=None and error details preserved. This prevents one failing service from crashing your entire batch of checks.

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 →