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
Steps to reproduce
Astroid-level demonstration (
self.datainsideHeader.process):Current behavior
[Uninferable]→ pylint stays silent.[<Instance of .Data>]→ pylint emitsE1101: Instance of 'Data' has no 'is_multiline' membereven though the code works at runtime:SpecialReaderoverridesdata_class, soself.datais aSpecialDataat runtime andis_multilineexists.Bisects to 8bb408f (#3106, "Fix classmethod inference boundnode leak"). That fix is legitimate — it makes
selfinReader.__init__resolve where it previously didn't, so the cross-object assignmentself.header.data = self.datanow lands inHeader.instance_attrswith the inferred valueData()— anchored to the base class from thedata_classclass attribute, ignoring subclass overrides. Before #3106 the whole chain wasUninferable, which masked the problem.Expected behavior
Ideally the inferred value of
self.datawould account for subclass overrides of the factory class attribute (union ofData/SpecialData), or stayUninferablewhen 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):
astropy.io.ascii:BaseReader.__init__does exactly the pattern above withheader_class/data_class, e.g.Instance of 'BaseData' has no 'is_multiline' memberindaophot.py,self.header.start_line is not callableinlatex.py),clone().whereinferred asNodewhile the runtime object is aWhereNodewithresolve_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)"output4.3.0