whytrail¶
Python tells you where something happened. whytrail tells you why.
Two tiers, one function¶
Tier 1 -- zero configuration. why(some_exception) reassembles a causal
chain from data CPython already retains: __traceback__, __cause__,
__context__, and the locals of the frame where it actually originated.
No setup, no tracing engine, no overhead when unused.
Tier 2 -- opt-in, scoped. why(some_tracked_value) walks a small
provenance graph built only for values you deliberately watched:
import whytrail
with whytrail.trace():
raw = whytrail.track(fetch_row(), label="raw CSV row")
price = whytrail.track(float(raw["price"]), derived_from=raw)
print(whytrail.why(price))
Something that was never tracked gets an honest answer, not a guess:
why(3.14): unknown -- no provenance captured.
This value was never tracked. Wrap it with whytrail.track(),
@whytrail.tracked, or raise it as an exception to get an answer.
whytrail will never fabricate a causal chain it isn't sure about. See
docs/adr/0001-whytrail-architecture.md
for the full reasoning behind that design choice, and why a fully automatic
why(anything) isn't possible in the first place.
Plain-English output¶
whytrail.why(exc).text is terse and technical, by design. For someone
who doesn't read tracebacks for a living, .plain_text renders the exact
same facts as prose, with general guidance for common exception types --
paraphrase, not new information, same honesty guarantee as .text:
Here's what happened, from the root cause to the final result:
1. ValueError -- got a value that didn't make sense for what it was doing (discount code table missing region 'EU') -- in load_codes(), line 12 of pricing.py
At that point: region was 'EU', table was {}.
How to avoid this: validate the value before using it, or check what produced it further up this chain.
2. KeyError -- tried to look up something that wasn't there ('SUMMER') -- in apply_discount(), line 31 of pricing.py
At that point: price was 12.5, code was 'SUMMER'.
How to avoid this: check the key exists before accessing it (`if key in d`), or use `d.get(key, default)` instead of `d[key]`.
Install¶
pip install whytrail # core, zero dependencies
pip install whytrail[rich] # + Explanation.rich() tree rendering
pip install whytrail[cli] # + the `whytrail` CLI
pip install whytrail[requests] # + auto-explain requests.RequestException, etc.
pip install whytrail[all] # every integration below, one install
One package, one version -- all 30 integrations below are extras of
whytrail itself, not separate PyPI packages (ADR 0006). pip install
whytrail[X] pulls in exactly the library X explains, nothing else;
why() picks it up automatically the moment it's installed, no further
setup.
Why not just use a debugger / logging / OpenTelemetry?¶
Each answers a different question:
| Tool | Answers |
|---|---|
pdb / IDE debugger |
What is the state right now, interactively |
logging |
Whatever you decided in advance to record |
traceback |
Where it crashed |
| OpenTelemetry | Cross-service request flow |
whytrail |
What produced this specific value, on demand, after the fact |
Public API¶
Five verbs (why, track, tracked, trace, register) plus two
persistence helpers (snapshot, restore). Domain-specific integrations
are extras of this same package, not new verbs -- see
docs/plugin-guide.md.
Ecosystem¶
33 integrations today and growing, each earning its place by clearing
one of three bars (structured error data, a security-sensitive
boundary, or a non-standard capture mechanism) rather than existing
just to exist -- see docs/adr/0003-ecosystem-scale-triage.md for the
full reasoning, the next candidates, and the much longer list of
libraries deliberately not wrapped, because generic track()/
@tracked already covers them with zero extra code, or because they
were checked directly and found to carry no structured data beyond
what tier 1 already shows for free. Full table with what each one
adds: docs/plugin-guide.md.
whytrail[requests] |
whytrail[httpx] |
whytrail[aiohttp] |
whytrail[huggingface-hub] |
whytrail[openai] |
whytrail[anthropic] |
whytrail[boto3] |
whytrail[google-cloud] |
whytrail[sqlalchemy] |
whytrail[asyncpg] |
whytrail[pymongo] |
whytrail[grpcio] |
whytrail[pydantic] |
whytrail[marshmallow] |
whytrail[jsonschema] |
whytrail[pyyaml] |
whytrail[pandas] |
whytrail[polars] |
whytrail[stripe] |
whytrail[alembic] |
whytrail[paramiko] |
whytrail[sentry] |
whytrail[ddtrace] |
whytrail[celery] |
whytrail[rq] |
whytrail[dramatiq] |
whytrail[prefect] |
whytrail[scrapy] |
whytrail[pytest] |
whytrail[fastapi] |
whytrail[django] |
whytrail[flask] |
whytrail[langchain] |
All of the above in one install: pip install whytrail[all]. Want to publish your
own, outside this repo, for a library not on this list?
python scripts/new_plugin.py <library> --kind explainer|integration
scaffolds that (ADR 0006 -- the entry-point extensibility mechanism the
bundled 30 used to use is still there for exactly this). .github/actions
/whytrail-run packages the CLI as a GitHub Action for CI.
On test coverage: every integration above is verified against a real
object from the real library, not a mock, and every one's stated minimum
dependency version is confirmed to actually install and work on the
newest supported Python -- both caught real bugs, twenty of them
version-compatibility gaps invisible from the version number alone,
eight of those found only once this project's CI actually ran on real
Linux for the first time rather than the Windows sandbox it was built in
(see CHANGELOG.md). It is not the same claim as "battle-tested in
every condition": see docs/testing-maturity.md for exactly what is and
isn't covered (the full Python 3.10-3.13 matrix, concurrency beyond the
three web frameworks, and full exception-surface breadth are the current
gaps).
Status¶
Pre-1.0. The public API may still change between minor versions. See
CHANGELOG.md for what's shipped at each version and docs/adr/ for the
architecture this was built from:
0001-- feasibility and the original two-tier architecture.0002-- category positioning and the pre-1.0 API fixes it drove.0003-- how the plugin ecosystem scales (and how it doesn't).0004-- why this project is calledwhytrailand notbutwhy.0005-- VS Code extension scope assessment (not started, and why).0006-- why the 30 integrations became extras of one package instead of 30 separate PyPI distributions.
Full documentation site (same content, easier to browse): https://bhouvana.github.io/Whytrail/