Skip to content

Attribute assigned through an overridable factory class attribute infers to the base class (new no-member false positives in the pylint primer) #3210

Description

@Pierre-Sassoulas

Steps to reproduce

class Header:
    def process(self):
        return self.data.is_multiline  # <-- pylint: no-member


class Data:
    pass


class SpecialData(Data):
    def __init__(self):
        self.is_multiline = True


class Reader:
    header_class = Header
    data_class = Data

    def __init__(self):
        self.header = self.header_class()
        self.data = self.data_class()
        self.header.data = self.data


class SpecialReader(Reader):
    data_class = SpecialData


print(SpecialReader().header.process())  # works at runtime: prints True

Astroid-level demonstration (self.data inside Header.process):

import astroid

mod = astroid.parse(code)
attr = mod.body[0].body[0].body[0].value.expr  # `self.data` in Header.process
print(list(attr.infer()))

Current behavior

  • astroid 4.2.0b5: [Uninferable] → pylint stays silent.
  • astroid 4.3.0: [<Instance of .Data>] → pylint emits E1101: Instance of 'Data' has no 'is_multiline' member even though the code works at runtime: SpecialReader overrides data_class, so self.data is a SpecialData at runtime and is_multiline exists.

Bisects to 8bb408f (#3106, "Fix classmethod inference boundnode leak"). That fix is legitimate — it makes self in Reader.__init__ resolve where it previously didn't, so the cross-object assignment self.header.data = self.data now lands in Header.instance_attrs with the inferred value Data() — anchored to the base class from the data_class class attribute, ignoring subclass overrides. Before #3106 the whole chain was Uninferable, which masked the problem.

Expected behavior

Ideally the inferred value of self.data would account for subclass overrides of the factory class attribute (union of Data/SpecialData), or stay Uninferable when the value flows through an overridable class attribute — either avoids the false positive. Anchoring to the single base class asserts more than astroid knows.

Real-world impact

Seen in the pylint primer when upgrading pylint's pin to 4.3.0 (pylint-dev/pylint#11234, primer comment):

  • 6 new messages in astropy (astropy.io.ascii: BaseReader.__init__ does exactly the pattern above with header_class / data_class, e.g. Instance of 'BaseData' has no 'is_multiline' member in daophot.py, self.header.start_line is not callable in latex.py),
  • 1–2 in django (clone().where inferred as Node while the runtime object is a WhereNode with resolve_expression).

The overall primer effect of 4.3.0 is strongly positive (~70 false positives removed); this is the one new false-positive family it surfaced.

python -c "from astroid import __pkginfo__; print(__pkginfo__.version)" output

4.3.0

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