sentry-sdk
sentry-sdk is the official Python client for Sentry. You call sentry_sdk.init() once with a DSN and it installs an excepthook plus a pile of framework integrations, so unhandled exceptions arrive at Sentry with a stack trace, local variables, breadcrumbs, request context, release, and environment already attached. The same client also carries performance tracing (transactions and spans, with trace headers propagated to downstream services), continuous profiling, cron check-ins, and structured logs. Integrations for Django, Flask, FastAPI, Celery, SQLAlchemy, httpx, boto3, OpenAI and dozens more turn on by themselves when the library is importable, so most apps get useful data from a single init call.
The default choice for Python error tracking, and the tracing and profiling that grew around it are good enough that most teams stop shopping. Budget for the parts nobody mentions: sampling decisions, PII scrubbing, and calling flush() in anything short-lived.
Use it if
- You want unhandled exceptions reported with stack frames, local variables, and request context without writing that plumbing yourself
- You run Django, Flask, FastAPI, or Celery and want automatic instrumentation of requests, database queries, outbound HTTP calls, and task failures from one init call
- You already send errors from a JavaScript or mobile app to Sentry and want the Python backend on the same trace, which the sentry-trace and baggage headers give you for free
- You need cron monitoring or release health for Python jobs and would rather not build check-in plumbing around your scheduler
- You just want errors in your logs: this SDK exists to feed a hosted (or self-hosted) Sentry instance, and without a DSN and an event quota it does nothing useful
- You are standardizing on OpenTelemetry collectors and want vendor-portable telemetry: opentelemetry-sdk keeps your instrumentation independent of one backend's ingest format
- You work under strict data rules: default integrations can attach request bodies, headers, and local variables, so before_send scrubbing and the data_collection option need to be settled before this reaches production
- You pin dependencies loosely and dislike migrations: the 1.x to 2.x jump replaced Hub with scopes, and the 2.x line keeps moving tracing APIs (description became name, and Scope.start_span goes no-op once trace_lifecycle is set to stream)
- You want zero added latency in a hot path: full trace sampling plus profiling costs real CPU, and auto-enabled integrations patch a long list of libraries at import time
Setup reality
The install is unusually light for what it does: only urllib3 and certifi. The friction is where and when init() runs. It has to happen as early as possible in the process, and in prefork servers (gunicorn, uvicorn workers, Celery) it must run in the child, because the background transport thread does not survive a fork. Nothing is sampled unless you ask: traces_sample_rate defaults to unset, enable_logs defaults to False, send_default_pii defaults to False, and profiling needs its own sample rate. Framework extras like sentry-sdk[fastapi] only pull test-verified versions of the framework; integrations auto-enable purely from what is importable, which means an unexpected library in your environment can start getting patched after an unrelated dependency bump. Short-lived processes and serverless handlers must call sentry_sdk.flush() before exit or the last events are simply lost.
Patterns
Initialize the SDK at process startinit-basic
import os
import sentry_sdk
sentry_sdk.init(
dsn=os.environ["SENTRY_DSN"],
environment=os.getenv("ENV", "development"),
release=os.getenv("GIT_SHA"),
traces_sample_rate=0.1,
send_default_pii=False,
)Call this before importing or building your app object, and inside the worker process when you fork (gunicorn, Celery). The transport runs on a background thread that a fork does not carry over.
Report a handled exception yourselfcapture-exception
import sentry_sdk
try:
charge_card(order)
except PaymentError as exc:
event_id = sentry_sdk.capture_exception(exc)
return {"error": "payment failed", "ref": event_id}capture_exception returns the event id, which is worth surfacing to users so a support ticket maps to one Sentry event. Called with no argument inside an except block it picks up sys.exc_info().
Attach user, tags, and context to everything in this requesttag-current-request
import sentry_sdk
sentry_sdk.set_user({"id": user.id, "email": user.email})
sentry_sdk.set_tag("tenant", tenant.slug)
sentry_sdk.set_context("order", {"id": order.id, "total_cents": order.total})These write to the isolation scope, which the web integrations reset per request, so they do not bleed between requests. In a plain script or worker loop there is no such reset, and values stay set until you clear them.
Scope data to one block onlytemporary-scope
import sentry_sdk
with sentry_sdk.new_scope() as scope:
scope.set_tag("import_batch", batch_id)
scope.set_level("warning")
sentry_sdk.capture_message("partial import")
# tag and level are gone herenew_scope() is the 2.x replacement for push_scope(). Use isolation_scope() instead when you are starting an independent unit of work such as a consumed queue message.
Leave a trail before the failurebreadcrumbs
import sentry_sdk
sentry_sdk.add_breadcrumb(
category="sync",
message=f"fetched {len(rows)} rows from {source}",
level="info",
data={"source": source, "cursor": cursor},
)Breadcrumbs only ship attached to an event, so they cost nothing until something fails. The buffer is capped (max_breadcrumbs, 100 by default) and drops oldest first, so a chatty loop can push out the useful ones.
Decide sampling per transactiontraces-sampler
def traces_sampler(sampling_context):
path = sampling_context.get("asgi_scope", {}).get("path", "")
if path in ("/health", "/metrics"):
return 0.0
if path.startswith("/checkout"):
return 1.0
return 0.05
sentry_sdk.init(dsn=DSN, traces_sampler=traces_sampler)traces_sampler wins over traces_sample_rate when both are set. Returning 0.0 for health checks is usually the single biggest reduction in trace volume and bill.
Time your own work inside a tracecustom-span
import sentry_sdk
with sentry_sdk.start_span(op="pdf.render", name="invoice pdf") as span:
span.set_data("page_count", len(pages))
render(pages)
@sentry_sdk.trace
def reprice_catalog(items):
...Spans are only sent when a transaction is already active, so a span started in a background thread with no transaction quietly goes nowhere. The description keyword is deprecated; pass name.
Strip sensitive data before it leaves the processscrub-before-send
def before_send(event, hint):
request = event.get("request", {})
headers = request.get("headers", {})
for key in ("Authorization", "Cookie", "X-Api-Key"):
if key in headers:
headers[key] = "[filtered]"
if isinstance(hint.get("exc_info"), tuple):
exc = hint["exc_info"][1]
if isinstance(exc, ExpectedTimeout):
return None # drop the event entirely
return event
sentry_sdk.init(dsn=DSN, before_send=before_send)Returning None drops the event. This runs in your process on every event, so keep it cheap and total: an exception raised inside before_send loses the event you were trying to report.
Send structured logs alongside errorsstructured-logs
import sentry_sdk
from sentry_sdk import logger as sentry_logger
sentry_sdk.init(dsn=DSN, enable_logs=True)
sentry_logger.info(
"import finished for {tenant}",
tenant=tenant.slug,
attributes={"rows": len(rows), "duration_ms": elapsed},
)enable_logs is False by default, so the calls are no-ops until you turn it on. The template is formatted with your keyword arguments and the raw template is sent too, which is what lets Sentry group logs that differ only by value.
Control what stdlib logging turns intologging-integration
import logging
import sentry_sdk
from sentry_sdk.integrations.logging import LoggingIntegration
sentry_sdk.init(
dsn=DSN,
integrations=[LoggingIntegration(
level=logging.INFO, # INFO and above become breadcrumbs
event_level=logging.ERROR, # ERROR and above become Sentry events
)],
)The defaults already send every logger.error() as its own event, which is how a noisy retry loop turns into thousands of issues. Set event_level=None to stop that and report only through capture_exception.
Do not lose the last event in a short-lived processflush-before-exit
import sentry_sdk
def lambda_handler(event, context):
try:
return process(event)
except Exception:
sentry_sdk.capture_exception()
raise
finally:
sentry_sdk.flush(timeout=2.0)Events go out on a background worker thread. Scripts, cron jobs, and serverless handlers can exit or be frozen before that thread sends anything, so flush with a timeout you can afford to wait.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| opentelemetry-sdk | PyPI | You want vendor-neutral traces and metrics you can point at any backend, and accept assembling exporters and instrumentation yourself |
| logfire | PyPI | You want an OpenTelemetry-based observability SDK with first-class Pydantic and FastAPI ergonomics rather than an error tracker that grew tracing |
| rollbar | PyPI | You want error tracking only, with a smaller feature surface and no tracing or profiling to configure |