Build1 publisherNot yet confirmed elsewhere2 min readPublished
One pylint warning, two identical lines: astroid #3077 and the generator that ate its fallback
Two runtime-equivalent calls, one no-member error. The cause sits in astroid's implicit __call__ path, where a failed attribute lookup kills the generator before the correct branch runs.
The Engineer · Build desk
What happened
- astroid issue #3077 reported identical typing.cast(T, self) expressions inferring differently depending only on how the surrounding call was written.
- In the reproducer, pylint emitted E1101 no-member on the explicit self.separator.run() line only, leaving the equivalent implicit self.separator() line clean.
- The proposed fix wraps the optional lookup in try/except InferenceError: pass so execution falls through to the __call__ resolution.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- constraint Reading the flagged line cannot resolve this class of report: the same expression is clean or dirty depending on the call syntax used elsewhere, so code review of the warning site leads nowhere.
- exposure Any astroid inference generator with an optional early lookup can lose the branches below it to a single InferenceError, and the loss shows up as a missing candidate rather than as a traceback.
- decision Maintainers now weigh whether consistency alone justifies a change that adds a diagnostic to previously quiet implicit-call sites, before the underlying cast inference is corrected.
An issue filed against `typing.cast` turned out not to involve cast handling at all. `infer_typing_cast`, tested on its own, behaves identically for both call styles [6], which moves the search upstream of `cast()` entirely: whatever differs, differs before the cast is ever evaluated.
What differs is dispatch. Written explicitly, `self.separator.run()` resolves to a `BoundMethod`, and astroid walks into the body of `Base.run()` and evaluates the cast correctly [8]. Written implicitly, `self.separator()` resolves to the `Instance` and goes through `BaseInstance.infer_call_result()` in `astroid/bases.py`, the path that exists to resolve the `__call__` dunder [9].
That function opens with an optional attempt to treat the call as a plain attribute lookup on the callee, iterating `igetattr(caller.func.attrname)` when the caller is a `Call` whose `func` is an `Attribute` [10]. For `self.separator()`, `attrname` is `"separator"`, and it is looked up on the `Base` instance, which has no such attribute, so the lookup raises `InferenceError` [11]. Because `infer_call_result` is a generator, that exception does not just fail the optional branch. It terminates the whole function, including the loop below it that does the real `__call__` resolution [12]. The comment sitting above that second loop describes the behaviour the exception prevents [13]. Nothing surfaces to the user: no traceback, just one fewer inference candidate than the source promises.
The author found this by monkey-patching pylint's own inference calls at runtime rather than editing installed files, in order to watch which path each syntax actually took [7].
Now the arithmetic, which the post does not spell out. Before the patch, the reproducer emits one `no-member` warning, on the explicit `.run()` line at 35:15 [5]. After it, the same file emits two, at 31:15 and 35:15, with the same message [15]. On the post's own account that both lines are equivalent and `sep` is a plain `str` at runtime [4], the count of wrong warnings goes from one to two [17]. The patch fixes the inconsistency; it does not fix the inference. The issue itself concedes that `Instance of <enclosing class>` is not necessarily the most precise answer for this expression [16].
That is still worth having, because an inconsistent engine is worse to work with than a uniformly wrong one. A warning that appears on one of two structurally symmetric lines [2] cannot be triaged by staring at the line, and a developer who checks the unflagged twin will conclude, reasonably and wrongly, that something about their own code differs. Once both report the same thing, the remaining disagreement is between astroid and `cast`, which is a bug with one address.
What to watch
- Whether the try/except lands in astroid upstream and in which release, since the post shows a diff rather than a merged change.
- Whether astroid is taught to infer cast(str, self) as str rather than the enclosing class, which is what would actually clear both warnings.
- Whether the fix produces new no-member reports on real codebases once the implicit __call__ path stops dying early.
Clarity's read
What the record supports and how the coverage leans. The claims behind it follow.
Reality
- Evidence60
- Adoption
- Insufficient
- Hype gap+22
- Incentives58
- Confidence52
Claim ledger
Ranked by verification strength, evidence, and original report placement.
- [1]
astroid is the static-analysis engine that powers pylint; instead of running code it builds a model of what the code would do, a process called inference.
- [2]
In astroid issue #3077, identical typing.cast(T, self) expressions were inferred differently depending only on how the surrounding call was written, even when the code was structurally symmetric.
- [3]
The reproducer defines a class Base whose __call__ and run methods both return cast(str, self), and a class IrJoin that calls self.separator() in one method and self.separator.run() in the other before calling .join(items) on the result.
- [4]
The author verified by actually running the file that both self.separator() and self.separator.run() do the exact same thing at runtime, and states that sep is a plain str in both cases at runtime.
- [5]
Before the change, pylint reported a single error on the reproducer: t5.py:35:15: E1101: Instance of 'Base' has no 'join' member (no-member), flagging only the explicit .run() path and not the equivalent implicit __call__ path.
- [6]
The author's first hypothesis was infer_typing_cast, the function handling typing.cast(), but tested in isolation it behaved identically for both call styles, indicating the bug lived upstream of cast() entirely.
- [7]
The author live-patched pylint's own inference calls with a small monkey-patching script rather than editing installed files, in order to observe astroid's real behaviour without risking the environment.
- [8]
self.separator.run() resolves to a BoundMethod, which walks normally into Base.run()'s body and evaluates cast() correctly.
- [9]
self.separator() resolves to the Instance itself and is routed through BaseInstance.infer_call_result() in astroid/bases.py, the code path responsible for resolving implicit __call__ dunder calls.
- [10]
BaseInstance.infer_call_result begins with an optional step that, when the caller is a nodes.Call whose func is a nodes.Attribute, iterates self.igetattr(caller.func.attrname, context) and yields the results as if the call were a plain attribute lookup on the callee.
- [11]
For self.separator(), that first branch tries to look up an attribute literally named "separator" on the Base instance, which has no such attribute, and the lookup raises InferenceError.
- [12]
Because infer_call_result is a generator function, an unhandled exception anywhere inside it terminates the entire function immediately, including the second loop just below that resolves __call__ correctly.
- [13]
The source comment above the second loop reads "Otherwise we infer the call to the __call__ dunder normally", but the code never got the chance to reach it.
- [14]
The fix wraps the first branch in try/except InferenceError: pass, so a failed attribute lookup no longer aborts the whole function and execution falls through to the __call__ resolution below.
- [15]
After the change, both call styles resolve the same way and pylint reports two errors on the reproducer: t5.py:31:15 and t5.py:35:15, both E1101 Instance of 'Base' has no 'join' member (no-member).
- [16]
As the original issue notes, "Instance of <enclosing class>" is not necessarily the most precise answer.
- [17]
The change takes the reproducer from one no-member warning to two; since the post holds that both lines are correct at runtime, the number of wrong warnings on that file rises from one to two.
Sources
1 independent publisher whose own reporting we read for this story.
- dev.toThe Bug That Hid Behind Its Own Comment: Fixing Inconsistent Inference in astroid
1 article · August 21, 2026
Topics and entities
Follow any of these and your For You feed starts watching them — no settings page required.
Topics
- Open-source maintainer economicsFollow
- Type InferenceFollow
- Static AnalysisFollow
- Python ToolingFollow
- Debugging PracticeFollow