# Understanding the TrioProgress Instrument for Tracking Scan Progress in Holehe

> Learn how TrioProgress in megadose/holehe enhances async email scans with real-time progress tracking using tqdm. Get visual feedback as each module completes.

- Repository: [Palenath/holehe](https://github.com/megadose/holehe)
- Tags: internals
- Published: 2026-08-20

---

**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`](https://github.com/megadose/holehe/blob/main/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`](https://github.com/megadose/holehe/blob/main/holehe/instruments.py), the class definition follows this structure:

```python
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`](https://github.com/megadose/holehe/blob/main/holehe/core.py) (lines 16-22), the instrument is created, registered, and later cleaned up:

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

```python
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`](https://github.com/megadose/holehe/blob/main/holehe/instruments.py) | Defines `TrioProgress` instrument class |
| [`holehe/core.py`](https://github.com/megadose/holehe/blob/main/holehe/core.py) | Creates, registers, and removes the instrument around the nursery |
| [`holehe/__init__.py`](https://github.com/megadose/holehe/blob/main/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`](https://github.com/megadose/holehe/blob/main/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.