Create a custom Python exception with a custom message and extra attributes.
A custom exception is a class that inherits from Exception, or from a more specific built-in exception type.
class ValidationError(ValueError):
def __init__(self, field, message):
super().__init__(f"{field}: {message}")
self.field = field
self.message = message
try:
raise ValidationError("email", "must contain @")
except ValidationError as err:
print(err) # email: must contain @
print(err.field) # email
print(isinstance(err, ValueError)) # TrueThe class inherits from ValueError rather than Exception, and that is the important design decision. It means callers who already handle ValueError — which plenty of validation code does — will catch your error too, without knowing it exists. Deriving from a built-in exception is how you fit your error into an existing hierarchy rather than creating an island.
The custom __init__ builds a message that includes the field name, calls super().__init__ with it, and then stores the field and the raw message on the instance. Calling super().__init__ is what makes str(err) work, so the error reads properly in a traceback while still carrying structured data for programmatic use.
Two habits worth keeping. Store the structured data on the instance rather than only in the message, so callers can react to it without parsing strings. And keep the class small — an exception that carries data and nothing else is easy to test and easy to document.