Errors & Logging Best Practices
Operational rules for exceptions and logs in production Python services.
How to Use This List
- Apply at service boundaries first - HTTP handlers, CLI entrypoints, workers.
- Pair each rule with CI (
pytest,-W error, log format tests). - Review on-call logs quarterly - if a rule did not help incidents, tighten it.
A - Exceptions
- Catch specific exception types. Never bare
except:in application code. - Chain with
raise DomainError() from excat boundaries. Preserve root cause in logs and Sentry. - Use a shallow custom hierarchy per bounded context. Handlers catch
BillingError, notException. - Fail fast on programmer errors. Let
AssertionErrorandTypeErrorbubble in dev; fix code. - Handle ExceptionGroup with
except*in async TaskGroup code. Do not assume single failure.
B - Logging Setup
- Configure logging once at application entry. Libraries only
getLogger(__name__). - Use lazy
%formatting:log.info("id=%s", id). Avoid f-strings in log calls. - Set level from environment. DEBUG locally, INFO/WARNING in production.
- Attach rotating file or ship to aggregator. stderr alone is lost on container restart.
- Silence third-party noise narrowly.
urllib3,asyncio- filter by module, document why.
C - Structured Operations
- Emit JSON or key=value in production. Search
request_id,user_id,duration_ms. - Bind correlation IDs in middleware.
contextvarsfor request-scoped fields. - Log exceptions with
log.exceptioninside except. One stack per incident, not duplicate prints. - Static event message, dynamic fields.
"payment_failed"+order_id=, not interpolated sentences. - Redact secrets and PII in processors. Tokens and emails never in plain log fields.
D - Retries & Warnings
- Retry only transient, idempotent failures. Cap attempts; exponential backoff with jitter.
- Raise chained error when retries exhaust. Include attempt count in message.
- Deprecate with
DeprecationWarningand removal version. Run CI with warnings as errors for your code. - Use
stacklevel=2+in warning wrappers. Blame caller line in traceback. - Never use warnings for business branching. Warnings notify; exceptions control flow.
E - Policy & Review
- No silent swallow - log or re-raise.
suppressonly for truly optional paths. - Map domain exceptions to HTTP/status codes in one layer. Not scattered in repositories.
- Test exception types and warning emissions.
pytest.raises,deprecated_call. - Document on-call runbook: which log query means what. Link from alert to dashboard.
- Review error rates after releases. New exception types should be expected and documented.
FAQs
What is the single worst habit?
Bare except: pass - hides data corruption until reconciliation fails weeks later.
Should I log and raise?
Yes at boundaries when operators need visibility and callers need failure - not on every inner loop.
How do I enforce in CI?
ruff E722, pytest with filterwarnings=error, and integration tests asserting log JSON schema.
print vs logging?
print for notebooks and one-off scripts; logging for anything deployed.
How verbose should INFO be?
One line per successful request boundary; DEBUG for internals. High-cardinality per-row INFO floods aggregators.
Do I need Sentry if I have logs?
Complementary - Sentry groups stack traces; logs give chronological context. Wire both with same release tag.
How do I handle third-party exceptions?
Wrap once at adapter boundary into domain error; log original with from exc.
Should libraries log errors?
Libraries log context at WARNING/ERROR; application decides handlers and final user message.
What about logging in asyncio?
Use async-friendly handlers or queue to thread; avoid blocking IO on event loop thread.
How do I migrate from print debugging?
Replace with log.debug and run locally at DEBUG; remove before merge or gate behind flag.
Related
- Exceptions Basics - try/except primer
- Structured & Contextual Logging - JSON detail
- Retries & Backoff - retry policies
- Warnings & Deprecations - soft removals
Stack versions: This page was written for Python 3.14.0 (stable 3.14, maintenance 3.13), FastAPI 0.115+, Django 5.2, Flask 3.1, Pydantic 2, PyTorch 2.6+, pandas 2.2+, Polars 1.x, ruff 0.9+, and uv 0.6+.