<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Kartikey</title>
    <description>The latest articles on DEV Community by Kartikey (@kartikey_d47d5d0a50247b86).</description>
    <link>https://dev.to/kartikey_d47d5d0a50247b86</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4088476%2F7a2bf67b-bfc0-4e57-bdd9-0978f75907f8.png</url>
      <title>DEV Community: Kartikey</title>
      <link>https://dev.to/kartikey_d47d5d0a50247b86</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kartikey_d47d5d0a50247b86"/>
    <language>en</language>
    <item>
      <title>The Bug That Hid Behind Its Own Comment: Fixing Inconsistent Inference in astroid</title>
      <dc:creator>Kartikey</dc:creator>
      <pubDate>Sat, 22 Aug 2026 06:20:26 +0000</pubDate>
      <link>https://dev.to/kartikey_d47d5d0a50247b86/the-bug-that-hid-behind-its-own-comment-fixing-inconsistent-inference-in-astroid-10io</link>
      <guid>https://dev.to/kartikey_d47d5d0a50247b86/the-bug-that-hid-behind-its-own-comment-fixing-inconsistent-inference-in-astroid-10io</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/bugsmash"&gt;DEV's Summer Bug Smash: Clear the Lineup&lt;/a&gt; powered by &lt;a href="https://sentry.io/" rel="noopener noreferrer"&gt;Sentry&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Project Overview
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/pylint-dev/astroid" rel="noopener noreferrer"&gt;astroid&lt;/a&gt; is the static-analysis engine that powers &lt;a href="https://pylint.org/" rel="noopener noreferrer"&gt;pylint&lt;/a&gt; — one of the most widely used linters in the Python ecosystem. Instead of running your code, astroid builds a model of what your code &lt;em&gt;would&lt;/em&gt; 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug Fix or Performance Improvement
&lt;/h2&gt;

&lt;p&gt;I picked up &lt;a href="https://github.com/pylint-dev/astroid/issues/3077" rel="noopener noreferrer"&gt;astroid issue #3077&lt;/a&gt;: identical &lt;code&gt;typing.cast(T, self)&lt;/code&gt; expressions were being inferred &lt;em&gt;differently&lt;/em&gt; depending only on how the surrounding call was written — even when the code was structurally symmetric.&lt;/p&gt;

&lt;p&gt;In a class like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Base&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__call__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;cast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;cast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;IrJoin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;separator&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Base&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__call__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;sep&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;separator&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;       &lt;span class="c1"&gt;# implicit __call__ sugar
&lt;/span&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;sep&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;sep&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;separator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;   &lt;span class="c1"&gt;# explicit method call
&lt;/span&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;sep&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both &lt;code&gt;self.separator()&lt;/code&gt; and &lt;code&gt;self.separator.run()&lt;/code&gt; do the exact same thing at runtime — I verified this by actually running the file. But pylint only flagged one of them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; pylint t5.py
&lt;span class="go"&gt;t5.py:35:15: E1101: Instance of 'Base' has no 'join' member (no-member)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The explicit &lt;code&gt;.run()&lt;/code&gt; path got a false positive; the equivalent implicit &lt;code&gt;__call__&lt;/code&gt; path did not, even though &lt;code&gt;sep&lt;/code&gt; is a plain &lt;code&gt;str&lt;/code&gt; in both cases at runtime.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;PR:&lt;/strong&gt; &lt;a href="https://github.com/pylint-dev/astroid/pull/3242" rel="noopener noreferrer"&gt;https://github.com/pylint-dev/astroid/pull/3242&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  My Improvements
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Ruling out the obvious suspect
&lt;/h3&gt;

&lt;p&gt;My first hypothesis was &lt;code&gt;infer_typing_cast&lt;/code&gt;, the function that handles &lt;code&gt;typing.cast()&lt;/code&gt; itself — it seemed like the natural place for a cast-related inconsistency to live. Tested in isolation, though, it behaves &lt;em&gt;identically&lt;/em&gt; for both call styles. Dead end — but a useful one, because it told me the bug lived somewhere upstream of &lt;code&gt;cast()&lt;/code&gt; entirely.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tracing the real divergence
&lt;/h3&gt;

&lt;p&gt;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 &lt;strong&gt;completely different astroid code paths&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;self.separator.run()&lt;/code&gt; resolves to a &lt;code&gt;BoundMethod&lt;/code&gt;, which walks normally into &lt;code&gt;Base.run()&lt;/code&gt;'s body and evaluates &lt;code&gt;cast()&lt;/code&gt; correctly.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;self.separator()&lt;/code&gt; resolves to the &lt;code&gt;Instance&lt;/code&gt; itself, routed through &lt;code&gt;BaseInstance.infer_call_result()&lt;/code&gt; — the code path specifically responsible for resolving implicit &lt;code&gt;__call__&lt;/code&gt; dunder calls.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Finding the actual bug
&lt;/h3&gt;

&lt;p&gt;Inside &lt;code&gt;BaseInstance.infer_call_result&lt;/code&gt; (&lt;code&gt;astroid/bases.py&lt;/code&gt;), there's an &lt;em&gt;optional&lt;/em&gt; first step that tries to resolve the call as if it were a plain attribute lookup on the callee:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;caller&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Call&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;caller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;func&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Attribute&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;igetattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;caller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;func&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;attrname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;inferred&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt;

&lt;span class="c1"&gt;# Otherwise we infer the call to the __call__ dunder normally
&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_proxied&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;igetattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__call__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For &lt;code&gt;self.separator()&lt;/code&gt;, that first branch tries to look up an attribute literally named &lt;code&gt;"separator"&lt;/code&gt; — on the &lt;code&gt;Base&lt;/code&gt; instance, which obviously has no such attribute. That lookup raises an &lt;code&gt;InferenceError&lt;/code&gt;. Because this is a &lt;strong&gt;generator function&lt;/strong&gt;, an unhandled exception anywhere inside it terminates the &lt;em&gt;entire&lt;/em&gt; function immediately — including the second loop just below, which is the code that actually resolves &lt;code&gt;__call__&lt;/code&gt; correctly.&lt;/p&gt;

&lt;p&gt;The comment right there in the source literally says &lt;em&gt;"Otherwise we infer the call to the &lt;code&gt;__call__&lt;/code&gt; dunder normally"&lt;/em&gt; — but the code never got the chance to reach it. The bug was hiding directly behind its own explanation.&lt;/p&gt;

&lt;h3&gt;
  
  
  The fix
&lt;/h3&gt;

&lt;p&gt;A small, surgical change: wrap that first branch in &lt;code&gt;try/except InferenceError: pass&lt;/code&gt;, so a failed attribute lookup no longer aborts the whole function — it simply falls through to the &lt;code&gt;__call__&lt;/code&gt; resolution below, exactly as the existing comment always promised.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt; if isinstance(caller, nodes.Call) and isinstance(caller.func, nodes.Attribute):
&lt;span class="gd"&gt;-    for res in self.igetattr(caller.func.attrname, context):
-        inferred = True
-        yield res
&lt;/span&gt;&lt;span class="gi"&gt;+    try:
+        for res in self.igetattr(caller.func.attrname, context):
+            inferred = True
+            yield res
+    except InferenceError:
+        pass
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Result&lt;/strong&gt; — both call styles now consistently resolve the same way:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;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)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(As the original issue notes, &lt;code&gt;Instance of &amp;lt;enclosing class&amp;gt;&lt;/code&gt; isn't necessarily the &lt;em&gt;most precise&lt;/em&gt; answer &lt;code&gt;cast()&lt;/code&gt; could give — but consistency is what the bug was actually about, and this fix delivers it cleanly.)&lt;/p&gt;

&lt;h3&gt;
  
  
  Testing
&lt;/h3&gt;

&lt;p&gt;I added a regression test, &lt;code&gt;test_infer_call_result_dunder_call_consistent_with_attribute_call&lt;/code&gt;, reproducing the minimal case directly in astroid's own suite (&lt;code&gt;tests/test_inference.py&lt;/code&gt;), and ran the &lt;strong&gt;entire existing test suite&lt;/strong&gt; (2,000+ tests) to confirm nothing else broke. The only failures present were pre-existing, unrelated Windows-environment issues (symlink permissions, missing fixtures) and one unrelated &lt;code&gt;TypedDict&lt;/code&gt; failure — confirmed via &lt;code&gt;git stash&lt;/code&gt; to also occur on unmodified &lt;code&gt;main&lt;/code&gt;, ruling out any regression from this change.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>bugsmash</category>
    </item>
  </channel>
</rss>
