Skip to content

Sort extra keys when formatting ForbiddenExtraKeysError - #776

Merged
Tinche merged 1 commit into
python-attrs:mainfrom
MaxFreedomPollard:sort-forbidden-extra-keys
Sep 7, 2026
Merged

Sort extra keys when formatting ForbiddenExtraKeysError#776
Tinche merged 1 commit into
python-attrs:mainfrom
MaxFreedomPollard:sort-forbidden-extra-keys

Conversation

@MaxFreedomPollard

Copy link
Copy Markdown
Contributor

transform_error reports the extra keys of a ForbiddenExtraKeysError in a different order on every run. ForbiddenExtraKeysError.extra_fields is a set, and format_exception in src/cattrs/v.py line 44 joins it as it comes: res = f"extra fields found ({', '.join(exc.extra_fields)})". String hashing is randomized per process, so the message text changes between runs as soon as there is more than one extra key, and anything that compares or snapshots the output of transform_error fails intermittently.

ForbiddenExtraKeysError.__str__ in src/cattrs/errors.py line 141 already sorts the same set, so the two renderings of a single error disagree with each other too.

from attrs import define
from cattrs import Converter, transform_error

@define
class C:
    a: int

c = Converter(forbid_extra_keys=True, detailed_validation=True)
try:
    c.structure({"a": 1, "b": 2, "c": 3, "d": 4, "e": 5}, C)
except Exception as exc:
    print(transform_error(exc))
    print(str(exc.exceptions[0]))

Three runs of that, on main:

['extra fields found (c, d, b, e) @ $']
Extra fields in constructor for C: b, c, d, e
['extra fields found (e, c, d, b) @ $']
Extra fields in constructor for C: b, c, d, e
['extra fields found (d, c, b, e) @ $']
Extra fields in constructor for C: b, c, d, e

The fix is to sort in format_exception as well. The regression test uses five extra keys, because the existing test_class_errors only ever produces one and so cannot see an ordering at all.

Verification, all on the branch unless stated otherwise:

uv run --all-extras --group test --group lint pytest tests/test_v.py::test_extra_keys_are_sorted -q under PYTHONHASHSEED 1 through 8 with the test applied to unmodified main: 8 failures out of 8, for example At index 0 diff: 'extra fields found (c, d, b, e) @ $' != 'extra fields found (b, c, d, e) @ $'. The same command on tests/test_v.py with the fix under PYTHONHASHSEED 1 through 12: 12 passes out of 12.

uv run --all-extras --group test --group lint pytest -n 2 tests -q on 3.11: 941 passed, 15 xfailed.

uv run -p <version> ... pytest tests/test_v.py -q on 3.10, 3.11, 3.14 and pypy3.10: 10 passed each. I ran pypy without --all-extras because ujson has no pypy wheel on macOS, which is unrelated to this change.

uv run -p python3.14 --group lint ruff check src/ tests bench: All checks passed. uv run -p python3.14 --group lint ruff format --check src tests docs/conf.py: 115 files already formatted.

`ForbiddenExtraKeysError.extra_fields` is a `set`, and `format_exception`
in src/cattrs/v.py joined it directly, so `transform_error` put the extra
keys in whatever order that set happened to iterate in. String hashing is
randomized per process, so the message text changed from run to run once
more than one extra key was present, which breaks anything that compares
or snapshots the output of `transform_error`.

`ForbiddenExtraKeysError.__str__` in src/cattrs/errors.py already sorts
the same set, so the two renderings of one error disagreed as well.

Sort the keys in `format_exception` too, and add a regression test with
five extra keys, since the existing test only used one and could not
observe the ordering.
@codspeed-hq

codspeed-hq Bot commented Sep 7, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 64 untouched benchmarks


Comparing MaxFreedomPollard:sort-forbidden-extra-keys (f0c5a80) with main (df4e4bb)

Open in CodSpeed

@Tinche

Tinche commented Sep 7, 2026

Copy link
Copy Markdown
Member

LGTM, thanks!

@Tinche
Tinche merged commit bc34a46 into python-attrs:main Sep 7, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants