How Holehe Dynamically Imports and Registers Modules at Runtime
Holehe discovers and executes service-check modules at runtime using pkgutil.walk_packages and importlib, eliminating the need for manual registration or hard-coded imports.
The open-source OSINT tool Holehe (maintained at megadose/holehe) probes hundreds of services for email account existence. Its architecture relies on dynamic module import and registration to stay extensible: developers drop a Python file into the modules directory, and the framework automatically discovers, loads, and schedules it for execution without modifying core code.
Runtime Module Discovery Architecture
The dynamic loading system in holehe/core.py operates through three coordinated phases: package tree traversal, callable extraction, and concurrent execution.
Recursive Package Walking with import_submodules
At lines 37–47 of holehe/core.py, the import_submodules function uses pkgutil.walk_packages to recursively traverse the holehe.modules package tree. For every submodule encountered, it constructs the full dotted name and imports it via importlib.import_module.
import importlib
import pkgutil
def import_submodules(package_name: str):
"""Recursively import every submodule of *package_name*."""
package = importlib.import_module(package_name)
loaded = {}
for loader, name, is_pkg in pkgutil.walk_packages(package.__path__):
full_name = f"{package.__name__}.{name}"
loaded[full_name] = importlib.import_module(full_name)
if is_pkg:
loaded.update(import_submodules(full_name))
return loaded
If the discovered entry is itself a package, the function recurses to ensure nested modules are also imported and added to the returned dictionary.
Extracting Callables via get_functions
Once import_submodules returns a dictionary mapping dotted names to module objects, get_functions (lines 50–63) filters this collection to isolate leaf modules—those whose dotted names contain more than three components (e.g., holehe.modules.social_media.instagram). It then extracts the public function from each module’s __dict__ using the final segment of the dotted name.
def get_functions(modules_dict):
"""Extract the public function from each leaf module."""
functions = []
for dotted_name, module in modules_dict.items():
# Leaf modules have at least 4 parts: holehe.modules.<category>.<service>
if len(dotted_name.split(".")) > 3:
func_name = dotted_name.split(".")[-1] # e.g., "instagram"
functions.append(module.__dict__[func_name]) # The function object
return functions
This convention requires that the function name inside instagram.py must exactly match the filename (def instagram(...)).
Concurrent Execution in maincore
The maincore function (lines 20–30) receives the list of callable functions and executes them concurrently using Trio’s nursery pattern. Each check receives the target email, a shared httpx.AsyncClient instance, and a results list to populate.
import trio
import httpx
async def launch_module(func, email, client, out):
await func(email, client, out)
async def maincore(email):
client = httpx.AsyncClient()
out = []
modules = import_submodules("holehe.modules")
checks = get_functions(modules)
async with trio.open_nursery() as nursery:
for check in checks:
nursery.start_soon(launch_module, check, email, client, out)
await client.aclose()
return out
Extending Holehe with New Services
Adding a new service check requires no changes to holehe/core.py. Create a Python file under holehe/modules/<category>/ where the filename matches the function name defined inside. For example, holehe/modules/social_media/instagram.py must contain def instagram(email, client, out):. The runtime import machinery automatically discovers the file on the next execution, extracts the callable, and schedules it for concurrent probing.
Summary
import_submodulesinholehe/core.py(lines 37–47) usespkgutil.walk_packagesandimportlib.import_moduleto recursively load all submodules underholehe.modules.get_functions(lines 50–63) filters for leaf modules (dotted names with more than three segments) and extracts functions whose names match their filenames.maincoreexecutes extracted callables concurrently via Trio’snursery.start_soon(), passing shared HTTP clients and result collectors.- The architecture supports drop-in extensions: adding a new service only requires placing a correctly named Python file in the appropriate subdirectory.
Frequently Asked Questions
How does Holehe find new modules without explicit imports?
Holehe traverses the holehe/modules directory at runtime using pkgutil.walk_packages, which yields every submodule regardless of depth. It then uses importlib.import_module to load each discovered package dynamically, building a registry of available checks without hard-coded import statements.
What naming convention must service modules follow?
Each module must reside at holehe/modules/<category>/<service>.py and define a function whose name exactly matches the filename. For instance, instagram.py must contain def instagram(email, client, out):. The get_functions utility extracts callables by splitting the dotted module name and indexing into module.__dict__ using the final segment.
Why does Holehe use Trio for executing modules?
Trio provides structured concurrency through nurseries, allowing Holehe to launch hundreds of service checks simultaneously while maintaining clean error propagation and cancellation semantics. The nursery.start_soon(launch_module, ...) pattern in maincore fires each check as an independent task that shares the httpx.AsyncClient for efficient connection pooling.
Where is the dynamic module loading logic implemented?
All runtime import and registration logic resides in holehe/core.py. Specifically, lines 37–47 handle recursive module discovery, lines 50–63 filter and extract callables, and lines 20–30 orchestrate concurrent execution. Individual service implementations live under holehe/modules/ (e.g., holehe/modules/social_media/instagram.py).
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →