Skip to content

Passing these=d to attr.s skips attributes defined in class body #621

Open

Description

@perkinslr
@attr.s(these=dict(a=attr.ib()))
class test:
    b: int = attr.ib(default=0)

c = test(5)
print(c.b)

This fails even if passing auto_attribs=True, albeit in a slightly different way.

The obvious workaround is to only put attributes in these, but that is harder for code analysis tools to understand (it lacks type annotations, even if the tools are attrs aware).

The second workaround would be to put the attributes from these into the class body, but that doesn't work if the attribute is inherited or describe a data descriptor (like getter/setters).

The final workaround is to duplicate the definition into both these and the class body, but that creates an opportunity for the two to get out of sync.

Anyway, ideally, attrs would add anything from the class body which is an instance of _CountingAttr, (and anything from the annotations dict if auto_attribs is set). If that is not practical, then a warning or exception should be raised rather than having weird runtime behavior.

Activity

  1. wbolster commented on Mar 21, 2022

    @wbolster
    Member

    taking one step back, what is the use case for using both styles at the same time?

  2. perkinslr commented on Mar 21, 2022

    @perkinslr
    Author

    I believe I described the use case already, but to reiterate, passing a long list of attributes via the these parameters is ugly, but if you want to include an inherited data descriptor in your init/repr,eq and so on, you cannot put that annotation in the class body (maybe you can with the auto_attribs, but I don't think so).

  3. rawsun007 commented on Aug 21, 2026

    @rawsun007

    Still reproduces on main (0.1.dev20). The mechanism is the intersection of two separately documented behaviours, which may be why it has stayed confusing.

    >>> attr.fields(test)          # only 'a'
    >>> c.b
    _CountingAttr(counter=7, _default=0, repr=True, eq=True, order=True, hash=None,
                  init=True, on_setattr=None, alias=None, metadata={})
    

    Two lines cause it, both intentional on their own:

    1. _make.py, _transform_attrs: if these is not None: ca_list = list(these.items()). The class body is never consulted, exactly as the docstring says ("If these is passed, use that and don't look for them on the class"), so b is never collected as a field.
    2. _make.py: self._delete_attribs = not bool(these). With these passed, class-body attributes are not deleted either, per the 18.1.0 note ("If these is passed, no attributes are deleted from the class body").

    So b is neither collected nor cleaned up, and the _CountingAttr sentinel that would normally be consumed and removed survives as a plain class attribute. test(5).b then returns attrs' private placeholder rather than 0 or an AttributeError.

    The two documented behaviours are each reasonable; the undocumented part is the result, which is a private object reaching user code with no error. If the intent is to keep both, a note under these saying that attr.ib() in the class body will be left in place as an unconsumed sentinel would have answered this in 2020. If leaking _CountingAttr is not intended, the narrow fix is to raise when these is passed and the class body still contains _CountingAttr instances, which is detectable at exactly the point in (1) where the body is skipped.

    Not sending a patch since which of those you want is your call.

    Disclosure: I investigated this with Claude Code. I ran the reproducer and read both call sites myself; the line references above are from current main.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions