4. Error Handling & Logging

Fail loudly with context, log meaningfully, and avoid hiding bugs with broad exceptions.

Question: What are some best practices for exception handling in Python?

Answer: Best practices include being specific in except blocks, using raise ... from e to chain exceptions while preserving the original traceback, and creating custom exception types for your application domain.

Explanation: Catching broad exceptions like except Exception: can hide bugs. It's better to catch specific errors you expect and can handle. When you catch an exception and raise a new one, raise DomainError(...) from e links the new exception to the original cause, which is invaluable for debugging.

class DomainError(Exception):
    pass

try:
    risky_operation()
except SpecificError as e:
    raise DomainError("Failed to do X due to a specific issue") from e

Question: Why is logging important in production, and how should it be configured?

Answer: Logging provides visibility into the application's behavior in production, which is essential for debugging, monitoring, and auditing. It should be configured to be structured, leveled, and directed to the appropriate output.

Explanation: Instead of using print(), use the standard logging module.

  • Levels: Use levels like DEBUG, INFO, WARNING, ERROR to control verbosity.

  • Structured Logging: Format logs as JSON. This makes them machine-readable and easy to parse in log aggregation systems (like ELK stack or Datadog).

  • Correlation IDs: Include a unique ID for each request to trace its path through a distributed system.

import logging, sys
from pythonjsonlogger import jsonlogger

handler = logging.StreamHandler(sys.stdout)
handler.setFormatter(jsonlogger.JsonFormatter('%(asctime)s %(levelname)s %(name)s %(message)s %(request_id)s %(version)s'))
log = logging.getLogger("app")
log.setLevel(logging.INFO)
log.addHandler(handler)

log.info("service started", extra={"version": "1.0.0", "request_id": "abc-123"})

Question: What is ExceptionGroup and except* (Python 3.11+), and when is it useful?

Answer: ExceptionGroup aggregates multiple exceptions; except* handles them by type concurrently. Useful in async/parallel contexts where multiple failures occur.

Explanation: React to subsets while preserving all tracebacks instead of losing information.

try:
    raise ExceptionGroup("multi", [ValueError("x"), RuntimeError("y")])
except* ValueError as eg:
    handle_values(eg)
except* RuntimeError as eg:
    handle_runs(eg)

Question: How do you emit and manage deprecation warnings?

Answer: Use the warnings module and filter globally or per-test.

import warnings

warnings.warn("old_api will be removed", DeprecationWarning, stacklevel=2)
warnings.filterwarnings("ignore", category=DeprecationWarning)

Question: How do you configure logging cleanly for production?

Answer: Use logging.config.dictConfig to declare handlers/formatters/levels.

import logging.config

logging.config.dictConfig({
  "version": 1,
  "formatters": {"json": {"()": "pythonjsonlogger.jsonlogger.JsonFormatter"}},
  "handlers": {"stdout": {"class": "logging.StreamHandler", "formatter": "json"}},
  "root": {"level": "INFO", "handlers": ["stdout"]},
})

Question: How do you carry correlation IDs across async boundaries?

Answer: Use contextvars.ContextVar to store per-request data (like request_id) and include it in logs.

Explanation: contextvars propagate with task switches and executors (via copy_context), unlike thread-locals.

import contextvars
request_id_var = contextvars.ContextVar("request_id", default="-")
# Middleware sets: request_id_var.set(incoming_id)
# Log formatter reads: request_id_var.get()