Build1 distinct publisher2 min readUpdated
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
Compiled by The EngineerSomething wrong?How this is made
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.
Follow any of these and your For You feed starts watching them — no settings page required.
Ranked by verification strength, evidence, and original report placement.
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.
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.
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.
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.
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.
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.
Evidence-backed comparisons of source perspectives and observed adoption signals. Read the methodology
Which Builder, Operator, and Investor concerns the observed source mix emphasized—not a truth score.
Evidence, demonstrated adoption, hype gap, incentives, and confidence are assessed independently, each on its own current evidence. How these are measured.
Detailed but single-source and self-reported
The claim chain is unusually concrete for a single source: a minimal reproducer, the quoted astroid/bases.py branch, a mechanistic explanation of generator abort, the patch diff, and pasted pylint output before and after. That makes the diagnosis checkable in principle. But every element comes from one self-published post by the fix's author, with no maintainer confirmation, no linked review, and no second observer, so verification remains a reader exercise.
No adoption evidence supplied
The supplied material does not state whether the patch was submitted as a pull request, reviewed, merged into astroid, or shipped in any release, and no downstream user, deployment, or version is identified. Nothing supports an adoption reading in either direction, so this dimension is left unmeasured rather than inferred from the existence of a fix.
Mildly overstated: 'fixed cleanly' outruns the shown outcome
The post's framing — a surgical fix that delivers consistency cleanly — runs modestly ahead of what it demonstrates. On the author's own reproducer the change raises the warning count from one to two on lines the post says are correct at runtime, so consistency is obtained by propagating a false positive; the imprecision is conceded only in a parenthetical. Add the absence of any merge or release evidence and the claim of a resolved bug is somewhat stronger than the demonstrated state, though the technical substance is real, keeping the gap small rather than severe.
Contest submission by the patch author
The post opens by identifying itself as a submission to DEV's Summer Bug Smash powered by Sentry, and the narrator is the person who wrote the fix. Both facts create a disclosed incentive toward a tidy narrative arc — dead-end hypothesis, elegant root cause, surgical patch — and toward presenting the outcome as a win. The incentive is transparent rather than hidden, and the technical detail supplied is verifiable, which caps the score below the high range.
Moderate on mechanism, low on consequence
Confidence in the mechanical account — divergent inference paths, an InferenceError in the first branch, a generator that aborts before its fallback — is reasonably high because it is quoted from source and demonstrated with linter output. Confidence in what the story means is much lower: single publisher, author-as-subject, an explicit contest incentive, no upstream status, and an unexamined side effect from swallowing InferenceError. The blended value reflects a solid technical core inside a thin evidentiary and adoption envelope.
build
The duplicate def that ate the trim, and the lint rule nobody was running1 distinct publisher
build
A guard that only speaks in exit codes cannot tell you it stopped guarding1 distinct publisher
build
py-libp2p bounds a dev-only /sdp endpoint that believed whatever Content-Length it was told1 distinct publisher
build
A green test suite that proved nothing: aiortc's mangled cert slot versus libp2p certhash pinning1 distinct publisher
Distinct publishers with included, body-backed reporting in this cluster.
dev.to
1 article · August 21, 2026