Errors and Exceptions
What are errors and exceptions?
Errors (also called exceptions) are Python's way of signaling that something went wrong during program execution. When Python encounters a problem it can't handle, it raises an exception, which stops normal execution and can crash your program if not handled.
>>> 10 / 0
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ZeroDivisionError: division by zero
>>> int("hello")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: invalid literal for int() with base 10: 'hello'
In Python, "error" and "exception" are often used interchangeably. Technically, exceptions are a type of error that can be caught and handled, while some errors (like syntax errors) happen before the program runs and can't be caught.
Why this matters
Understanding errors is crucial because:
- Debugging: Knowing what different error types mean helps you fix bugs faster
- Error handling: You can catch specific errors and handle them appropriately
- Code quality: Raising meaningful errors makes your code more maintainable
- User experience: Custom errors provide clearer feedback than generic ones
- API design: Well-designed error hierarchies make your code easier to use
Errors aren't just problems to avoid, they're a communication mechanism. When used well, they tell you (and other developers) exactly what went wrong and why.
Common built-in exception types
Python has many built-in exception types. Here are the most common ones you'll encounter:
TypeError
Raised when an operation is performed on an object of inappropriate type:
>>> "hello" + 5
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: can only concatenate str (not "int") to str
>>> len(42)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: object of type 'int' has no len()
ValueError
Raised when a function receives an argument of correct type but inappropriate value:
>>> int("abc")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: invalid literal for int() with base 10: 'abc'
>>> [1, 2, 3].index(5)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: 5 is not in list
IndexError
Raised when trying to access an index that doesn't exist:
>>> numbers = [1, 2, 3]
>>> numbers[5]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
IndexError: list index out of range
KeyError
Raised when trying to access a dictionary key that doesn't exist:
>>> data = {"name": "Alice", "age": 30}
>>> data["email"]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
KeyError: 'email'
AttributeError
Raised when trying to access an attribute or method that doesn't exist:
>>> text = "hello"
>>> text.append("world")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'str' object has no attribute 'append'
NameError
Raised when a variable name is not found:
>>> print(undefined_variable)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'undefined_variable' is not defined
ZeroDivisionError
Raised when trying to divide by zero:
>>> 10 / 0
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ZeroDivisionError: division by zero
FileNotFoundError
Raised when trying to open a file that doesn't exist:
>>> open("nonexistent.txt")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
FileNotFoundError: [Errno 2] No such file or directory: 'nonexistent.txt'
Raising exceptions
You can raise exceptions in your own code using the raise statement. This is useful for:
- Validating input
- Signaling error conditions
- Stopping execution when something goes wrong
Basic raise syntax
raise ExceptionType("Error message")
Raising built-in exceptions
def divide(a, b):
if b == 0:
raise ZeroDivisionError("Cannot divide by zero")
return a / b
divide(10, 0) # Raises ZeroDivisionError: Cannot divide by zero
Raising with a message
Always include a helpful error message:
def get_age():
age = input("Enter your age: ")
if not age.isdigit():
raise ValueError(f"'{age}' is not a valid age. Please enter a number.")
return int(age)
Exception propagation
When you raise an exception, it propagates up through the call stack until it's caught or the program crashes. This means exceptions raised in a function will bubble up to the caller:
def inner_function():
raise ValueError("Error in inner function")
def outer_function():
inner_function() # Exception propagates from here
print("This won't execute")
outer_function() # Exception propagates here and crashes the program
When you learn about exception handling, you'll see how to catch exceptions at different levels and optionally re-raise them to let them continue propagating.
Exception hierarchy
Python exceptions are organized in a hierarchy. All exceptions inherit from BaseException, but most user-defined exceptions should inherit from Exception:
BaseException
├── SystemExit
├── KeyboardInterrupt
├── GeneratorExit
└── Exception
├── StopIteration
├── StopAsyncIteration
├── ArithmeticError
│ ├── FloatingPointError
│ ├── OverflowError
│ └── ZeroDivisionError
├── AssertionError
├── AttributeError
├── BufferError
├── EOFError
├── ImportError
├── LookupError
│ ├── IndexError
│ └── KeyError
├── MemoryError
├── NameError
├── OSError
│ ├── FileNotFoundError
│ └── PermissionError
├── ReferenceError
├── RuntimeError
├── SyntaxError
├── SystemError
├── TypeError
└── ValueError
Understanding this hierarchy is important because:
- Related exceptions share a base class:
IndexErrorandKeyErrorboth inherit fromLookupError - You can create custom exceptions: Inherit from appropriate base classes to fit into the hierarchy
- Exception handling benefits: When you learn about catching exceptions, you can catch a base class to handle all related exceptions
For example, LookupError is the parent of both IndexError and KeyError, so catching LookupError would catch both types of errors. This is covered in detail in the exception handling guide.
Creating custom exceptions
You can create your own exception types by inheriting from Exception or one of its subclasses. This is useful for:
- Making errors more specific to your domain
- Providing clearer error messages
- Creating error hierarchies for your codebase
- Making it easier for users of your code to catch specific errors
Basic custom exception
A custom exception is a named error that inherits from Python’s built-in Exception class.
class InvalidEmailError(Exception):
pass
Most of the time, pass is enough. The exception already knows how to behave because it inherits from Exception.
You only add more code if you want the error to store extra information or change how it’s displayed. For example:
class InvalidEmailError(Exception):
def __init__(self, email):
self.email = email
super().__init__(f"Invalid email address: {email}")
Now the exception remembers which email caused the problem.
For beginners, using pass is the normal and recommended approach.
Custom exception with additional data
You can add attributes to store additional information:
class InsufficientFundsError(Exception):
"""Raised when account has insufficient funds."""
def __init__(self, balance, amount):
self.balance = balance
self.amount = amount
message = f"Insufficient funds. Balance: ${balance}, Required: ${amount}"
super().__init__(message)
def withdraw(account_balance, amount):
if amount > account_balance:
raise InsufficientFundsError(account_balance, amount)
return account_balance - amount
# When withdrawal exceeds balance, raises exception with additional data
withdraw(100, 200)
# Raises: InsufficientFundsError: Insufficient funds. Balance: $100, Required: $200
# The exception object contains the additional information
# When caught, you can access: e.balance and e.amount
The exception stores both the balance and amount as attributes, making it easy to access this information when the exception is caught and handled.
Exception hierarchies
Create related exceptions by inheriting from a base exception:
class DatabaseError(Exception):
"""Base exception for database-related errors."""
pass
class ConnectionError(DatabaseError):
"""Raised when database connection fails."""
pass
class QueryError(DatabaseError):
"""Raised when a database query fails."""
pass
# You can raise any of these exceptions
raise ConnectionError("Failed to connect to database")
# Raises: ConnectionError: Failed to connect to database
raise QueryError("Invalid SQL query")
# Raises: QueryError: Invalid SQL query
# Because ConnectionError and QueryError inherit from DatabaseError,
# they are both instances of DatabaseError
error = ConnectionError("Connection failed")
isinstance(error, DatabaseError) # True
This hierarchy allows you to catch all database-related errors by catching DatabaseError, or catch specific errors like ConnectionError individually. Exception handling is covered in detail in the next guide.
When to create custom exceptions
Create custom exceptions when:
- You need specific error handling: Different code paths for different error types
- Built-in exceptions don't fit: Your error doesn't match any standard exception
- You're building a library: Users need to catch specific errors from your code
- You want clearer error messages: Custom exceptions can provide domain-specific context
# Good: Custom exception for domain-specific error
class UserNotFoundError(Exception):
"""Raised when a user cannot be found."""
pass
def get_user(user_id):
user = database.find_user(user_id)
if not user:
raise UserNotFoundError(f"User with ID {user_id} not found")
return user
# When user doesn't exist, raises specific exception
get_user(999)
# Raises: UserNotFoundError: User with ID 999 not found
# When user exists, returns normally
user = get_user(123)
# Returns: user object
Custom exceptions make it clear what went wrong. When you learn about exception handling, you'll be able to catch UserNotFoundError specifically and provide appropriate feedback, or catch DatabaseError to handle database connection issues separately.
Best practices
- Use specific exception types
# Good: Specific exception
if age < 0:
raise ValueError("Age cannot be negative")
# Less ideal: Generic exception
if age < 0:
raise Exception("Age cannot be negative")
- Include helpful error messages
# Good: Descriptive message
raise ValueError(f"Expected positive number, got {value}")
# Less helpful: Generic message
raise ValueError("Invalid value")
- Don't raise generic Exception
# Avoid this
raise Exception("Something went wrong")
# Prefer specific exceptions
raise ValueError("Invalid input")
raise TypeError("Expected string, got int")
- Document your exceptions
class PaymentError(Exception):
"""Base exception for payment processing errors.
Attributes:
amount: The payment amount that caused the error
reason: Human-readable reason for the failure
"""
def __init__(self, amount, reason):
self.amount = amount
self.reason = reason
super().__init__(f"Payment of ${amount} failed: {reason}")
- Use exception chaining
When you catch one exception and raise another, you can use from to preserve the original exception. This creates a chain that shows both the original error and the new error:
# Example of exception chaining (you'll learn to catch exceptions in the next guide)
# When FileNotFoundError is caught, you can raise a new exception while preserving the original:
# raise ConfigurationError("Failed to load configuration") from original_exception
This preserves the original exception in the __cause__ attribute, making debugging easier by showing the full chain of errors. Exception chaining is covered in detail when you learn about exception handling.
Common patterns
Validation functions
def validate_positive_number(value):
"""Validate that value is a positive number."""
if not isinstance(value, (int, float)):
raise TypeError(f"Expected number, got {type(value).__name__}")
if value <= 0:
raise ValueError(f"Expected positive number, got {value}")
return value
Assertion-style checks
def process_order(order):
if not order.items:
raise ValueError("Order must contain at least one item")
if order.total < 0:
raise ValueError("Order total cannot be negative")
# Process order...
Resource availability
class ResourceUnavailableError(Exception):
"""Raised when a required resource is unavailable."""
pass
def connect_to_service():
if not is_service_available():
raise ResourceUnavailableError("Service is currently unavailable")
# Connect...
Summary
Understanding errors and exceptions is essential for writing robust Python code:
- Built-in exceptions: Python provides many exception types for common error conditions
- Raising exceptions: Use
raiseto signal errors in your code - Custom exceptions: Create domain-specific exceptions for clearer error handling
- Exception hierarchy: Understand how exceptions relate to catch related errors
- Best practices: Use specific exceptions, helpful messages, and proper documentation
Errors aren't failures, they communicate problems clearly to the user. Well designed error handling makes your code more maintainable, debuggable, and user friendly.