The Bug That Hid Behind Its Own Comment: Fixing Inconsistent Inference in astroid A developer fixed a bug in astroid, the static-analysis engine behind pylint, where identical typing.cast expressions were inferred differently depending on the call style, causing false positives. The issue was traced to an unhandled InferenceError in BaseInstance.infer_call_result that prematurely terminated the generator, skipping the correct __call__ resolution. The fix, submitted in PR #3242, ensures consistent inference for implicit and explicit method calls. This is a submission for DEV's Summer Bug Smash: Clear the Lineup powered by Sentry. astroid https://github.com/pylint-dev/astroid is the static-analysis engine that powers pylint https://pylint.org/ — one of the most widely used linters in the Python ecosystem. Instead of running your code, astroid builds a model of what your code would do a process called "inference" so pylint can catch real bugs before you ever hit run. That means astroid's inference logic has to be extremely consistent: if it gets confused about what a piece of code returns, pylint either misses real bugs, or — as in this case — flags perfectly correct code as broken. I picked up astroid issue 3077 https://github.com/pylint-dev/astroid/issues/3077 : identical typing.cast T, self expressions were being inferred differently depending only on how the surrounding call was written — even when the code was structurally symmetric. In a class like this: php class Base: def call self - str: return cast str, self def run self - str: return cast str, self class IrJoin: separator: Base def call self, items : sep: str = self.separator implicit call sugar return sep.join items def run self, items : sep: str = self.separator.run explicit method call return sep.join items Both self.separator and self.separator.run do the exact same thing at runtime — I verified this by actually running the file. But pylint only flagged one of them: bash $ python -m pylint t5.py t5.py:35:15: E1101: Instance of 'Base' has no 'join' member no-member The explicit .run path got a false positive; the equivalent implicit call path did not, even though sep is a plain str in both cases at runtime. PR: https://github.com/pylint-dev/astroid/pull/3242 https://github.com/pylint-dev/astroid/pull/3242 My first hypothesis was infer typing cast , the function that handles typing.cast itself — it seemed like the natural place for a cast-related inconsistency to live. Tested in isolation, though, it behaves identically for both call styles. Dead end — but a useful one, because it told me the bug lived somewhere upstream of cast entirely. I live-patched pylint's own inference calls with a small monkey-patching script rather than editing installed files directly, so I could observe astroid's real behavior without risking my environment and found the two call styles actually go through completely different astroid code paths : self.separator.run resolves to a BoundMethod , which walks normally into Base.run 's body and evaluates cast correctly. self.separator resolves to the Instance itself, routed through BaseInstance.infer call result — the code path specifically responsible for resolving implicit call dunder calls.Inside BaseInstance.infer call result astroid/bases.py , there's an optional first step that tries to resolve the call as if it were a plain attribute lookup on the callee: if isinstance caller, nodes.Call and isinstance caller.func, nodes.Attribute : for res in self.igetattr caller.func.attrname, context : inferred = True yield res Otherwise we infer the call to the call dunder normally for node in self. proxied.igetattr " call ", context : ... For self.separator , that first branch tries to look up an attribute literally named "separator" — on the Base instance, which obviously has no such attribute. That lookup raises an InferenceError . Because this is a generator function , an unhandled exception anywhere inside it terminates the entire function immediately — including the second loop just below, which is the code that actually resolves call correctly. The comment right there in the source literally says "Otherwise we infer the call to the call dunder normally" — but the code never got the chance to reach it. The bug was hiding directly behind its own explanation. A small, surgical change: wrap that first branch in try/except InferenceError: pass , so a failed attribute lookup no longer aborts the whole function — it simply falls through to the call resolution below, exactly as the existing comment always promised. if isinstance caller, nodes.Call and isinstance caller.func, nodes.Attribute : - for res in self.igetattr caller.func.attrname, context : - inferred = True - yield res + try: + for res in self.igetattr caller.func.attrname, context : + inferred = True + yield res + except InferenceError: + pass Result — both call styles now consistently resolve the same way: t5.py:31:15: E1101: Instance of 'Base' has no 'join' member no-member t5.py:35:15: E1101: Instance of 'Base' has no 'join' member no-member As the original issue notes, Instance of