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

> Learn to import and run Holehe modules dynamically in Python using core utilities for runtime service discovery and execution without CLI invocation. Enhance your email checking capabilities.

- Repository: [Palenath/holehe](https://github.com/megadose/holehe)
- Tags: how-to-guide
- Published: 2026-08-17

---

**Dynamic module loading in Holehe relies on three core utilities in [`holehe/core.py`](https://github.com/megadose/holehe/blob/main/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](https://github.com/megadose/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`](https://github.com/megadose/holehe/blob/main/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`](https://github.com/megadose/holehe/blob/main/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`](https://github.com/megadose/holehe/blob/main/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`](https://github.com/megadose/holehe/blob/main/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.

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

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

```python
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`](https://github.com/megadose/holehe/blob/main/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`](https://github.com/megadose/holehe/blob/main/holehe/modules/social_media/twitter.py) | Example check file exposing `def twitter(email, client, out)` |
| [`holehe/localuseragent.py`](https://github.com/megadose/holehe/blob/main/holehe/localuseragent.py) | Default User-Agent string for HTTP clients |
| [`holehe/instruments.py`](https://github.com/megadose/holehe/blob/main/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:

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