Skip to content

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

How we use AISend a correction

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
Why these scores

Claim ledger

Ranked by verification strength, evidence, and original report placement.

  1. [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.

    ReportedSupportedView cited source
  2. [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.

    ReportedSupportedView cited source
  3. [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.

    ReportedSupportedView cited source

Sources

1 independent publisher whose own reporting we read for this story.

  1. dev.to

    1 article · August 21, 2026

    The Bug That Hid Behind Its Own Comment: Fixing Inconsistent Inference in astroid

Share your take

Let Clarity write the post for you.

Signed-in readers get a short post drafted on this story in the register they choose — narrative, analytical, or a direct position — editable to the last word before it goes anywhere. The share buttons at the top of this story work without an account.

Topics and entities

Follow any of these and your For You feed starts watching them — no settings page required.

Loading related stories