Skip to content

LY001: Exception exc_info

Enforce exc_info=True in exception handlers

Aka: "The rule that will save your career at 3 AM"

Problem

Log exceptions without exc_info=True and watch your future self suffer. You've just thrown away your debugging lifeline.

try:
    result = api.fetch_user(user_id)
except Exception:
    logger.error("Failed to fetch user")  # ❌ Congratulations, you broke debugging

When this fails in production at 2:47 AM, your log will scream:

ERROR - Failed to fetch user

And you'll have absolutely no idea: - What exception type actually blew up (ConnectionError? TypeError? A ghost in the machine?) - Where exactly in api.fetch_user() things went sideways - What the call stack looked like (your fortune cookie of debugging wisdom) - How deep this rabbit hole goes

Solution

Add exc_info=True and let the healing begin:

try:
    result = api.fetch_user(user_id)
except Exception:
    logger.error("Failed to fetch user", exc_info=True)  # ✅ Now you're being smart

Now your logs actually tell a story:

ERROR - Failed to fetch user
Traceback (most recent call last):
  File "app.py", line 42, in fetch_user
    response = requests.get(url)
  ...
ConnectionError: Connection refused

BOOM. You just went from "I have no idea what happened" to "I see exactly what went wrong."

Impact

Without stack traces (aka "the nightmare path"): - Incident resolution time: 2-4 hours - 3 engineers huddled around a desk, sweating - Someone has to reproduce the issue. In prod. On a Friday at 6 PM. - Career advancement: delayed

With stack traces (aka "the hero path"): - Incident resolution time: 15-30 minutes - One engineer, one terminal, one coffee - Root cause visible in the first glance at logs - You leave the office on time. Your family remembers your name.

Configuration

exception_exc_info:
  levels: [error]  # Check error logs only
  severity: fail   # Block CI if violations found

Options

levels: Which log levels to enforce

  • [error] - Only error logs (recommended)
  • [error, warning] - Error and warning logs
  • [any] - All log levels

severity: Violation handling

  • fail - Exit with error (recommended for CI)
  • warn - Print warning only
  • info - Informational

Examples

✅ Pass (The Responsible Developer)

# With exc_info=True - the safe bet
try:
    connect_db()
except Exception:
    logger.error("DB connection failed", exc_info=True)  # Gold standard

# With exception() - Python's built-in shortcut
try:
    connect_db()
except Exception:
    logger.exception("DB connection failed")  # Equivalent to exc_info=True

# Non-exception context - you don't need exc_info here, dummy
if not user:
    logger.error("User not found")  # Not in an exception handler, so it's fine

❌ Fail (The Regret Path)

# Missing exc_info in exception handler - future you will hate present you
try:
    process_payment()
except Exception:
    logger.error("Payment failed")  # 3 AM: "WHY NO STACK TRACE?!?!"

# Also fails with string formatting - you just moved the error into the message
try:
    load_config()
except Exception as e:
    logger.error(f"Config error: {e}")  # You removed the stack trace! For what?!

When to Use

Always, without exception (pun intended), use in production code: - Exception handlers that log errors (duh) - Background job failures (yes, those too) - API request failures (especially those) - Anywhere an exception happens and you log it

Optional in development: - Exploratory logging (you're debugging locally anyway) - Known/expected exceptions (use your judgment) - Rate-limited error handlers (be reasonable)

Alternative: logger.exception()

Python's logger.exception() is the lazy developer's blessing—it automatically includes exc_info=True:

try:
    risky_operation()
except Exception:
    logger.exception("Operation failed")  # She's included by default! No questions!

It's equivalent to:

logger.error("Operation failed", exc_info=True)

Use whichever makes you happy. Both are correct. Pick one and be consistent.

Real-World Example

From our battle-test analysis of real-world codebases (aka "code that broke in production"):

Celery codebase - Missing exc_info in async backend:

# celery/backends/asynchronous.py:130
try:
    self._shutdown.set()
except RuntimeError as e:
    logging.error(f"Failed to set shutdown event: {e}")  # ❌ Stack trace? Gone. With the wind.

When this breaks at 3:47 AM, you're left with just the exception message, no context. No way to understand where it came from. Just pure chaos.


← Back to Policies - Go read about LY002 while you're at it.