Alternatives to BaseExceptionGroup.derive() for Custom Exception Groups


What is BaseExceptionGroup?

  • It serves as a base class for more specific exception groups.
  • It's a class within the exceptions module that represents a collection of exceptions.

What is BaseExceptionGroup.derive()?

  • The new group inherits the properties (message, traceback, cause, context, and notes) from the original group.

  • It's a method defined in BaseExceptionGroup that creates a new exception group derived from the calling group.

Purpose of BaseExceptionGroup.derive()

  • It's intended to be used for creating new exception groups that share characteristics with an existing group, but potentially wrap different exceptions or have additional modifications.

Example (Limited functionality due to the current issue)

from exceptions import BaseExceptionGroup

class MyCustomGroup(BaseExceptionGroup):
    # ... (custom logic)

original_group = ExceptionGroup(...)  # Create an exception group

derived_group = original_group.derive()  # Create a derived group

# Derived group will (ideally) have the same message and traceback as original_group
# However, `cause`, `context`, and `notes` might not be preserved due to the issue.
  • Consider alternative approaches for creating customized exception groups if attribute preservation is crucial.
  • Due to the current limitation, BaseExceptionGroup.derive() might not be fully reliable for preserving all attributes.


from exceptions import BaseExceptionGroup, Exception, ValueError

class MyCustomGroup(BaseExceptionGroup):
    def __init__(self, message, cause=None, context=None, notes=None):
        super().__init__(message)  # Call base group's constructor with message
        self.cause = cause
        self.context = context
        self.notes = notes

def create_original_group():
    try:
        10 / 0  # Deliberate division by zero to raise a ZeroDivisionError
    except ZeroDivisionError as exc:
        cause_exc = ValueError("Invalid input value")  # Simulate a cause exception
        return MyCustomGroup(
            message="Division by zero occurred",
            cause=cause_exc,
            context={"operation": "division"},
            notes=["Double-check your input values"]
        )
    except Exception as exc:  # Catch any other exceptions
        return MyCustomGroup(message=str(exc))  # Use the exception's message

original_group = create_original_group()

try:
    # Code that might raise the original_group exception
    raise original_group
except MyCustomGroup as derived_group:
    print("Derived group message:", derived_group.message)
    # Due to the current issue, the following attributes might not be preserved:
    print("Derived group cause:", derived_group.cause)  # Might be None
    print("Derived group context:", derived_group.context)  # Might be empty
    print("Derived group notes:", derived_group.notes)  # Might be empty

    # Handle the exception based on the preserved message and potentially additional logic
  1. MyCustomGroup
    This custom group inherits from BaseExceptionGroup and adds attributes for cause, context, and notes.
  2. create_original_group
    This function simulates raising an exception with additional information. It raises a ZeroDivisionError with a custom ValueError as cause, context, and notes.
  3. original_group
    The created group has the message, cause, context, and notes (ideally).
  4. Raising and catching the exception
    The code deliberately raises the original_group exception, which is then caught using the MyCustomGroup type hint.
  5. Accessing attributes
    • derived_group.message should be preserved as it's part of the base group's functionality.
    • derived_group.cause, derived_group.context, and derived_group.notes might be None or empty due to the current issue in BaseExceptionGroup.derive().
  • If preserving all attributes is critical, consider alternative approaches like creating a new derived exception class with custom attributes and manually assigning values during exception creation.
  • This example highlights the potential limitations of BaseExceptionGroup.derive().


Manual Subclassing

  • When raising the exception, create an instance of the new class and set the desired attributes.
  • Define custom attributes within the new class to store additional information like cause, context, and notes.
  • Create a new class that inherits directly from Exception or a relevant base exception class.
class MyCustomException(Exception):
    def __init__(self, message, cause=None, context=None, notes=None):
        super().__init__(message)
        self.cause = cause
        self.context = context
        self.notes = notes

try:
    # Code that might raise an exception
    raise MyCustomException("An error occurred", cause=ValueError("Invalid input"), context={"operation": "calculation"}, notes=["Review input values"])
except MyCustomException as exc:
    print(exc.message)
    print("Cause:", exc.cause)
    print("Context:", exc.context)
    print("Notes:", exc.notes)

Composing Exceptions

  • You can also add additional attributes for cause, context, and notes within the wrapper exception.
  • Store the original exception as an attribute within the wrapper exception.
  • Wrap the original exception in another exception class.
class MyCustomWrapper(Exception):
    def __init__(self, original_exc, cause=None, context=None, notes=None):
        super().__init__(str(original_exc))  # Use original exception's message
        self.original_exception = original_exc
        self.cause = cause
        self.context = context
        self.notes = notes

try:
    # Code that might raise an exception
    10 / 0
except ZeroDivisionError as exc:
    raise MyCustomWrapper(exc, cause=ValueError("Invalid input value"), context={"operation": "division"}, notes=["Double-check division"])
except Exception as exc:
    raise MyCustomWrapper(exc)  # Wrap any other exception

except MyCustomWrapper as exc:
    print(exc)  # Access message from superclass
    print("Original exception:", exc.original_exception)
    print("Cause:", exc.cause)
    print("Context:", exc.context)
    print("Notes:", exc.notes)
  • If you want to capture the original exception's behavior and add custom information, composing exceptions can be useful.
  • If you need a simple exception group with additional attributes, manual subclassing is often sufficient.