From d44c753bd1ba5642ab34a919d3658d2234cdd2a1 Mon Sep 17 00:00:00 2001 From: Laurie O Date: Thu, 26 Feb 2026 19:59:39 +1000 Subject: [PATCH 1/3] Add base exception --- src/cattrs/errors.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/cattrs/errors.py b/src/cattrs/errors.py index 21012bbd..13df1009 100644 --- a/src/cattrs/errors.py +++ b/src/cattrs/errors.py @@ -6,7 +6,11 @@ from cattrs._compat import ExceptionGroup -class StructureHandlerNotFoundError(Exception): +class CattrsError(Exception): + """Base ``cattrs`` exception.""" + + +class StructureHandlerNotFoundError(CattrsError): """ Error raised when structuring cannot find a handler for converting inputs into :attr:`type_`. @@ -21,7 +25,7 @@ def __str__(self) -> str: return self.message -class BaseValidationError(ExceptionGroup): +class BaseValidationError(ExceptionGroup, CattrsError): cl: type def __new__(cls, message: str, excs: Sequence[Exception], cl: type) -> Self: @@ -111,7 +115,7 @@ def group_exceptions( return excs_with_notes, other_excs -class ForbiddenExtraKeysError(Exception): +class ForbiddenExtraKeysError(CattrsError): """ Raised when `forbid_extra_keys` is activated and such extra keys are detected during structuring. From e4fb3751cc1ff26d96d94fda1bc846c0e5be7682 Mon Sep 17 00:00:00 2001 From: Laurie O Date: Thu, 26 Feb 2026 20:06:05 +1000 Subject: [PATCH 2/3] Avoid raising plain exception --- src/cattrs/converters.py | 5 +++-- src/cattrs/preconf/__init__.py | 3 ++- tests/preconf/test_pyyaml.py | 8 +++++--- tests/test_enums.py | 3 ++- 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/cattrs/converters.py b/src/cattrs/converters.py index 54d67a48..f787fe74 100644 --- a/src/cattrs/converters.py +++ b/src/cattrs/converters.py @@ -79,6 +79,7 @@ ) from .enums import enum_structure_factory, enum_unstructure_factory from .errors import ( + CattrsError, IterableValidationError, IterableValidationNote, StructureHandlerNotFoundError, @@ -708,7 +709,7 @@ def _structure_call(obj: Any, cl: type[T]) -> Any: @staticmethod def _structure_simple_literal(val, type): if val not in type.__args__: - raise Exception(f"{val} not in literal {type}") + raise CattrsError(f"{val} not in literal {type}") return val @staticmethod @@ -717,7 +718,7 @@ def _structure_enum_literal(val, type): try: return vals[val] except KeyError: - raise Exception(f"{val} not in literal {type}") from None + raise CattrsError(f"{val} not in literal {type}") from None def _structure_newtype(self, val: UnstructuredValue, type) -> StructuredValue: base = get_newtype_base(type) diff --git a/src/cattrs/preconf/__init__.py b/src/cattrs/preconf/__init__.py index 6cab7cb3..a15d0ffc 100644 --- a/src/cattrs/preconf/__init__.py +++ b/src/cattrs/preconf/__init__.py @@ -5,12 +5,13 @@ from .._compat import is_subclass from ..converters import Converter, UnstructureHook +from ..errors import CattrsError from ..fns import identity def validate_datetime(v, _): if not isinstance(v, datetime): - raise Exception(f"Expected datetime, got {v}") + raise CattrsError(f"Expected datetime, got {v}") return v diff --git a/tests/preconf/test_pyyaml.py b/tests/preconf/test_pyyaml.py index 61174afd..ff60d7b8 100644 --- a/tests/preconf/test_pyyaml.py +++ b/tests/preconf/test_pyyaml.py @@ -8,7 +8,7 @@ from yaml import safe_dump, safe_load from cattrs._compat import FrozenSetSubscriptable -from cattrs.errors import ClassValidationError +from cattrs.errors import CattrsError, ClassValidationError from cattrs.preconf.pyyaml import make_converter from ..test_preconf import Everything, everythings, native_unions @@ -70,13 +70,15 @@ class A: date: 1 """ - with raises(ClassValidationError if detailed_validation else Exception) as exc_info: + with raises( + ClassValidationError if detailed_validation else CattrsError + ) as exc_info: converter.loads(bad_data, A) if detailed_validation: assert ( repr(exc_info.value.exceptions[0]) - == "Exception('Expected datetime, got 1')" + == "CattrsError('Expected datetime, got 1')" ) assert ( repr(exc_info.value.exceptions[1]) == "ValueError('Expected date, got 1')" diff --git a/tests/test_enums.py b/tests/test_enums.py index efb28d51..bdf591f1 100644 --- a/tests/test_enums.py +++ b/tests/test_enums.py @@ -8,6 +8,7 @@ from cattrs import BaseConverter from cattrs._compat import Literal +from cattrs.errors import CattrsError from .untyped import enums_of_primitives @@ -27,7 +28,7 @@ def test_enum_failure(enum): converter = BaseConverter() type = Literal[next(iter(enum))] - with raises(Exception) as exc_info: + with raises(CattrsError) as exc_info: converter.structure("", type) assert exc_info.value.args[0] == f" not in literal {type!r}" From 7c8f9b5d5e396fab166c3202caf775e4457cfbbd Mon Sep 17 00:00:00 2001 From: Laurie O Date: Thu, 26 Feb 2026 20:13:59 +1000 Subject: [PATCH 3/3] Add history entry --- HISTORY.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/HISTORY.md b/HISTORY.md index f8f7df14..c59eeca1 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -15,6 +15,9 @@ Our backwards-compatibility policy can be found [here](https://github.com/python - Fix an `AttributeError` in `cattrs` internals that could be triggered by using the `include_subclasses` strategy in a `structure_hook_factory` ([#721](https://github.com/python-attrs/cattrs/issues/721), [#722](https://github.com/python-attrs/cattrs/pull/722)) +- Add `CattrsError` exception type: all exceptions raised by `cattrs` inherit from this + ([#728](https://github.com/python-attrs/cattrs/pull/728)) + - Literal and date-time validation raise this directly, instead of `Exception` ## 26.1.0 (2026-02-18)