<?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: Doeke Zanstra</title>
    <description>The latest articles on DEV Community by Doeke Zanstra (@doekman).</description>
    <link>https://dev.to/doekman</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%2F501711%2Ff4bb6244-dbc7-42f0-b665-b9d748dd3cb0.jpeg</url>
      <title>DEV Community: Doeke Zanstra</title>
      <link>https://dev.to/doekman</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/doekman"/>
    <language>en</language>
    <item>
      <title>A New Take on Raw SQL in Python</title>
      <dc:creator>Doeke Zanstra</dc:creator>
      <pubDate>Mon, 21 Sep 2026 11:11:56 +0000</pubDate>
      <link>https://dev.to/doekman/a-new-take-on-raw-sql-in-python-13ne</link>
      <guid>https://dev.to/doekman/a-new-take-on-raw-sql-in-python-13ne</guid>
      <description>&lt;p&gt;I've been using the excellent &lt;a href="https://pugsql.org" rel="noopener noreferrer"&gt;PugSQL library&lt;/a&gt; for some years now. It's very easy to use: just annotate your SQL queries in a text-file with a name, result-type and parameters; then load the sql file with a PugSQL module and you can run queries by calling a method with the name of the query. The calling in Python is handled by the &lt;code&gt;__call__&lt;/code&gt;-method of the module. While this is elegant by itself, you don't get to have all the standard goodness you get by using Python's native function definitions.&lt;/p&gt;

&lt;p&gt;What if you could link a Python function to a SQL query in some way? That's exactly why I made &lt;strong&gt;PySQLe&lt;/strong&gt;. Let me demonstrate how this works:&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;pysqle&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;PysqleModule&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;one&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MyQueries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PysqleModule&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nd"&gt;@one&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;find_user&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;user_id&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;select * from users where user_id = :user_id&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="n"&gt;queries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MyQueries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sqlite:///foo.db&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;queries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;666&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First you create a class (&lt;code&gt;MyQueries&lt;/code&gt;) and subclassing it from the &lt;code&gt;PysqleModule&lt;/code&gt;-class. Then you define a query by writing a method (&lt;code&gt;find_user&lt;/code&gt;). This method can have arguments, which will be passed to the database when executing the queries. The query-method itself should returns the SQL query as a string. Finally, you decorate the class with one of the supplied query-decorators. That is where the magic happens: &lt;code&gt;@one&lt;/code&gt; will take care of executing the SQL when the method is called, and will return the expected results.&lt;/p&gt;

&lt;p&gt;By instantiating the class with a SQLAlchemy connection string, you create an object to call queries with. By calling &lt;code&gt;queries.find_user&lt;/code&gt;, the SQL is executed with the &lt;code&gt;foo.db&lt;/code&gt;-database, and &lt;code&gt;user&lt;/code&gt; will contain a named tuple of the found user, just as SQLAlchemy would.&lt;/p&gt;

&lt;p&gt;You can use other decorators, like &lt;code&gt;many&lt;/code&gt; which returns a recordset, or &lt;code&gt;scalar&lt;/code&gt; which returns one single value. Because it's standard Python, query parameters can have default values, type-annotations and be used as positional or keyword arguments. Example:&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;# ...
&lt;/span&gt;    &lt;span class="c1"&gt;# Omited class definition, assuming extending the class MyQueries from above
&lt;/span&gt;    &lt;span class="nd"&gt;@many&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_users&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;category_filter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;Optional&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="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;select * from users 
                   where :category_filter is null 
                      or :category_filter = category&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="nd"&gt;@scalar&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_username&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;user_id&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="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;select username from users 
                  where user_id = :user_id&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="c1"&gt;# ...
&lt;/span&gt;
&lt;span class="n"&gt;all_users&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;queries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_users&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;admin_users&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;queries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_users&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;category_filter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;admin&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;queries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_username&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;666&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;a href="https://codeberg.org/doekman/PySQLe/" rel="noopener noreferrer"&gt;repository can be found at Codeberg&lt;/a&gt;. The library itself has been &lt;a href="https://pypi.org/project/pysqle/" rel="noopener noreferrer"&gt;published to PyPI.org&lt;/a&gt;, and can be installed via &lt;code&gt;pip install pysqle&lt;/code&gt; or equivalent. The repo contains some documentation: an &lt;a href="https://codeberg.org/doekman/PySQLe/src/branch/main/doc/overview.md" rel="noopener noreferrer"&gt;overview with some more examples&lt;/a&gt;, and there is &lt;a href="https://codeberg.org/doekman/PySQLe/src/branch/main/doc/guide.md" rel="noopener noreferrer"&gt;the complete guide&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;Writing this library is a learning experience for me. I never had an idea for a reusable Python library until now. This is clearly a first version; I have some more ideas to make this library more usable. One thing is to make get the type annotations work everywhere (for example: &lt;code&gt;reveal_type&lt;/code&gt; is not reporting the correct return type on annotated query-methods). Another idea is to add support for loading PugSQL annotated files. If you have any ideas or comments, I would like to hear them.&lt;/p&gt;

</description>
      <category>python</category>
      <category>sql</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
