Clued
Leave clues for your future debugging self.
clued attaches structured, human-readable context to exceptions as they propagate — without changing the exception type, wrapping in a new exception, or parsing strings later.
Install
Install with pip
pip install clued
or install with uv
uv add clued
Requires Python 3.11+. Zero dependencies.
The Problem
When an error happens deep in your call stack, you typically get one of two outcomes:
Option A — bare exception, no context:
def process_order(order_id):
user = get_user(order_id) # raises ValueError("invalid id format")
charge(user)
ValueError: invalid id format
File "app.py", line 2, in get_user
The traceback tells you where it happened, not why. You don't know which order, which user, or what the system was trying to do at a higher level.
Option B — verbose try/except wrapping:
def process_order(order_id):
try:
user = get_user(order_id)
except ValueError as e:
raise OrderProcessingError(
f"failed to process order {order_id}: could not fetch user"
) from e
This works, but it's tedious. Every layer of your call stack needs its own try/except/raise from block. Real codebases end up with dozens of custom exception classes and repetitive wrapping code. Most developers just don't bother — and then they're debugging production issues with a KeyError: 'name' and no idea which record or request caused it.
With clued:
from clued import clue
def process_order(order_id: str, user_id: int) -> None:
with clue("processing order", order_id=order_id, user_id=user_id) as ctx:
ctx.refine(step="fetch user")
user = get_user(order_id)
ctx.refine(step="charge")
charge(user)
ValueError: invalid id format
File "app.py", line 4, in get_user
- 0: processing order [order_id='BAD', user_id=-1, step='fetch user'] (app.py:8)
Clues nest naturally across call boundaries — each layer adds its own note, inner-to-outer:
from clued import clue
def process_order(order_id: str, user_id: int) -> None:
with clue("processing order", order_id=order_id, user_id=user_id):
charge_user(order_id, user_id)
def charge_user(order_id: str, user_id: int) -> None:
with clue("charging user", user_id=user_id) as ctx:
ctx.refine(step="fetch card")
card = get_card(user_id)
ctx.refine(step="apply charge")
apply_charge(card, order_id)
ValueError: invalid card format
File "app.py", line 4, in get_card
- 0: charging user [user_id=-1, step='fetch card'] (app.py:11)
- 1: processing order [order_id='BAD', user_id=-1] (app.py:5)
One with block. No custom exception class. No raise from. The context travels with the exception automatically.
Quick Start
from clued import clue, get_clues
with clue("loading config", path=path, env=env) as ctx:
ctx.refine(section="database")
load_db_config(path)
On any exception raised inside the block, clued calls add_note() (PEP 678) to attach a formatted string, and stores a structured ClueRecord you can query in code:
except Exception as e:
for record in get_clues(e):
print(record.msg, dict(record.kv), f"{record.filename}:{record.lineno}")
Nested with clue(...) blocks each add their own note — outermost last, so the traceback reads inner-to-outer.
Features
- Structured kv — context stored as typed key-value pairs, not just strings
- Source location — note includes the exact file and line where
clue()orrefine()was called refine()— narrow context in-place as a block progresses (e.g. track loop index, current step)- Async-safe — uses
ContextVarfor per-task isolation; works withasyncio.gatherand thread pools - Zero dependencies — stdlib only
- Fully typed — ships a
py.typedmarker, passes mypy strict and pyright
API Reference
| Symbol | Description |
|---|---|
clue(msg, **kv) |
Context manager. Attaches context on any exception raised within the block. Yields a ClueHandle. |
ClueHandle.refine(msg=None, **kv) |
Update context in-place. None values delete a key. |
ClueHandle.reset() |
Restore to original message and clear all kv. |
clue_on_error(msg_template, **kv) |
Decorator. Formats msg_template with bound arguments and wraps the call in clue(). Works on async functions too. |
ctx(fn, *args, msg, **kv) |
Inline wrapper. Calls fn(*args) inside a clue() context. |
get_clues(exc) |
Returns list[ClueRecord] attached to the exception. |
get_clue_dict(exc) |
Returns a flat merged dict of all kv (inner clues override outer). |
current_clues() |
Returns the active tuple[ClueHandle, ...] stack for the current context (useful for logging). |
ClueRecord
@dataclass(frozen=True)
class ClueRecord:
msg: str
kv: frozenset[tuple[str, Any]]
filename: str
lineno: int
Usage Patterns
Decorator
from clued import clue_on_error, clue_on_error_async
@clue_on_error("processing item {item_id}", source="worker")
def process_item(item_id: str) -> None:
...
@clue_on_error_async("fetch user {user_id}")
async def fetch_user(user_id: int) -> dict[str, str]:
...
Inline wrapper
from clued import ctx
result = ctx(load_file, path, msg="loading file", path=path)
Tracking progress in a loop
with clue("batch processing", total=len(items)) as ctx:
for i, item in enumerate(items):
ctx.refine(index=i, item_id=item["id"])
process_item(item)
If the batch fails at item 42, the exception note shows exactly which item and index.
Logging integration
import logging
from clued import current_clues
class ClueFilter(logging.Filter):
def filter(self, record):
for handle in current_clues():
record.__dict__.update(handle.kv)
return True
How It Works
clued is built on two Python stdlib primitives:
- PEP 678
add_note()(Python 3.11+) — the standard way to attach strings to exceptions. Tools like pytest, rich, and IPython already render__notes__, so you get readable output for free. contextvars.ContextVar— each asyncio task and thread inherits a copy-on-write context, so nested tasks each carry their own clue stack and cannot interfere with each other.
When an exception exits a clue block, two things happen:
- A formatted string (
- Clue N: msg [k=v] (file:line)) is appended viaadd_note(), whereNis the depth index (0 = innermost). - A
ClueRecordis appended toexc.__clues__for structured access.
No monkeypatching. No custom exception base class. No runtime overhead on the happy path.
Development
# Set up or Sync python environment
make sync
# Ruff formatter
make fmt
# Ruff linter & Mypy static type check
make check
# Pytest
make test
License
MIT
Metadata
Release files for clued 0.1.3
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| clued-0.1.3.tar.gz | 6.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| clued-0.1.3-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 14.3 kB
Release files / clued-0.1.3.tar.gz
| Download URL | clued-0.1.3.tar.gz |
|---|---|
| Size | 6.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
6dfeaf9e6cca2b617a48d30758ed2a49662f80821a6ef0c7a3ed3c901eecb89e
|
|
BLAKE2b-256 checksum How to use checksums |
128557c7f22bcef2e94cfbb215c99c02381f5253ba46d4d8b2394b53ad057a32
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.11.5 {"installer":{"name":"uv","version":"0.11.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / clued-0.1.3-py3-none-any.whl
| Download URL | clued-0.1.3-py3-none-any.whl |
|---|---|
| Size | 8.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
a8652889ab92442c695f43996e4e7d8d7c800ba83457d566cfc381ab0a45bec5
|
|
BLAKE2b-256 checksum How to use checksums |
6f99492f8751967f7e5287ac33b587dc052935c013531a815fd7f973012f1dac
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.11.5 {"installer":{"name":"uv","version":"0.11.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|