How to print an exception in Python
Discover multiple ways to print exceptions in Python. Explore tips, real-world applications, and how to debug common errors effectively.

To manage errors in Python, you must know how to print exceptions. This is a fundamental skill for any developer who wants to debug code and build robust applications.
In this article, you'll explore several techniques to display exception details. You'll find practical tips, real-world examples, and advice to help you select the right approach for your code.
Using try-except to print error messages
try:
1/0
except Exception as e:
print(f"An error occurred: {e}")--OUTPUT--An error occurred: division by zero
The try-except block is Python's standard for handling errors without crashing your program. The code inside the try block is your primary operation, while the except block defines the fallback plan if something goes wrong.
By using except Exception as e, you catch a wide range of errors and assign the exception object to the variable e. This object contains the error message itself. Printing e is a simple way to get a human-readable description of the problem, which is essential for quick debugging.
Basic exception handling techniques
While printing the exception provides a quick overview, you can dig deeper by converting it to a string, accessing its attributes, or printing a complete stack trace. For more comprehensive coverage of using try and except in Python, these techniques build upon the fundamental concepts.
Using str() to convert exceptions to strings
try:
undefined_variable
except NameError as e:
exception_string = str(e)
print(f"Exception as string: {exception_string}")--OUTPUT--Exception as string: name 'undefined_variable' is not defined
Explicitly converting an exception to a string using str() gives you greater flexibility. This function captures the error message, allowing you to store it in a variable like exception_string for later use. This is incredibly useful when you need to do more than just display the error on the screen.
- You can log the error message to a file for later analysis.
- You can send the details to a monitoring service.
While simply printing the exception object often works, calling str() directly gives you a string that you can manipulate or store as needed.
Accessing exception attributes
try:
x = [1, 2, 3]
print(x[10])
except IndexError as e:
print(f"Exception type: {type(e).__name__}")
print(f"Exception args: {e.args}")
print(f"Exception message: {e}")--OUTPUT--Exception type: IndexError
Exception args: ('list index out of range',)
Exception message: list index out of range
Exception objects are more than just simple messages; they're full-fledged objects with useful attributes. Accessing these attributes allows you to programmatically inspect and handle errors with greater precision. This is especially helpful when your code needs to react differently to various error types.
type(e).__name__: This gives you the exception's class name as a string, likeIndexError, so you know exactly what kind of error you're dealing with.e.args: This attribute contains a tuple of the arguments passed to the exception, which often includes the core error message.
Using traceback.print_exc() for stack traces
import traceback
try:
1/0
except ZeroDivisionError:
traceback.print_exc()--OUTPUT--Traceback (most recent call last):
File "<stdin>", line 2, in <module>
ZeroDivisionError: division by zero
When a simple error message isn't enough, the traceback module is your best friend. After importing it, you can call traceback.print_exc() inside an except block to print the full stack trace. This trace is a detailed report showing the sequence of function calls that led to the error, giving you a complete picture of what went wrong and where.
- It pinpoints the exact file, line number, and function that failed.
- It's invaluable for debugging complex applications where an error might originate deep within your call stack.
Advanced exception handling techniques
Beyond simple print statements, you can build more sophisticated error handling by creating custom formats, using the logging module, and linking exceptions with raise from.
Creating custom exception formats
try:
1/0
except Exception as e:
exception_info = {
'type': type(e).__name__,
'message': str(e),
'module': e.__class__.__module__
}
print(exception_info)--OUTPUT--{'type': 'ZeroDivisionError', 'message': 'division by zero', 'module': 'builtins'}
You can create custom, structured formats for your exceptions. This is perfect for when you need to log errors in a consistent way or send them to an external service. By building a dictionary like exception_info, you organize the error details into a machine-readable format, making them easier to parse and analyze automatically.
type(e).__name__captures the class of the error.str(e)provides the human-readable message.e.__class__.__module__tells you which module the exception originated from, such asbuiltinsfor standard errors.
Using the logging module for exceptions
import logging
logging.basicConfig(level=logging.ERROR)
try:
1/0
except Exception as e:
logging.error("An error occurred", exc_info=True)--OUTPUT--ERROR:root:An error occurred
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
ZeroDivisionError: division by zero
Using the logging module is the standard for handling errors in production applications. Unlike print(), it lets you create structured, level-based messages that can be routed to files or monitoring services, not just your console. For persistent error tracking, you'll want to learn about creating a log file in Python.
- The key is the
exc_info=Trueargument in functions likelogging.error(). - This single parameter tells the logger to automatically capture and include the full stack trace with your message, giving you complete diagnostic information in a clean, manageable way.
Working with exception chaining using raise from
try:
try:
1/0
except ZeroDivisionError as e:
raise ValueError("Invalid operation") from e
except ValueError as e:
print(f"Error: {e}")
print(f"Original error: {e.__cause__}")--OUTPUT--Error: Invalid operation
Original error: division by zero
Exception chaining with raise from allows you to wrap an original error in a new one without losing the context of what first went wrong. This is perfect for translating a low-level issue, like a ZeroDivisionError, into a more meaningful application-specific error, such as a ValueError.
- The
raise fromstatement explicitly links the two exceptions together. - You can then access the original error through the
__cause__attribute on the new exception, which is essential for effective debugging.
Move faster with Replit
Replit is an AI-powered development platform that comes with all Python dependencies pre-installed, so you can skip setup and start coding instantly. It’s designed to help you move from learning individual techniques, like the ones in this article, to building complete applications.
Instead of piecing together code, you can use Agent 4 to take an idea to a working product. Describe the app you want to build, and the Agent handles the code, databases, APIs, and deployment. For example, you could build:
- An error log parser that automatically ingests raw exception text and organizes it into a structured, searchable format.
- A data validation utility that checks inputs against a set of rules and raises clear, specific errors instead of generic ones.
- A simple API monitoring tool that pings a list of endpoints, catches timeout or connection errors, and logs the status.
Simply describe your app, and Replit will write the code, test it, and fix issues automatically, all within your browser.
Common errors and challenges
Handling exceptions effectively means avoiding common pitfalls that can hide bugs and complicate your code.
It’s tempting to use a broad except Exception: clause to catch every possible error, but this approach can cause more harm than good. It silences all errors indiscriminately, including ones you might not want to suppress, like a user pressing Ctrl+C to exit (KeyboardInterrupt).
- This practice can hide bugs by making your program continue running even when it's in an unstable state.
- You lose the ability to handle different errors in specific ways, making your code less robust.
Instead, you should always catch the most specific exceptions possible, such as ValueError or TypeError. This ensures you're only handling the errors you expect and letting unexpected ones surface for debugging.
Another common issue is forgetting to clean up resources like open files or network connections when an exception occurs. If your code errors out before reaching the cleanup step, those resources can remain open, leading to memory leaks or other problems.
The best way to prevent this is by using a context manager with the with statement. This structure automatically guarantees that cleanup code runs, even if an error is raised inside the block.
- It simplifies your code by removing the need for a verbose
try...finallyblock. - You can be confident that resources are always released properly, making your application more reliable.
When you have multiple except blocks, their order is critical. Python checks them sequentially, and the first one that matches the raised exception is executed. This can lead to unexpected behavior if you're not careful.
For example, if you place except Exception: before except ValueError:, the more specific ValueError block will never be reached. Since ValueError is a type of Exception, the broader clause will always catch it first.
- The rule is simple: always list your exception handlers from most specific to most general.
- This ensures that you can apply specialized logic for specific errors while still having a general fallback for everything else.
Avoiding overly broad except clauses
Using a bare except: clause is a risky shortcut. It catches every error indiscriminately, which can make your program fail silently. This hides the real problem, turning a straightforward bug into a frustrating mystery you have to solve later. The code below shows this in action—it returns None for two completely different errors, so you can't tell what actually went wrong.
def process_data(data):
try:
result = data['key'] / 0
return result
except:
return None
print(process_data({'key': 10}))
Here, the division by zero triggers the bare except: clause, which returns None and hides the ZeroDivisionError. This makes it impossible to know what went wrong. The following code shows a more robust way to handle this.
def process_data(data):
try:
result = data['key'] / 0
return result
except ZeroDivisionError as e:
print(f"Division error: {e}")
return None
except KeyError as e:
print(f"Missing key: {e}")
return None
print(process_data({'key': 10}))
This improved version replaces the broad except: clause with specific handlers, letting you provide tailored feedback for each issue instead of returning a generic None. By catching specific exceptions, you can pinpoint the exact cause of a failure, which makes debugging and code repair much faster.
- It handles
ZeroDivisionErrorandKeyErrorseparately. - This approach is crucial whenever a function can fail in multiple ways, as you can address each case appropriately.
Properly closing resources with context managers
It’s easy to forget to close resources like files, especially when an error occurs. If an exception is raised before your cleanup code runs, you can end up with resource leaks. The following example shows a function where a file isn't closed properly.
def read_file(filename):
file = open(filename, 'r')
try:
return file.read()
except FileNotFoundError:
return "File not found"
# File never gets closed if an exception occurs!
print(read_file("non_existent_file.txt"))
The try block only catches a FileNotFoundError. If a different error occurs while reading the file, the function will exit without ever closing it, causing a resource leak. The next example shows how to prevent this.
def read_file(filename):
try:
with open(filename, 'r') as file:
return file.read()
except FileNotFoundError:
return "File not found"
print(read_file("non_existent_file.txt"))
This improved version uses a with statement, which acts as a context manager. It automatically closes the file as soon as the code inside the block is finished, even if an error occurs. This simple change prevents resource leaks and makes your code far more reliable. You should always use the with statement when handling resources like files or network connections, as it guarantees they are properly cleaned up without needing a separate finally block.
Catching exceptions in the correct order
The order of your except blocks is crucial. If a general handler like except Exception comes before a specific one, it will catch everything, and your specialized logic will never run. The following code demonstrates how this common mistake can hide errors.
try:
num = int(input("Enter a number: "))
result = 100 / num
print(result)
except Exception as e:
print(f"An error occurred: {e}")
except ValueError:
print("Please enter a valid number")
except ZeroDivisionError:
print("Cannot divide by zero")
The broad except Exception handler runs first, catching every error before the more specific ValueError and ZeroDivisionError blocks are reached. This results in a generic message that hides the true problem. See how this logic is handled in the following example.
try:
num = int(input("Enter a number: "))
result = 100 / num
print(result)
except ValueError:
print("Please enter a valid number")
except ZeroDivisionError:
print("Cannot divide by zero")
except Exception as e:
print(f"An unexpected error occurred: {e}")
This improved version correctly orders the except blocks from most specific to most general. Python checks each handler sequentially, so placing ValueError and ZeroDivisionError first ensures those specific errors are caught and handled with tailored messages. The final except Exception as e clause then acts as a safety net for any other unexpected issues.
- This structure prevents a general exception from masking a more specific one.
- Always check this order when you have multiple failure modes.
Real-world applications
With an understanding of common pitfalls, you can now apply these techniques to real-world scenarios like file handling and form validation.
Handling missing files with graceful try-except fallbacks
When your application needs to read a file that might not exist, a try-except block lets you gracefully handle a FileNotFoundError by providing a default fallback. Alternatively, you might want to prevent the error entirely by checking if a file exists in Python before attempting to read it.
def read_user_preferences(filename):
try:
with open(filename, 'r') as file:
return file.read()
except FileNotFoundError:
print(f"Preferences file not found, using defaults")
return "default_theme=dark\ndefault_font=Arial"
prefs = read_user_preferences("user_prefs.txt")
print(f"Loaded preferences: {prefs.split()[0]}")
The read_user_preferences function demonstrates how to build resilient file-reading logic by anticipating that a configuration file might be missing.
- The core operation is wrapped in a
tryblock, which attempts to open and read the file using awithstatement for safety. - If a
FileNotFoundErroroccurs, theexceptblock is triggered. Instead of crashing the program, it returns a default set of preferences.
This approach ensures your application can continue running smoothly even when expected files aren't present, creating a more stable user experience.
Form validation with nested try-except blocks
You can use nested try-except blocks to create multi-layered validation logic, where an inner block handles a specific failure like a type conversion, and an outer block manages the overall form integrity.
def validate_user_input(form_data):
try:
if len(form_data['username']) < 3:
raise ValueError("Username must be at least 3 characters")
try:
age = int(form_data['age'])
except ValueError:
raise ValueError("Age must be a number")
if age < 18:
raise ValueError("User must be 18 or older")
return "Validation successful"
except KeyError as e:
return f"Validation failed: Missing {e}"
except ValueError as e:
return f"Validation failed: {e}"
print(validate_user_input({'username': 'jo', 'age': '16'}))
print(validate_user_input({'username': 'john', 'age': 'twenty'}))
The validate_user_input function shows how to manage multiple validation rules in a clean way. It uses raise ValueError to signal different logical errors, like an invalid username or age, with a specific message for each.
- The outer
except ValueError as eblock catches these custom-raised errors. - This centralizes all validation failures, allowing the function to return a clear, user-friendly message that pinpoints the exact problem.
This approach makes your input validation logic both robust and easy to debug.
Get started with Replit
Now, turn these error handling skills into a real tool. Tell Replit Agent to “build a file parser that logs ValueError exceptions” or “create an API client that handles connection errors gracefully.”
Replit Agent writes the code, tests for errors, and deploys your app directly from your browser. Start building with Replit.
Describe what you want to build, and Replit Agent writes the code, handles the infrastructure, and ships it live. Go from idea to real product, all in your browser.
Describe what you want to build, and Replit Agent writes the code, handles the infrastructure, and ships it live. Go from idea to real product, all in your browser.



