Tutorial: Errors and Exceptions
TL;DR
Python separates syntax errors (caught before running) from exceptions (caught during execution). The try/except/finally system lets you catch errors, recover gracefully, and guarantee cleanup code runs.
Interesting!
Python automatically chains exceptions raised during exception handling, but the implicit chain just says “During handling of the above exception, another exception occurred” without explaining why. Using raise ... from exc creates an explicit chain with clear causation, while raise ... from None suppresses chaining entirely to hide implementation details from API users.
Syntax Errors vs. Exceptions
Syntax errors occur during parsing, before your code runs. The parser identifies the problematic line and points to where it detected the issue:
python code snippet start
>>> while True print('Hello')
File "<stdin>", line 1
while True print('Hello')
^^^^^
SyntaxError: invalid syntaxpython code snippet end
Exceptions occur during execution. Common built-in exceptions include ZeroDivisionError, NameError, TypeError, and ValueError. Unlike syntax errors, exceptions can be caught and handled.
Handling Exceptions with Try/Except
The try statement allows you to catch and handle specific exceptions:
python code snippet start
try:
x = int(input("Enter a number: "))
result = 10 / x
except ValueError:
print("That's not a valid number")
except ZeroDivisionError:
print("Cannot divide by zero")python code snippet end
You can catch multiple exception types in one clause:
python code snippet start
except (RuntimeError, TypeError, NameError):
print("One of several errors occurred")python code snippet end
The Else Clause
An else clause after all except clauses runs only if no exception was raised:
python code snippet start
try:
f = open('file.txt', 'r')
except FileNotFoundError:
print("File not found")
else:
print(f.read())
f.close()python code snippet end
This prevents accidentally catching exceptions from code you didn’t intend to protect.
The Finally Clause
The finally clause executes regardless of what happens in the try block:
python code snippet start
try:
result = risky_operation()
except Exception as e:
print(f"Error: {e}")
finally:
cleanup_resources() # Always runspython code snippet end
This guarantees cleanup code runs even if you return early or an unhandled exception propagates.
Raising Exceptions
You can raise exceptions explicitly with the raise statement:
python code snippet start
if age < 0:
raise ValueError("Age cannot be negative")
# Re-raise the current exception
try:
dangerous_operation()
except Exception:
log_error()
raise # Re-raises the caught exceptionpython code snippet end
Exception Chaining
When an exception occurs while handling another exception, Python automatically chains them. The implicit chain shows both exceptions but just says “During handling of the above exception, another exception occurred” without explaining the relationship.
Use raise ... from to create an explicit chain that clarifies the cause:
python code snippet start
try:
open_database()
except ConnectionError as e:
raise RuntimeError("Failed to initialize") from epython code snippet end
The traceback now says “The above exception was the direct cause of the following exception”, making the relationship clear. The original exception is preserved as the __cause__ attribute.
For library code where implementation details should stay hidden, use raise ... from None to suppress chaining:
python code snippet start
try:
internal_operation()
except InternalError:
raise APIError("Operation failed") from Nonepython code snippet end
This prevents exposing internal implementation exceptions to API users.
User-Defined Exceptions
Built-in exceptions cover common cases, but domain-specific errors make your code more readable. A ValidationError is clearer than a generic ValueError when reading application code:
python code snippet start
class InvalidFruitError(Exception):
"""Raised when fruit is not in the approved list"""
pass
def validate_fruit(fruit):
valid_fruits = ['apple', 'banana', 'orange', 'grape']
if fruit.lower() not in valid_fruits:
raise InvalidFruitError(f"Unknown fruit: {fruit}")python code snippet end
By convention, exception names end with “Error”.
Best Practices
Catch specific exceptions, not everything. A bare except: clause will silence KeyboardInterrupt and SystemExit, making your program hard to stop and masking bugs you didn’t anticipate. Specify exactly what you expect to go wrong:
python code snippet start
# Bad: catches everything, including bugs
try:
process_data()
except:
pass
# Good: catches only expected failures
try:
process_data()
except (ValueError, KeyError):
handle_invalid_data()python code snippet end
Use context managers for cleanup. Files, database connections, and locks need to be released whether your code succeeds or fails. The with statement guarantees cleanup without verbose try/finally blocks:
python code snippet start
with open('file.txt') as f:
data = f.read()
# File automatically closed, even if an error occurspython code snippet end
Preserve error context when wrapping exceptions. When catching an exception to raise a more meaningful one, use raise ... from to keep the original traceback. Without it, you lose the root cause and make debugging harder.
Python’s exception handling provides the tools to write robust programs that fail gracefully and provide useful error information when things go wrong. For more on preserving error context, see PEP 3134: Exception Chaining .
Reference: Python Tutorial - Errors and Exceptions