Intermediate
Your production script catches an exception in a broad except block, logs the message, and moves on. Three weeks later, a customer reports a silent failure and all you have in your log file is "Error: 'NoneType' object is not subscriptable" — no file name, no line number, no call stack. Diagnosing that takes hours instead of minutes. The fix is giving Python’s traceback module ten minutes of your attention now.
The traceback module exposes the same machinery that Python uses when it prints unhandled exceptions to the terminal. You can capture that output as a string, route it to a log file, send it to a monitoring service, strip it to just the last line, or walk its structure line by line. It is in the standard library and requires no installation.
In this article you will learn traceback.format_exc() for capturing tracebacks as strings, print_exc() for immediate output, TracebackException for structured inspection, chained exceptions, and a real-world error reporting pipeline that writes structured JSON error logs.
Python traceback Module: Quick Example
Here is the minimum viable pattern: catch an exception, capture its full traceback as a string, and log it instead of losing it.
# traceback_quick.py
import traceback
import logging
logging.basicConfig(level=logging.ERROR, format='%(asctime)s %(levelname)s %(message)s')
def divide(a, b):
return a / b
def process(values):
results = []
for pair in values:
try:
results.append(divide(pair[0], pair[1]))
except Exception:
tb_str = traceback.format_exc()
logging.error("Failed to process %s:\n%s", pair, tb_str)
results.append(None)
return results
pairs = [(10, 2), (5, 0), (8, 4)]
print(process(pairs))
2026-04-26 10:00:00,000 ERROR Failed to process (5, 0):
Traceback (most recent call last):
File "traceback_quick.py", line 11, in process
results.append(divide(pair[0], pair[1]))
File "traceback_quick.py", line 5, in divide
return a / b
ZeroDivisionError: division by zero
[5.0, None, 2.0]
traceback.format_exc() captures the current exception’s full traceback as a string — file names, line numbers, and the full call stack — exactly as Python would print it unhandled. Call it inside an except block while the exception is still active. Outside an except block it returns 'NoneType: None\n', which is useless.
What Is the traceback Module?
When Python raises an unhandled exception, it calls internal machinery to format and print the traceback you see in the terminal. The traceback module exposes that machinery as a public API so your code can do the same thing programmatically.
| Function | Output | Best for |
|---|---|---|
format_exc() | String | Logging, monitoring, string manipulation |
print_exc() | Prints to stderr | Quick debug prints in scripts |
format_exception(exc) | List of strings | Building structured output line by line |
print_exception(exc) | Prints to stderr | Printing a stored exception later |
extract_tb(tb) | StackSummary | Walking frames individually |
TracebackException | Object | Structured inspection, JSON logging |
The most important distinction is between “format” functions (return strings/lists you can do things with) and “print” functions (write directly to stderr). In production code, almost always use the format versions so you can route output to your logging infrastructure.

format_exc and print_exc
format_exc() is the single most useful function in the module. It takes no required arguments and returns the full traceback of the current exception as a single string, ready to log or store.
# traceback_format.py
import traceback
import sys
def level3():
raise ValueError("Something went wrong deep in level3")
def level2():
level3()
def level1():
level2()
# --- format_exc: capture as string ---
try:
level1()
except ValueError:
tb_string = traceback.format_exc()
print("=== Captured traceback ===")
print(tb_string)
print("=== Last line only ===")
print(tb_string.strip().split('\n')[-1])
# --- print_exc: print directly to stderr ---
try:
level1()
except ValueError:
print("\n=== print_exc output (goes to stderr) ===", file=sys.stderr)
traceback.print_exc()
# --- format_exc with limit: show only top 2 frames ---
try:
level1()
except ValueError:
limited = traceback.format_exc(limit=2)
print("\n=== Limited to 2 frames ===")
print(limited)
=== Captured traceback ===
Traceback (most recent call last):
File "traceback_format.py", line 13, in <module>
level1()
File "traceback_format.py", line 11, in level1
level2()
File "traceback_format.py", line 8, in level2
level3()
File "traceback_format.py", line 5, in level3
raise ValueError("Something went wrong deep in level3")
ValueError: Something went wrong deep in level3
=== Last line only ===
ValueError: Something went wrong deep in level3
Extracting the last line (tb_string.strip().split('\n')[-1]) gives you the exception type and message — ideal for a brief alert subject line. The limit parameter controls how many frames to show (positive = from the top, negative = from the bottom), which is useful for very deep call stacks where only the innermost frames matter.
TracebackException for Structured Inspection
TracebackException is the object-oriented API for exceptions. It captures all the information about an exception in a structured form that you can inspect field by field — perfect for building JSON error logs or custom error display formats.
# traceback_te.py
import traceback
import json
def risky_parse(text):
return int(text) # will fail if text is not a number
try:
risky_parse("hello")
except ValueError as exc:
te = traceback.TracebackException.from_exception(exc)
# Structured fields
print("Type :", te.exc_type.__name__)
print("Message:", str(te))
print()
# Walk the stack frames
print("Stack frames:")
for frame in te.stack:
print(f" {frame.filename}:{frame.lineno} in {frame.name}")
print(f" {frame.line}")
# Format as a list of strings (same as format_exc but as a list)
lines = list(te.format())
print()
print(f"Total lines in traceback: {len(lines)}")
# Build a JSON-serializable error record
error_record = {
'type': te.exc_type.__name__,
'message': str(te),
'frames': [
{'file': f.filename, 'line': f.lineno, 'function': f.name, 'code': f.line}
for f in te.stack
]
}
print()
print(json.dumps(error_record, indent=2))
Type : ValueError
Message: invalid literal for int() with base 10: 'hello'
Stack frames:
traceback_te.py:12 in <module>
risky_parse("hello")
traceback_te.py:5 in risky_parse
return int(text)
Total lines in traceback: 7
{
"type": "ValueError",
"message": "invalid literal for int() with base 10: 'hello'",
"frames": [
{"file": "traceback_te.py", "line": 12, "function": "<module>", "code": "risky_parse(\"hello\")"},
{"file": "traceback_te.py", "line": 5, "function": "risky_parse", "code": "return int(text)"}
]
}
The JSON-serializable error record is the foundation of structured error logging. Send it to your monitoring service (Sentry, Datadog, Elasticsearch) and you can filter by exception type, group by file, alert on specific error messages, or build dashboards showing which functions fail most often.

Chained Exceptions and __cause__ vs __context__
When one exception is raised while handling another, Python chains them. The traceback module understands and displays these chains, and TracebackException exposes them as __cause__ (explicit: raise X from Y) and __context__ (implicit: exception raised inside an except block).
# traceback_chained.py
import traceback
def fetch_config(path):
raise FileNotFoundError(f"Config file not found: {path}")
def start_app(config_path):
try:
fetch_config(config_path)
except FileNotFoundError as e:
raise RuntimeError("Application startup failed") from e
try:
start_app('/etc/app/config.yaml')
except RuntimeError:
# format_exc shows the full chain automatically
print(traceback.format_exc())
# Inspect the chain with TracebackException
try:
start_app('/etc/app/config.yaml')
except RuntimeError as exc:
te = traceback.TracebackException.from_exception(exc, chain=True)
for part in te.format():
print(part, end='')
Traceback (most recent call last):
File "traceback_chained.py", line 9, in start_app
fetch_config(config_path)
File "traceback_chained.py", line 4, in fetch_config
raise FileNotFoundError(f"Config file not found: {path}")
FileNotFoundError: Config file not found: /etc/app/config.yaml
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "traceback_chained.py", line 13, in <module>
start_app('/etc/app/config.yaml')
File "traceback_chained.py", line 11, in start_app
raise RuntimeError("Application startup failed") from e
RuntimeError: Application startup failed
The raise X from Y pattern creates an explicit cause chain and is the recommended way to wrap low-level exceptions in higher-level ones without losing the original cause. Always use it when you catch a library exception and re-raise as a domain exception — it preserves the root cause in logs and debuggers.
Real-Life Example: Structured Error Logging Pipeline
Here is a production-ready error logging helper that captures structured error data and writes it to a JSON log file, plus sends a one-line summary to stderr.
# traceback_logger.py
import traceback
import json
import sys
import datetime
from pathlib import Path
ERROR_LOG = Path('/tmp/app_errors.jsonl')
def log_error(context: str, exc: Exception) -> None:
"""Capture structured error info and write to JSONL log."""
te = traceback.TracebackException.from_exception(exc, chain=True)
record = {
'timestamp': datetime.datetime.utcnow().isoformat(),
'context': context,
'type': te.exc_type.__name__,
'message': str(te),
'frames': [
{
'file': frame.filename,
'line': frame.lineno,
'function': frame.name,
'code': (frame.line or '').strip()
}
for frame in te.stack
],
'traceback': ''.join(te.format())
}
# Write to JSONL (one JSON object per line)
with open(ERROR_LOG, 'a') as f:
f.write(json.dumps(record) + '\n')
# One-line summary to stderr
print(f"[ERROR] {record['timestamp']} {context}: "
f"{record['type']}: {record['message']}", file=sys.stderr)
# --- Simulate application code ---
def load_user(user_id: int) -> dict:
users = {1: {'name': 'Alice'}, 2: {'name': 'Bob'}}
if user_id not in users:
raise KeyError(f"User {user_id} not found")
return users[user_id]
def get_user_email(user_id: int) -> str:
user = load_user(user_id)
return user['email'] # KeyError: no 'email' key
for uid in [1, 99, 2]:
try:
email = get_user_email(uid)
print(f"User {uid} email: {email}")
except Exception as e:
log_error(f"get_user_email(uid={uid})", e)
# Read back and summarize the log
print(f"\n=== Error log: {ERROR_LOG} ===")
with open(ERROR_LOG) as f:
for line in f:
rec = json.loads(line)
print(f" [{rec['type']}] {rec['context']} -- {rec['message']}")
[ERROR] 2026-04-26T10:00:00 get_user_email(uid=1): KeyError: 'email'
[ERROR] 2026-04-26T10:00:00 get_user_email(uid=99): KeyError: 'User 99 not found'
=== Error log: /tmp/app_errors.jsonl ===
[KeyError] get_user_email(uid=1) -- 'email'
[KeyError] get_user_email(uid=99) -- 'User 99 not found'
The JSONL format (one JSON object per line) is directly ingestible by tools like Elasticsearch, Splunk, and AWS CloudWatch Logs. The context field tells you which operation failed; the frames array tells you exactly where; the full traceback string gives you everything a debugger would show. You can extend this pattern by adding request IDs, user IDs, or environment names to the record for correlation across distributed services.

Frequently Asked Questions
Why does format_exc() return ‘NoneType: None’ sometimes?
Because format_exc() reads the current exception from Python’s internal exception state, which is only set while you are inside an except block. If you call format_exc() after the except block exits, the exception state has been cleared. Always call format_exc() or create a TracebackException inside the except block, before any other code that might clear the state.
Should I use traceback directly or let the logging module handle it?
For simple cases, use logging.exception('message') inside an except block — it automatically includes the traceback in the log output. Use the traceback module directly when you need the traceback as a string for non-logging purposes (storing in a database, sending via HTTP, or building structured error objects), or when you need to customize the format beyond what the logging module provides.
When do I need TracebackException vs format_exc?
Use format_exc() when you just need the traceback as a string for logging. Use TracebackException when you need to inspect the structure — access individual frames, get the exception type as an object rather than a string, walk chained exceptions, or build a custom formatted output. TracebackException is also better when you need to capture the exception and format it later, outside the except block.
How do I show only the most relevant frames?
Pass a limit argument: positive values take from the top of the stack (outermost frames), negative values take from the bottom (innermost, most recent frames). For application errors where the inner frames are most relevant, use limit=-3 or similar. For library wrappers where you want to show only the user’s code and hide internal library frames, you can filter te.stack by filename prefix before formatting.
Is it safe to log the traceback and then re-raise?
Yes — format_exc() captures the traceback data as a string without consuming or modifying the exception. You can log it and then use a bare raise to re-raise the original exception with its original traceback intact. Never use raise exc (with the exception variable) if you want to preserve the original traceback — that replaces the traceback with the current location. Use bare raise inside the except block.
Conclusion
The traceback module turns opaque exception messages into actionable, structured error reports. You learned how format_exc() captures tracebacks as strings inside except blocks, how TracebackException gives you structured access to every frame and chained exception, and how to build a JSON error logging pipeline that production systems can ingest directly.
The next step is to integrate the log_error function from the real-world example into one of your own scripts. Replace all bare except: pass and except Exception as e: print(e) patterns with structured logging — your future self at 2 a.m. will be grateful. Then explore sys.excepthook to install a global handler that catches unhandled exceptions and logs them the same way.
Official documentation: https://docs.python.org/3/library/traceback.html.