Stop guessing. Start understanding. Your Python errors, explained.
Project description
butwhy
Stop guessing. Start understanding.
Your Python errors, explained — in plain English.
pip install butwhy → import butwhy → done.
The 30-second pitch
You know how Python tracebacks look like this:
Traceback (most recent call last):
File "demo.py", line 2, in <module>
result = data.split(",")
AttributeError: 'NoneType' object has no attribute 'split'
And then you spend 5 minutes on Google figuring out what went wrong?
why turns that into this:
============================================================
AttributeError: 'NoneType' object has no attribute 'split'
at demo.py:2
============================================================
[pattern match · 93%]
Summary:
Calling .split() on None
Cause:
The variable data is None, not a string.
This usually means a function returned None (maybe it
forgot a return statement), or a lookup failed silently.
Variables at error:
data = None (NoneType)
Fix:
Add a None check: if data is not None: data.split(",")
Or trace back to find why data is None.
============================================================
One import. Zero config. Instant understanding.
Quick start
pip install butwhy
import butwhy # That's it. Errors are now explained.
data = None
result = data.split(",") # Boom — why explains it
Want AI-powered deep explanations?
Set one environment variable and why upgrades automatically:
export OPENAI_API_KEY=sk-... # or ANTHROPIC_API_KEY
No API key? why still works — it falls back to built-in pattern matching that covers 12+ common error types with zero dependencies.
Prefer a local model? (No API key, fully offline)
# Install Ollama: https://ollama.com
ollama pull qwen2.5-coder:7b
export BUTWHY_PROVIDER=ollama
Three ways to use why
1. Global (recommended) — import butwhy
import butwhy
# Every uncaught exception in your script is now explained
1 / 0
2. Context manager — with butwhy.trace()
import butwhy
with butwhy.trace():
# Only errors inside this block are explained
risky_operation()
3. Decorator — @butwhy.explain
import butwhy
@butwhy.explain
def divide(a, b):
return a / b
divide(10, 0) # Error is explained, then re-raised
butwhy.fix() — Don't just explain, fix
After an error, call butwhy.fix() to get an AI-generated patch:
import butwhy
data = None
result = data.split(",") # crashes
# In interactive mode:
>>> butwhy.fix()
Suggested fix for demo.py:2
--------------------------------------------------
- result = data.split(",")
+ if data is not None:
+ result = data.split(",")
+ else:
+ result = []
--------------------------------------------------
Apply this fix? [y/N] y
Applied fix to demo.py:2
Requires an AI provider (OpenAI/Anthropic/Ollama).
butwhy.this — The Zen of Why
>>> import butwhy
>>> butwhy.this
The Zen of Why, by why
Errors are not failures, they are teachers.
The traceback tells you what; why tells you why.
A good message answers the question before you ask it.
Read the variables, not just the line numbers.
...
Jupyter / IPython support
# In a notebook:
%load_ext why
# Or just:
import butwhy
# Now all cell errors are explained inline
Configuration
All settings via environment variables — no config file needed:
| Variable | Default | Description |
|---|---|---|
OPENAI_API_KEY |
— | OpenAI API key (enables AI explanations) |
ANTHROPIC_API_KEY |
— | Anthropic API key (enables AI explanations) |
BUTWHY_PROVIDER |
auto |
auto / openai / anthropic / ollama / patterns |
BUTWHY_LANGUAGE |
en |
en / zh (Chinese) |
BUTWHY_MODEL |
— | Override the model name for the active provider |
BUTWHY_TIMEOUT |
30 |
API timeout in seconds |
OLLAMA_URL |
http://localhost:11434 |
Ollama server URL |
OLLAMA_MODEL |
qwen2.5-coder:7b |
Ollama model to use |
BUTWHY_AUTOSTART |
1 |
Set to 0 to disable auto-install of the hook |
BUTWHY_NO_COLOR |
— | Set to disable colored output |
NO_COLOR |
— | Standard no-color env var (respected) |
Programmatic config
import butwhy
butwhy.set_config(butwhy.WhyConfig(
provider="openai",
openai_api_key="sk-...",
language="zh", # Chinese explanations
show_source=True,
show_vars=True,
cache=True,
))
How it works
why uses a three-layer fallback to ensure you always get a useful explanation:
| Layer | When | How | Quality |
|---|---|---|---|
| AI explanation | API key available | Sends error + source context + variables to LLM | Best — covers everything, gives specific fixes |
| Pattern matching | No API key (or AI failed) | Heuristic matching on 12+ common error types | Good — covers the errors you hit daily |
| Enhanced traceback | No pattern matched | Colored output with variables and source context | Fallback — still better than default |
Key design principles:
- Zero dependencies — pure Python stdlib. No rich, no httpx, no openai SDK. Just
urlliband friends. - Never crashes — if the AI fails, pattern matching takes over. If that fails, you still get a prettier traceback.
- Privacy-aware — your code only goes to an LLM if you explicitly set an API key. Pattern matching is 100% local.
- Caching — repeated errors don't re-call the API. Cached for 7 days.
Comparison
| Feature | why |
rich | better-exceptions | stackprinter | Copilot |
|---|---|---|---|---|---|
| Works without API key | ✅ | ✅ | ✅ | ✅ | ❌ |
| AI-powered explanations | ✅ | ❌ | ❌ | ❌ | ✅ |
import and done |
✅ | ❌ | ❌ | ❌ | ❌ |
| Local model support | ✅ Ollama | — | — | — | ❌ |
| Zero dependencies | ✅ | ❌ | ❌ | ❌ | — |
| Variable values in output | ✅ | ✅ | ✅ | ✅ | ❌ |
Auto-fix (butwhy.fix()) |
✅ | ❌ | ❌ | ❌ | ✅ |
| Works in CI/CD & servers | ✅ | ✅ | ✅ | ✅ | ❌ |
| Chinese explanations | ✅ | ❌ | ❌ | ❌ | ✅ |
| Jupyter magic | ✅ | ❌ | ❌ | ❌ | ❌ |
Supported error types (pattern matching)
The built-in pattern matcher covers these error types without any API key:
NameError— undefined variableTypeError— wrong type operationsValueError— invalid values (int conversion, unpacking)IndexError— list index out of rangeKeyError— missing dictionary keyAttributeError— wrong attribute/method (including NoneType)ZeroDivisionError— division by zeroImportError/ModuleNotFoundError— missing modules (with install hints)FileNotFoundError— file path issuesSyntaxError— common syntax mistakesRecursionError— infinite recursionUnicodeDecodeError— encoding issuesIndentationError/TabError— indentation problems
More patterns are added regularly. With an AI provider, all error types are covered.
Examples
See the examples/ directory for runnable demos:
basic.py— common errors with pattern matchingwith_ai.py— AI-powered deep explanationsjupyter_demo.ipynb— notebook usagefix_demo.py— auto-fix workflow
Contributing
Contributions welcome! Especially:
- New pattern matchers for error types not yet covered
- Translations for
BUTWHY_LANGUAGE - Bug reports and edge cases
git clone https://github.com/yourname/butwhy.git
cd butwhy
pip install -e ".[dev]"
pytest
License
MIT — see LICENSE.
import butwhy — because every error deserves an explanation.
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 butwhy-0.1.0.tar.gz.
File metadata
- Download URL: butwhy-0.1.0.tar.gz
- Upload date:
- Size: 32.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
601f69ff33898557cc55c4d9cde693cd8a7b155d0b54610f4986049b5a065bab
|
|
| MD5 |
31656f99d9ced66add194d122d0d87f4
|
|
| BLAKE2b-256 |
1760df5d4a09bf4f8a195f83c8305989a749a226272e89f57bcc243b30810b10
|
File details
Details for the file butwhy-0.1.0-py3-none-any.whl.
File metadata
- Download URL: butwhy-0.1.0-py3-none-any.whl
- Upload date:
- Size: 31.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
635f50318b7746f34bd4a2c625712abd61db7b19cfc46193a38166373e038060
|
|
| MD5 |
fb2ade0c41d3819d00dd98a717ecfd39
|
|
| BLAKE2b-256 |
4a43b8510f4d6726501513c1b8d4df8935cee8ad2076422cc60292e66ea6fec2
|