Python
Logging
Exception Handling
Traceback
Debugging

Log exception with traceback in Python

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

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

Related reading
Course
Beginner
27 lessons
10 hours
System Design Fundamentals

Build a strong foundation in designing scalable, reliable distributed systems.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

All Rights Reserved.