Python
Logging
Exception Handling
Traceback
Debugging

Log exception with traceback in Python

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

When an exception occurs in a Python application, the traceback — the chain of function calls leading to the error — is essential for debugging. Simply catching and printing the exception message loses this context. Python's logging module provides logger.exception() and exc_info=True to capture the full traceback in log output, making it possible to diagnose errors from log files without a debugger attached.

Method 1: logging.exception()

The simplest and most common approach — call logger.exception() inside an except block:

python
1import logging
2
3logger = logging.getLogger(__name__)
4logging.basicConfig(level=logging.DEBUG)
5
6def divide(a, b):
7    return a / b
8
9try:
10    result = divide(10, 0)
11except Exception:
12    logger.exception("Division failed")
13
14# Output:
15# ERROR:__main__:Division failed
16# Traceback (most recent call last):
17#   File "example.py", line 10, in <module>
18#     result = divide(10, 0)
19#   File "example.py", line 7, in divide
20#     return a / b
21# ZeroDivisionError: division by zero

logger.exception() automatically logs at ERROR level and includes the full traceback. It must be called inside an except block where the exception is active.

Method 2: exc_info=True

Use exc_info=True with any log level — not just ERROR:

python
1try:
2    result = divide(10, 0)
3except Exception:
4    logger.warning("Division failed, using fallback", exc_info=True)
5    result = 0
6
7# Logs the warning message WITH the full traceback
8# WARNING:__main__:Division failed, using fallback
9# Traceback (most recent call last):
10#   ...
11# ZeroDivisionError: division by zero

This is useful when you handle the error gracefully but still want the traceback logged at a non-ERROR level.

Method 3: traceback Module

For more control over traceback formatting, use the traceback module directly:

python
1import traceback
2
3try:
4    result = divide(10, 0)
5except Exception:
6    # Get traceback as a string
7    tb_str = traceback.format_exc()
8    logger.error(f"Division failed:\n{tb_str}")
9
10    # Or print to stderr without logging
11    traceback.print_exc()

Extract Structured Traceback Info

python
1import traceback
2import sys
3
4try:
5    result = divide(10, 0)
6except Exception:
7    exc_type, exc_value, exc_tb = sys.exc_info()
8
9    # List of FrameSummary objects
10    frames = traceback.extract_tb(exc_tb)
11    for frame in frames:
12        print(f"  File {frame.filename}, line {frame.lineno}, in {frame.name}")
13        print(f"    {frame.line}")
14
15    # Just the last frame (where the error occurred)
16    last_frame = frames[-1]
17    print(f"Error at {last_frame.filename}:{last_frame.lineno}")

Configuring Log Format for Exceptions

python
1import logging
2
3# Include timestamp, level, and module in log output
4logging.basicConfig(
5    level=logging.DEBUG,
6    format='%(asctime)s %(levelname)s %(name)s %(message)s',
7    datefmt='%Y-%m-%d %H:%M:%S'
8)
9
10logger = logging.getLogger('myapp')
11
12# Log to file
13file_handler = logging.FileHandler('app.log')
14file_handler.setLevel(logging.ERROR)
15file_handler.setFormatter(logging.Formatter(
16    '%(asctime)s %(levelname)s %(name)s\n%(message)s'
17))
18logger.addHandler(file_handler)
19
20try:
21    result = divide(10, 0)
22except Exception:
23    logger.exception("Division failed")
24    # Written to both console and app.log with full traceback

Logging Exceptions in Async Code

python
1import asyncio
2import logging
3
4logger = logging.getLogger(__name__)
5
6async def fetch_data(url):
7    raise ConnectionError(f"Failed to connect to {url}")
8
9async def main():
10    try:
11        await fetch_data("https://example.com")
12    except ConnectionError:
13        logger.exception("Connection failed")
14
15asyncio.run(main())
16# The traceback correctly shows the async call stack

Logging with Context (Extra Fields)

Add structured context to exception logs:

python
1def process_order(order_id, user_id):
2    try:
3        # ... processing logic ...
4        raise ValueError("Invalid quantity")
5    except Exception:
6        logger.exception(
7            "Order processing failed",
8            extra={'order_id': order_id, 'user_id': user_id}
9        )
10
11# With a custom formatter that includes extra fields:
12# 2026-03-01 10:30:00 ERROR Order processing failed [order_id=123, user_id=456]
13# Traceback (most recent call last):
14#   ...

Anti-Pattern: Swallowing Exceptions

python
1# BAD — loses the traceback entirely
2try:
3    result = divide(10, 0)
4except Exception as e:
5    logger.error(f"Error: {e}")
6    # Only logs: "Error: division by zero"
7    # No traceback, no file/line information
8
9# BAD — bare except with pass
10try:
11    result = divide(10, 0)
12except Exception:
13    pass  # Silent failure — impossible to debug
14
15# GOOD — always log with traceback
16try:
17    result = divide(10, 0)
18except Exception:
19    logger.exception("Division failed")
20    raise  # Re-raise if the caller should handle it

Common Pitfalls

  • Using logger.error(str(e)) instead of logger.exception(): This logs only the exception message, losing the traceback. Always use logger.exception() or exc_info=True to capture the full stack trace.
  • Calling logger.exception() outside an except block: It will log NoneType: None as the traceback because there is no active exception. Only call it inside an except clause.
  • Catching too broadly: except Exception catches everything including programming errors like TypeError and AttributeError. Catch specific exceptions when possible, and use except Exception only as a top-level safety net.
  • Logging and re-raising without care: If you logger.exception() and then raise, the same exception may be logged multiple times as it propagates up through multiple try/except blocks. Log at the point where you handle the error, not at every level.
  • Missing logger configuration: If logging.basicConfig() is never called and no handlers are configured, log messages are silently discarded (Python 3.2+ shows a warning). Always configure logging at application startup.

Summary

  • Use logger.exception("message") inside except blocks to log the full traceback at ERROR level
  • Use logger.error("message", exc_info=True) or any other level with exc_info=True for non-ERROR traceback logging
  • Use the traceback module (traceback.format_exc()) when you need the traceback as a string
  • Never log just str(e) — the traceback is the most valuable debugging information
  • Configure logging with file handlers and formatters for production applications

Course illustration
Course illustration

All Rights Reserved.