Enriches exceptions with plain-English causal chains at throw time
Project description
because
Stack traces show symptoms. because shows causes.
The problem
Your monitoring dashboard lights up with ConnectionError: connection refused on localhost:5432. The stack trace points to db/pool.py:142. You have no idea why.
The real story: a recent deploy added a synchronous DB call to a hot path. Under load, 48 of 50 connections filled up. Three TimeoutErrors were silently swallowed over the prior 10 seconds. By the time the ConnectionError fired, all the context was gone.
This is the ABS warning light telling you the battery is dead. Today's error tooling stops at the symptom.
because captures the context before the crash and attaches it to the exception at throw time.
What it looks like
Without because:
ConnectionError: connection refused on localhost:5432
File "db/pool.py", line 142, in execute
With because:
ConnectionError: connection refused on localhost:5432
File "db/pool.py", line 142, in execute
[because context]
Likely cause: Database connection pool may be exhausted
• Exception message contains 'QueuePool limit'
• 8 DB queries in context window (pool was active)
• 5/8 recent DB queries failed (63% failure rate)
Recent operations (8):
[ok] db_query 2.1ms SELECT * FROM orders WHERE user_id = ?
[ok] db_query 1.8ms SELECT * FROM orders WHERE user_id = ?
[ok] db_query 2.4ms SELECT * FROM orders WHERE user_id = ?
[FAIL] db_query 0.5ms SELECT * FROM orders WHERE user_id = ? error=OperationalError
[FAIL] db_query 0.5ms SELECT * FROM orders WHERE user_id = ? error=OperationalError
[FAIL] db_query 0.5ms SELECT * FROM orders WHERE user_id = ? error=OperationalError
[FAIL] db_query 0.5ms SELECT * FROM orders WHERE user_id = ? error=OperationalError
[FAIL] db_query 0.5ms SELECT * FROM orders WHERE user_id = ? error=OperationalError
Install
pip install because-py
With instrumentation extras:
pip install "because-py[sqlalchemy]"
pip install "because-py[sqlalchemy,requests,httpx]"
With the LLM explainer:
pip install "because-py[llm]" # Anthropic (default)
pip install "because-py[llm,openai]" # + OpenAI support
Zero-config setup
import because
because.install()
That's it. because hooks sys.excepthook and starts recording operations in the background. Any uncaught exception automatically gets enriched context appended to stderr — no changes to your exception handlers required.
Instrumenting libraries
because ships with instruments for common libraries. Attach them to your existing clients:
from because.instruments.sqlalchemy import instrument as instrument_sa
from because.instruments.requests import instrument as instrument_requests
from because.instruments.httpx import instrument as instrument_httpx
from because.instruments.redis import instrument as instrument_redis
from because.instruments.logging import instrument as instrument_logging
import requests
instrument_sa(engine) # SQLAlchemy engine
instrument_requests(requests.Session()) # requests Session
instrument_httpx(httpx_client) # httpx Client or AsyncClient
instrument_redis(redis_client) # redis-py client (sync or async)
instrument_logging() # root logger (WARNING and above)
Each instrument records operation timing, success/failure, and relevant metadata into a per-thread/per-task ring buffer. Zero I/O on the hot path.
Framework integrations
# Flask
from because.integrations.flask import instrument as instrument_flask
instrument_flask(app)
# FastAPI
from because.integrations.fastapi import BecauseMiddleware
app.add_middleware(BecauseMiddleware)
# Django — add to MIDDLEWARE in settings.py
MIDDLEWARE = [
"because.integrations.django.BecauseMiddleware",
...
]
Recording swallowed exceptions
Caught-and-not-reraised exceptions are often the real cause of a downstream crash. Wrap risky calls with because.catch() to make them visible:
def get_user(user_id):
with because.catch(Exception):
return db.query(User).filter_by(id=user_id).one()
return None # reached only if exception was swallowed
When a downstream AttributeError: 'NoneType' object has no attribute 'email' fires, because will surface the swallowed DB error as the likely cause.
Enriching caught exceptions manually
For exceptions you handle yourself:
try:
process_order(order_id)
except Exception as exc:
because.enrich_with_swallowed(exc)
logger.error("Order processing failed", extra={"context": exc.__context_chain__})
raise
__context_chain__ serializes cleanly into Sentry extra, Datadog error attributes, or structured log fields.
LLM-based explanation (v0.2)
Get a plain-English root cause analysis powered by Claude or GPT-4o:
import because
because.configure_llm(api_key="sk-ant-...") # Anthropic by default
try:
risky_operation()
except Exception as exc:
because.enrich_with_swallowed(exc)
explanation = await because.explain_async(exc)
print(explanation.root_cause)
print(explanation.suggested_fix)
Sync version (avoid in async contexts):
explanation = because.explain(exc)
Use OpenAI instead:
because.configure_llm(api_key="sk-...", provider="openai")
Bring your own provider by implementing the LLMProvider protocol:
class MyProvider:
async def complete(self, prompt: str) -> str:
...
CLI
Analyze any stack trace from the command line — no code changes required:
# pipe from a log file
cat error.log | because explain
# or pass a file directly
because explain error.log
# paste interactively (Ctrl-D to submit)
because explain
# use OpenAI instead
because explain --provider openai --model gpt-4o error.log
Reads ANTHROPIC_API_KEY or OPENAI_API_KEY from the environment, or pass --api-key directly.
Heuristic patterns
because ships with deterministic cascade patterns that run at throw time — no API key required:
| Pattern | Fires when |
|---|---|
pool_exhaustion |
Connection/pool error + recent DB activity or explicit pool message |
silent_failure |
Swallowed exception preceded the current error |
retry_storm |
Timeout + high concentration of repeated HTTP requests to the same host |
Each pattern is a small, independently testable unit. Output always uses hedged language ("likely cause", "contributing factor") — because never claims certainty.
Observability integrations
# Sentry
from because.integrations.sentry import before_send
sentry_sdk.init(..., before_send=before_send)
# Datadog
from because.integrations.datadog import tag_current_span
tag_current_span(exc)
# Structured logging
from because.integrations.logging import BecauseFormatter
handler.setFormatter(BecauseFormatter())
Design principles
- Honest framing. Output uses "likely cause" and "contributing factor." Wrong-but-confident destroys trust.
- Zero-config default.
import because; because.install()does something useful immediately. - No hot-path cost. Instrumentation is bounded ring buffers. Enrichment runs only on exception.
- Composable with existing tools. Attaches to Sentry, Datadog, and structured logging — doesn't replace them.
- Library, not platform. Pure Python, no required backend, ships as a pip package.
Examples
Runnable demos in examples/:
python examples/pool_exhaustion.py # connection pool saturated under load
python examples/silent_failure.py # swallowed DB error causes downstream crash
Each demo shows the cascade being triggered and because surfacing the cause.
Roadmap
- v1.0 — Cross-process / cross-service causal reasoning
- More instruments: stdlib
socket,grpc - OpenTelemetry span export
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file because_py-0.2.2.tar.gz.
File metadata
- Download URL: because_py-0.2.2.tar.gz
- Upload date:
- Size: 37.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: Hatch/1.16.5 cpython/3.13.3 HTTPX/0.28.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7ab4b52f378c29d7656241858204ddedc5eecb33806b4ac9a1d95f973d9d7c35
|
|
| MD5 |
0bb8e0ed716abb135d6d48f3444719fc
|
|
| BLAKE2b-256 |
a195f8d5848d83a6fc992f3d3e83e0f23107384af753889b5a8f48ffc33ce88e
|
File details
Details for the file because_py-0.2.2-py3-none-any.whl.
File metadata
- Download URL: because_py-0.2.2-py3-none-any.whl
- Upload date:
- Size: 29.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: Hatch/1.16.5 cpython/3.13.3 HTTPX/0.28.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
263329caee14f91fdc02189fd76a4ea7ccdc515b75ad68c8f15543c5ca5f7c7c
|
|
| MD5 |
5b2cf1c136a7e9c053094dae4f426779
|
|
| BLAKE2b-256 |
3875a90c5f6f8ed3249380c1844c85c4069a27e77c373862c39e9c68a384132c
|