The Right Way to Print Stack Traces in Python
π¨οΈ The Right Way to Print Stack Traces in Pythonβ
In Python, displaying the stack trace (or traceback) is essential for debugging. It provides a historical record of all function calls leading up to the point where an exception occurred. However, simply using print() within an except block is insufficient and incorrect.
This article details the correct methods for capturing, formatting, and logging the stack trace, emphasizing the difference between developer debugging and production logging.
1. The Incorrect Way: Manual Tracebackβ
Beginner developers often try to catch a generic exception and manually print the error object, which completely loses the necessary historical context of the function calls.
β Incorrect Method: Losing Contextβ
def deep_call():
return 10 / 0 # Error occurs here
def middle_call():
deep_call()
try:
middle_call()
except Exception as e:
# This only prints the message, not the call history (traceback)
print(f"Error caught: {e}")
# Output: Error caught: division by zero
This output is useless for debugging, as it doesn't show where in the code the division by zero happened (deep_call on line 2).
2. The Built-in Tool: The traceback Moduleβ
The Python standard library provides the traceback module, which contains functions designed specifically to capture and format stack trace information. This is ideal for command-line tools or one-off debugging scripts.
A. Printing to Standard Error (traceback.print_exc())β
This is the most common and simplest method for displaying the full, traditional traceback format (which includes the error message, the exception type, and the file/line history). By convention, tracebacks should be sent to sys.stderr.
import traceback
import sys
def deep_call():
# Error occurs here
raise ValueError("Invalid configuration setting.")
try:
deep_call()
except ValueError:
# 1. Prints the full traceback to sys.stderr
print("--- TRACEBACK START ---", file=sys.stderr)
traceback.print_exc()
print("--- TRACEBACK END ---", file=sys.stderr)
Output (to stderr):
--- TRACEBACK START ---
Traceback (most recent call last):
File "script.py", line 7, in <module>
deep_call()
File "script.py", line 4, in deep_call
raise ValueError("Invalid configuration setting.")
ValueError: Invalid configuration setting.
--- TRACEBACK END ---
B. Capturing as a String (traceback.format_exc())β
If you need to capture the traceback into a string variable (e.g., to send it in an email, log it to a non-standard system, or add custom formatting), use traceback.format_exc().
import traceback
try:
# Error occurs
int("hello")
except ValueError:
# Capture the full traceback as a string
error_details = traceback.format_exc()
# You can now manipulate or log the string
custom_log_message = f"*** UNEXPECTED FAILURE ***\n{error_details}"
print(custom_log_message)
3. The Production Standard: The logging Moduleβ
In any serious application, the correct way to handle exceptions and capture tracebacks is through Python's built-in logging module. Logging is designed to handle this cleanly, especially when using structured logs (as covered in a previous article).
The logging module provides two main ways to automatically include traceback information:
A. Using the exc_info=True Flagβ
You can add the exc_info=True flag to any standard logging call (.error(), .warning(), .info()) inside an except block. This tells the logger to capture the current exception information and format the full traceback.
import logging
logging.basicConfig(level=logging.ERROR, format='%(asctime)s - %(levelname)s - %(message)s')
def process_file(file_path):
try:
with open(file_path, 'r') as f:
data = f.read()
except FileNotFoundError as e:
# Use exc_info=True to automatically append the traceback
logging.error("File processing failed.", exc_info=True)
process_file("non_existent_file.txt")
Output (in the log file/console):
2025-12-14 10:15:30,123 - ERROR - File processing failed.
Traceback (most recent call last):
File "script.py", line 9, in process_file
with open(file_path, 'r') as f:
FileNotFoundError: [Errno 2] No such file or directory: 'non_existent_file.txt'
B. The Dedicated logger.exception() Methodβ
This is the most recommended practice inside an except block. The logger.exception() method is syntactical sugar for calling logger.error(message, exc_info=True). It is cleaner and explicitly signals that an exception is being handled.
import logging
# Assume logging is configured...
def calculate():
# ... error here ...
result = 100 / 0
try:
calculate()
except Exception:
# BEST PRACTICE: Log the error and the full traceback in one clean call
logging.exception("Critical error during calculation.")
# Output is identical to the exc_info=True method, but the code is cleaner.
4. Advanced: Chained Exceptionsβ
When re-raising an exception using raise NewError from OriginalError, the traceback mechanism is designed to capture both stack traces, showing the full history of the error and its cause.
The traceback and logging modules handle this automatically, displaying all chained exception information for comprehensive debugging.
class CustomFailure(Exception): pass
def api_call():
raise TimeoutError("API timed out.")
def wrapper():
try:
api_call()
except TimeoutError as e:
# Re-raising with chaining
raise CustomFailure("Service unresponsive.") from e
try:
wrapper()
except CustomFailure:
# logging.exception() will show both the CustomFailure AND the original TimeoutError
logging.exception("Fatal high-level failure.")
