Courseiva
PCAP-31-03Chapter 8 of 17Objective 3.2

Custom Exceptions and Assertions

The PCAP-31-03 exam domain on object-oriented programming includes how to define your own error types and use assertions for debugging. This chapter is your survival guide for when things go wrong in a predictable way—and for catching impossible problems before they happen. If you've ever written a program that crashed with a generic 'something broke' message, you already know the problem: Python's built-in errors (like TypeError or ValueError) are too vague for the unique situations your code creates. Custom exceptions let you speak the language of your own programme, and assertions act like your personal debugger that double-checks your assumptions.

12 min read
Intermediate
Updated Jul 23, 2026
Reviewed by Johnson Ajibi· Senior Network & Security Engineer · MSc IT Security

A simple way to picture Custom Exceptions and Assertions

The Home Renovation Contractor Analogy

Have you ever hired someone to renovate your kitchen and they painted the bathroom instead? That's a recipe for a disaster. Custom exceptions and assertions in Python are like having a good contract and a sharp-eyed supervisor on that renovation project.

Imagine you're a homeowner who has hired a contractor to install a new sink. The contract (which is like a custom exception) should say: 'If the plumber arrives and finds the water main is off, stop work and call me immediately.' This is a specific, named problem—just as you'd define a WaterMainOffError exception in your code. The contractor doesn't just get a generic 'problem occurred' note; they get a clear, actionable message about exactly what went wrong. That mapping—specific error, specific name—is what custom exceptions do: they create new, unique categories of mishaps your program can recognise and handle.

Then there's the supervisor who occasionally makes spot checks. Before the contractor drills into a wall, the supervisor might say, 'Assert that the blueprints show no pipes here.' That's an assertion: a sanity check that says, 'I am assuming this condition is true; if it's not, stop everything right now.' If the blueprint is wrong, you want to discover that early, not after you've flooded the kitchen. In programming, assertions let you test assumptions during development, catching bugs before they become the equivalent of a burst pipe in the living room.

How It Actually Works

First, let's get the basics straight. In Python, an exception is just a fancy word for an error that occurs while your program is running. When a function tries to divide by zero, Python raises a ZeroDivisionError. When you try to open a file that doesn't exist, it raises a FileNotFoundError. These are built-in exceptions—the standard set of problems that Python knows about. But what happens when your own code encounters a problem that those built-in errors don't cover? For example, imagine you're writing a banking app. A user tries to withdraw more money than they have in their account. There's no built-in InsufficientFundsError. So, you need to invent one. That's a custom exception.

To create a custom exception, you define a new class that inherits from Python's Exception class (or any of its subclasses). Here's the simplest possible example:

class InsufficientFundsError(Exception): pass

The word 'class' is Python's way of defining a new type of thing. By putting 'Exception' in parentheses after the name of your new class, you tell Python: 'This new error is a special kind of Exception, so it behaves like one.' The 'pass' keyword just means 'do nothing extra right now.' Once you've defined this class, you can use the 'raise' statement to trigger it:

raise InsufficientFundsError('Your balance is too low.')

When this line runs, Python stops the normal flow of your program and looks for a handler—a 'try-except' block that knows how to deal with this specific error. If no handler catches it, the program crashes. But if you wrote a handler like:

try: withdraw(account, 500) except InsufficientFundsError as e: print(f'Transaction failed: {e}')

...then your program can respond gracefully, perhaps by asking the user to try a smaller amount. The goal of custom exceptions is to make your code more organised and easier to debug. Instead of a vague 'something went wrong' message, you get a specific error type that tells you, and anyone reading your code, exactly what the problem is.

Now, let's talk about assertions. An assertion is a more aggressive form of checking. It's like a checkpoint that says, 'This condition must be true, or everything I'm about to do is meaningless.' In Python, you write an assertion using the 'assert' keyword:

assert account_balance >= withdrawal_amount, 'Balance cannot be negative after withdrawal'

If the condition (account_balance >= withdrawal_amount) is True, nothing happens—Python just moves on. But if it's False, Python raises an AssertionError and prints the optional message you provided. The key difference between an assertion and a regular 'if' statement is that assertions are intended for catching programmer mistakes during development, not for handling predictable runtime errors. You use them to check assumptions that you believe should always hold true—like 'this list isn't empty' or 'this parameter has a valid value.'

Why would you use an assertion instead of an 'if' statement? Because you can disable all assertions globally when you run Python with the '-O' (optimise) flag. That means you can leave assertions in your code as a safety net during testing, but turn them off for a production release where you don't want the overhead of extra checks. A regular 'if' statement can't be turned off—it always runs. So, use assertions for conditions that should never happen if your code is correct, and use custom exceptions for situations that might reasonably occur (like a user typing invalid input).

Let's solidify this with a real coding example. Suppose you're writing a function that calculates the average of a list of numbers. You might add an assertion that the list is not empty:

def average(numbers): assert len(numbers) > 0, 'numbers list must not be empty' return sum(numbers) / len(numbers)

If someone mistakenly calls average([]), Python will crash with an AssertionError, alerting you to the bug during development. But if you expect that sometimes users will legitimately pass an empty list, you should instead raise a custom exception (or return a special value) and handle it in a try-except block. The choice depends on your design: assertions for programmer errors, custom exceptions for anticipated runtime problems.

Finally, a word on exception hierarchies. You can make your custom exceptions more sophisticated by creating a base exception for your entire application and then subclasses for more specific errors. For example:

class BankError(Exception): pass class InsufficientFundsError(BankError): pass class AccountNotFoundError(BankError): pass

Now, any except clause that catches BankError will also catch InsufficientFundsError and AccountNotFoundError. This lets you handle groups of related errors with a single handler, while still being able to catch each specific error individually. This is one of the most powerful features of custom exceptions—you can build a error-handling hierarchy that mirrors your application's domain logic.

Flowchart showing the decision path for using custom exceptions versus assertions in a Python program.

Walk-Through

1

Identify the specific runtime problem

Look at your code and think: what predictable errors could occur that Python's built-in exceptions don't cover? For example, a user trying to withdraw money with insufficient funds. This step is about recognising the gap—a missing error type that matches your domain logic.

2

Define the custom exception class

Write a class that inherits from Exception. For example: class InsufficientFundsError(Exception): pass. Optionally add an __init__ method to store extra data (like the account balance). This step creates the new error type your code can recognise.

3

Raise the custom exception at the right place

Where the problem occurs (e.g., in a withdrawal function), use the 'raise' statement to trigger the exception: raise InsufficientFundsError('Balance too low'). This replaces a generic crash with a specific, meaningful error.

4

Write a try-except block to handle the exception

Wrap the code that might raise the custom exception in a try block, and add an except clause that catches that specific exception type. For example: try: withdraw() except InsufficientFundsError as e: print(e). This allows your program to respond gracefully instead of crashing.

5

Add assertions to check assumptions during development

Identify conditions that should always be true (like a list not being empty). Add an assert statement: assert len(items) > 0, 'List must not be empty'. If the condition fails during testing, Python immediately raises an AssertionError, alerting you to the bug early.

6

Test and toggle assertions with -O flag

Run your code normally during development (assertions active). When ready for production, run with the -O flag (python -O script.py) to disable all assertions for performance. The custom exceptions remain active because they are not assertions.

What This Looks Like on the Job

Imagine you work for a small e-commerce company that sells handmade furniture. The company's IT department manages a Python application that processes customer orders. One day, a customer tries to place an order for a table that's been discontinued. The current code just crashes with a generic 'KeyError' because the product ID isn't in the database. The support team gets a confused call, and no one knows exactly what went wrong.

Your job is to fix this. You decide to create a set of custom exceptions for the ordering system. You start by defining:

class OrderProcessingError(Exception): pass class ProductNotFoundError(OrderProcessingError): pass class OutOfStockError(OrderProcessingError): pass class InvalidQuantityError(OrderProcessingError): pass

Now, when the database lookup fails, instead of letting Python raise a generic KeyError, you write:

if product_id not in product_database: raise ProductNotFoundError(f'Product {product_id} not found')

This tells everyone exactly what went wrong: a product was requested that doesn't exist. Later, when someone tries to order a quantity of -3, you can raise InvalidQuantityError. The support team can now see clear error messages in logs, and you can write a single except block that catches OrderProcessingError and sends a polite email to the customer.

Then, you decide to add assertions to check your own assumptions. In the function that calculates shipping costs, you add:

def calculate_shipping(order_weight_kg): assert order_weight_kg > 0, 'Order weight must be positive' # ... calculate shipping

This assertion catches a bug where a developer accidentally passed a negative weight. It's not a customer-facing problem yet—it's a programming mistake you want to catch early. During testing, if an assertion fires, you immediately know to fix the calling code. When you deploy to production, you might turn off assertions with the '-O' flag to avoid the performance cost, but your custom exceptions (ProductNotFoundError, etc.) stay active because they handle real user-facing errors.

The real-world routine of an IT professional involves reading error logs from production systems. When you see a generic 'Exception' logged, you have no clue what went wrong. But when you see 'ProductNotFoundError: product id 12345', you know exactly where to start investigating. Custom exceptions transform your logs from noise into actionable intelligence.

In a larger team, custom exceptions also serve as a contract between developers. If you write a library function that raises InsufficientFundsError, other developers know they must either handle it or let it propagate. The exception hierarchy tells them: 'If you catch OrderProcessingError, you'll cover all order-related errors.' This reduces bugs because teammates don't accidentally catch a generic Exception and swallow a completely unrelated error.

How PCAP-31-03 Actually Tests This

The PCAP-31-03 exam (exam objective 3.2) tests your understanding of three distinct skills: (1) defining custom exception classes, (2) raising exceptions properly, and (3) using assertions. The exam will not ask you to write a long program, but it will present you with short code snippets and ask you to predict the output or identify the correct syntax.

Here are the specific traps and patterns the exam loves:

Trap 1: Forgetting the 'Exception' base class. A custom exception class must inherit from Exception (or a subclass of Exception). If you write 'class MyError:' without parentheses, that's not an exception—it's just a regular class. The exam may show you a class definition that omits the base class and ask whether it will raise correctly. The answer is no.

Trap 2: The 'raise' syntax. You must know that 'raise MyError()' and 'raise MyError' are both valid, but they're different. 'raise MyError' raises the class itself (which creates an instance automatically), while 'raise MyError()' raises a specific instance. The exam sometimes tests this nuance with multiple-choice questions about what gets passed to an except clause.

Trap 3: Assertion syntax. The 'assert' statement requires a condition, optionally followed by a comma and a message. 'assert x > 0, 'x must be positive'' is correct. 'assert(x > 0, 'x must be positive')' is WRONG because the parentheses turn the whole thing into a tuple, and a tuple is always truthy, so the assertion never fails. Exam questions frequently hide this mistake.

Trap 4: The behaviour of assertions when the condition is False. An assert raises an AssertionError. You cannot catch it with 'except Exception' if you're using the '-O' flag (since assertions become disabled). But without '-O', AssertionError is a subclass of Exception, so you can catch it. The exam expects you to know that assertions are meant for debugging and can be disabled.

Trap 5: Exception chaining. You might see 'raise ... from ...' syntax, which allows you to chain exceptions (e.g., 'raise MyError from original_exception'). This is tested occasionally, but less frequently than the basics above.

Trap 6: The order of except clauses matters. If you define custom exceptions in a hierarchy (e.g., BankError -> InsufficientFundsError), and you write 'except BankError:' before 'except InsufficientFundsError:', the more specific handler is never reached because the more general one catches everything. The exam will test that you understand this.

The exam questions are typically multiple-choice or drag-and-drop style. Expect questions like: 'What is the output of the following code?' 'Which of these definitions is a valid custom exception?' 'What does the assert statement do when its condition evaluates to False?' 'How can you disable all assertions in a Python script?' 'The answer to that last one is: by running python with the -O flag.

Key definitions to memorise:

A custom exception is a class that inherits from Exception.

The 'raise' statement triggers an exception.

An assertion (assert) checks a condition and raises AssertionError if it's False.

Assertions are disabled in optimised mode (-O).

Exception hierarchy allows catching multiple related exceptions with one handler.

Key Takeaways

A custom exception class must inherit from the built-in Exception class (or a subclass of Exception).

Use the 'raise' keyword followed by an instance (or class) of your custom exception to trigger it.

An assertion (assert) checks a condition; if False, it raises an AssertionError and is intended for catching programmer mistakes during development.

All assertions can be globally disabled by running Python with the -O (optimise) flag, making them perfect for debugging without production overhead.

You can build an exception hierarchy by creating a base exception (e.g., ApplicationError) and subclassing it for specific errors.

When catching exceptions, order your except clauses from most specific to most general, or the specific handler will never execute.

Easy to Mix Up

These come up on the exam all the time. Here's how to tell them apart.

Custom Exception

Used for expected runtime errors (e.g., invalid input, missing data)

Cannot be disabled with the -O flag

Defined as a class inheriting from Exception

Assertion

Used for catching programmer mistakes during development

Can be globally disabled with the -O flag

Uses the 'assert' keyword; raises AssertionError automatically

raise Statement

Explicitly triggers an exception at any point in code

Can raise any exception class (custom or built-in)

Followed by an exception instance or class

assert Statement

Triggers only AssertionError when the condition is False

Used implicitly inside a conditional check

Syntax: assert condition, message

except BankError (specific)

Catches only BankError and its subclasses

More precise and avoids swallowing unrelated errors

Recommended for handling known error types

except Exception (general)

Catches all exceptions that inherit from Exception

Can mask bugs by catching unintended errors

Discouraged in production except at top-level logging

Watch Out for These

Mistake

I can create a custom exception by just writing a function named MyError. As long as I raise it, it works.

Correct

A custom exception must be a class that inherits from Exception (or a subclass). A function cannot be raised as an exception. You get a TypeError if you try.

This mistake comes from thinking that calling something with parentheses makes it work in Python. Many beginners see 'raise ValueError' and assume any name works, forgetting the class relationship.

Mistake

An assertion is just a shorter way to write an if statement that prints an error message.

Correct

An assertion does not just print a message—it raises an AssertionError, which stops execution (unless caught). And unlike an if statement, assertions can be globally disabled with the -O flag.

Beginners often treat assert like a glorified print statement because they only see the message part. They miss that it's a control flow mechanism, not just a logging tool.

Mistake

Custom exceptions must always have an __init__ method to store additional data.

Correct

Custom exceptions don't need an __init__ method. They inherit one from Exception that accepts an optional message. You can add an __init__ if you want extra attributes, but it's not required.

Overcomplication. Beginners see advanced examples with __init__ and think that's mandatory. The exam tests the simple case: class MyError(Exception): pass is perfectly valid.

Mistake

If I use an assertion in my code, it will always run, even in production, so I should avoid them if performance matters.

Correct

Assertions can be disabled by running Python with the -O flag, so you can leave them in your code for development and turn them off for production releases.

This misconception comes from not knowing about the -O flag. Beginners think assert is just a permanent check, ignoring the intended debugging-use-case distinction.

Mistake

I must handle every custom exception with a try-except block or my program will always crash.

Correct

If an exception is not handled, the program crashes. But you are not forced to handle every possible exception—you can let it propagate up to a higher level or to the default handler. The decision depends on where you want to catch the error.

Beginners think every raise must have a matching except nearby, which isn't true. You can let exceptions bubble up through function calls until something catches them.

Do You Actually Know This?

Reveal each answer, then mark whether you got it right. Score 60%+ to unlock the next chapter.

Frequently Asked Questions

Do I always need to catch AssertionError in a try-except block?

No. Assertions are meant to crash your program during development to alert you to a bug. You typically don't catch them—you let them fail so you fix the bug. If you catch them, you risk hiding the issue.

What happens if I raise a custom exception but never handle it?

Your program will crash and display a traceback showing the exception type and the message, just like any unhandled built-in exception. The crash stops execution unless a higher-level handler catches it.

Can a custom exception inherit from multiple classes?

Yes, Python supports multiple inheritance, but for exceptions it's strongly recommended to inherit from only one exception class (preferably Exception or a subclass) to keep the hierarchy simple and avoid confusion.

How do I pass additional data (like the account balance) with a custom exception?

Add an __init__ method to your class that stores the extra attributes. For example: class MyError(Exception): def __init__(self, message, balance): super().__init__(message); self.balance = balance. Then you can access error_instance.balance in the except block.

Is there a limit to how many custom exceptions I can define?

No, there is no technical limit. However, best practice suggests using a small number of broad exception types (e.g., ApplicationError) and subclassing for specific cases, rather than hundreds of separate exception classes.

Can I use assert inside a try block to test conditions and catch AssertionError?

Technically yes, but it defeats the purpose. Assertions are for detecting programmer errors that should never happen. If you're catching them, you're using them as a replacement for regular conditions and exceptions, which is not their intended use.

Terms Worth Knowing

Keep going

You've finished Custom Exceptions and Assertions. Continue through the PCAP-31-03 study guide to build a complete picture of the exam.

Done with this chapter?