7.F Fail: Error Control - JulTob/Python GitHub Wiki

Raising Exceptions in Python 🚨

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.


πŸ”° Core Idea

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.”


βš™οΈ Basic Syntax

raise Exception("Error message")

You specify:

  1. The type of exception
  2. The explanatory message

πŸ§ͺ Example 1 β€” Guarding Against Invalid Values

Suppose negative numbers are not allowed:

x = -1

if x < 0:
    raise Exception("Sorry, no numbers below zero")

What happens:

  1. Python evaluates the condition x < 0
  2. The condition is True
  3. The program stops immediately
  4. The message is displayed

This pattern is called a guard clause β€” a defensive programming technique.


⚠️ Why Raise Exceptions?

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.


🎯 Choosing the Right Exception Type

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.


🧠 Conceptual Model

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 / except blocks
  • Error propagation across functions
  • Debugging tools

πŸ’‘ Best Practices

Prefer specific exceptions

Good:

raise ValueError("Age must be positive")

Less good:

raise Exception("Something went wrong")

Specific errors improve readability and maintainability.


Validate early

Check inputs at the boundaries of your functions.

def sqrt(x):
    if x < 0:
        raise ValueError("Cannot compute square root of negative number")

Write meaningful messages

Error messages should explain:

  • What went wrong
  • Why it is invalid

πŸͺ„ Advanced Insight β€” Exceptions as Contracts

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.


βœ… Summary

  • raise allows 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

🧩 Creating Custom Exceptions (Your Own Error Types)

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

πŸ”° Core Idea

A custom exception is simply a class that inherits from Exception (or another exception type).

class MyError(Exception):
    pass

That’s it.

You now have a brand-new error type.


βš™οΈ Basic Example

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.


🎯 Why Custom Errors Matter

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.


🧠 Conceptual Model β€” Errors as Types

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.


πŸ§ͺ Adding Information to Custom Errors

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 = temperature

Usage:

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)

⚠️ Inheriting from Specific Exception Types

You don’t always need to inherit directly from Exception.

You can specialize existing categories.

Example:

class InvalidEmailError(ValueError):
    pass

This communicates:

β€œThis is a ValueError β€” but more specific.”

This helps large systems where different error families exist.


πŸͺ„ Designing an Error Hierarchy

In complex projects, you can build error trees.

class AppError(Exception):
    pass


class DatabaseError(AppError):
    pass


class ConnectionError(DatabaseError):
    pass

Benefits:

  • Catch broad categories when needed
  • Catch specific errors when necessary

Example:

try:
    connect_to_db()
except DatabaseError:
    print("Database problem detected")

πŸ’‘ Best Practices

1. Name errors with β€œError” suffix

InvalidStateError
ConfigurationError
PermissionDeniedError

Consistency improves clarity.


2. Keep them lightweight

Most custom exceptions only need:

class MyError(Exception):
    pass

Add complexity only when useful.


3. Use them to express contracts

Instead of vague checks:

if not valid:
    raise Exception("Bad input")

Prefer:

if not valid:
    raise InvalidConfigurationError("Missing API key")

🧬 Advanced Insight β€” Exceptions as Architecture

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.


βœ… Summary

  • 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.