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
andexcept*
(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()