<?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>Forem: Nicolas Galler</title>
    <description>The latest articles on Forem by Nicolas Galler (@nicocrm).</description>
    <link>https://forem.com/nicocrm</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%2F2045686%2Fee9d9daf-b652-4458-86eb-5d1328977f98.png</url>
      <title>Forem: Nicolas Galler</title>
      <link>https://forem.com/nicocrm</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/nicocrm"/>
    <language>en</language>
    <item>
      <title>Managing Python Monorepos with uv Workspaces and AWS Lambda</title>
      <dc:creator>Nicolas Galler</dc:creator>
      <pubDate>Fri, 16 Jan 2026 17:31:52 +0000</pubDate>
      <link>https://forem.com/nicocrm/managing-python-monorepos-with-uv-workspaces-and-aws-lambda-5a2i</link>
      <guid>https://forem.com/nicocrm/managing-python-monorepos-with-uv-workspaces-and-aws-lambda-5a2i</guid>
      <description>&lt;p&gt;uv workspaces are a super tool when developing interconnected Python packages, especially in mono-repo setups. uv will set it up pretty automatically if you have a &lt;code&gt;pyproject.toml&lt;/code&gt; at the root of the repo and you run &lt;code&gt;uv init&lt;/code&gt; inside a subfolder.&lt;/p&gt;

&lt;p&gt;What this means is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;only &lt;strong&gt;one&lt;/strong&gt; venv will be set up, with all dependencies for all projects (this is pretty nice for the IDE because you don't need to keep switching venvs, but note the important caveat below)&lt;/li&gt;
&lt;li&gt;you can reference a local project by adding it in &lt;code&gt;[tool.uv.sources]&lt;/code&gt;, and uv will automatically install it as editable (so you won't need to build each time you make a change and keep reinstalling your project):
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[tool.uv.sources]&lt;/span&gt;
&lt;span class="py"&gt;common_logging&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;workspace&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;one caveat: if your projects have conflicting dependencies, uv will fail the install! For microservices, it is probably best to keep dependencies on compatible versions anyway&lt;/li&gt;
&lt;li&gt;another caveat: since there is only one venv, it's possible the IDE will detect a package that is not really in the project dependencies, and you accidentally import it! This can easily go unnoticed during local development.&lt;/li&gt;
&lt;li&gt;for that, you might be better off forgoing workspaces and using a path dependency:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[tool.uv.sources]&lt;/span&gt;
&lt;span class="py"&gt;common_logging&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="py"&gt;path&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"../common/logging"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="py"&gt;editable&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I have decided to keep using uv workspaces, but we have a CI check to catch that type of problem before it reaches production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running uv in a workspace
&lt;/h2&gt;

&lt;p&gt;From the root of a workspace, &lt;code&gt;uv sync&lt;/code&gt; will install &lt;strong&gt;only&lt;/strong&gt; the workspace-level packages.  Likewise &lt;code&gt;uv run&lt;/code&gt; will not install all the packages.  This is &lt;strong&gt;not&lt;/strong&gt; a way to specify common dependencies, because they will only be installed if the workspace root is installed (so for example &lt;code&gt;uv export&lt;/code&gt; inside of the package will not include those "shared" deps).&lt;/p&gt;

&lt;p&gt;Now, as you go into a package and run &lt;code&gt;uv sync&lt;/code&gt;, uv will add the dependencies from that package to the venv... so eventually you will have everything installed.  But you can't be sure that is always the case.  This is also nice if you want to install deps for only one package, before running tests in CI, to make sure it does not accidentally depend on the other packages' dependencies.&lt;/p&gt;

&lt;p&gt;To run and include all package dependencies (needed for example for a type checker!), use&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;uv run --all-packages
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Use in Docker build
&lt;/h2&gt;

&lt;p&gt;The following examples assume &lt;strong&gt;AWS Lambda container images&lt;/strong&gt;, which explains some of the paths (&lt;code&gt;/var/task&lt;/code&gt;, &lt;code&gt;/var/lang/lib&lt;/code&gt;) and the base images used. The general approach still applies to non-Lambda containers, but paths and base images would need to be adjusted.&lt;/p&gt;

&lt;p&gt;There are two catches:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;you don't want to install all the workspace dependencies necessarily — some are not shared among all your microservices&lt;/li&gt;
&lt;li&gt;you don't want to install the local dependencies in the same layer as the core dependencies, because the core dependencies are less likely to change (and, more importantly, are much bigger)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is especially important for Lambda images, where image size and layer caching directly impact cold start performance.&lt;/p&gt;

&lt;p&gt;I struggled a bit with the latter, but in fact uv makes it rather easy. It's not super well documented though — there are many different ways to get there that will sort of work but leave you with some cruft in the final image.&lt;/p&gt;

&lt;h3&gt;
  
  
  First attempt using uv sync
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;you do need to copy all the &lt;code&gt;pyproject.toml&lt;/code&gt; files and the single &lt;code&gt;uv.lock&lt;/code&gt; file that you are going to need. This includes all your local dependencies, and that makes sense — if you changed a requirement in a local dependency, it's now part of your core deps, and you can include it in the base layer
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; pyproject.toml uv.lock /build&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; services/common/logging/pyproject.toml /build/services/common/logging/&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; services/${SERVICE_NAME}/pyproject.toml ./&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;then, install the core deps, making sure you specify the &lt;code&gt;--package&lt;/code&gt; argument to install only for that part of the workspace, and the &lt;code&gt;--no-install-local&lt;/code&gt; argument to prevent including other workspace members:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;RUN &lt;/span&gt;uv &lt;span class="nb"&gt;sync&lt;/span&gt; &lt;span class="nt"&gt;--frozen&lt;/span&gt; &lt;span class="nt"&gt;--no-install-local&lt;/span&gt; &lt;span class="nt"&gt;--no-dev&lt;/span&gt; &lt;span class="nt"&gt;--package&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SERVICE_NAME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;next, the local dependencies… uv will figure out which ones of those are needed for the project based on the workspace graph:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; services/common /build/services/common&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;uv &lt;span class="nb"&gt;sync&lt;/span&gt; &lt;span class="nt"&gt;--frozen&lt;/span&gt; &lt;span class="nt"&gt;--no-editable&lt;/span&gt; &lt;span class="nt"&gt;--no-install-project&lt;/span&gt; &lt;span class="nt"&gt;--no-dev&lt;/span&gt; &lt;span class="nt"&gt;--package&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SERVICE_NAME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;and finally, the code for this service:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; services/${SERVICE_NAME} ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;uv &lt;span class="nb"&gt;sync&lt;/span&gt; &lt;span class="nt"&gt;--frozen&lt;/span&gt; &lt;span class="nt"&gt;--no-editable&lt;/span&gt; &lt;span class="nt"&gt;--no-dev&lt;/span&gt; &lt;span class="nt"&gt;--package&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SERVICE_NAME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I refined this a little bit, because the above will leave two copies of the source code (one under &lt;code&gt;/build/services&lt;/code&gt; and one under the venv), which is undesirable in a Lambda image where size matters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# Copy dependency files: workspace, and service project&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; pyproject.toml uv.lock /build&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; services/${SERVICE_NAME}/pyproject.toml ./&lt;/span&gt;

&lt;span class="c"&gt;# Install core dependencies&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;uv &lt;span class="nb"&gt;sync&lt;/span&gt; &lt;span class="nt"&gt;--frozen&lt;/span&gt; &lt;span class="nt"&gt;--no-install-local&lt;/span&gt; &lt;span class="nt"&gt;--no-dev&lt;/span&gt; &lt;span class="nt"&gt;--package&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SERVICE_NAME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; PYTHONPATH=/build/.venv/lib/python3.14/site-packages&lt;/span&gt;

&lt;span class="c"&gt;# Install local dependencies&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nt"&gt;--mount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;bind&lt;/span&gt;,source&lt;span class="o"&gt;=&lt;/span&gt;services/common,target&lt;span class="o"&gt;=&lt;/span&gt;/build/services/common &lt;span class="se"&gt;\
&lt;/span&gt;    uv &lt;span class="nb"&gt;sync&lt;/span&gt; &lt;span class="nt"&gt;--frozen&lt;/span&gt; &lt;span class="nt"&gt;--no-editable&lt;/span&gt; &lt;span class="nt"&gt;--no-install-project&lt;/span&gt; &lt;span class="nt"&gt;--no-dev&lt;/span&gt; &lt;span class="nt"&gt;--package&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SERVICE_NAME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;# Finally, copy the project source&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; services/${SERVICE_NAME}/src /var/task&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, the downside is that you are left with a few unneeded things in this final image: there are some leftovers from the venv and the uv command itself. I was also worried that running the uv command directly in the final image could leave me with some surprises, especially in a production Lambda environment.&lt;/p&gt;

&lt;h3&gt;
  
  
  Second attempt using pip
&lt;/h3&gt;

&lt;p&gt;To work around the above problem, I install the dependencies in two steps into separate folders. Then, I do another stage in the Docker build to collect all these dependencies into a clean final image.&lt;/p&gt;

&lt;p&gt;Despite the title, this still relies on uv for dependency resolution — pip is only used for the final installation step, which keeps the runtime image simpler.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# --- Build Stage ---&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;public.ecr.aws/lambda/python:3.14-arm64&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;builder&lt;/span&gt;

&lt;span class="c"&gt;# Install uv&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/&lt;/span&gt;

&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; SERVICE_NAME&lt;/span&gt;

&lt;span class="c"&gt;# Set up the working directory&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /build&lt;/span&gt;

&lt;span class="c"&gt;# Copy dependency files: workspace, and service project&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; pyproject.toml uv.lock /build&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; services/${SERVICE_NAME}/pyproject.toml /build/services/${SERVICE_NAME}/pyproject.toml&lt;/span&gt;

&lt;span class="c"&gt;# Install core dependencies&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nt"&gt;--mount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cache,target&lt;span class="o"&gt;=&lt;/span&gt;/root/.cache/uv &lt;span class="se"&gt;\
&lt;/span&gt;    uv &lt;span class="nb"&gt;sync&lt;/span&gt; &lt;span class="nt"&gt;--frozen&lt;/span&gt; &lt;span class="nt"&gt;--no-install-local&lt;/span&gt; &lt;span class="nt"&gt;--no-dev&lt;/span&gt; &lt;span class="nt"&gt;--package&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SERVICE_NAME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;# Install common dependencies&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;uv &lt;span class="nb"&gt;export&lt;/span&gt; &lt;span class="nt"&gt;--package&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SERVICE_NAME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="nt"&gt;--no-editable&lt;/span&gt; &lt;span class="nt"&gt;--no-dev&lt;/span&gt; &lt;span class="nt"&gt;--frozen&lt;/span&gt; &lt;span class="nt"&gt;--format&lt;/span&gt; requirements.txt | &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;'^./services/common'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; common.txt
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nt"&gt;--mount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;bind&lt;/span&gt;,source&lt;span class="o"&gt;=&lt;/span&gt;services/common,target&lt;span class="o"&gt;=&lt;/span&gt;/build/services/common &lt;span class="se"&gt;\
&lt;/span&gt;    uv pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--no-deps&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; common.txt &lt;span class="nt"&gt;--target&lt;/span&gt; /build/common

&lt;span class="c"&gt;# Application code (we could use uv pip install here too, but this is simpler)&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; services/${SERVICE_NAME}/src /build/app&lt;/span&gt;

&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;public.ecr.aws/lambda/python:3.14-arm64&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;final&lt;/span&gt;

&lt;span class="c"&gt;# Copy core dependencies from venv&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /build/.venv/lib /var/lang/lib&lt;/span&gt;

&lt;span class="c"&gt;# Copy common&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /build/common /var/task&lt;/span&gt;

&lt;span class="c"&gt;# Application code&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /build/app /var/task&lt;/span&gt;

&lt;span class="c"&gt;# Set the CMD to your handler&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; [ "ingestion_worker.main.handler" ]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This results in a smaller, cleaner final image that contains only what Lambda needs at runtime.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>python</category>
      <category>tooling</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Exploring Modern Python Type Checkers</title>
      <dc:creator>Nicolas Galler</dc:creator>
      <pubDate>Mon, 12 Jan 2026 14:23:41 +0000</pubDate>
      <link>https://forem.com/nicocrm/exploring-modern-python-type-checkers-572l</link>
      <guid>https://forem.com/nicocrm/exploring-modern-python-type-checkers-572l</guid>
      <description>&lt;p&gt;I’ve been playing around with alternative Python type checkers and language servers, looking beyond the more legacy options to find a setup that’s powerful without getting in the way. Along the way, I compared &lt;a href="https://docs.astral.sh/ty/" rel="noopener noreferrer"&gt;ty&lt;/a&gt; and &lt;a href="https://pyrefly.org/" rel="noopener noreferrer"&gt;Pyrefly&lt;/a&gt;, tweaked extensions and inlay hints, and ran into a few interesting differences in behavior, ergonomics, and typing semantics. The result isn’t a definitive winner yet, but it did surface some useful insights—and a couple of gotchas about Python typing that are easy to miss.&lt;/p&gt;

&lt;p&gt;Both extensions were very easy to install from the Extension Marketplace in Cursor.&lt;/p&gt;

&lt;p&gt;I disabled the &lt;strong&gt;“Python” extension from Anysphere&lt;/strong&gt; (keeping the &lt;code&gt;ms-python&lt;/code&gt; one). Anysphere’s extension is essentially &lt;strong&gt;pyright&lt;/strong&gt;, which was doing double duty with &lt;strong&gt;ty&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For &lt;strong&gt;ty&lt;/strong&gt;, I also disabled the language server, as suggested in the documentation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"python.languageServer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"None"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After that, there were still some logs being output:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ty&lt;/li&gt;
&lt;li&gt;ty language server&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There were also &lt;strong&gt;inlay hints&lt;/strong&gt; showing inferred types. They’re somewhat useful, but overall a bit too much visual clutter, so I disabled them. I can still see variable types via tooltips when needed.&lt;/p&gt;




&lt;p&gt;I also tried &lt;strong&gt;Pyrefly&lt;/strong&gt;. The setup was similar, but I didn’t need to manually disable the language server—Pyrefly automatically disables &lt;strong&gt;Pylance&lt;/strong&gt;. It offers a few more customization options. One thing to note: by default, it will &lt;strong&gt;not display type errors unless a configuration file is found in the project&lt;/strong&gt;. This can be forced via the &lt;em&gt;“Display Type Errors”&lt;/em&gt; setting.&lt;/p&gt;




&lt;p&gt;Where &lt;strong&gt;Pyrefly’s language server&lt;/strong&gt; was better:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In one instance, it was able to automatically locate missing imports when I used &lt;code&gt;Cmd + .&lt;/code&gt;. This involved a type alias, where &lt;strong&gt;ty&lt;/strong&gt; took a few more shortcuts.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;Inlay hints comparison&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Ty was a bit more obnoxious with inlay hints out of the box, while Pyrefly tended to include them only when they were genuinely useful. For 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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;merge_data&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;source&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;dest&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;source&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;check_sql_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source&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="nf"&gt;_validate_source_schema&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;stmt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;build_update_statement&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;con&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;namespace&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both tools correctly inferred the type of &lt;code&gt;source&lt;/code&gt; as &lt;code&gt;SqlIdString&lt;/code&gt; (rather than the original &lt;code&gt;str&lt;/code&gt;), but:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ty&lt;/strong&gt; added an inlay hint&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;pyrefly&lt;/strong&gt; did not, unless I was also renaming the variable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ty also tended to union types with &lt;code&gt;| Unknown&lt;/code&gt; when using list comprehensions or list literals, which was pretty weird and annoying. Thankfully, there’s already an open issue for this, so I’ll keep tracking it.&lt;/p&gt;

&lt;p&gt;Pyrefly had better support for Pydantic models, correctly reporting type errors when using field aliases for example.&lt;/p&gt;

&lt;p&gt;Pyrefly had one case where it refused to redefine a variable’s type via annotation. That’s valid Python, though arguably questionable style.&lt;/p&gt;

&lt;p&gt;Finally, I use Protocol for my interfaces on the project, and Pyrefly had generally more helpful messages when an interface was not correctly implemented.&lt;/p&gt;




&lt;p&gt;Both of those are easy to integrate in the CI workflow, and they play nicer than pyright with Python's package manager.  They both have LSP support, though the one of Pyrefly is a little more polished.&lt;/p&gt;




&lt;p&gt;Overall, I’m more aligned with &lt;strong&gt;Pyrefly&lt;/strong&gt;.   I want to keep evaluating &lt;strong&gt;ty&lt;/strong&gt; because Astral's tools have been so great, I have no doubt they will catch up!  I’ll revisit that choice if ty starts to get ahead.&lt;/p&gt;

&lt;p&gt;One final typing takeaway: &lt;strong&gt;a type alias is not the same as &lt;code&gt;NewType&lt;/code&gt;&lt;/strong&gt;. A type alias doesn’t provide real type safety—it’s just a nickname for an existing type.&lt;/p&gt;

</description>
      <category>python</category>
      <category>tooling</category>
      <category>vscode</category>
    </item>
    <item>
      <title>Stop Fighting Circular Imports</title>
      <dc:creator>Nicolas Galler</dc:creator>
      <pubDate>Sun, 05 Oct 2025 16:09:01 +0000</pubDate>
      <link>https://forem.com/nicocrm/stop-fighting-circular-imports-5312</link>
      <guid>https://forem.com/nicocrm/stop-fighting-circular-imports-5312</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;The code is good, but you are missing the type hints!&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Oh, I know, but as soon as I add them, I get these circular import errors!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Mmm, that's often a smell, let's dig a little&lt;/p&gt;


&lt;/blockquote&gt;

&lt;p&gt;Sounds familiar?  Yet sometimes even when digging in, the design is fine, and the modules really need to import each other, especially if you want to keep things simple and not introduce a Java-style interface hierarchy.&lt;/p&gt;

&lt;p&gt;Turns out, modern Python has a solution for this, in fact, several of them.  Let's check it out :) &lt;/p&gt;

&lt;p&gt;Here a simple 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;# post.py
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;models.user&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;User&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Post&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="n"&gt;author&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;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;author&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;author&lt;/span&gt;

&lt;span class="c1"&gt;# user.py
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;models.post&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Post&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;User&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="n"&gt;name&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;posts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Post&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;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;name&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;posts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;posts&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;ImportError: cannot import name 'Post' from partially initialized module 'models.post' (most likely due to a circular import) (/home/nico/scratch/models/post.py)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So, first solution, from PEP-563, was to be able to do the annotations as strings.&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;Post&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="n"&gt;author&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;User&lt;/span&gt;&lt;span class="sh"&gt;"&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;author&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;author&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But that's not enough for most tools, you also need to tell it where to find &lt;code&gt;User&lt;/code&gt;... yet you can't just import it as it would be back to a circular import, so you use a special "TYPE_CHECKING" guard:&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;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;TYPE_CHECKING&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;TYPE_CHECKING&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;models.user&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;User&lt;/span&gt; 

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Post&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="n"&gt;author&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;User&lt;/span&gt;&lt;span class="sh"&gt;"&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;author&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;author&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And finally you can turn on these string annotations for the whole module using a &lt;strong&gt;future&lt;/strong&gt; import:&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;__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="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;TYPE_CHECKING&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;TYPE_CHECKING&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;models.user&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;User&lt;/span&gt; 

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Post&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="n"&gt;author&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;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;author&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;author&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, as often in Python, there are a few layers added over time.  For the full details, you'll want to read the PEPs: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;start with &lt;a href="https://peps.python.org/pep-0563/" rel="noopener noreferrer"&gt;PEP-563&lt;/a&gt;, which is rather simple, and is available &lt;strong&gt;today&lt;/strong&gt; (and as early as Python 3.7)&lt;/li&gt;
&lt;li&gt;then move on &lt;a href="https://peps.python.org/pep-0649/" rel="noopener noreferrer"&gt;PEP-649&lt;/a&gt;
and its little sister &lt;a href="https://peps.python.org/pep-0749/" rel="noopener noreferrer"&gt;PEP-749&lt;/a&gt;.  They change the way this is done, but this won't be available until Python 3.14.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then, under the hood things will be quite different and it won't use the "string" annotations anymore.  For most usage (when annotating code for static type hints), you will just be able to drop the &lt;code&gt;__future__&lt;/code&gt; import.  &lt;/p&gt;

</description>
      <category>python</category>
      <category>static</category>
      <category>typing</category>
    </item>
    <item>
      <title>Your database is not elastic!</title>
      <dc:creator>Nicolas Galler</dc:creator>
      <pubDate>Fri, 12 Sep 2025 08:44:59 +0000</pubDate>
      <link>https://forem.com/nicocrm/your-database-is-not-elastic-3o18</link>
      <guid>https://forem.com/nicocrm/your-database-is-not-elastic-3o18</guid>
      <description>&lt;p&gt;This post stems from a recent experience with a web application that was not scaling as expected. The application was designed to be able to scale horizontally, but the database was not. This is a common problem in web development, and one that is often overlooked until it is too late. The problem is that databases, especially relational databases, are not as elastic as the rest of the application stack. You can spin a new web server up in seconds, add some queue workers even faster, but the database is a different beast. Because of the strict principles it has to adhere to in order to ensure data integrity and consistency through a convenient API, scaling it horizontally (adding more servers to share the load) is quite complex - read-only replicas are possible, but not particularly cheap or easy to set up (cloud vendors will make that easier of course). Sharding is another option, but it is not trivial to implement and maintain (and can be quite costly too). And then there is the problem of transactions that span multiple shards, which is a whole other can of worms. You can scale it up (vertically) but that is not a sustainable solution. Eventually you will hit the limits of the biggest server you can afford, and then what?&lt;/p&gt;

&lt;p&gt;It can be counter intuitive. In the earlier days of distributed application development (Windows-based CRM applications were my bread and butter in the 2000's), before the cloud, the database was often the only thing that could scale, and the application was limited by the performance of the client machines, which were essentially not upgradeable (or at least not easily). Now it is the other way around.&lt;/p&gt;

&lt;p&gt;This reality hit home for me last week: a deployment which had been carefully planned and tested for weeks (though as it turned out, not well enough) quickly went south when it became obvious that too much of a burden was shifted to the database.  Unfortunately our load testing did not accurately reflect the production usage and the new API was called a lot more than expected.  The web workers happily scaled out to try and answer the requests faster, which actually compounded the problem since it added to the load on the one thing that would not, could not scale: the database.  &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F48iw5vx3s5s23n8khn1w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F48iw5vx3s5s23n8khn1w.png" alt="Database CPU Utilization Chart" width="635" height="177"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Eventually we were able to resolve this with not too much broken bones by scaling up the database, until the application could be corrected. The cloud managed database service made that easier but it was still not a seamless process (and is also rather costly since it can't automatically scale back down either). And the feature had to be re-architected to be less database intensive - thankfully the change was rather isolated so we could turn that around quickly.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Reading partitioned Delta table with Polars</title>
      <dc:creator>Nicolas Galler</dc:creator>
      <pubDate>Wed, 10 Sep 2025 10:46:35 +0000</pubDate>
      <link>https://forem.com/nicocrm/reading-partitioned-delta-table-with-polars-28bb</link>
      <guid>https://forem.com/nicocrm/reading-partitioned-delta-table-with-polars-28bb</guid>
      <description>&lt;p&gt;I recently had the occasion to revisit some findings about the best practice for reading partitioned Deltalake data from Python.  We use Polars for processing data, so something that integrates well with Polars is more convenient.  Polars has a &lt;code&gt;scan_delta&lt;/code&gt; method, but this used to not be optimal for reading partitioned data.  The library has progressed so much recently though that it was due for a retest.&lt;/p&gt;

&lt;p&gt;I had 2 datasets, each partitioned in 3 roughly equal chunks - 1 very small (700kb), and a somewhat bigger one (79mb), and wrote a script (or rather I had cursor generate a script) to analyze the memory utilization and running time for 3 possible approaches: use lazyframe with &lt;code&gt;scan_delta&lt;/code&gt; (which is the most convenient), use dataframe with &lt;code&gt;read_delta&lt;/code&gt;, or use the deltatable API directly with pyarrow options to specify the partition to read.&lt;/p&gt;

&lt;p&gt;Pleasantly I was surprised to see that the LazyFrame + filter option, which is the most natural and convenient to use, is also the fastest and the most memory efficient.  The script gave me this cute results table:&lt;/p&gt;

&lt;h2&gt;
  
  
  📈 FILTER + AGGREGATION STATISTICAL ANALYSIS
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Time (s)&lt;/th&gt;
&lt;th&gt;Memory (MB)&lt;/th&gt;
&lt;th&gt;Records&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2. DataFrame + Filter + GroupBy&lt;/td&gt;
&lt;td&gt;0.653±0.134&lt;/td&gt;
&lt;td&gt;967.9±1.8&lt;/td&gt;
&lt;td&gt;1330378±0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1. LazyFrame + Filter + GroupBy&lt;/td&gt;
&lt;td&gt;0.282±0.033&lt;/td&gt;
&lt;td&gt;26.8±0.6&lt;/td&gt;
&lt;td&gt;1330378±0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3. DeltaTable + Filter + GroupBy&lt;/td&gt;
&lt;td&gt;0.417±0.010&lt;/td&gt;
&lt;td&gt;696.5±8.6&lt;/td&gt;
&lt;td&gt;1330378±0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;🏆 &lt;strong&gt;BEST PERFORMERS (Based on Averages):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;⚡ &lt;strong&gt;Fastest:&lt;/strong&gt; 1. LazyFrame + Filter + GroupBy
Average time: 0.282s ± 0.033s
&lt;/li&gt;
&lt;li&gt;💾 &lt;strong&gt;Most Memory Efficient:&lt;/strong&gt; 1. LazyFrame + Filter + GroupBy
Average memory: 26.8MB ± 0.6MB
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Find the code on &lt;a href="https://github.com/nicocrm/deltalake-partition-performance-analysis" rel="noopener noreferrer"&gt;Github&lt;/a&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>polars</category>
      <category>deltalake</category>
    </item>
  </channel>
</rss>
