The dumpio-client package (Python ≥ 3.8, no required dependencies) sends faithful, typed value dumps and structured messages to Dumpio over HTTP. The serializer understands dict/list/tuple/set, dataclasses, arbitrary objects via __dict__/repr, bytes, and special float values; ORM models (SQLAlchemy, Django) render as their loaded columns with the internal state dropped (already-loaded data only — no lazy query is triggered). It infers member visibility from the _/__ convention, breaks cycles with ref nodes, and captures the calling file:line automatically. Ready-made integrations for Django, Flask, and FastAPI/Starlette.
pip install dumpio-client # optional framework extras:pip install "dumpio-client[django]"pip install "dumpio-client[flask]"pip install "dumpio-client[fastapi]"Every helper is fire-and-forget and never raises into your app — if the viewer isn't running, the dump is dropped silently. Sending is synchronous (an HTTP POST with a short timeout, 1.5 s by default), so no flush() is needed.
Why send dumps to Dumpio instead of print()
- It doesn't flood stdout. The dump travels over the network to a separate window, so you don't have to print objects into a WSGI server's log or a test runner's output.
- It preserves types and structure. Instead of
<object at 0x…>you see a typed tree — dataclasses,set,bytes, cycles viaref, attribute visibility. - It sorts things for you. Color flags, labels, and channels separate output from different places.
- It won't crash the request. Everything is wrapped so a send error (e.g. when the viewer isn't running) can never leak into your app.
For the full list of dump types and how Dumpio renders them, see Dump types; if nothing shows up, start with Troubleshooting.
Quick start
from dumpio_client import dumpio, dd dumpio(user) # dump a variabledumpio(user, label="user", flag="green") # with a label and flagdd(a, b) # dump every argument and stop the script — sys.exit(1)dumpio() returns the value it was given, so you can drop it into an expression without breaking it:
return dumpio(compute_total(cart), label="total") # dumps and carries onWhich entry point should you use?
| I want to… | Use |
|---|---|
| Quickly dump a value | dumpio(x) |
| Dump mid-expression without breaking the code | dumpio(x) (returns x unchanged) |
| Add a flag / label / channel | dumpio(x, label=…, flag=…, channel=…) |
| Dump and stop the script | dd(x) |
| Send something other than a variable (exception, query, HTTP…) | typed helpers dumpio_exception(), dumpio_query(), … |
Configuration
Defaults are read from the environment, or you can set them via configure(...):
| Option | Env var | Default |
|---|---|---|
host |
DUMPIO_HOST |
localhost |
port |
DUMPIO_PORT |
21234 |
path |
– | /dumps |
token |
DUMPIO_TOKEN |
"" |
timeout (s) |
– | 1.5 |
enabled |
DUMPIO_DISABLE (set it to turn off) |
true |
max_depth |
– | 6 |
max_items |
– | 100 |
max_string |
– | 2000 |
from dumpio_client import configure configure(host="127.0.0.1", port=21234, token="Dio-…")The token is sent as the X-Dumpio-Token header. configure() returns the resulting config (you can also reach the exported config dict). The SDK speaks HTTP only (no TCP).
Core functions
| Function | Purpose |
|---|---|
dumpio(value, label=None, flag="blue", channel="default") -> Any |
Send a variable dump and return value |
dd(*values) -> None |
Dump every argument and stop the script with sys.exit(1) |
configure(**options) -> dict |
Override config and return the result |
serialize(value, ...) -> dict |
Return the typed tree without sending |
Fluent builder & flood control
dio(value) returns a chainable builder that mirrors the PHP/Node clients. It
ships on .send(), when used as a with block, or automatically when the object
is dropped (CPython sends it at the end of the statement, like the PHP destructor).
from dumpio_client import dio, stopwatch dio(user).red().label("user").channel("auth") # auto-sendsdio(payload).green().send() # explicit dio(state).when(debug) # send only if truthy (.unless() is the opposite) # Keep loops from flooding the viewer (keyed by call site, or a shared name):for row in rows: dio(row).once() # only the first iteration dio(row).limit(5) # at most 5 dio(row).count("rows") # one live-updating "×N" entryColors: red() yellow() blue() gray() purple() pink() green() (or flag("…")),
plus label(), channel(), when()/unless(), once()/limit(n)/count(name?), send().
Display modifiers
These four don't change what you send, only how the viewer shows the entry:
dio(config).expand() # arrives fully open instead of two levels deepdio(huge_payload).collapse() # arrives closeddio(report).large() # bigger detail panel (small() shrinks it)dio(token).hide() # shipped, but masked until you click to revealsize("sm"|"md"|"lg") is the general form (small()/large() are shorthands; an
unknown value is ignored). hide() marks the list row hidden and puts a
placeholder in the detail panel — good for noisy or sensitive payloads. Same
method names as the PHP and Node clients.
On non-CPython runtimes (e.g. PyPy) object collection isn't immediate, so call
.send()or usewith dio(x) as d:when you need the dump sent at a precise point.
Your own dump types (macros)
Register how a domain object should be dumped once, then send it by name:
from dumpio_client import extend, custom extend("invoice", lambda i: {"type": "invoice", "message": f"Invoice {i.number}", "total": i.total})custom("invoice", order)custom("invoice", order, flag="green", channel="billing")The maker returns the payload to send; without an explicit type the macro's
name is used, and the viewer renders an unknown type with the generic value view.
A name that collides with a real helper is refused. has_macro(name) and
forget_macros(name=None) round it out. Same registry in the PHP and Node clients.
Timing a request as a tree (spans)
A stopwatch measures one thing. A span measures a section of work and knows what it contains, so the request timeline in the viewer becomes a tree:
from dumpio_client import span with span("handler", "controller"): with span("load orders", "db"): ... # queries here appear under "load orders" with span("render", "view"): ...The second argument is a free-form category (db, view, http, …) that
colours the bar. Spans opened inside another become its children automatically —
the open stack lives in a ContextVar, so threads and asyncio tasks never nest
into each other. The with form ends the span even when the work raises, and the
exception continues on its way untouched.
Timing — stopwatch
sw = stopwatch("import")# … work …sw.lap("parsed") # split time, keeps running# … more work …sw.stop("done") # final timeEach lap()/stop() sends a measure dump with the elapsed milliseconds (and a
memory delta when tracemalloc is running).
Typed helpers
Each renders a dedicated view in the viewer and picks a sensible default flag. All accept the keyword arguments flag=, channel=, timestamp= and any additional payload fields.
| Helper | Default flag | Renders |
|---|---|---|
message(text, **opts) |
blue | generic titled data (extra keys as payload) |
dumpio_exception(exc, context=None, **opts) |
red | error + stack trace |
dumpio_query(sql, bindings=None, time_ms=None, **opts) |
purple | SQL query (connection=, message=) |
dumpio_http(method, url, status=None, headers=None, body=None, response_time=None, **opts) |
by status | HTTP request |
dumpio_log(level, message, details=None, **opts) |
by level | log |
dumpio_model(cls_name, attributes, relations=None, exists=None, connection=None, **opts) |
one domain object | |
dumpio_collection(items, count=None, **opts) |
list | |
dumpio_table(columns, rows, **opts) |
explicit table | |
dumpio_measure(name, time_ms, memory=None, context=None, **opts) |
one timing | |
dumpio_performance(metrics, breakdown=None, context=None, **opts) |
a metric bundle | |
dumpio_event(event, entity=None, entity_id=None, actor=None, data=None, metadata=None, **opts) |
a domain event | |
dumpio_html(html, message=None, title=None, **opts) |
an HTML fragment/page in a sandboxed iframe | |
dumpio_mail(mail, **opts) |
a rendered email (envelope + body); mail = dict of html/text/subject/from/to/cc/bcc/attachments |
|
dumpio_json(json_value, message=None, title=None, **opts) |
a JSON document (string or any value) as tree + source | |
dumpio_request(request, **opts) |
by status | one inbound request, summarized |
dumpio_xml(xml, message=None, title=None, **opts) |
an XML document as an element tree + indented source | |
dumpio_image(url, message=None, title=None, **opts) |
an image — data: URI inline, remote URL behind an opt-in (alt/width/height/mime) |
|
dumpio_notify(message_text, title=None, **opts) |
yellow | a desktop notification, raised as soon as it lands |
Flag by HTTP status: None → blue, ≥ 500 → red, ≥ 400 → yellow, ≥ 300 → blue, else green.
Flag by log level: emergency/alert/critical/error → red, warning/notice → yellow, info → blue, else gray.
Examples
from dumpio_client import ( dumpio_exception, dumpio_query, dumpio_http, dumpio_log, dumpio_table, dumpio_event,) # An error with a structured stack trace and contexttry: gateway.charge(order)except Exception as e: dumpio_exception(e, context={"order_id": order.id, "user": user.email}) raise # A database query with parameters and timedumpio_query("select * from orders where status = ?", ["paid"], 12.4) # An outgoing HTTP call — the flag is derived from the statusdumpio_http("POST", "https://api.stripe.com/v1/charges", 402, response_time=240) # A log line with details (flag by level)dumpio_log("error", "Payment failed", {"gateway": "stripe", "code": "card_declined"}) # A table — columns + rowsdumpio_table(["id", "status"], [[1, "paid"], [2, "pending"]], message="Orders") # A business event with an actor and datadumpio_event("order.refunded", entity="Order", entity_id=42, actor={"email": user.email}, data={"amount": 1990})For a detailed look at how Dumpio renders each type (and how it parses exceptions), see Dump types and Exceptions.
Auto-capture outbound HTTP
capture_http() patches http.client — which urllib, requests and urllib3 all funnel through — so every outgoing request your app makes is forwarded to the viewer as an http dump. No per-call code needed.
from dumpio_client import capture_http stop = capture_http(channel="outbound")# ... your app makes HTTP calls ...stop() # restore the originals when you're doneIt's idempotent, defensive (a capture failure never disturbs the request), and skips requests to the Dumpio viewer itself, so the SDK never dumps its own traffic. Pass ignore=lambda info: ... to skip noisy endpoints — info carries method / url / host / port.
httpxuses its own transport (httpcore), nothttp.client, so it isn't captured by this patch.
Auto-capture ORM queries (SQLAlchemy)
capture_sqlalchemy(engine) attaches to SQLAlchemy's public before_cursor_execute / after_cursor_execute events (no monkey-patching) and forwards every executed statement as a query dump with its bound parameters and timing. Works with a sync Engine or an AsyncEngine (its sync_engine is used).
from dumpio_client import capture_sqlalchemy stop = capture_sqlalchemy(engine, channel="db")# ... your app runs queries ...stop() # detach the listenersThe captured SQL and bindings are the driver-level ones (SQLAlchemy has already compiled named params to the DBAPI's paramstyle). Pass ignore=lambda info: ... to skip statements — info carries sql / bindings.
Request correlation & summaries
Every dump made while serving one request can share a requestId, so the viewer
groups them under one collapsible header — "what happened in this request".
The Django, Flask and FastAPI integrations do it for you: they open a scope
around the request and, when the response is ready, send a request summary
(method, url, route, status, total time, IP, and the signed-in user where the
framework already resolved one) that the viewer puts on the group's header and
uses as its front page.
For code of your own — a worker, a management command, a framework we don't ship an integration for:
from dumpio_client import request_scope, begin_request, end_request, dumpio_request with request_scope("import users"): ... # every dump in here shares one requestId begin_request("nightly rebuild") # straight-line alternative...end_request()The scope lives in a ContextVar, which is per-thread and per-task, so a
threaded WSGI server and an async ASGI server are both safe: two requests being
served at the same time never stamp each other's dumps.
Framework integrations
Each integration imports its framework lazily and is defensive: importing the module without the framework, or the viewer being down, never breaks anything.
Django
# settings.pyMIDDLEWARE = [ # ... "dumpio_client.django.DumpioMiddleware",]Sends an http dump per request and an exception dump on view errors.
Flask
from flask import Flaskfrom dumpio_client.flask import DumpioFlask app = Flask(__name__)DumpioFlask(app) # or: DumpioFlask().init_app(app)Uses before_request / after_request / teardown_request to send http dumps and exceptions.
FastAPI / Starlette
from fastapi import FastAPIfrom dumpio_client.fastapi import DumpioMiddleware app = FastAPI()app.add_middleware(DumpioMiddleware)An ASGI BaseHTTPMiddleware that sends one http dump per request. Degrades to a transparent no-op if Starlette isn't installed.
Standard logging (optional)
import loggingfrom dumpio_client.logging import DumpioHandler logging.getLogger().addHandler(DumpioHandler())Forwards log records as log dumps (and as exception dumps when a record carries exc_info).
How Python values look in the viewer
The serializer maps Python onto the viewer's language-neutral value tree:
- dicts render as
dict(n) { … }, lists asarray:n [ 0: … ](the viewer numbers the entries), tuples asarraywith the classtuple, sets asset(n). - Attributes follow Python's naming convention:
nameis public,_nameprotected,__nameprivate — shown under the name you declared, even though the interpreter mangles it to_Class__name. datetime/date/timearrive as an object with an ISO field, the same shape the Node client sends for aDate, so a timestamp reads the same in either language.Decimal,UUIDandpathlibpaths arrive as their string form with the type as the class.- bytes are decoded (replacing invalid sequences) and carry the class
bytes; functions render ascallable. - Depth, item count and string length are bounded by
max_depth/max_items/max_string; anything clipped is marked truncated in the viewer rather than silently shortened.
What gets sent (the wire format)
Every dump carries a standard envelope (timestamp, flag, channel, type, language: "python", …) plus type-specific fields. The exact envelope, flags, and limits are described in Dump format; the value tree is the same as in the other SDKs.