-
Notifications
You must be signed in to change notification settings - Fork 0
7.F Fail: Error Control
One of the most powerful tools you have as a Python developer is the ability to stop execution when something goes wrong โ intentionally.
Instead of letting bugs propagate silently, you can explicitly signal that a condition is invalid. This is done by raising an exception.
An exception is a runtime signal that something unexpected or invalid has occurred.
Python allows you to trigger this signal yourself using the raise keyword.
Think of it as:
โThis situation should not happen โ abort and report.โ
raise Exception("Error message")You specify:
- The type of exception
- The explanatory message
Suppose negative numbers are not allowed:
x = -1
if x < 0:
raise Exception("Sorry, no numbers below zero")What happens:
- Python evaluates the condition
x < 0 - The condition is
True - The program stops immediately
- The message is displayed
This pattern is called a guard clause โ a defensive programming technique.
Without exceptions:
- Errors may propagate silently
- Debugging becomes difficult
- Incorrect data contaminates later computations
With exceptions:
- Failures occur early
- Causes are easier to identify
- Code becomes safer and more predictable
This aligns with a key engineering principle:
Fail fast, fail loudly.
Python provides many built-in exception classes.
You should use the most appropriate one to communicate intent.
Example: enforcing integer input.
x = "hello"
if not type(x) is int:
raise TypeError("Only integers are allowed")Here we use:
-
TypeErrorโ wrong data type
This is more informative than a generic Exception.
You can think of raise as creating an error signal in the program flow:
Normal execution โ Condition detected โ raise โ Program interrupted
This mechanism integrates with:
-
try / exceptblocks - Error propagation across functions
- Debugging tools
Good:
raise ValueError("Age must be positive")Less good:
raise Exception("Something went wrong")Specific errors improve readability and maintainability.
Check inputs at the boundaries of your functions.
def sqrt(x):
if x < 0:
raise ValueError("Cannot compute square root of negative number")Error messages should explain:
- What went wrong
- Why it is invalid
Exceptions are closely related to design by contract.
A function implicitly states:
โIf you give me valid input, I guarantee correct output.
Otherwise, I will raise an exception.โ
This turns runtime checks into formal guarantees.
-
raiseallows you to trigger exceptions manually - Exceptions stop execution and report errors
- Use specific exception types when possible
- Raising exceptions is essential for robust, safe programs
Built-in exceptions are useful, but sometimes your program has domain-specific rules that deserve their own error language.
For example:
- A banking app โ
InsufficientFundsError - A game engine โ
InvalidMoveError - A scientific simulation โ
ConvergenceError
When you define custom exceptions, you are giving your code a vocabulary for failure.
This dramatically improves:
- Readability
- Debugging clarity
- Architectural design
A custom exception is simply a class that inherits from Exception (or another exception type).
class MyError(Exception):
passThatโs it.
You now have a brand-new error type.
Suppose we want to forbid negative ages.
class NegativeAgeError(Exception):
pass
age = -5
if age < 0:
raise NegativeAgeError("Age cannot be negative")Now the error communicates meaning, not just failure.
Compare these two messages:
Generic:
ValueError: Invalid input
Specific:
NegativeAgeError: Age cannot be negative
The second one tells you immediately:
- What failed
- Why it failed
- Where to look
This reduces debugging time dramatically.
In strongly-typed thinking (similar to engineering disciplines or Ada-style design), exceptions are part of your type system of behavior.
Your program does not only define:
- Data types
- Functions
It also defines:
- Failure types
So the system becomes:
Valid State โ Normal execution
Invalid State โ Specific exception type
This is a powerful abstraction.
Custom exceptions can carry data.
class TemperatureTooHighError(Exception):
def __init__(error, temperature):
message = f"Temperature {temperature}ยฐC exceeds safe limit"
super().__init__(message)
error.temperature = temperatureUsage:
temp = 120
if temp > 100:
raise TemperatureTooHighError(temp)Now the exception contains structured information.
You can access it later:
except TemperatureTooHighError as e:
print(e.temperature)You donโt always need to inherit directly from Exception.
You can specialize existing categories.
Example:
class InvalidEmailError(ValueError):
passThis communicates:
โThis is a ValueError โ but more specific.โ
This helps large systems where different error families exist.
In complex projects, you can build error trees.
class AppError(Exception):
pass
class DatabaseError(AppError):
pass
class ConnectionError(DatabaseError):
passBenefits:
- Catch broad categories when needed
- Catch specific errors when necessary
Example:
try:
connect_to_db()
except DatabaseError:
print("Database problem detected")InvalidStateError
ConfigurationError
PermissionDeniedErrorConsistency improves clarity.
Most custom exceptions only need:
class MyError(Exception):
passAdd complexity only when useful.
Instead of vague checks:
if not valid:
raise Exception("Bad input")Prefer:
if not valid:
raise InvalidConfigurationError("Missing API key")Custom exceptions help define system boundaries.
Each module can expose:
- Public functions
- Public error types
This creates a clean API:
Module API =
Functions
Exceptions
Professional libraries always do this.
- Custom exceptions are classes that inherit from
Exception - They give semantic meaning to failures
- They improve debugging and architecture
- They can store additional data
- Large systems often define error hierarchies
Custom errors turn bugs into structured information instead of chaos.