Python • Python-Specific Topics

Custom Exceptions — Define a custom exception class (intro)

Learn how to create your own exception classes to make Python error handling clearer and more intentional.

Code braces with terminal and markup symbols representing Python programming.
Inside this lesson 10 sections 5 questions 7 figures

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

Early languages like C used integer return values (e.g., −1) to indicate errors. Programmers had to check every function call manually, which was tedious and error-prone.

1980s • Exception Handling Emerges

Languages like C++ and Ada introduced structured exception handling with try/catch blocks, separating normal logic from error-handling logic for the first time.

1991 • Python Is Born

Guido van Rossum released Python with built-in exception handling using try/except. Python's clean syntax made exceptions easy to use and extend through inheritance.

2000s • Custom Exceptions Become Best Practice

As Python projects grew larger — frameworks like Django and Flask — developers adopted custom exception classes as standard practice to create self-documenting, domain-specific error types.

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.

1

Inheritance

A new class can inherit attributes and methods from a parent class. Custom exceptions inherit from Python's built-in Exception class.
2

The Exception Hierarchy

Python organizes exceptions in a tree. BaseException sits at the top, Exception is below it, and all common exceptions branch from there. Custom exceptions should extend Exception, not BaseException.
3

raise Statement

The raise keyword tells Python to stop normal execution and create an exception object. You can raise both built-in and custom exceptions.
4

try / except Block

Code that might fail goes inside try. The except block catches specific exception types and handles them gracefully instead of crashing.
5

Descriptive Naming

Custom exception names should clearly describe the error. Names like InsufficientFundsError or InvalidMoveError instantly communicate what went wrong.
✦

KEY TAKEAWAY

Think of Python's built-in exceptions like the generic warning lights on a car dashboard — they tell you something is wrong, but not exactly what. A custom exception is like having a dashboard message that reads "Low tire pressure — rear left tire." It gives specific, actionable information. By creating your own exception classes, you give your code the ability to communicate exactly what problem occurred in language that makes sense for your application.

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.

Python Exception HierarchyBaseExceptionExceptionValueErrorTypeErrorRuntimeError▼ Your custom exceptions go here ▼InvalidAgeErrorInsufficientFundsError(inherits from ValueError)(inherits from TypeError)Legend:Root (BaseException)Parent (Exception)Built-inCustom (yours!)
The exception hierarchy tree shows how custom exceptions like 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

classMyError(Exception):passclass MyError(Exception): pass

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

class InvalidAgeError(Exception): def __init__(self, age, message="Age is not valid"): self.age = age self.message = message super().__init__(self.message)

__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

try:age=int(input("Enterage:"))ifage<0:raiseInvalidAgeError(age)exceptInvalidAgeErrorase:print(f"Caught:e.message→e.age")try: age = int(input("Enter age: ")) if age < 0: raise InvalidAgeError(age) except InvalidAgeError as e: print(f"Caught: {e.message} → {e.age}")

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?

You could write 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.

Anatomy of a Custom Exception Classclass InsufficientFundsError(Exception):"""Raised when a withdrawal exceeds balance."""def __init__(self, balance, amount):self.balance = balanceself.amount = amountself.deficit = amount - balanceself.message = (f"Need ${self.deficit:.2f} more")super().__init__(self.message)def __str__(self):return self.message① CLASS NAMEEnds in "Error" by convention② DOCSTRINGDescribes when it's raised③ __init__ CONSTRUCTORStores context data asinstance attributes④ super().__init__()Calls parent constructor⑤ __str__ METHODControls print() outputUsage: raise InsufficientFundsError(balance=50.00, amount=75.00)→ "Need $25.00 more"
Each numbered annotation corresponds to a key part of a custom exception class: ① the class name, ② the docstring, ③ the constructor with context data, ④ the super() call, and ⑤ the __str__ method for readable output.

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

    Step 1 — Define the Custom Exception Class

    Start by creating a class that inherits from Exception. Give it a constructor that accepts the invalid grade and a custom message. Store both as instance attributes and call super().__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 class InvalidGradeError is now defined and ready to use.
  2. 2

    Step 2 — Write a Function That Raises It

    Create a function record_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 raise InvalidGradeError for any grade below 0 or above 100.
  3. 3

    Step 3 — Catch the Exception with try/except

    Wrap the function call in a try/except block. Use as e to 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. 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, a TypeError from passing a string instead of a number — our except InvalidGradeError block 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 skipped
    The TypeError is 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.

CriteriaBuilt-in ExceptionCustom Exception
SpecificityGeneral-purpose; may match many unrelated errorsHighly specific; describes exactly one type of domain error
ReadabilityRecognized instantly by any Python developerSelf-documenting if well-named; new developers understand the domain
Custom DataOnly carries a message stringCan carry any attributes (balance, grade, coordinates, etc.)
Selective CatchingCatching ValueError catches ALL ValueErrorsCatching InvalidGradeError catches only grade issues
Setup EffortZero — already existsMinimal — as few as two lines of code
Best ForQuick scripts, universal errors (files, types)Larger projects, libraries, apps with domain logic

Comparison of built-in and custom exceptions

🎯

WHEN TO GO CUSTOM

A good rule of thumb: if you find yourself writing 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 ConceptAdvanced TechniqueUse Case
Single custom exception classException 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 causeDebugging complex failures where one error causes another
try / excepttry / except / else / finally for complete control flowCleanup actions like closing files or database connections
__str__ methodCustom __repr__, logging integration, and serialization to JSONError 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

Why should a custom exception class inherit from Exception rather than from BaseException? What could go wrong if you inherit from BaseException?
Show answer
The 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

Write a custom exception class called 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
Here is the solution: 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

You're building a password checker. Create a custom exception 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
Here is one approach: 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

You're developing an inventory system for a school store. Products have a name and a stock count. Write a custom exception 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
Here is a complete solution: 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

Suppose you're designing a small game engine and you want three related custom exceptions: 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
Here is the hierarchy: 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.

Varsity Tutors • Python • Custom Exceptions — Define a custom exception class (intro)