See it in action
Every terminal below is real output, captured from the scripts that actually ship in examples/ — nothing here is a mockup. Run any of them yourself with uv run python examples/<name>.py, or click Replay to watch a session again.
Why this changes the game for Python logging
Traditional logging asks you to describe execution one disconnected line at a time — you thread request IDs through function signatures by hand, you scroll a 30-frame traceback to find the one line that's actually yours, and every polling loop floods the log forever. runtime-narrative asks a different question: what is the story of this execution? Wrap it in story()/stage() once, and correlation, diagnostics, and noise control come for free.
| Traditional print() / logging | runtime-narrative | |
|---|---|---|
| Correlating one request across functions | Thread a request ID through every function signature and every log call by hand | Nest story() calls — sub-stories auto-link via ContextVar, no threading, no new API |
| Understanding a failure | Scroll a 30+ frame traceback mixing your code with framework internals | One pinpointed frame, a source snippet, and — in rich mode — the exact local values involved |
| Noisy polling loops | One log line per iteration, forever | CoalescingRenderer collapses repeats into a single summary line |
| Explaining "why it broke" | You reread the traceback and guess | An optional LLM analyzer turns the traceback into a plain-English cause and a targeted fix |
| Changing where output goes | Rewrite your logging calls | Swap renderers — same events, any combination of console/JSON/SQLite/OTel/Prometheus/webhooks |
1. A wall of traceback vs. one pinpointed line
examples/fastapi_ugly_traceback_demo.py runs the exact same async bug two ways: an asyncio.gather fan-out inside a retry-wrapped pricing engine, called through FastAPI routing, that multiplies a price by a BOGO promo dict instead of a discount factor. Left: no instrumentation. Right: the same call stack, wrapped in story()/stage().
Both sides hit the identical bug through the identical call stack. Run it yourself: uv run python examples/fastapi_ugly_traceback_demo.py. The Why (exact) line naming item['price'] and promo by name — instead of restating the generic TypeError message — is FailureOccurred.exact_cause; see the Event Reference.
2. Call tracing across functions, for free
Open a story() while another is already active — an API handler calling a DB helper, say — and it automatically becomes a linked sub-story: its own success/failure and duration, tagged with a short ID, indented by nesting depth, colored consistently per story family. No correlation ID to thread through the call, no manual span-linking.
[5752d4]'s story inherited [7a6211]'s renderers and diagnostics automatically and shares its root_story_id — this holds up under real concurrency for free, since asyncio.Task copies ContextVar state at creation. Details: Sub-stories & Log Capture.
3. Structured logs and a failure box, from one story()
NarrativeLogHandler folds ordinary logging.warning()/.error() calls into the same event stream as story()/stage() — one timeline instead of two. level_icons prepends any string (typically an emoji) per log level, no custom renderer required, and a failure still gets the full diagnostics box automatically.
The 🔍/ℹ️/⚠️/🔥 icons are three lines of config (ConsoleRenderer(level_icons={...})), not a custom renderer. Color and the story/stage glyphs are on by default. Details: Renderers.
4. No more flooded logs from polling loops
Status-polling loops — waiting on a pipeline, an upload, a job queue — are the classic source of log floods: one line per iteration, sometimes for minutes. CoalescingRenderer wraps any other renderer and collapses a run of identical back-to-back stages into a single summary line once it's clear they're repeating.
The wrapped renderer still sees the first two occurrences in full — so it's visible that polling started — before collapsing the rest. Machine-consumption renderers like JsonRenderer or SqliteStoryRenderer should stay unwrapped for full fidelity. Details: CoalescingRenderer.
Try it on your own code
None of the above needed a rewrite — every demo is a normal function with story()/stage() wrapped around existing logic. Start with Installation and Quick Start, or browse every runnable script in Examples.