Study guide

3.4 Custom Exceptions and Exception Chaining

OpenEDG PCAP — Certified Associate in Python Programming Exam

Track your progress as you read

Finished sections get a check mark.

1 Chapter 1: The PCAP-31-03 Exam Blueprint and Test-Day Logistics

1.1 The PCAP-31-03 Blueprint, Scoring, and Exam-Day Logistics

2 Chapter 2: Modules, Packages, and the Standard Library

2.1 Module Imports and Namespace Mechanics 2.2 Module Construction and Execution 2.3 Package Hierarchy and Navigation 2.4 Standard Library Modules and Pip

3 Chapter 3: Exceptions and Error Handling

3.1 Exception Control Flow and Syntax 3.2 The Built-in Exception Hierarchy 3.3 Raising Exceptions and Assertions 3.4 Custom Exceptions and Exception Chaining

4 Chapter 4: Strings, Encodings, and Text Processing

4.1 Character Encodings and String Immutability 4.2 String Indexing, Slicing, and Comparison 4.3 String Inspection and Classification Methods 4.4 String Transformation and Formatting Methods 4.5 Iterating, Sorting, and Comparing Strings Against Other Types

5 Chapter 5: Object-Oriented Programming Fundamentals

5.1 Classes, Instances, and Attributes 5.2 Encapsulation, Privacy, and Name Mangling 5.3 Dunder Methods and Object Lifecycle 5.4 Discovering Class Structure: __name__, __module__, __bases__, and Identity

6 Chapter 6: Advanced OOP, Inheritance, and Metaprogramming

6.1 Single Inheritance, Method Overriding, and Polymorphism 6.2 Multiple Inheritance and Method Resolution Order (MRO) 6.3 Class Methods, Static Methods, and Properties

7 Chapter 7: Functional Python, Comprehensions, and Closures

7.1 List, Dictionary, and Set Comprehensions 7.2 Lambda Expressions and Higher-Order Functions 7.3 Variable Scopes, the LEGB Rule, and Closures

8 Chapter 8: Iterators, Generators, and File I/O

8.1 Iterators and the Iteration Protocol 8.2 Generator Functions and Generator Expressions 8.3 File Input/Output and Context Managers 8.4 Predefined Streams, Handles vs. Streams, and errno Diagnostics

3.4 Custom Exceptions and Exception Chaining

Key Takeaways Explain This Topic AI

Custom exception classes must inherit from Exception (or a relevant standard exception subclass) to follow standard Python error conventions.

Custom constructors should call super(). init (*args) to properly initialize the .args tuple and ensure standard string representations function properly.

Implicit exception chaining automatically assigns the prior active exception to the context attribute when a secondary exception occurs inside an except or finally block.

Explicit exception chaining uses the raise NewException(...) from original_exc syntax to set the cause attribute and document direct causality in tracebacks.

Chained exception tracebacks can be suppressed using raise NewException(...) from None, setting suppress_context to True.

Last updated: August 2026

Custom Exceptions and Exception Chaining

While Python's built-in exceptions cover standard operating system, arithmetic, and data structure errors, complex applications require domain-specific error representations. Creating custom exception classes allows libraries and applications to signal domain failures (such as payment rejections, inventory shortages, or authentication timeouts) cleanly. Furthermore, Python 3 provides sophisticated exception chaining mechanisms to trace root-cause failures across architectural layers.

Defining User-Defined Exceptions

All user-defined exception classes must inherit from Exception (or one of its subclasses). Never inherit directly from BaseException .

The Minimalist Custom Exception

The simplest custom exception requires only a class definition and a pass statement: class ApplicationError(Exception):

  • """Base exception for all domain errors in this application."""
  • pass

class UserNotFoundError(ApplicationError):

  • """Raised when a requested user ID does not exist."""
  • pass

Inheriting from Exception automatically provides full support for constructor arguments, .args tuple storage, and standard traceback formatting.

Designing Domain Exception Hierarchies

In production systems, custom exceptions should be structured in a hierarchical tree. This allows consuming code to choose between catching broad category-level errors or handling granular, specific failures. class PaymentGatewayError(Exception):

  • """Root exception for payment processing system."""
  • pass

class CardValidationError(PaymentGatewayError):

  • """Raised when credit card format or checksum fails."""
  • pass

class ExpiredCardError(CardValidationError):

  • """Raised specifically when card expiration date is in the past."""
  • pass

class InsufficientFundsError(PaymentGatewayError):

  • """Raised when account balance cannot cover the charge."""
  • pass
  • Polymorphic Consumption

Callers can intercept errors at any desired level of granularity: try:

process_charge(user_card, amount=250.00)

except ExpiredCardError:

prompt_user_for_new_card()

except PaymentGatewayError as e:

  • # Catches InsufficientFundsError, CardValidationError, or general gateway errors
  • log_payment_failure(e)
  • Customizing Constructors and __str__

Custom exceptions can accept additional domain-specific attributes (e.g. error codes, timestamps, metadata) by overriding __init__() . Always invoke super().__init__() to keep the built-in .args attribute synchronized. class ValidationError(Exception):

def __init__(self, field_name, value, rule_violation):

  • self.field_name = field_name
  • self.value = value
  • self.rule_violation = rule_violation
  • # Generate a descriptive message
  • message = f"Field '{field_name}' with value '{value}' failed validation: {rule_violation}"
  • # Forward message to Exception base class to populate .args
  • super().__init__(message)

def __str__(self):

return f"[Validation Failure] {self.field_name}: {self.rule_violation} (received: {self.value})" try:

raise ValidationError("email", "invalid-user@", "Missing top-level domain")

except ValidationError as err:

  • print(f"Display: {err}")
  • print(f"Field: {err.field_name}")
  • print(f"Args: {err.args}")

# Output:

  • # Display: [Validation Failure] email: Missing top-level domain (received: invalid-user@)
  • # Field: email
  • # Args: ("Field 'email' with value 'invalid-user@' failed validation: Missing top-level domain",)
  • Exception Chaining: Implicit vs Explicit

When building multi-layered software (e.g. database adapters, web frameworks), an exception in a lower layer often triggers an exception in a higher layer. Python 3 provides built-in mechanisms to link these exceptions together.

1. Implicit Exception Chaining ( __context__ )

If an exception is raised while handling another exception inside an except or finally block, Python automatically captures the original exception and attaches it to the new exception's __context__ attribute: try:

int('invalid_number') # Raises ValueError

except ValueError:

  • # An unexpected error occurs while handling ValueError
  • data = 10 / 0 # Raises ZeroDivisionError

When this runs, Python's default traceback displays both errors linked with the message: Traceback (most recent call last):

  • File "script.py", line 2, in <module>
  • int('invalid_number')
  • ValueError: invalid literal for int() with base 10: 'invalid_number'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):

  • File "script.py", line 5, in <module>
  • data = 10 / 0
  • ZeroDivisionError: division by zero
  • 2. Explicit Exception Chaining ( __cause__ and raise ... from )

When deliberately translating a low-level exception into a high-level domain exception, use the raise NewException(...) from original_exc syntax. This explicitly populates the new exception's __cause__ attribute: class DatabaseConnectionError(Exception):

pass

def connect_to_database():

try:

  • import socket
  • s = socket.create_connection(('192.0.2.1', 5432), timeout=1.0)

except (socket.timeout, OSError) as original_err:

  • # Explicitly declare that original_err caused DatabaseConnectionError
  • raise DatabaseConnectionError("Unable to reach primary DB replica") from original_err

When explicit chaining is used, Python displays: Traceback (most recent call last):

  • File "script.py", line 7, in connect_to_database
  • s = socket.create_connection(('192.0.2.1', 5432), timeout=1.0)
  • TimeoutError: timed out

The above exception was the direct cause of the following exception:

Traceback (most recent call last):

  • File "script.py", line 9, in connect_to_database
  • raise DatabaseConnectionError("Unable to reach primary DB replica") from original_err
  • DatabaseConnectionError: Unable to reach primary DB replica
  • 3. Suppressing Exception Context ( raise ... from None )

In public APIs or security-sensitive modules, you may want to raise a clean domain exception without exposing low-level system details, file paths, or internal tracebacks. Using raise NewException(...) from None sets __suppress_context__ = True and suppresses the chained output: def get_secret_config(key):

try:

return raw_internal_dictionary[key]

except KeyError:

# Suppress internal dictionary KeyError and show only clean ConfigurationError

raise ConfigurationError(f"Configuration key '{key}' not set.") from None Traceback (most recent call last):

  • File "script.py", line 6, in get_secret_config
  • raise ConfigurationError(f"Configuration key '{key}' not set.") from None

ConfigurationError: Configuration key 'DATABASE_PASSWORD' not set.

Loading diagram... Custom Exception Hierarchy and Chaining Flow