SECTION 1
Historical Context & Motivation
Every programmer has encountered a moment when their code crashes with a cryptic error message. In the early days of programming, errors were handled using numeric codes — a function might return −1 to signal failure, leaving the programmer to guess what went wrong. As languages evolved, developers realized they needed a better system: one that could describe what went wrong, where it went wrong, and why. This led to the concept of exceptions — special objects that carry error information up through the program until something handles them.
1960s • Error Codes Era
1980s • Exception Handling Emerges
1991 • Python Is Born
2000s • Custom Exceptions Become Best Practice
Python ships with dozens of built-in exceptions like ValueError, TypeError, and FileNotFoundError. But what happens when none of them accurately describes the error your program needs to communicate? For example, if you're building a game and a player tries to move to an invalid tile, raising a generic ValueError doesn't tell you much. This is where custom exceptions come in — they let you define error types that match your program's specific needs.
SECTION 2
Core Principles & Definitions
Before you create your own exception classes, you need to understand a few foundational ideas. Python's exception system is built on class inheritance — the mechanism by which one class can borrow behavior from another. Every exception in Python is an object, and every exception class traces its ancestry back to a base class called BaseException. The key ideas below form the foundation for building your own.
Inheritance
Exception class.The Exception Hierarchy
BaseException sits at the top, Exception is below it, and all common exceptions branch from there. Custom exceptions should extend Exception, not BaseException.raise Statement
raise keyword tells Python to stop normal execution and create an exception object. You can raise both built-in and custom exceptions.try / except Block
try. The except block catches specific exception types and handles them gracefully instead of crashing.Descriptive Naming
InsufficientFundsError or InvalidMoveError instantly communicate what went wrong.KEY TAKEAWAY
SECTION 3
Visual Explanation — The Exception Hierarchy
The diagram below shows how Python's exception hierarchy works. At the top sits BaseException, the root of all exceptions. Below it is Exception, which is the parent of every exception you'd normally catch in your programs. Your custom exception classes branch off from Exception (or one of its children), inserting themselves naturally into this tree.
InvalidAgeError and InsufficientFundsError inherit from built-in exception classes, fitting naturally into Python's exception system.Notice how the custom exceptions (shown with glowing borders) attach to the tree using dashed lines. InvalidAgeError inherits from ValueError because an invalid age is a type of value problem. InsufficientFundsError inherits from TypeError in this example, but in practice you would most likely inherit directly from Exception. The key insight is that a try/except block that catches ValueError will also catch InvalidAgeError, because a child class "is a" type of its parent.
SECTION 4
How It Works — Defining a Custom Exception
Creating a custom exception in Python is surprisingly simple. At its core, you just define a new class that inherits from Exception (or one of its subclasses). The minimal version requires only two lines of code. Let's look at the syntax pattern and then build up from there.
The Minimal Custom Exception
MINIMAL PATTERN
class — keyword to define a new class. MyError — your chosen name (by convention, ends in 'Error'). (Exception) — the parent class being inherited from. pass — a placeholder meaning 'no extra behavior needed.'
Adding a Custom Message
CUSTOM MESSAGE PATTERN
__init__ — the constructor method that runs when the exception is created. self.age — stores the invalid age value for later inspection. super().__init__() — calls the parent class's constructor to set up the standard exception behavior.
Raising and Catching the Custom Exception
RAISE AND CATCH PATTERN
raise — triggers the exception. except InvalidAgeError as e — catches only this specific type, binding it to variable e so you can access its attributes.
Why not just use Exception directly?
raise Exception("Age is not valid"), but then your except block would catch every exception, not just age-related ones. Custom exceptions let you be selective — you catch only the specific errors you expect and let unexpected ones bubble up to reveal real bugs.SECTION 5
Anatomy of a Custom Exception Class
Let's dissect a more complete custom exception to understand every part. The diagram below shows a custom exception called InsufficientFundsError that you might use in a banking application. Each section of the class is annotated so you can see exactly what each piece does and why it's there.
The five parts shown in the diagram are not all required. The simplest custom exception only needs the class name and the parent class (parts ① and ④ with just pass in the body). However, adding a constructor with custom attributes makes the exception much more useful because the code that catches it can inspect exactly what went wrong.
SECTION 6
Worked Example — Building a Grade Validator
Let's walk through a complete example. Imagine you're writing a program for a teacher that records student grades. Grades must be between 0 and 100. We'll create a custom exception called InvalidGradeError that fires whenever someone enters a grade outside that range.
Creating and Using InvalidGradeError
- 1
Step 1 — Define the Custom Exception Class
Start by creating a class that inherits fromException. Give it a constructor that accepts the invalid grade and a custom message. Store both as instance attributes and callsuper().__init__()to set up the parent class.class InvalidGradeError(Exception): def __init__(self, grade): self.grade = grade self.message = f"Grade {grade} is out of range (0–100)" super().__init__(self.message)Custom exception classInvalidGradeErroris now defined and ready to use. - 2
Step 2 — Write a Function That Raises It
Create a functionrecord_grade()that checks whether the grade is valid. If not, raise the custom exception.def record_grade(name, grade): if grade < 0 or grade > 100: raise InvalidGradeError(grade) print(f"Recorded {name}: {grade}")The function will raiseInvalidGradeErrorfor any grade below 0 or above 100. - 3
Step 3 — Catch the Exception with try/except
Wrap the function call in atry/exceptblock. Useas eto capture the exception object and access its attributes.try: record_grade("Alice", 95) # Works fine record_grade("Bob", 150) # Raises error except InvalidGradeError as e: print(f"Error: {e.message}") print(f"The invalid grade was: {e.grade}")Output:Recorded Alice: 95 Error: Grade 150 is out of range (0–100) The invalid grade was: 150 - 4
Step 4 — Verify It Doesn't Catch Other Errors
An important feature of custom exceptions is specificity. If a completely different error occurs — say, aTypeErrorfrom passing a string instead of a number — ourexcept InvalidGradeErrorblock won't catch it. This is intentional: unexpected errors should crash loudly so you can find and fix them.try: record_grade("Eve", "A+") # TypeError, not caught except InvalidGradeError as e: print(e.message) # This block is skippedTheTypeErroris NOT caught — it propagates up, alerting you to a real bug in the calling code.
SECTION 7
Custom vs. Built-in Exceptions
You might wonder when it's worth creating a custom exception versus simply using one of Python's built-in exceptions. The table below compares the two approaches across several important criteria.
| Criteria | Built-in Exception | Custom Exception |
|---|---|---|
| Specificity | General-purpose; may match many unrelated errors | Highly specific; describes exactly one type of domain error |
| Readability | Recognized instantly by any Python developer | Self-documenting if well-named; new developers understand the domain |
| Custom Data | Only carries a message string | Can carry any attributes (balance, grade, coordinates, etc.) |
| Selective Catching | Catching ValueError catches ALL ValueErrors | Catching InvalidGradeError catches only grade issues |
| Setup Effort | Zero — already exists | Minimal — as few as two lines of code |
| Best For | Quick scripts, universal errors (files, types) | Larger projects, libraries, apps with domain logic |
Comparison of built-in and custom exceptions
WHEN TO GO CUSTOM
raise ValueError("some specific message") in multiple places, that's a signal to create a custom exception. It's like labeling folders in a filing cabinet — sure, you could put everything in one drawer marked "Stuff," but labeled folders like "Homework," "Tests," and "Projects" make finding things much faster.SECTION 8
Connection to Advanced Exception Techniques
The simple custom exception you've learned here is the foundation for more advanced patterns used in professional Python development. As your projects grow, you'll encounter situations where a single custom exception isn't enough — you might need entire exception hierarchies for your application. The table below previews how the introductory concepts you've learned connect to more advanced techniques.
| Intro Concept | Advanced Technique | Use Case |
|---|---|---|
| Single custom exception class | Exception hierarchy (multiple related custom exceptions inheriting from a common base) | Large libraries with many error types, e.g., a database library with ConnectionError, QueryError, AuthError |
| Storing attributes (self.grade) | Chaining exceptions with 'raise ... from ...' to preserve the original cause | Debugging complex failures where one error causes another |
| try / except | try / except / else / finally for complete control flow | Cleanup actions like closing files or database connections |
| __str__ method | Custom __repr__, logging integration, and serialization to JSON | Error reporting systems and automated monitoring |
How intro concepts lead to advanced exception techniques
Don't worry about mastering these advanced patterns right now. The important thing is that the two-line custom exception you learned today — class MyError(Exception): pass — uses exactly the same inheritance mechanism that powers all of these advanced techniques. You're building on a solid foundation.
SECTION 9
Practice Problems
Question 1: CONCEPTUAL
Exception rather than from BaseException? What could go wrong if you inherit from BaseException?Show answer
BaseException class is the root of all exceptions, including system-level ones like KeyboardInterrupt and SystemExit that should rarely be caught in normal code. When someone writes except Exception, they expect to catch application errors without interfering with system signals. If your custom exception inherits from BaseException instead, a generic except Exception block would miss it entirely, causing confusing behavior. Always inherit from Exception (or one of its subclasses) for application-level errors.Question 2: BASIC CALCULATION
NegativeNumberError that stores the invalid number in an attribute called self.number and produces the message "Negative numbers are not allowed: X" where X is the number.Show answer
class NegativeNumberError(Exception):
def __init__(self, number):
self.number = number
self.message = f"Negative numbers are not allowed: {number}"
super().__init__(self.message)
The class inherits from Exception. The constructor accepts the invalid number, stores it as self.number, builds a descriptive message, and passes that message to the parent class via super().__init__(). Now code that catches this exception can inspect e.number to see which value caused the problem.Question 3: INTERMEDIATE
WeakPasswordError that stores the password length and a list of reasons why the password is weak (e.g., 'too short', 'no uppercase'). Then write a function validate_password(password) that raises it when the password is fewer than 8 characters or has no uppercase letter.Show answer
class WeakPasswordError(Exception):
def __init__(self, length, reasons):
self.length = length
self.reasons = reasons
self.message = f"Weak password ({length} chars): {', '.join(reasons)}"
super().__init__(self.message)
def validate_password(password):
reasons = []
if len(password) < 8:
reasons.append('too short')
if not any(c.isupper() for c in password):
reasons.append('no uppercase')
if reasons:
raise WeakPasswordError(len(password), reasons)
return True
The function collects all reasons into a list before raising the exception. This way the caller gets a complete picture of every problem with the password, not just the first one. The exception stores both the length and the reasons list, so the catching code can present a helpful message to the user.Question 4: APPLIED
OutOfStockError that stores the product name, requested quantity, and available quantity. Then write a function purchase(product_name, quantity, inventory) where inventory is a dictionary like {"pencils": 10, "erasers": 0}. The function should raise OutOfStockError if the quantity requested exceeds stock, and otherwise subtract from inventory.Show answer
class OutOfStockError(Exception):
def __init__(self, product, requested, available):
self.product = product
self.requested = requested
self.available = available
self.message = (
f"Cannot purchase {requested} {product}(s). "
f"Only {available} in stock."
)
super().__init__(self.message)
def purchase(product_name, quantity, inventory):
available = inventory.get(product_name, 0)
if quantity > available:
raise OutOfStockError(product_name, quantity, available)
inventory[product_name] -= quantity
return inventory
The inventory.get(product_name, 0) call returns 0 if the product isn't in the dictionary at all. The exception stores all three values — product name, requested count, and available count — so the caller can craft a user-friendly response or suggest alternatives. The function modifies and returns the inventory dictionary when the purchase succeeds.Question 5: CRITICAL THINKING
GameError (base), InvalidMoveError (for illegal moves), and GameOverError (for end-of-game conditions). Design this three-class hierarchy. Then explain: if a caller writes except GameError, which exceptions will it catch, and why? What advantage does this hierarchy give you over three unrelated custom exceptions?Show answer
class GameError(Exception):
"""Base exception for all game-related errors."""
pass
class InvalidMoveError(GameError):
def __init__(self, move, reason="Move is not allowed"):
self.move = move
self.message = f"{reason}: {move}"
super().__init__(self.message)
class GameOverError(GameError):
def __init__(self, winner=None):
self.winner = winner
self.message = f"Game over. Winner: {winner}" if winner else "Game over."
super().__init__(self.message)
Writing except GameError will catch GameError, InvalidMoveError, and GameOverError because both children inherit from GameError. This is inheritance in action: a child class 'is a' type of its parent. The advantage over three unrelated exceptions is flexibility — you can catch all game errors at once with except GameError, or catch specific ones like except InvalidMoveError when you need fine-grained handling. This is the same pattern used by professional frameworks.SUMMARY
Lesson Summary
Summary
A custom exception is a Python class that inherits from Exception (or one of its subclasses) and represents a specific error condition in your program. At minimum, it takes just two lines: class MyError(Exception): pass. For more useful exceptions, you add a __init__ constructor that stores context data (like the invalid value) and calls super().__init__() to set up the parent class. You trigger the exception with the raise keyword and handle it with a try/except block.
Custom exceptions shine when built-in exceptions are too generic. By giving your errors descriptive names and custom attributes, you make your code self-documenting and your error handling precise. The same inheritance mechanism that makes this work also lets you build entire exception hierarchies for larger projects, giving callers the choice to catch errors broadly or narrowly.