<?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: Jonathan Townsend</title>
    <description>The latest articles on DEV Community by Jonathan Townsend (@jon-edward).</description>
    <link>https://dev.to/jon-edward</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3930240%2F9fb8993f-a770-40a9-93d1-6a2b47efdb68.jpeg</url>
      <title>DEV Community: Jonathan Townsend</title>
      <link>https://dev.to/jon-edward</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jon-edward"/>
    <language>en</language>
    <item>
      <title>stubgen-pyx: The stubs mypy can't generate</title>
      <dc:creator>Jonathan Townsend</dc:creator>
      <pubDate>Tue, 09 Jun 2026 00:20:10 +0000</pubDate>
      <link>https://dev.to/jon-edward/stubgen-pyx-the-stubs-mypy-cant-generate-4hkk</link>
      <guid>https://dev.to/jon-edward/stubgen-pyx-the-stubs-mypy-cant-generate-4hkk</guid>
      <description>&lt;p&gt;NVIDIA's &lt;a href="https://github.com/NVIDIA/cuda-python" rel="noopener noreferrer"&gt;cuda-python&lt;/a&gt;, the official Python bindings for the CUDA toolkit, recently added automatically-generated &lt;code&gt;.pyi&lt;/code&gt; stub files using &lt;a href="https://pypi.org/project/stubgen-pyx/" rel="noopener noreferrer"&gt;stubgen-pyx&lt;/a&gt;. Their description of why:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"This allows IDE auto-completion to work (which is also used by IDE-integrated coding agents). This has also found 2 real bugs in our code already. The ability to catch a certain class of bugs with this will be really helpful going forward, especially since our linting abilities with &lt;code&gt;cython-lint&lt;/code&gt; are a bit behind what they are in pure Python."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;When you commit &lt;code&gt;.pyi&lt;/code&gt; stubs alongside a Cython extension, you get an artifact your normal Python linting and type-checking pipeline &lt;em&gt;can&lt;/em&gt; analyze. Inconsistencies between what the Cython source does and what the stub claims become visible. NVIDIA found two real bugs this way before they were reported.&lt;/p&gt;

&lt;p&gt;I'm the author of stubgen-pyx, and this post is a technical walk-through of how those stubs are produced.&lt;/p&gt;




&lt;h2&gt;
  
  
  The problem with existing stub generators
&lt;/h2&gt;

&lt;p&gt;When you compile a Cython module, the source disappears. What you get is a &lt;code&gt;.so&lt;/code&gt; (or &lt;code&gt;.pyd&lt;/code&gt;) file: a compiled extension with no type information readable by a language server. Tools like mypy's &lt;code&gt;stubgen&lt;/code&gt; can generate stubs for these by importing the compiled binary and using runtime introspection.&lt;/p&gt;

&lt;p&gt;The results are usually disappointing. Take this typed Cython module:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cython"&gt;&lt;code&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Mathematical utilities for scientific computing.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="k"&gt;cdef&lt;/span&gt; &lt;span class="kr"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Matrix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;A simple matrix class.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="k"&gt;cdef&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;
    &lt;span class="k"&gt;cdef&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;cols&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&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="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;cols&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Initialize a matrix.&lt;/span&gt;&lt;span class="sh"&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;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rows&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;cols&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cols&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;shape&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;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Get matrix dimensions.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
        &lt;span class="nf"&gt;return &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;rows&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;cols&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;cpdef&lt;/span&gt; &lt;span class="nf"&gt;scale&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="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;factor&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Scale all elements.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
        &lt;span class="k"&gt;pass&lt;/span&gt;

    &lt;span class="k"&gt;cdef&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;_validate&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="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Internal validation (not exposed).&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;matrix_product&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Matrix&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;Matrix&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Matrix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Compute matrix product.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;Matrix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cols&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Running &lt;code&gt;stubgen&lt;/code&gt; on the compiled binary produces:&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;_cython_3_2_4&lt;/span&gt;
&lt;span class="n"&gt;matrix_product&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;_cython_3_2_4&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cython_function_or_method&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Matrix&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;__init__&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;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;...&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;scale&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;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="bp"&gt;...&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;shape&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;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="bp"&gt;...&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__reduce__&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="bp"&gt;...&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__reduce_cython__&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;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="bp"&gt;...&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__setstate_cython__&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;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&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;The type annotations you wrote on every argument don't survive into the compiled binary in a form introspection can recover. You get missing docstrings and untyped &lt;code&gt;*args, **kwargs&lt;/code&gt; everywhere.&lt;/p&gt;

&lt;p&gt;stubgen-pyx takes a different approach: it never touches the compiled binary. It reads the Cython source directly, parses it using Cython's own compiler internals, and extracts the type annotations and documentation the author wrote. The output for the same module:&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="c1"&gt;# This file was generated by stubgen-pyx
&lt;/span&gt;
&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Mathematical utilities for scientific computing.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;__future__&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;annotations&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Matrix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;A simple matrix class.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;scale&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;factor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Scale all elements.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&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;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cols&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Initialize a matrix.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;shape&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;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Get matrix dimensions.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;matrix_product&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Matrix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Matrix&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Matrix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Compute matrix product.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Using it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;stubgen-pyx
stubgen-pyx ./your_package
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or programmatically:&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="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;stubgen_pyx&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;StubgenPyx&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;stubgen_pyx.config&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;StubgenPyxConfig&lt;/span&gt;

&lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;StubgenPyxConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;continue_on_error&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# don't abort on one bad file
&lt;/span&gt;    &lt;span class="n"&gt;include_private&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="c1"&gt;# include _private functions
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;stubgen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;StubgenPyx&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;stubgen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;convert_glob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;src/**/*.pyx&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For library maintainers, the recommended pattern is to run stubgen-pyx as a step in your release CI and commit the generated &lt;code&gt;.pyi&lt;/code&gt; files alongside your compiled extension. Users get full IDE support without generating stubs themselves.&lt;/p&gt;




&lt;h2&gt;
  
  
  The five-stage pipeline
&lt;/h2&gt;

&lt;p&gt;The full flow from &lt;code&gt;.pyx&lt;/code&gt; source to &lt;code&gt;.pyi&lt;/code&gt; output:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Preprocessing&lt;/strong&gt; - normalize source so Cython's parser reports accurate line numbers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AST parsing&lt;/strong&gt; - feed to Cython's own &lt;code&gt;parse_from_strings&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Visitor analysis&lt;/strong&gt; - walk the AST collecting functions, classes, enums, imports&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Conversion&lt;/strong&gt; - map raw AST nodes to intermediate &lt;code&gt;PyiElement&lt;/code&gt; dataclasses&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Building + postprocessing&lt;/strong&gt; - emit stub text, normalize types, trim imports&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Stage 1: Preprocessing
&lt;/h2&gt;

&lt;p&gt;The Cython compiler does not report accurate line numbers for certain node types, necessitating workarounds so that lines can be copied from source to the resulting &lt;code&gt;.pyi&lt;/code&gt; file accurately. This matters especially for pulling through function/class decorators and assignment statements. It runs six sequential string transforms, each using Python's &lt;code&gt;tokenize&lt;/code&gt; module:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;replace_tabs_with_spaces&lt;/code&gt; - leading tabs to 4 spaces&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;remove_comments&lt;/code&gt; - strips all comment tokens, replacing with spaces&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;collapse_line_continuations&lt;/code&gt; - backslash + newline to space&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;remove_contained_newlines&lt;/code&gt; - removes newlines inside brackets/parens/braces, tracked with a token-based bracket stack&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;expand_colons&lt;/code&gt; - splits &lt;code&gt;def f(): return 0&lt;/code&gt; onto proper new lines; only colons that open blocks&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;expand_semicolons&lt;/code&gt; - same treatment for semicolons&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One tricky detail: &lt;code&gt;# type: int&lt;/code&gt; comments (PEP 484 style) are extracted &lt;em&gt;before&lt;/em&gt; comments are stripped, and their line numbers are adjusted to account for lines removed by the bracket-newline collapsing step.&lt;/p&gt;




&lt;h2&gt;
  
  
  Stages 2-3: Parsing and visitor analysis
&lt;/h2&gt;

&lt;p&gt;After preprocessing, the source goes to Cython's own &lt;code&gt;parse_from_strings&lt;/code&gt;, receiving the same AST a compilation would produce, with all type information intact.&lt;/p&gt;

&lt;p&gt;Four visitor classes then walk the AST, all extending Cython's &lt;code&gt;TreeVisitor&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ModuleVisitor&lt;/code&gt; - top-level entry point, delegates to others&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ScopeVisitor&lt;/code&gt; - collects functions, classes, enums, assignments, cdef variables&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ClassVisitor&lt;/code&gt; - &lt;code&gt;ScopeVisitor&lt;/code&gt; with &lt;code&gt;in_class=True&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ImportVisitor&lt;/code&gt; - collects all import and cimport statements&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The key design decision is what gets collected and what gets ignored. &lt;code&gt;ScopeVisitor.visit_CFuncDefNode&lt;/code&gt; checks &lt;code&gt;node.declarator.overridable&lt;/code&gt;, a boolean that separates &lt;code&gt;cpdef&lt;/code&gt; (Python-callable) from pure &lt;code&gt;cdef&lt;/code&gt; (invisible to Python importers):&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;def&lt;/span&gt; &lt;span class="nf"&gt;visit_CFuncDefNode&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;node&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;declarator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;overridable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;  &lt;span class="c1"&gt;# pure cdef — not Python-visible, drop it
&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;cdef_functions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Public &lt;code&gt;cdef class&lt;/code&gt; attributes (&lt;code&gt;cdef public int count&lt;/code&gt;) are also collected since those are exposed to Python callers.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ImportVisitor&lt;/code&gt; has one notable special case: it passes through &lt;code&gt;if TYPE_CHECKING:&lt;/code&gt; blocks. Any imports your Cython file guards behind &lt;code&gt;TYPE_CHECKING&lt;/code&gt;, a common pattern for avoiding circular imports, still get picked up and included in the stub.&lt;/p&gt;




&lt;h2&gt;
  
  
  Stage 4: Signature extraction and the &lt;code&gt;PyiElement&lt;/code&gt; intermediate representation
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;Converter&lt;/code&gt; class turns each collected AST node into a &lt;code&gt;PyiElement&lt;/code&gt; dataclass. This intermediate representation sits between the Cython AST and the output text: it holds a function or class's name, argument list, return type, docstring, and decorators in a neutral form that the postprocessing stages can work with without knowing anything about Cython's AST structure. This boundary is what makes it straightforward to add a new postprocessing pass without touching the parser.&lt;/p&gt;

&lt;p&gt;The core of conversion is &lt;code&gt;get_signature&lt;/code&gt;, which handles both &lt;code&gt;def&lt;/code&gt; and &lt;code&gt;cdef&lt;/code&gt;/&lt;code&gt;cpdef&lt;/code&gt; nodes. They have different declarator structures in the AST.&lt;/p&gt;

&lt;p&gt;For argument types there are two sources. Python-style annotations (&lt;code&gt;def f(x: int)&lt;/code&gt;) are read from &lt;code&gt;arg.annotation.string.value&lt;/code&gt;. C-style Cython typing (&lt;code&gt;cdef int x&lt;/code&gt;) is extracted from &lt;code&gt;arg.base_type&lt;/code&gt; via &lt;code&gt;extract_type_from_base_type&lt;/code&gt;, which traverses the base type node to reconstruct dotted module paths.&lt;/p&gt;

&lt;p&gt;Positional-only and keyword-only markers are preserved: the signature builder emits &lt;code&gt;/&lt;/code&gt; and &lt;code&gt;*&lt;/code&gt; in the right places based on &lt;code&gt;num_posonly_args&lt;/code&gt; and &lt;code&gt;num_kwonly_args&lt;/code&gt; from the AST.&lt;/p&gt;

&lt;p&gt;Enums get special treatment. A &lt;code&gt;cpdef enum&lt;/code&gt; (&lt;code&gt;create_wrapper=True&lt;/code&gt;) becomes a proper class with &lt;code&gt;int&lt;/code&gt;-typed attributes. A plain &lt;code&gt;cdef enum&lt;/code&gt; becomes &lt;code&gt;MyEnum = int&lt;/code&gt;, which is accurate since C enums are just integers at the Python boundary.&lt;/p&gt;




&lt;h2&gt;
  
  
  Stage 5: Postprocessing
&lt;/h2&gt;

&lt;p&gt;The raw generated stub, a collection of &lt;code&gt;PyiElement&lt;/code&gt; values serialized to text, goes through several passes, each operating on a Python &lt;code&gt;ast.AST&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Type name normalization
&lt;/h3&gt;

&lt;p&gt;Cython has its own type vocabulary that means nothing to Python type checkers. The normalizer maps every Cython-specific name to its Python equivalent via an &lt;code&gt;ast.NodeTransformer&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Cython type&lt;/th&gt;
&lt;th&gt;Python equivalent&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;bint&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bool&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;unicode&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;str&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;void&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;None&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;char&lt;/code&gt;, &lt;code&gt;short&lt;/code&gt;, &lt;code&gt;long&lt;/code&gt;, &lt;code&gt;int8_t&lt;/code&gt;...&lt;code&gt;uint64_t&lt;/code&gt;, &lt;code&gt;Py_ssize_t&lt;/code&gt;, &lt;code&gt;size_t&lt;/code&gt;...&lt;/td&gt;
&lt;td&gt;&lt;code&gt;int&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;double&lt;/code&gt;, &lt;code&gt;longdouble&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;float&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;doublecomplex&lt;/code&gt;, &lt;code&gt;floatcomplex&lt;/code&gt;...&lt;/td&gt;
&lt;td&gt;&lt;code&gt;complex&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So &lt;code&gt;cpdef uint32_t compute(int64_t x)&lt;/code&gt; becomes &lt;code&gt;def compute(x: int) -&amp;gt; int&lt;/code&gt; in the stub. All integer widths collapse to &lt;code&gt;int&lt;/code&gt;. Callers who care about &lt;code&gt;uint32_t&lt;/code&gt; vs &lt;code&gt;int64_t&lt;/code&gt; distinctions won't see them, but Python has no equivalent types, so &lt;code&gt;int&lt;/code&gt; is the honest annotation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Import trimming
&lt;/h3&gt;

&lt;p&gt;The stub begins with all imports from the original &lt;code&gt;.pyx&lt;/code&gt;, but many will be &lt;code&gt;cimport&lt;/code&gt;s of C headers with no Python-side meaning. They exist to pull in C type definitions the Cython compiler needs but that don't exist as importable Python modules. The trimmer collects every name actually used in type annotations and signatures, then removes any import that doesn't feed a used name.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;cimport&lt;/code&gt; keyword itself is rewritten to &lt;code&gt;import&lt;/code&gt; by &lt;code&gt;PyiImport.__post_init__&lt;/code&gt;, via a simple regex substitution. A &lt;code&gt;cimport&lt;/code&gt; that survives trimming becomes a standard Python import in the stub.&lt;/p&gt;

&lt;h3&gt;
  
  
  Deduplication and sorting
&lt;/h3&gt;

&lt;p&gt;Identical import statements are merged. Imports are sorted in isort-style order: &lt;code&gt;from __future__&lt;/code&gt; first, then stdlib, then third-party, then local.&lt;/p&gt;




&lt;h2&gt;
  
  
  What it doesn't handle
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Memory views&lt;/strong&gt; (&lt;code&gt;double[::1]&lt;/code&gt;, &lt;code&gt;int[:, :]&lt;/code&gt;) fall back to untyped annotations in some cases. The base type extraction handles &lt;code&gt;CSimpleBaseTypeNode&lt;/code&gt; and basic &lt;code&gt;TemplatedTypeNode&lt;/code&gt;, but the multi-dimensional slice syntax is a more complex AST node.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fused types&lt;/strong&gt; (&lt;code&gt;ctypedef fused numeric: int | double&lt;/code&gt;) have basic support. The stub includes the fused type name as-is.&lt;/p&gt;

&lt;p&gt;Getting 95% of signatures correctly typed is more useful than attempting full coverage and producing incorrect stubs.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;

&lt;p&gt;The codebase is separated across six subpackages: &lt;code&gt;parsing&lt;/code&gt;, &lt;code&gt;analysis&lt;/code&gt;, &lt;code&gt;conversion&lt;/code&gt;, &lt;code&gt;models&lt;/code&gt;, &lt;code&gt;builders&lt;/code&gt;, &lt;code&gt;postprocessing&lt;/code&gt;, each with a single responsibility. The &lt;code&gt;PyiElement&lt;/code&gt; intermediate representation decouples the Cython AST from the output text, making it easy to add postprocessing passes or extend the type normalization without touching unrelated code.&lt;/p&gt;




&lt;p&gt;If this was useful, you can &lt;a href="https://github.com/sponsors/jon-edward" rel="noopener noreferrer"&gt;sponsor me on GitHub&lt;/a&gt; or &lt;a href="https://buymeacoffee.com/jon.edward" rel="noopener noreferrer"&gt;buy me a coffee&lt;/a&gt; to support my work. Feedback and contributions welcome at &lt;a href="https://github.com/jon-edward/stubgen-pyx" rel="noopener noreferrer"&gt;github.com/jon-edward/stubgen-pyx&lt;/a&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>tooling</category>
      <category>cython</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
