Understanding the TrioProgress Instrument for Tracking Scan Progress in Holehe

The TrioProgress class is a custom Trio instrument that wraps tqdm to provide real-time visual feedback during Holehe's asynchronous email scans, automatically updating when each module completes.

Holehe runs dozens of independent website checks concurrently using Trio, Python's structured concurrency library. To keep users informed without cluttering the core logic, the project implements a clean instrumentation pattern. This article explains how TrioProgress works, why it uses Trio's low-level API, and how you can apply the same pattern in your own async projects.

What Is TrioProgress?

TrioProgress is a lightweight progress-tracking instrument defined in holehe/instruments.py. It inherits from trio.abc.Instrument and integrates with tqdm to display a progress bar that advances automatically as each email-checking module finishes.

The instrument solves a specific problem: how to track completion of many concurrent tasks without modifying the tasks themselves. Since Holehe runs 40+ module checks in parallel, manually incrementing a counter inside each module would create tight coupling and error-prone boilerplate.

Core Components and Architecture

The TrioProgress Class

The instrument wraps a tqdm progress bar and implements Trio's instrument protocol:

Component Role
__init__(total) Initializes tqdm(total=total) with the number of modules to run
task_exited(task) Hook called by Trio after any task ends; updates bar only for launch_module tasks

In holehe/instruments.py, the class definition follows this structure:

from tqdm import tqdm
import trio.abc

class TrioProgress(trio.abc.Instrument):
    def __init__(self, total):
        self.tqdm = tqdm(total=total)

    def task_exited(self, task):
        # Only count module completions, not internal Trio tasks

        if task.name.split(".")[-1] == "launch_module":
            self.tqdm.update(1)

The task_exited hook is the critical piece. Trio calls this method automatically whenever any task exits, but the instrument filters by task.name to count only relevant work.

Integration in Holehe's Main Loop

In holehe/core.py (lines 16-22), the instrument is created, registered, and later cleaned up:

from holehe.instruments import TrioProgress

# total matches the number of website modules to check

instrument = TrioProgress(len(websites))
trio.lowlevel.add_instrument(instrument)

async with trio.open_nursery() as nursery:
    for website in websites:
        nursery.start_soon(
            launch_module, website, email, client, out,
            name="launch_module"  # Name used for filtering

        )

trio.lowlevel.remove_instrument(instrument)

The flow is intentionally simple: create → register → run → remove. No module knows the progress bar exists.

Why Use a Trio Instrument?

Three design principles make this approach superior to manual progress tracking:

  • Transparency — Trio guarantees task_exited fires even if a module raises an exception, so the bar advances correctly for failures and successes alike

  • Zero coupling — Modules contain no progress-related code; they focus purely on email checking logic

  • Thread safety — Trio's instrument hooks are serialized, and tqdm handles concurrent updates safely, eliminating race conditions

Complete Working Example

This standalone script demonstrates the pattern with simulated work:

import trio
from tqdm import tqdm


class TrioProgress(trio.abc.Instrument):
    def __init__(self, total):
        self.tqdm = tqdm(total=total)

    def task_exited(self, task):
        if task.name.split(".")[-1] == "launch_module":
            self.tqdm.update(1)


async def dummy_module(i):
    """Simulates a slow email check."""
    await trio.sleep(0.2)


async def main():
    total = 5
    instrument = TrioProgress(total)
    trio.lowlevel.add_instrument(instrument)

    async with trio.open_nursery() as n:
        for i in range(total):
            n.start_soon(dummy_module, i, name="launch_module")

    trio.lowlevel.remove_instrument(instrument)


if __name__ == "__main__":
    trio.run(main)

Running this produces output like:


100%|██████████| 5/5 [00:01<00:00,  4.97it/s]

Key Source Files

File Purpose
holehe/instruments.py Defines TrioProgress instrument class
holehe/core.py Creates, registers, and removes the instrument around the nursery
holehe/__init__.py Entry point that invokes core.main()

According to the megadose/holehe source code, these files implement the entire progress-tracking mechanism in under 50 lines of instrumentation code.

Extending the Pattern

You can adapt TrioProgress for other use cases:

  • Filter by coroutine function instead of task name: compare task.coro.cr_code.co_name
  • Multiple bars by creating separate instruments with different filters
  • Rich(progress) or other renderers by replacing tqdm in __init__

The Trio instrument API also supports task_scheduled, before_task_step, and after_task_step hooks for more advanced observability.

Summary

  • TrioProgress wraps tqdm inside a trio.abc.Instrument subclass to track async task completion
  • The task_exited hook filters by task name (launch_module) to count only relevant work
  • Registration happens via trio.lowlevel.add_instrument() before the nursery opens, removal after it closes
  • This pattern decouples progress reporting from business logic using Trio's built-in instrumentation system

Frequently Asked Questions

How does TrioProgress know when a module finishes?

Trio automatically calls the task_exited hook whenever any task completes. The instrument checks task.name.split(".")[-1] == "launch_module" to identify module tasks and ignore internal Trio infrastructure. This filtering ensures only actual email checks increment the progress bar.

What happens if a module raises an exception?

The progress bar still advances. Trio's instrument guarantees task_exited fires regardless of exit status—success, exception, or cancellation. This prevents the bar from stalling when websites timeout or return errors, which is common during large scans.

Can I use this pattern with asyncio instead of Trio?

Not directly. The trio.abc.Instrument API is Trio-specific. Asyncio lacks an equivalent low-level instrumentation system; you'd typically use asyncio.gather() with tqdm's manual update or tqdm.asyncio. The structured concurrency and instrument hooks are unique advantages of Trio for this use case.

Where is the progress bar total set?

In holehe/core.py, the total equals len(websites)—the count of discovered check modules. This happens before any async work begins, so the bar shows accurate percentage completion throughout the scan.

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 →