Errors and Exceptions
The Try-Except Construct
Exception handling in Python allows programs to handle errors gracefully, ensuring that unexpected situations do not crash the program.
Understanding Try-Except Blocks
The try-except construct lets you test a block of code for errors and handle them accordingly.
Syntax
try:
risky_operation()
except SpecificException:
handle_exception()
except AnotherException:
handle_another_exception()
else:
proceed_normally()
finally:
cleanup_operations()
Description
- Execution Flow:
tryBlock: Executes first.exceptBlocks: If an exception occurs, the first matchingexceptblock is executed.elseBlock: Executes if no exceptions were raised.finallyBlock: Executes regardless of what happened earlier.
Examples
Example 1: Basic try-except
try:
number = int(input("Enter a number: "))
reciprocal = 1 / number
except ValueError:
print("That's not a valid number!")
except ZeroDivisionError:
print("Cannot divide by zero!")
Example 2: Using else
try:
with open('data.txt', 'r') as file:
data = file.read()
except FileNotFoundError:
print("File not found.")
else:
print("File read successfully!")
Example 3: Using finally
try:
file = open('data.txt', 'r')
data = file.read()
except IOError:
print("Error reading file.")
finally:
file.close()
print("File closed.")
Example 4: Combining try, except, else, and finally
try:
file = open('data.txt', 'r')
data = file.read()
except FileNotFoundError:
print("File not found.")
except IOError:
print("Error reading file.")
else:
print("File read successfully!")
finally:
file.close()
print("File closed.")
Example 5: Raising an Exception
def set_age(age):
if age < 0:
raise ValueError("Age cannot be negative.")
print(f"Age set to {age}")
try:
set_age(-5)
except ValueError as e:
print(e)
Example 6: Custom Exception
class InsufficientFundsError(Exception):
pass
def withdraw(amount, balance):
if amount > balance:
raise InsufficientFundsError("Insufficient funds for withdrawal.")
return balance - amount
try:
new_balance = withdraw(150, 100)
except InsufficientFundsError as e:
print(e)
tryBlock: Encapsulates code that may raise an exception.exceptClause: Handles specific exceptions that occur in thetryblock.elseClause: Executes if no exceptions are raised in thetryblock.finallyClause: Executes code regardless of whether an exception was raised.raiseStatement: Manually triggers an exception, allowing for custom error signaling.
Example: Handling File I/O Errors
Consider a function that counts the frequency of each character in a file:
def character_frequency(filename):
"""Counts the frequency of each character in the given file."""
# First try to open the file
try:
f = open(filename)
except OSError:
return None
# Now process the file
characters = {}
for line in f:
for char in line:
characters[char] = characters.get(char, 0) + 1
f.close()
return characters
- Try Block: Attempts to open the file, which might raise an
OSError. - Except Block: Returns
Noneif anOSErroroccurs, indicating the file couldn't be opened. - Processing: If no exception, counts character frequency in the file.
When to Use Try-Except
- File Operations: Opening, reading, or writing files that may not exist or be inaccessible.
- Type Conversions: Converting data types that might fail (e.g., string to integer).
- External Resources: Interacting with network services or system commands that might fail.
Best Practices
- Specific Exceptions: Catch specific exceptions rather than a general
Exceptionto handle anticipated errors. - Graceful Handling: Provide meaningful fallback or error messages to aid debugging.
- Resource Management: Ensure resources like files are properly closed, even if an error occurs.
Raising Errors
Sometimes, it's necessary to raise exceptions intentionally to signal that an error condition has occurred.
The raise Statement
Use the raise keyword to trigger an exception when a condition isn't met.
Syntax
if error_condition:
raise ExceptionType("Error message")
Example: Validating Function Arguments
def validate_user(username, minlen):
if minlen < 1:
raise ValueError("minlen must be at least 1")
if len(username) < minlen:
return False
if not username.isalnum():
return False
return True
- Input Validation: Checks if
minlenis at least 1. - Raise Exception: Uses
raise ValueErrorto signal invalidminlen. - Function Logic: Continues with username validation if inputs are valid.
Using Assertions
The assert statement is used to test conditions that should always be true during execution.
Syntax
assert condition, "Error message"
Example: Type Checking with Assertions
def validate_user(username, minlen):
assert type(username) == str, "username must be a string"
if minlen < 1:
raise ValueError("minlen must be at least 1")
# Additional validation...
- Assertion: Ensures
usernameis a string. - Error Message: Provides a custom message if the assertion fails.
- Note: Assertions can be disabled globally, so they're best used for debugging rather than input validation.
When to Use Raise vs. Assert
raise: Use when handling expected error conditions that require action.assert: Use for debugging purposes to check for conditions that should never occur.
Testing for Expected Errors
Testing your code includes verifying that it properly handles error conditions.
Using unittest to Test Exceptions
The unittest module provides methods to test for exceptions.
Example: Testing Exception Raising
import unittest
from validations import validate_user
class TestValidateUser(unittest.TestCase):
def test_valid(self):
self.assertEqual(validate_user("validuser", 3), True)
def test_too_short(self):
self.assertEqual(validate_user("inv", 5), False)
def test_invalid_characters(self):
self.assertEqual(validate_user("invalid_user", 1), False)
def test_invalid_minlen(self):
self.assertRaises(ValueError, validate_user, "user", -1)
if __name__ == '__main__':
unittest.main()
assertRaises: Checks thatvalidate_userraises aValueErrorwhenminlenis invalid.- Test Cases: Include both valid and invalid inputs to thoroughly test the function.
Running the Tests
Execute the test script:
python validations_test.py
Expected Output:
....
----------------------------------------------------------------------
Ran 4 tests in 0.001s
OK
- Indicates all tests passed successfully.
Summary
- Exception Handling: Use
try-exceptblocks to catch and handle exceptions. - Raising Exceptions: Employ
raiseto signal errors when input conditions aren't met. - Assertions: Use
assertstatements to enforce conditions during development. - Testing Exceptions: Utilize
unittest'sassertRaisesto ensure functions raise expected exceptions.
By effectively handling exceptions and testing for error conditions, you create robust programs that can handle unexpected situations gracefully.
Code Examples
character_frequency. Py
#!/usr/bin/env python3
def character_frequency(filename):
"""Counts the frequency of each character in the given file."""
# First try to open the file
try:
f = open(filename)
except OSError:
return None
# Now process the file
characters = {}
for line in f:
for char in line:
characters[char] = characters.get(char, 0) + 1
f.close()
return characters
Validations. Py
#!/usr/bin/env python3
def validate_user(username, minlen):
assert type(username) == str, "username must be a string"
if minlen < 1:
raise ValueError("minlen must be at least 1")
if len(username) < minlen:
return False
if not username.isalnum():
return False
return True
validations_test. Py
#!/usr/bin/env python3
import unittest
from validations import validate_user
class TestValidateUser(unittest.TestCase):
def test_valid(self):
self.assertEqual(validate_user("validuser", 3), True)
def test_too_short(self):
self.assertEqual(validate_user("inv", 5), False)
def test_invalid_characters(self):
self.assertEqual(validate_user("invalid_user", 1), False)
def test_invalid_minlen(self):
self.assertRaises(ValueError, validate_user, "user", -1)
if __name__ == '__main__':
unittest.main()
Additional Notes
- Exception Types: Familiarize yourself with built-in exceptions like
TypeError,ValueError,OSError, etc. - Custom Exceptions: You can define custom exception classes by inheriting from
Exception. - Clean-Up Actions: Use
finallyblocks to execute code regardless of whether an exception occurred, useful for resource clean-up.