Skip to content

evolve: accept an attribute's underlying name when it differs from the alias#1586

Closed
HrachShah wants to merge 5 commits into
python-attrs:mainfrom
HrachShah:fix/evolve-accept-attr-name-when-alias-dif
Closed

evolve: accept an attribute's underlying name when it differs from the alias#1586
HrachShah wants to merge 5 commits into
python-attrs:mainfrom
HrachShah:fix/evolve-accept-attr-name-when-alias-dif

Conversation

@HrachShah

Copy link
Copy Markdown

Problem

attrs.evolve(inst, name=...) raises TypeError: __init__() got an unexpected keyword argument when the caller uses the attribute's underlying storage name rather than its public alias. The generated __init__ itself only accepts the alias as a keyword, so a user who works out that the field is a and tries evolve(C(1), _a=2) (intending to set the underlying attribute) is met with a confusing crash instead of the field being updated.

Fix

In attrs.evolve(), when iterating the class's fields, accept either the alias (the existing path) or the underlying attribute name. If the caller's changes dict has a key matching the underlying name, rename it to the alias before delegating to the generated __init__. A name/alias collision (the same key in changes matching both) still raises, so the new path is strictly more permissive.

Test

tests/test_funcs.py::TestEvolve::test_private is updated: the case that asserted evolve(C(1), _a=2) raises is now a positive assertion that the call succeeds and sets _a to 2. The other expectations in that test (the alias a is the canonical address, and a name/alias collision still raises) are unchanged.

Verified: pytest tests/test_funcs.py -x -q -- 53 funcs tests pass; broader pytest tests/ -x --ignore=tests/attr_test_pytest.py -- 1401 tests pass overall (2 unrelated pre-existing failures in test_attr_test_pytest_3). The previous test_private assertion was the only behavior changed.

Zo Bot and others added 5 commits June 27, 2026 03:52
os.environb yields bytes on POSIX and the documented use case
for to_bool is reading values out of environment variables. The
truthy and falsy lookup tuples only contain str and int, so a
bytes('true') input raised 'Cannot convert value to bool: b\'true\''
even though its ASCII-decoded value is the canonical truthy
literal.

Decode bytes and bytearray as ASCII before the lowercase + lookup
step, matching the str path. Non-ASCII bytes raise ValueError
('Cannot convert value to bool: ...') the same way an unknown
string does, since the decoded text would still not match any
known literal. Unknown byte values raise the same ValueError.
When a class is rebuilt from attrs.fields() via attrs.make_class, the
'these' mapping contains Attribute instances rather than _CountingAttr
objects. from_counting_attr was only reading the private _default,
_validator, and _converter attributes, which Attribute does not expose,
so the rebuild crashed with "'Attribute' object has no attribute
'_default'".

Detect an Attribute argument and read the public default/validator/
converter attributes directly. Add a regression test and a changelog
entry.
…e alias

`attrs.evolve(inst, name=...)` previously raised a `TypeError` from the generated
__init__ when the attribute's underlying name (e.g. `_x` for a field declared
`x = attr.ib(alias='_x')`) was used as a keyword. The alias is the public way
to address a field, but a caller using the attribute's underlying name should
also be recognized, since `__init__` itself only accepts the alias -- a
caller would never have guessed that `evolve` is more restrictive.

Walk the changes dict and rename any entry keyed by the attribute's underlying
name to its alias before passing the dict on to `__init__`. The check happens
in evolve's existing per-attribute loop, so an attribute that has a custom
alias still works exactly as before, and a name/alias collision that would
have raised before still raises.

The test that locked in the previous behavior (`test_private`) is updated:
it now asserts that addressing the field by its underlying name succeeds,
which is the new contract.
@hynek hynek added the AI slop label Jul 9, 2026
@hynek hynek closed this Jul 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants