<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0"><channel><title><![CDATA[CodeCut Newsletter]]></title><description><![CDATA[CodeCut is a newsletter for busy developers, data professionals, and AI engineers, delivering short, under-two-minute tips on data science, AI, and modern tools, three times per week.]]></description><link>https://newsletter.codecut.ai</link><image><url>https://newsletter.codecut.ai/img/substack.png</url><title>CodeCut Newsletter</title><link>https://newsletter.codecut.ai</link></image><generator>Substack</generator><lastBuildDate>Mon, 06 Jul 2026 20:55:15 GMT</lastBuildDate><atom:link href="https://newsletter.codecut.ai/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[CodeCut Newsletter]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[codecut@substack.com]]></webMaster><itunes:owner><itunes:email><![CDATA[codecut@substack.com]]></itunes:email><itunes:name><![CDATA[CodeCut]]></itunes:name></itunes:owner><itunes:author><![CDATA[CodeCut]]></itunes:author><googleplay:owner><![CDATA[codecut@substack.com]]></googleplay:owner><googleplay:email><![CDATA[codecut@substack.com]]></googleplay:email><googleplay:author><![CDATA[CodeCut]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[Quick Tips: PydanticAI - Control OpenAI Data Retention with openai_store]]></title><description><![CDATA[Plus read an entire CSV folder with DuckDB]]></description><link>https://newsletter.codecut.ai/p/quick-tips-pydanticai-control-openai</link><guid isPermaLink="false">https://newsletter.codecut.ai/p/quick-tips-pydanticai-control-openai</guid><dc:creator><![CDATA[CodeCut]]></dc:creator><pubDate>Thu, 02 Jul 2026 16:01:10 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!MiHv!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbce026e1-c085-4707-b3c2-86ded1f89854_609x535.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Grab your coffee. Here are this week&#8217;s highlights.</p><div><hr></div><h2>&#128197; Today&#8217;s Picks</h2><h3>PydanticAI: Control OpenAI Data Retention with openai_store</h3><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!MiHv!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbce026e1-c085-4707-b3c2-86ded1f89854_609x535.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!MiHv!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbce026e1-c085-4707-b3c2-86ded1f89854_609x535.png 424w, https://substackcdn.com/image/fetch/$s_!MiHv!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbce026e1-c085-4707-b3c2-86ded1f89854_609x535.png 848w, https://substackcdn.com/image/fetch/$s_!MiHv!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbce026e1-c085-4707-b3c2-86ded1f89854_609x535.png 1272w, https://substackcdn.com/image/fetch/$s_!MiHv!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbce026e1-c085-4707-b3c2-86ded1f89854_609x535.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!MiHv!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbce026e1-c085-4707-b3c2-86ded1f89854_609x535.png" width="609" height="535" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/bce026e1-c085-4707-b3c2-86ded1f89854_609x535.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:535,&quot;width&quot;:609,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Code example: PydanticAI: Control OpenAI Data Retention with openai_store&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Code example: PydanticAI: Control OpenAI Data Retention with openai_store" title="Code example: PydanticAI: Control OpenAI Data Retention with openai_store" srcset="https://substackcdn.com/image/fetch/$s_!MiHv!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbce026e1-c085-4707-b3c2-86ded1f89854_609x535.png 424w, https://substackcdn.com/image/fetch/$s_!MiHv!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbce026e1-c085-4707-b3c2-86ded1f89854_609x535.png 848w, https://substackcdn.com/image/fetch/$s_!MiHv!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbce026e1-c085-4707-b3c2-86ded1f89854_609x535.png 1272w, https://substackcdn.com/image/fetch/$s_!MiHv!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbce026e1-c085-4707-b3c2-86ded1f89854_609x535.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"></div></div></a></figure></div><h4>Problem</h4><p>By default, OpenAI may retain your API request data for internal review and model improvement. For healthcare, finance, and legal applications, this default creates compliance risks you can&#8217;t afford.</p><h4>Solution</h4><p><strong><a href="https://github.com/pydantic/pydantic-ai">PydanticAI</a></strong> provides the <code>openai_store</code> setting to explicitly disable data retention in one line.</p><blockquote><p>&#128214; <a href="https://codecut.ai/enforce-structured-outputs-from-llms-with-pydanticai/?utm_source=Substack&amp;utm_medium=email&amp;utm_campaign=article_enforce-structured-outputs-from-llms-with-pydanticai">View Full Article</a></p></blockquote><div><hr></div><h3>DuckDB - read an entire CSV folder in one line</h3><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!8jib!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0fc0d50-643d-4a77-b6d8-fe6dfc459c10_1200x1456.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!8jib!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0fc0d50-643d-4a77-b6d8-fe6dfc459c10_1200x1456.jpeg 424w, https://substackcdn.com/image/fetch/$s_!8jib!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0fc0d50-643d-4a77-b6d8-fe6dfc459c10_1200x1456.jpeg 848w, https://substackcdn.com/image/fetch/$s_!8jib!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0fc0d50-643d-4a77-b6d8-fe6dfc459c10_1200x1456.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!8jib!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0fc0d50-643d-4a77-b6d8-fe6dfc459c10_1200x1456.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!8jib!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0fc0d50-643d-4a77-b6d8-fe6dfc459c10_1200x1456.jpeg" width="1200" height="1456" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b0fc0d50-643d-4a77-b6d8-fe6dfc459c10_1200x1456.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1456,&quot;width&quot;:1200,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Code example: DuckDB - read an entire CSV folder in one line&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Code example: DuckDB - read an entire CSV folder in one line" title="Code example: DuckDB - read an entire CSV folder in one line" srcset="https://substackcdn.com/image/fetch/$s_!8jib!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0fc0d50-643d-4a77-b6d8-fe6dfc459c10_1200x1456.jpeg 424w, https://substackcdn.com/image/fetch/$s_!8jib!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0fc0d50-643d-4a77-b6d8-fe6dfc459c10_1200x1456.jpeg 848w, https://substackcdn.com/image/fetch/$s_!8jib!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0fc0d50-643d-4a77-b6d8-fe6dfc459c10_1200x1456.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!8jib!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0fc0d50-643d-4a77-b6d8-fe6dfc459c10_1200x1456.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"></div></div></a></figure></div><h4>Problem</h4><p>Working with multiple CSV files often means writing extra code to load, combine, and process each file.</p><h4>Solution</h4><p><strong><a href="https://github.com/duckdb/duckdb">DuckDB</a></strong> makes this simpler by letting you query multiple CSV files directly with SQL and pattern matching, so you can analyze them in one efficient query.</p><blockquote><p>&#128214; <a href="https://codecut.ai/deep-dive-into-duckdb-data-scientists?utm_source=Substack&amp;utm_medium=email&amp;utm_campaign=article_deep-dive-into-duckdb-data-scientists">View Full Article</a></p></blockquote><div><hr></div><h3>&#128172; Rate Your Experience</h3><p>How would you rate your newsletter experience? <a href="https://r7yuob192zv.typeform.com/to/VODwGYMW">Share your feedback &#8594;</a></p><div><hr></div><h3>&#128269; Explore More on CodeCut</h3><ul><li><p><a href="https://codecut.ai/tool-selector/?utm_source=Substack&amp;utm_medium=email&amp;utm_campaign=article_tool-selector">Tool Selector</a> - Discover 70+ Python tools for AI and data science</p></li><li><p><a href="https://codecut.ai/production-ready-data-science/?utm_source=Substack&amp;utm_medium=email&amp;utm_campaign=article_production-ready-data-science">Production Ready Data Science</a> - A practical book for taking projects from prototype to production</p></li></ul>]]></content:encoded></item><item><title><![CDATA[pandas vs Polars vs DuckDB: A Data Scientist’s Guide to Choosing the Right Tool]]></title><description><![CDATA[Compare pandas, Polars, and DuckDB for data analysis. Learn when to use each tool based on data size, performance needs, and workflow preferences.]]></description><link>https://newsletter.codecut.ai/p/pandas-vs-polars-vs-duckdb-a-data</link><guid isPermaLink="false">https://newsletter.codecut.ai/p/pandas-vs-polars-vs-duckdb-a-data</guid><dc:creator><![CDATA[CodeCut]]></dc:creator><pubDate>Tue, 30 Jun 2026 16:01:01 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/5e309239-f7d0-4a8a-bb52-1ad77071e8e2_1198x627.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>Personal Update</h2><p>A quick personal update: I welcomed my baby boy last week, and both mom and baby are doing well.</p><p>I&#8217;ll be taking some time to enjoy this special chapter with my family, but I&#8217;ve prepared plenty of content in advance, so you can still expect the same high-quality posts and newsletters during my maternity leave.</p><h2>Introduction</h2><p>pandas has been the standard tool for working with tabular data in Python for over a decade. But as datasets grow larger and performance requirements increase, two modern alternatives have emerged: <strong>Polars</strong>, a DataFrame library written in Rust, and <strong>DuckDB</strong>, an embedded SQL database optimized for analytics.</p><p>Each tool excels in different scenarios:</p><pre><code><code>Tool    Backend   Execution Model             Best For
pandas  C/Python  Eager, single-threaded      Small datasets, prototyping, ML integration
Polars  Rust      Lazy/Eager, multi-threaded  Large-scale analytics, data pipelines
DuckDB  C++       SQL-first, multi-threaded   SQL workflows, embedded analytics, file queries</code></code></pre><p>This guide compares all three tools with practical examples, helping you choose the right one for your workflow.</p><blockquote><p>This is a condensed version focused on the performance benchmarks. For the full guide with side-by-side syntax comparisons, interoperability examples, and a decision matrix, see the <a href="https://codecut.ai/pandas-vs-polars-vs-duckdb-comparison/">complete comparison</a>.</p></blockquote><h2>Tool Strengths at a Glance</h2><h3>pandas</h3><p><a href="https://github.com/pandas-dev/pandas">pandas</a> is the original DataFrame library for Python that excels at interactive data exploration and integrates seamlessly with the ML ecosystem. Key capabilities include:</p><ul><li><p>Direct compatibility with scikit-learn, statsmodels, and visualization libraries</p></li><li><p>Rich ecosystem of extensions (pandas-profiling, pandasql, etc.)</p></li><li><p>Mature time series functionality</p></li><li><p>Familiar syntax that most data scientists already know</p></li></ul><h3>Polars</h3><p><a href="https://github.com/pola-rs/polars">Polars</a> is a Rust-powered DataFrame library designed for speed that brings multi-threaded execution and query optimization to Python. Key capabilities include:</p><ul><li><p>Speeds up operations by using all available CPU cores by default</p></li><li><p>Builds a query plan first, then executes only what&#8217;s needed</p></li><li><p>Streaming mode for processing datasets larger than RAM</p></li><li><p>Expressive method chaining with a pandas-like API</p></li></ul><h3>DuckDB</h3><p><a href="https://github.com/duckdb/duckdb">DuckDB</a> is an embedded SQL database optimized for analytics that brings database-level query optimization to local files. Key capabilities include:</p><ul><li><p>Native SQL syntax with full analytical query support</p></li><li><p>Queries CSV, Parquet, and JSON files directly without loading</p></li><li><p>Uses disk storage automatically when data exceeds available memory</p></li><li><p>Zero-configuration embedded database requiring no server setup</p></li></ul><h2>Setup</h2><p>Install all three libraries:</p><pre><code><code>pip install pandas polars duckdb
</code></code></pre><p>Generate sample data for benchmarking:</p><pre><code><code>import pandas as pd
import numpy as np

np.random.seed(42)
n_rows = 5_000_000

data = {
    "category": np.random.choice(["Electronics", "Clothing", "Food", "Books"], size=n_rows),
    "region": np.random.choice(["North", "South", "East", "West"], size=n_rows),
    "amount": np.random.rand(n_rows) * 1000,
    "quantity": np.random.randint(1, 100, size=n_rows),
}

df_pandas = pd.DataFrame(data)
df_pandas.to_csv("sales_data.csv", index=False)
print(f"Created sales_data.csv with {n_rows:,} rows")
</code></code></pre><pre><code><code>Created sales_data.csv with 5,000,000 rows
</code></code></pre><h2>Data Loading Performance</h2><p>pandas reads CSV files on a single CPU core. Polars and DuckDB use multi-threaded execution, distributing the work across all available cores to read different parts of the file simultaneously.</p><h3>pandas</h3><p>Single-threaded CSV parsing loads data sequentially.</p><pre><code><code>&#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;
&#9474; CPU Core 1                                  &#9474;
&#9474; &#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488; &#9474;
&#9474; &#9474; Chunk 1 &#8594; Chunk 2 &#8594; Chunk 3 &#8594; ... &#8594; End &#9474; &#9474;
&#9474; &#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496; &#9474;
&#9474; CPU Core 2  [idle]                          &#9474;
&#9474; CPU Core 3  [idle]                          &#9474;
&#9474; CPU Core 4  [idle]                          &#9474;
&#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496;
</code></code></pre><pre><code><code>pandas_time = %timeit -o pd.read_csv("sales_data.csv")
</code></code></pre><pre><code><code>1.05 s &#177; 26.9 ms per loop (mean &#177; std. dev. of 7 runs, 1 loop each)
</code></code></pre><h3>Polars</h3><p>Multi-threaded parsing distributes file reading across all available cores.</p><pre><code><code>&#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;
&#9474; CPU Core 1  &#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;              &#9474;
&#9474;             &#9474; &#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;   &#9474;              &#9474;
&#9474; CPU Core 2  &#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;              &#9474;
&#9474;             &#9474; &#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;   &#9474;              &#9474;
&#9474; CPU Core 3  &#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;              &#9474;
&#9474;             &#9474; &#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;   &#9474;              &#9474;
&#9474; CPU Core 4  &#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;              &#9474;
&#9474;             &#9474; &#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;   &#9474;              &#9474;
&#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496;
</code></code></pre><pre><code><code>import polars as pl

polars_time = %timeit -o pl.read_csv("sales_data.csv")
</code></code></pre><pre><code><code>137 ms &#177; 34 ms per loop (mean &#177; std. dev. of 7 runs, 1 loop each)
</code></code></pre><h3>DuckDB</h3><p>Similar to Polars, file reading is distributed across all available cores.</p><pre><code><code>&#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;
&#9474; CPU Core 1  &#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;              &#9474;
&#9474;             &#9474; &#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;   &#9474;              &#9474;
&#9474; CPU Core 2  &#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;              &#9474;
&#9474;             &#9474; &#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;   &#9474;              &#9474;
&#9474; CPU Core 3  &#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;              &#9474;
&#9474;             &#9474; &#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;   &#9474;              &#9474;
&#9474; CPU Core 4  &#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;              &#9474;
&#9474;             &#9474; &#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;   &#9474;              &#9474;
&#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496;
</code></code></pre><pre><code><code>import duckdb

duckdb_time = %timeit -o duckdb.sql("SELECT * FROM 'sales_data.csv'").df()
</code></code></pre><pre><code><code>762 ms &#177; 77.8 ms per loop (mean &#177; std. dev. of 7 runs, 1 loop each)
</code></code></pre><pre><code><code>print(f"Polars is {pandas_time.average / polars_time.average:.1f}&#215; faster than pandas")
print(f"DuckDB is {pandas_time.average / duckdb_time.average:.1f}&#215; faster than pandas")
</code></code></pre><pre><code><code>Polars is 7.7&#215; faster than pandas
DuckDB is 1.4&#215; faster than pandas
</code></code></pre><p>While Polars leads with a 7.7&#215; speedup in CSV reading, DuckDB&#8217;s 1.4&#215; improvement shows parsing isn&#8217;t its focus. DuckDB shines when querying files directly or running complex analytical queries.</p><h2>Query Optimization</h2><h3>pandas: No Optimization</h3><p>pandas executes operations eagerly, creating intermediate DataFrames at each step. This wastes memory and prevents optimization.</p><pre><code><code>&#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;
&#9474; Step 1: Load ALL rows          &#8594; 10M rows in memory         &#9474;
&#9474; Step 2: Filter (amount &gt; 100)  &#8594; 5M rows in memory          &#9474;
&#9474; Step 3: GroupBy                &#8594; New DataFrame              &#9474;
&#9474; Step 4: Mean                   &#8594; Final result               &#9474;
&#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496;
Memory: &#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608; (high - stores all intermediates)
</code></code></pre><pre><code><code>def pandas_query():
    return (
        pd.read_csv("sales_data.csv")
        .query('amount &gt; 100')
        .groupby('category')['amount']
        .mean()
    )

pandas_opt_time = %timeit -o pandas_query()
</code></code></pre><pre><code><code>1.46 s &#177; 88.9 ms per loop (mean &#177; std. dev. of 7 runs, 1 loop each)
</code></code></pre><p>This approach has three problems:</p><ul><li><p><strong>Full CSV load</strong>: All rows are read before filtering</p></li><li><p><strong>No predicate pushdown</strong>: Rows are filtered after loading the entire file into memory</p></li><li><p><strong>No projection pushdown</strong>: All columns are loaded, even unused ones</p></li></ul><p>You can manually add <code>usecols</code> to load fewer columns:</p><pre><code><code>def pandas_query_optimized():
    return (
        pd.read_csv("sales_data.csv", usecols=["category", "amount"])
        .query('amount &gt; 100')
        .groupby('category')['amount']
        .mean()
    )

pandas_usecols_time = %timeit -o pandas_query_optimized()
</code></code></pre><pre><code><code>1.06 s &#177; 48.2 ms per loop (mean &#177; std. dev. of 7 runs, 1 loop each)
</code></code></pre><p>This is faster, but has two drawbacks:</p><ul><li><p><strong>Manual tracking</strong>: You must specify columns yourself; change the query, update <code>usecols</code></p></li><li><p><strong>No row filtering</strong>: All rows still load before the filter applies</p></li></ul><p>Polars and DuckDB handle both automatically by analyzing your query before execution.</p><h3>Polars: Lazy Evaluation</h3><p>Polars supports lazy evaluation, which builds a query plan and optimizes it before execution:</p><pre><code><code>&#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;
&#9474; Query Plan Built:                                           &#9474;
&#9474;   scan_csv &#8594; filter &#8594; group_by &#8594; agg                        &#9474;
&#9474;                                                             &#9474;
&#9474; Optimizations Applied:                                      &#9474;
&#9474;   &#8226; Predicate pushdown (filter during scan)                 &#9474;
&#9474;   &#8226; Projection pushdown (read only needed columns)          &#9474;
&#9474;   &#8226; Multi-threaded execution (parallel across CPU cores)    &#9474;
&#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496;
Memory: &#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608; (low - no intermediate DataFrames)
</code></code></pre><pre><code><code>query_pl = (
    pl.scan_csv("sales_data.csv")
    .filter(pl.col("amount") &gt; 100)
    .group_by("category")
    .agg(pl.col("amount").mean().alias("avg_amount"))
)

# View the optimized query plan
print(query_pl.explain())
</code></code></pre><pre><code><code>AGGREGATE[maintain_order: false]
  [col("amount").mean().alias("avg_amount")] BY [col("category")]
  FROM
  Csv SCAN [sales_data.csv] [id: 4687118704]
  PROJECT 2/4 COLUMNS
  SELECTION: [(col("amount")) &gt; (100.0)]
</code></code></pre><p>The query plan shows these optimizations:</p><ul><li><p><strong>Predicate pushdown</strong>: <code>SELECTION</code> filters during scan, not after loading</p></li><li><p><strong>Projection pushdown</strong>: <code>PROJECT 2/4 COLUMNS</code> reads only what&#8217;s needed</p></li><li><p><strong>Operation reordering</strong>: Aggregate runs on filtered data, not the full dataset</p></li></ul><p>Execute the optimized query:</p><pre><code><code>def polars_query():
    return (
        pl.scan_csv("sales_data.csv")
        .filter(pl.col("amount") &gt; 100)
        .group_by("category")
        .agg(pl.col("amount").mean().alias("avg_amount"))
        .collect()
    )

polars_opt_time = %timeit -o polars_query()
</code></code></pre><pre><code><code>148 ms &#177; 32.3 ms per loop (mean &#177; std. dev. of 7 runs, 1 loop each)
</code></code></pre><h3>DuckDB: SQL Optimizer</h3><p>DuckDB&#8217;s SQL optimizer applies similar optimizations automatically:</p><pre><code><code>&#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;
&#9474; Query Plan Built:                                           &#9474;
&#9474;   SQL &#8594; Parser &#8594; Optimizer &#8594; Execution Plan                 &#9474;
&#9474;                                                             &#9474;
&#9474; Optimizations Applied:                                      &#9474;
&#9474;   &#8226; Predicate pushdown (WHERE during scan)                  &#9474;
&#9474;   &#8226; Projection pushdown (SELECT only needed columns)        &#9474;
&#9474;   &#8226; Vectorized execution (process 1024 rows per batch)      &#9474;
&#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496;
Memory: &#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608; (low - streaming execution)
</code></code></pre><pre><code><code>def duckdb_query():
    return duckdb.sql("""
        SELECT category, AVG(amount) as avg_amount
        FROM 'sales_data.csv'
        WHERE amount &gt; 100
        GROUP BY category
    """).df()

duckdb_opt_time = %timeit -o duckdb_query()
</code></code></pre><pre><code><code>245 ms &#177; 12.1 ms per loop (mean &#177; std. dev. of 7 runs, 1 loop each)
</code></code></pre><p>Let&#8217;s compare the performance of the optimized queries:</p><pre><code><code>print(f"Polars is {pandas_opt_time.average / polars_opt_time.average:.1f}&#215; faster than pandas")
print(f"DuckDB is {pandas_opt_time.average / duckdb_opt_time.average:.1f}&#215; faster than pandas")
</code></code></pre><pre><code><code>Polars is 9.9&#215; faster than pandas
DuckDB is 6.0&#215; faster than pandas
</code></code></pre><p>Polars outperforms DuckDB (9.9&#215; vs 6.0&#215;) in this benchmark because its Rust-based engine handles the filter-then-aggregate pattern efficiently. DuckDB&#8217;s strength lies in complex SQL queries with joins and subqueries.</p><h2>Memory Efficiency</h2><h3>pandas: Full Memory Load</h3><p>pandas loads the entire dataset into RAM:</p><pre><code><code>&#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;
&#9474;  RAM                                                        &#9474;
&#9474;  &#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488; &#9474;
&#9474;  &#9474;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9474; &#9474;
&#9474;  &#9474;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608; ALL 10M ROWS &#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9474; &#9474;
&#9474;  &#9474;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9474; &#9474;
&#9474;  &#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496; &#9474;
&#9474;  Usage: 707,495 KB (entire dataset in memory)               &#9474;
&#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496;
</code></code></pre><pre><code><code>df_pd_mem = pd.read_csv("sales_data.csv")
pandas_mem = df_pd_mem.memory_usage(deep=True).sum() / 1e3
print(f"pandas memory usage: {pandas_mem:,.0f} KB")
</code></code></pre><pre><code><code>pandas memory usage: 707,495 KB
</code></code></pre><p>For larger-than-RAM datasets, pandas throws an out-of-memory error.</p><h3>Polars: Streaming Mode</h3><p>Polars can process data in streaming mode, handling chunks without loading everything:</p><pre><code><code>&#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;
&#9474;  RAM                                                        &#9474;
&#9474;  &#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488; &#9474;
&#9474;  &#9474;&#9608;                                                       &#9474; &#9474;
&#9474;  &#9474;                    (result only)                       &#9474; &#9474;
&#9474;  &#9474;                                                        &#9474; &#9474;
&#9474;  &#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496; &#9474;
&#9474;  Usage: 0.06 KB (streams chunks, keeps only result)         &#9474;
&#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496;
</code></code></pre><pre><code><code>result_pl_stream = (
    pl.scan_csv("sales_data.csv")
    .group_by("category")
    .agg(pl.col("amount").mean())
    .collect(streaming=True)
)

polars_mem = result_pl_stream.estimated_size() / 1e3
print(f"Polars result memory: {polars_mem:.2f} KB")
</code></code></pre><pre><code><code>Polars result memory: 0.06 KB
</code></code></pre><p>For larger-than-RAM files, use <code>sink_parquet</code> instead of <code>collect()</code>. It writes results directly to disk as chunks are processed, never holding the full dataset in memory:</p><pre><code><code>(
    pl.scan_csv("sales_data.csv")
    .filter(pl.col("amount") &gt; 500)
    .sink_parquet("filtered_sales.parquet")
)
</code></code></pre><h3>DuckDB: Automatic Spill-to-Disk</h3><p>DuckDB automatically writes intermediate results to temporary files when data exceeds available RAM:</p><pre><code><code>&#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;
&#9474;  RAM                              Disk (if needed)          &#9474;
&#9474;  &#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;     &#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;  &#9474;
&#9474;  &#9474;&#9608;                         &#9474;     &#9474;&#9617;&#9617;&#9617;&#9617;&#9617;&#9617;&#9617;&#9617;&#9617;&#9617;&#9617;&#9617;&#9617;&#9617;&#9617;&#9617;&#9617;&#9617;&#9617;&#9617;&#9617;&#9617;&#9474;  &#9474;
&#9474;  &#9474;     (up to 500MB)        &#9474;  &#8594;  &#9474;    (overflow here)   &#9474;  &#9474;
&#9474;  &#9474;                          &#9474;     &#9474;                      &#9474;  &#9474;
&#9474;  &#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496;     &#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496;  &#9474;
&#9474;  Usage: 0.42 KB (spills to disk when RAM full)              &#9474;
&#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496;
</code></code></pre><pre><code><code># Configure memory limit and temp directory
duckdb.sql("SET memory_limit = '500MB'")
duckdb.sql("SET temp_directory = '/tmp/duckdb_temp'")

# DuckDB handles larger-than-RAM automatically
result_duckdb_mem = duckdb.sql("""
    SELECT category, AVG(amount) as avg_amount
    FROM 'sales_data.csv'
    GROUP BY category
""").df()

duckdb_mem = result_duckdb_mem.memory_usage(deep=True).sum() / 1e3
print(f"DuckDB result memory: {duckdb_mem:.2f} KB")
</code></code></pre><pre><code><code>DuckDB result memory: 0.42 KB
</code></code></pre><p>DuckDB&#8217;s out-of-core processing makes it ideal for embedded analytics where memory is limited.</p><pre><code><code>print(f"pandas: {pandas_mem:,.0f} KB (full dataset)")
print(f"Polars: {polars_mem:.2f} KB (result only)")
print(f"DuckDB: {duckdb_mem:.2f} KB (result only)")
print(f"\nPolars uses {pandas_mem / polars_mem:,.0f}&#215; less memory than pandas")
print(f"DuckDB uses {pandas_mem / duckdb_mem:,.0f}&#215; less memory than pandas")
</code></code></pre><pre><code><code>pandas: 707,495 KB (full dataset)
Polars: 0.06 KB (result only)
DuckDB: 0.42 KB (result only)

Polars uses 11,791,583&#215; less memory than pandas
DuckDB uses 1,684,512&#215; less memory than pandas
</code></code></pre><p>The million-fold reduction comes from streaming: Polars and DuckDB process data in chunks and only keep the 4-row result in memory, while pandas must hold all 10 million rows to compute the same aggregation.</p><h2>Final Thoughts</h2><p>If your code is all written in pandas, you don&#8217;t need to rewrite it all. You can migrate where it matters:</p><ul><li><p><strong>Profile first</strong>: Find which pandas operations are slow</p></li><li><p><strong>Replace with Polars</strong>: CSV reads, groupbys, and joins see the biggest gains</p></li><li><p><strong>Add DuckDB</strong>: When SQL is cleaner than chained DataFrame operations</p></li></ul><p>Keep pandas for final ML steps. Convert with <code>df.to_pandas()</code> when needed.</p><p>For side-by-side syntax comparisons, interoperability examples, and a full decision matrix with recommendations by data size and workflow, see the <a href="https://codecut.ai/pandas-vs-polars-vs-duckdb-comparison/">complete comparison</a>.</p><div><hr></div><p><em>Originally published on <a href="https://codecut.ai/pandas-vs-polars-vs-duckdb-comparison-quick/">CodeCut</a>.</em></p>]]></content:encoded></item><item><title><![CDATA[Quick Tips: dotenvx - Separate Dev, Staging, and Prod Configs Safely]]></title><description><![CDATA[Plus generate images locally with Ideogram 4]]></description><link>https://newsletter.codecut.ai/p/quick-tips-dotenvx-separate-dev-staging</link><guid isPermaLink="false">https://newsletter.codecut.ai/p/quick-tips-dotenvx-separate-dev-staging</guid><dc:creator><![CDATA[CodeCut]]></dc:creator><pubDate>Thu, 25 Jun 2026 16:01:31 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!HHrY!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1bf4f265-7387-4ebf-8a63-dc29ce40f21f_905x501.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Grab your coffee. Here are this week&#8217;s highlights.</p><div><hr></div><h2>&#128197; Today&#8217;s Picks</h2><h3>dotenvx - Separate Dev, Staging, and Prod Configs Safely</h3><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!HHrY!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1bf4f265-7387-4ebf-8a63-dc29ce40f21f_905x501.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!HHrY!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1bf4f265-7387-4ebf-8a63-dc29ce40f21f_905x501.png 424w, https://substackcdn.com/image/fetch/$s_!HHrY!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1bf4f265-7387-4ebf-8a63-dc29ce40f21f_905x501.png 848w, https://substackcdn.com/image/fetch/$s_!HHrY!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1bf4f265-7387-4ebf-8a63-dc29ce40f21f_905x501.png 1272w, https://substackcdn.com/image/fetch/$s_!HHrY!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1bf4f265-7387-4ebf-8a63-dc29ce40f21f_905x501.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!HHrY!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1bf4f265-7387-4ebf-8a63-dc29ce40f21f_905x501.png" width="905" height="501" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/1bf4f265-7387-4ebf-8a63-dc29ce40f21f_905x501.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:501,&quot;width&quot;:905,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Code example: dotenvx - Separate Dev, Staging, and Prod Configs Safely&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Code example: dotenvx - Separate Dev, Staging, and Prod Configs Safely" title="Code example: dotenvx - Separate Dev, Staging, and Prod Configs Safely" srcset="https://substackcdn.com/image/fetch/$s_!HHrY!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1bf4f265-7387-4ebf-8a63-dc29ce40f21f_905x501.png 424w, https://substackcdn.com/image/fetch/$s_!HHrY!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1bf4f265-7387-4ebf-8a63-dc29ce40f21f_905x501.png 848w, https://substackcdn.com/image/fetch/$s_!HHrY!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1bf4f265-7387-4ebf-8a63-dc29ce40f21f_905x501.png 1272w, https://substackcdn.com/image/fetch/$s_!HHrY!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1bf4f265-7387-4ebf-8a63-dc29ce40f21f_905x501.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h4>Problem</h4><p>A single <code>.env</code> file is fine for a small project.</p><p>Once you add dev, staging, production, and CI, those settings need to be separated because each environment connects to different resources and should not share the same secrets.</p><h4>Solution</h4><p><strong><a href="https://github.com/dotenvx/dotenvx">dotenvx</a></strong> makes each environment explicit: load <code>.env.production</code>, <code>.env.staging</code>, or <code>.env.ci</code> directly with <code>-f</code>.</p><p>Because each file can be encrypted, you can commit the config safely without sharing the private keys across environments.</p><blockquote></blockquote><div><hr></div><h3>Ideogram 4 - Run Open-Weight Image Generation Locally</h3><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!cYCH!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F67de4a11-f275-4c13-a1b6-61a6c50166d8_1080x947.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!cYCH!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F67de4a11-f275-4c13-a1b6-61a6c50166d8_1080x947.png 424w, https://substackcdn.com/image/fetch/$s_!cYCH!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F67de4a11-f275-4c13-a1b6-61a6c50166d8_1080x947.png 848w, https://substackcdn.com/image/fetch/$s_!cYCH!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F67de4a11-f275-4c13-a1b6-61a6c50166d8_1080x947.png 1272w, https://substackcdn.com/image/fetch/$s_!cYCH!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F67de4a11-f275-4c13-a1b6-61a6c50166d8_1080x947.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!cYCH!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F67de4a11-f275-4c13-a1b6-61a6c50166d8_1080x947.png" width="1080" height="947" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/67de4a11-f275-4c13-a1b6-61a6c50166d8_1080x947.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:947,&quot;width&quot;:1080,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Code example: Ideogram 4 - Run Open-Weight Image Generation Locally&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Code example: Ideogram 4 - Run Open-Weight Image Generation Locally" title="Code example: Ideogram 4 - Run Open-Weight Image Generation Locally" srcset="https://substackcdn.com/image/fetch/$s_!cYCH!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F67de4a11-f275-4c13-a1b6-61a6c50166d8_1080x947.png 424w, https://substackcdn.com/image/fetch/$s_!cYCH!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F67de4a11-f275-4c13-a1b6-61a6c50166d8_1080x947.png 848w, https://substackcdn.com/image/fetch/$s_!cYCH!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F67de4a11-f275-4c13-a1b6-61a6c50166d8_1080x947.png 1272w, https://substackcdn.com/image/fetch/$s_!cYCH!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F67de4a11-f275-4c13-a1b6-61a6c50166d8_1080x947.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h4>Problem</h4><p>Most image generation tools are hosted behind an API. That works for quick experiments, but it limits control over cost, infrastructure, privacy, and customization.</p><h4>Solution</h4><p><strong><a href="https://github.com/ideogram-oss/ideogram4">Ideogram 4</a></strong> changes that with an open-weight release. You can download the 9.3B parameter model files and run them on your own machine or server.</p><p>Key features:</p><ul><li><p>Strong text rendering inside images, with a 0.97 score on an English text-accuracy benchmark</p></li><li><p>Structured JSON prompts instead of only free-form sentences</p></li><li><p>Control over subject, style, lighting, typography, and color palettes by hex code</p></li><li><p>Bounding-box coordinates for placing visual elements exactly where you want them</p></li></ul><blockquote></blockquote><div><hr></div><h3>&#128172; Rate Your Experience</h3><p>How would you rate your newsletter experience? <a href="https://r7yuob192zv.typeform.com/to/VODwGYMW">Share your feedback &#8594;</a></p><div><hr></div><h3>&#128269; Explore More on CodeCut</h3><ul><li><p><a href="https://codecut.ai/tool-selector/?utm_source=Substack&amp;utm_medium=email&amp;utm_campaign=article_tool-selector">Tool Selector</a> - Discover 70+ Python tools for AI and data science</p></li><li><p><a href="https://codecut.ai/production-ready-data-science/?utm_source=Substack&amp;utm_medium=email&amp;utm_campaign=article_production-ready-data-science">Production Ready Data Science</a> - A practical book for taking projects from prototype to production</p></li></ul>]]></content:encoded></item><item><title><![CDATA[PDF Table Extraction: Docling vs Marker vs LlamaParse Compared]]></title><description><![CDATA[Compare three Python tools for PDF table extraction: Docling, Marker, and LlamaParse. Learn which handles merged cells and multi-level headers best.]]></description><link>https://newsletter.codecut.ai/p/pdf-table-extraction-docling-vs-marker</link><guid isPermaLink="false">https://newsletter.codecut.ai/p/pdf-table-extraction-docling-vs-marker</guid><dc:creator><![CDATA[CodeCut]]></dc:creator><pubDate>Tue, 23 Jun 2026 16:01:05 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/5b23efb9-4359-4e9c-8b0b-1e4cc2f18c7e_1200x628.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>Introduction</h2><p>Have you ever copied a table from a PDF into a spreadsheet only to find the formatting completely broken? These issues include cells shifting, values landing in the wrong columns, and merged headers losing their structure.</p><p>While doing research, I came across three Python tools for extracting tables from PDFs: Docling, Marker, and LlamaParse. To compare them fairly, I ran each tool on the same difficult table and evaluated the results.</p><p>In this article, I&#8217;ll walk through what I found on one dense numeric table that exposes how each tool handles tight layouts. For the full comparison across simple, medium, and hard tables, see the long version: <a href="https://codecut.ai/docling-vs-marker-vs-llamaparse/">PDF Table Extraction: Docling vs Marker vs LlamaParse Compared</a>.</p><h2>The Test Document</h2><p>All examples use the same PDF: the <a href="https://arxiv.org/pdf/2408.09869">Docling Technical Report</a> from arXiv:</p><pre><code><code>source = "https://arxiv.org/pdf/2408.09869"
</code></code></pre><p>Some tools require a local file path instead of a URL, so let&#8217;s download the PDF first:</p><pre><code><code>import urllib.request

# Download PDF locally (used by Marker later)
local_pdf = "docling_report.pdf"
urllib.request.urlretrieve(source, local_pdf)
</code></code></pre><h2>Docling: Vision-Language Model Pipeline</h2><p><a href="https://github.com/docling-project/docling">Docling</a> is IBM&#8217;s open-source document converter built specifically for structured extraction. It ships with two pipelines:</p><ul><li><p><strong>Default pipeline</strong> uses two small AI models trained specifically for tables. One spots tables on the page, the other reads the grid inside</p></li><li><p><strong>VLM pipeline</strong> uses one larger AI model that can understand images, similar to how ChatGPT can describe a photo. It reads the whole page and outputs the table structure directly</p></li></ul><p>The default pipeline is fast, but it can struggle with complex layouts like multi-level headers and merged cells. The VLM pipeline trades some speed for better accuracy on tricky tables, which is what we want for this comparison.</p><p>We&#8217;ll use <strong>GraniteDocling</strong>, IBM&#8217;s vision model built specifically for documents.</p><p>The result is a pandas DataFrame for each table, ready for analysis.</p><blockquote><p>For Docling&#8217;s full document processing capabilities beyond tables, including chunking and RAG integration, see <a href="https://codecut.ai/docling-pdf-rag-document-processing/">Transform Any PDF into Searchable AI Data with Docling</a>.</p></blockquote><p>To install Docling, pick the variant that matches your hardware:</p><pre><code><code>Platform                       Install command                     Model spec
Apple Silicon (M1+)            pip install "docling[vlm]" mlx-vlm  GRANITEDOCLING_MLX
Linux / Windows (CUDA or CPU)  pip install "docling[vlm]"          GRANITEDOCLING_TRANSFORMERS</code></code></pre><p><em>This article uses docling v2.93.0.</em></p><h3>Table Extraction</h3><p>To use the VLM pipeline, we configure <code>DocumentConverter</code> with <code>VlmPipeline</code> and select GraniteDocling as the model:</p><pre><code><code>from docling.datamodel import vlm_model_specs
from docling.datamodel.base_models import InputFormat
from docling.datamodel.pipeline_options import VlmPipelineOptions
from docling.document_converter import DocumentConverter, PdfFormatOption
from docling.pipeline.vlm_pipeline import VlmPipeline

pipeline_options = VlmPipelineOptions(
    vlm_options=vlm_model_specs.GRANITEDOCLING_MLX,           # Apple Silicon
    # vlm_options=vlm_model_specs.GRANITEDOCLING_TRANSFORMERS,  # Linux / Windows
)

converter = DocumentConverter(
    format_options={
        InputFormat.PDF: PdfFormatOption(
            pipeline_cls=VlmPipeline,
            pipeline_options=pipeline_options,
        )
    }
)
</code></code></pre><p>Now we can convert the PDF and measure how long it takes:</p><pre><code><code>%%time
result = converter.convert(source)
</code></code></pre><pre><code><code>Wall time: 1min 50s
</code></code></pre><p>Here&#8217;s the original table from the PDF:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!JTCr!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf0518c1-f505-4ca6-a2d7-ed242eada089_1064x758.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!JTCr!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf0518c1-f505-4ca6-a2d7-ed242eada089_1064x758.png 424w, https://substackcdn.com/image/fetch/$s_!JTCr!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf0518c1-f505-4ca6-a2d7-ed242eada089_1064x758.png 848w, https://substackcdn.com/image/fetch/$s_!JTCr!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf0518c1-f505-4ca6-a2d7-ed242eada089_1064x758.png 1272w, https://substackcdn.com/image/fetch/$s_!JTCr!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf0518c1-f505-4ca6-a2d7-ed242eada089_1064x758.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!JTCr!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf0518c1-f505-4ca6-a2d7-ed242eada089_1064x758.png" width="1064" height="758" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/bf0518c1-f505-4ca6-a2d7-ed242eada089_1064x758.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:758,&quot;width&quot;:1064,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;The benchmark table from the original PDF&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="The benchmark table from the original PDF" title="The benchmark table from the original PDF" srcset="https://substackcdn.com/image/fetch/$s_!JTCr!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf0518c1-f505-4ca6-a2d7-ed242eada089_1064x758.png 424w, https://substackcdn.com/image/fetch/$s_!JTCr!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf0518c1-f505-4ca6-a2d7-ed242eada089_1064x758.png 848w, https://substackcdn.com/image/fetch/$s_!JTCr!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf0518c1-f505-4ca6-a2d7-ed242eada089_1064x758.png 1272w, https://substackcdn.com/image/fetch/$s_!JTCr!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf0518c1-f505-4ca6-a2d7-ed242eada089_1064x758.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>And here&#8217;s what Docling extracted:</p><pre><code><code># Export the table as a DataFrame
table = result.document.tables[1]
df = table.export_to_dataframe(doc=result.document)
df
</code></code></pre><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!oYnt!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F19b7b3ef-1de3-464d-84ba-919283fa1b4b_988x555.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!oYnt!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F19b7b3ef-1de3-464d-84ba-919283fa1b4b_988x555.png 424w, https://substackcdn.com/image/fetch/$s_!oYnt!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F19b7b3ef-1de3-464d-84ba-919283fa1b4b_988x555.png 848w, https://substackcdn.com/image/fetch/$s_!oYnt!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F19b7b3ef-1de3-464d-84ba-919283fa1b4b_988x555.png 1272w, https://substackcdn.com/image/fetch/$s_!oYnt!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F19b7b3ef-1de3-464d-84ba-919283fa1b4b_988x555.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!oYnt!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F19b7b3ef-1de3-464d-84ba-919283fa1b4b_988x555.png" width="988" height="555" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/19b7b3ef-1de3-464d-84ba-919283fa1b4b_988x555.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:555,&quot;width&quot;:988,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:47614,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://newsletter.codecut.ai/i/199273666?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F19b7b3ef-1de3-464d-84ba-919283fa1b4b_988x555.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!oYnt!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F19b7b3ef-1de3-464d-84ba-919283fa1b4b_988x555.png 424w, https://substackcdn.com/image/fetch/$s_!oYnt!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F19b7b3ef-1de3-464d-84ba-919283fa1b4b_988x555.png 848w, https://substackcdn.com/image/fetch/$s_!oYnt!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F19b7b3ef-1de3-464d-84ba-919283fa1b4b_988x555.png 1272w, https://substackcdn.com/image/fetch/$s_!oYnt!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F19b7b3ef-1de3-464d-84ba-919283fa1b4b_988x555.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The VLM pipeline struggled badly with this denser table.</p><p><strong>Worked:</strong></p><ul><li><p>The 12 row labels (Caption, Footnote, ..., Title, All) match the original</p></li></ul><p><strong>Didn&#8217;t work:</strong></p><ul><li><p>Column headers are hallucinated: the original has &#8220;MRCNN R50&#8221;, &#8220;MRCNN R101&#8221;, &#8220;FRCNN R101&#8221;, &#8220;YOLO v5x6&#8221;, but the VLM output shows &#8220;R-CNN&#8221;, &#8220;R-CNN10-FPRN 3x&#8221;, &#8220;V1S&#8221;, &#8220;V2S&#8221;</p></li><li><p>Numeric values don&#8217;t match the original. The Footnote row reads &#8220;70.1 70.1 70.1 70.1 70.1&#8221; instead of &#8220;83-91 70.9 71.8 73.7 77.2&#8221;</p></li><li><p>Column 4 shows &#8220;64.4&#8221; repeating across 7 consecutive rows</p></li></ul><p>This happens because the VLM writes cells one at a time, similar to how ChatGPT writes a response word by word. When the table has many similar-looking numbers, the model can get stuck and keep repeating the same value, which is why &#8220;64.4&#8221; appears 7 times in a row.</p><p><strong>Conclusion:</strong> Docling&#8217;s VLM pipeline produces unreliable results on dense numeric data, where it can hallucinate column names, repeat values across rows, and lose track of merged cells.</p><h3>Performance</h3><p>Docling took about 1 minute 50 seconds for the full 6-page PDF on an Apple M5 Pro (64 GB RAM). Most of that time is spent on the GPU: GraniteDocling reads each page as an image and generates the table structure one token at a time, which pins the GPU at near-full utilization.</p><h2>Marker: Vision Transformer Pipeline</h2><p><a href="https://github.com/datalab-to/marker">Marker</a> is an open-source PDF-to-Markdown converter built on the Surya layout engine. Unlike Docling&#8217;s two-stage pipeline, Marker runs five stages for table extraction:</p><ul><li><p><strong>Layout detection</strong>: a Vision Transformer identifies table regions on each page</p></li><li><p><strong>OCR error detection</strong>: flags misrecognized text</p></li><li><p><strong>Bounding box detection</strong>: locates individual cell boundaries</p></li><li><p><strong>Table recognition</strong>: reconstructs row/column structure from detected cells</p></li><li><p><strong>Text recognition</strong>: extracts text from all detected regions</p></li></ul><p>To install Marker, run:</p><pre><code><code>pip install marker-pdf</code></code></pre><p><em>This article uses marker v1.10.2.</em></p><h3>Table Extraction</h3><p>Marker provides a dedicated <code>TableConverter</code> that extracts only tables from a document, returning them as Markdown:</p><pre><code><code>from marker.converters.table import TableConverter
from marker.models import create_model_dict
from marker.output import text_from_rendered

models = create_model_dict()
converter = TableConverter(artifact_dict=models)
</code></code></pre><p>Convert the PDF and measure how long it takes:</p><pre><code><code>%%time
rendered = converter(local_pdf)
table_md, _, images = text_from_rendered(rendered)
</code></code></pre><pre><code><code>Wall time: 47.1 s
</code></code></pre><p>Since <code>TableConverter</code> returns all tables as a single Markdown string, we split them on blank lines:</p><pre><code><code>tables = table_md.strip().split("\n\n")</code></code></pre><p>Here&#8217;s the original table from the PDF:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!JTCr!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf0518c1-f505-4ca6-a2d7-ed242eada089_1064x758.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!JTCr!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf0518c1-f505-4ca6-a2d7-ed242eada089_1064x758.png 424w, https://substackcdn.com/image/fetch/$s_!JTCr!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf0518c1-f505-4ca6-a2d7-ed242eada089_1064x758.png 848w, https://substackcdn.com/image/fetch/$s_!JTCr!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf0518c1-f505-4ca6-a2d7-ed242eada089_1064x758.png 1272w, https://substackcdn.com/image/fetch/$s_!JTCr!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf0518c1-f505-4ca6-a2d7-ed242eada089_1064x758.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!JTCr!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf0518c1-f505-4ca6-a2d7-ed242eada089_1064x758.png" width="1064" height="758" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/bf0518c1-f505-4ca6-a2d7-ed242eada089_1064x758.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:758,&quot;width&quot;:1064,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;The benchmark table from the original PDF&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="The benchmark table from the original PDF" title="The benchmark table from the original PDF" srcset="https://substackcdn.com/image/fetch/$s_!JTCr!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf0518c1-f505-4ca6-a2d7-ed242eada089_1064x758.png 424w, https://substackcdn.com/image/fetch/$s_!JTCr!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf0518c1-f505-4ca6-a2d7-ed242eada089_1064x758.png 848w, https://substackcdn.com/image/fetch/$s_!JTCr!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf0518c1-f505-4ca6-a2d7-ed242eada089_1064x758.png 1272w, https://substackcdn.com/image/fetch/$s_!JTCr!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf0518c1-f505-4ca6-a2d7-ed242eada089_1064x758.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>And here&#8217;s what Marker extracted:</p><pre><code><code>print(tables[1])</code></code></pre><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!aoOc!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F623c77bd-a91e-4e98-94ea-6a2a85de8cf5_995x603.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!aoOc!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F623c77bd-a91e-4e98-94ea-6a2a85de8cf5_995x603.png 424w, https://substackcdn.com/image/fetch/$s_!aoOc!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F623c77bd-a91e-4e98-94ea-6a2a85de8cf5_995x603.png 848w, https://substackcdn.com/image/fetch/$s_!aoOc!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F623c77bd-a91e-4e98-94ea-6a2a85de8cf5_995x603.png 1272w, https://substackcdn.com/image/fetch/$s_!aoOc!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F623c77bd-a91e-4e98-94ea-6a2a85de8cf5_995x603.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!aoOc!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F623c77bd-a91e-4e98-94ea-6a2a85de8cf5_995x603.png" width="995" height="603" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/623c77bd-a91e-4e98-94ea-6a2a85de8cf5_995x603.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:603,&quot;width&quot;:995,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:59497,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://newsletter.codecut.ai/i/199273666?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F623c77bd-a91e-4e98-94ea-6a2a85de8cf5_995x603.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!aoOc!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F623c77bd-a91e-4e98-94ea-6a2a85de8cf5_995x603.png 424w, https://substackcdn.com/image/fetch/$s_!aoOc!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F623c77bd-a91e-4e98-94ea-6a2a85de8cf5_995x603.png 848w, https://substackcdn.com/image/fetch/$s_!aoOc!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F623c77bd-a91e-4e98-94ea-6a2a85de8cf5_995x603.png 1272w, https://substackcdn.com/image/fetch/$s_!aoOc!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F623c77bd-a91e-4e98-94ea-6a2a85de8cf5_995x603.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Marker struggled with this denser table.</p><p><strong>Worked:</strong></p><ul><li><p>All 12 row labels are preserved (Caption, Footnote, ..., Title, All)</p></li><li><p>Values for the FRCNN R101 and YOLO v5x6 columns extracted correctly</p></li></ul><p><strong>Didn&#8217;t work:</strong></p><ul><li><p>Header parents merged: &#8220;human&#8221; and &#8220;MRCNN&#8221; share a column header, &#8220;FRCNN&#8221; and &#8220;YOLO&#8221; merged into one cell</p></li><li><p>The human, MRCNN R50, and MRCNN R101 values are packed into one cell per row (e.g., &#8220;84-89 68.4 71.5&#8221;), leaving the MRCNN columns empty</p></li><li><p>The Section-header row label merged with its data (&#8221;Section-header 83-84 67.6 69.3&#8221;), breaking that row&#8217;s alignment</p></li></ul><p><strong>Conclusion:</strong> Marker needs visible cell borders. When the same data appears redrawn with borders later in the PDF, Marker extracts it perfectly. See the <a href="https://codecut.ai/docling-vs-marker-vs-llamaparse/">long version</a> for that result.</p><h3>Performance</h3><p>Marker took about 47 seconds for the full 6-page PDF on an Apple M5 Pro (64 GB RAM), more than twice as fast as Docling&#8217;s VLM pipeline. The speed difference comes down to architecture:</p><ul><li><p><strong>Docling</strong> runs a single large vision-language model that reads each page as an image and generates the table structure one token at a time. Large models take time per token, so the total runtime adds up.</p></li><li><p><strong>Marker</strong> runs a 5-stage pipeline of smaller specialized models that mostly do classification or detection, avoiding the slow token-by-token generation that VLMs need.</p></li></ul><h2>LlamaParse: LLM-Guided Extraction</h2><p><a href="https://github.com/run-llama/llama_cloud_services">LlamaParse</a> is a cloud-hosted document parser by LlamaIndex that takes a different approach:</p><ul><li><p><strong>Cloud-based</strong>: the PDF is uploaded to LlamaCloud instead of being processed locally</p></li><li><p><strong>LLM-guided</strong>: an LLM interprets each page and identifies tables, returning structured row data</p></li></ul><blockquote><p>For extracting structured data from images like receipts using the same LlamaIndex ecosystem, see <a href="https://codecut.ai/llamaindex-receipt-data-extraction/">Turn Receipt Images into Spreadsheets with LlamaIndex</a>.</p></blockquote><p>To install LlamaParse, run:</p><pre><code><code>pip install llama-parse</code></code></pre><p><em>This article uses llama-parse v0.6.54.</em></p><p>LlamaParse requires an API key from <a href="https://cloud.llamaindex.ai/api-key">LlamaIndex Cloud</a>. The free tier includes 10,000 credits per month (basic parsing costs 1 credit per page; advanced modes like <code>parse_page_with_agent</code> cost more).</p><p>Create a <code>.env</code> file with your API key:</p><pre><code><code>LLAMA_CLOUD_API_KEY=llx-...</code></code></pre><pre><code><code>from dotenv import load_dotenv

load_dotenv()</code></code></pre><h3>Table Extraction</h3><p>To extract tables, we create a <code>LlamaParse</code> instance with two key settings:</p><ul><li><p><code>parse_page_with_agent</code>: tells LlamaCloud to use an LLM agent that reads each page and returns structured items (tables, text, figures)</p></li><li><p><code>output_tables_as_HTML=True</code>: returns tables as HTML instead of Markdown, which better preserves multi-level headers</p></li></ul><pre><code><code>from llama_cloud_services import LlamaParse

parser = LlamaParse(
    parse_mode="parse_page_with_agent",
    output_tables_as_HTML=True,
)
</code></code></pre><p>Now let&#8217;s convert the PDF and measure how long it takes:</p><pre><code><code>%%time
result = parser.parse(local_pdf)
</code></code></pre><pre><code><code>Wall time: 8.54 s
</code></code></pre><p>We can then iterate through each page&#8217;s items and collect only the tables:</p><pre><code><code>all_tables = []
for page in result.pages:
    for item in page.items:
        if item.type == "table":
            all_tables.append(item)
</code></code></pre><p>Not every item LlamaParse tagged as a table is actually a table. We filter out the title page and a figure that were misclassified, then pick our target table:</p><pre><code><code>incorrect_table_indices = (1, 3)
tables = [t for i, t in enumerate(all_tables) if i not in incorrect_table_indices]
</code></code></pre><p>Here&#8217;s the original table from the PDF:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!JTCr!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf0518c1-f505-4ca6-a2d7-ed242eada089_1064x758.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!JTCr!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf0518c1-f505-4ca6-a2d7-ed242eada089_1064x758.png 424w, https://substackcdn.com/image/fetch/$s_!JTCr!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf0518c1-f505-4ca6-a2d7-ed242eada089_1064x758.png 848w, https://substackcdn.com/image/fetch/$s_!JTCr!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf0518c1-f505-4ca6-a2d7-ed242eada089_1064x758.png 1272w, https://substackcdn.com/image/fetch/$s_!JTCr!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf0518c1-f505-4ca6-a2d7-ed242eada089_1064x758.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!JTCr!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf0518c1-f505-4ca6-a2d7-ed242eada089_1064x758.png" width="1064" height="758" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/bf0518c1-f505-4ca6-a2d7-ed242eada089_1064x758.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:758,&quot;width&quot;:1064,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;The benchmark table from the original PDF&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="The benchmark table from the original PDF" title="The benchmark table from the original PDF" srcset="https://substackcdn.com/image/fetch/$s_!JTCr!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf0518c1-f505-4ca6-a2d7-ed242eada089_1064x758.png 424w, https://substackcdn.com/image/fetch/$s_!JTCr!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf0518c1-f505-4ca6-a2d7-ed242eada089_1064x758.png 848w, https://substackcdn.com/image/fetch/$s_!JTCr!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf0518c1-f505-4ca6-a2d7-ed242eada089_1064x758.png 1272w, https://substackcdn.com/image/fetch/$s_!JTCr!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf0518c1-f505-4ca6-a2d7-ed242eada089_1064x758.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>And here&#8217;s what LlamaParse extracted:</p><pre><code><code>print(tables[1].md)
</code></code></pre><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!lLYM!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc7a68404-2a61-4d3a-9816-2473253ac808_985x556.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!lLYM!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc7a68404-2a61-4d3a-9816-2473253ac808_985x556.png 424w, https://substackcdn.com/image/fetch/$s_!lLYM!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc7a68404-2a61-4d3a-9816-2473253ac808_985x556.png 848w, https://substackcdn.com/image/fetch/$s_!lLYM!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc7a68404-2a61-4d3a-9816-2473253ac808_985x556.png 1272w, https://substackcdn.com/image/fetch/$s_!lLYM!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc7a68404-2a61-4d3a-9816-2473253ac808_985x556.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!lLYM!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc7a68404-2a61-4d3a-9816-2473253ac808_985x556.png" width="985" height="556" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/c7a68404-2a61-4d3a-9816-2473253ac808_985x556.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:556,&quot;width&quot;:985,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:61123,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://newsletter.codecut.ai/i/199273666?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc7a68404-2a61-4d3a-9816-2473253ac808_985x556.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!lLYM!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc7a68404-2a61-4d3a-9816-2473253ac808_985x556.png 424w, https://substackcdn.com/image/fetch/$s_!lLYM!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc7a68404-2a61-4d3a-9816-2473253ac808_985x556.png 848w, https://substackcdn.com/image/fetch/$s_!lLYM!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc7a68404-2a61-4d3a-9816-2473253ac808_985x556.png 1272w, https://substackcdn.com/image/fetch/$s_!lLYM!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc7a68404-2a61-4d3a-9816-2473253ac808_985x556.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>LlamaParse handled this table perfectly:</p><ul><li><p>All 12 row labels match the original (Caption, Footnote, ..., All)</p></li><li><p>All 5 columns are correctly named: human, MRCNN R50, MRCNN R101, FRCNN R101, YOLO v5x6</p></li><li><p>All numeric values match the source, including the &#8220;human&#8221; inter-annotator range column (84-89, 83-91, etc.)</p></li></ul><p><strong>Conclusion:</strong> LlamaParse produces the most accurate extraction of the three tools on this dense layout.</p><h3>Performance</h3><p>LlamaParse finished in 8.54 seconds, the fastest of the three tools (Docling took 1 min 50s, Marker took 47s).</p><p>Unlike Docling and Marker, LlamaParse runs no models on your machine. It uploads the PDF to LlamaCloud, an LLM agent reads each page, and the result comes back:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!PUDH!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fec73727e-5c69-49f3-8ecb-f27364efbe90_2560x1335.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!PUDH!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fec73727e-5c69-49f3-8ecb-f27364efbe90_2560x1335.png 424w, https://substackcdn.com/image/fetch/$s_!PUDH!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fec73727e-5c69-49f3-8ecb-f27364efbe90_2560x1335.png 848w, https://substackcdn.com/image/fetch/$s_!PUDH!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fec73727e-5c69-49f3-8ecb-f27364efbe90_2560x1335.png 1272w, https://substackcdn.com/image/fetch/$s_!PUDH!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fec73727e-5c69-49f3-8ecb-f27364efbe90_2560x1335.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!PUDH!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fec73727e-5c69-49f3-8ecb-f27364efbe90_2560x1335.png" width="1456" height="759" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ec73727e-5c69-49f3-8ecb-f27364efbe90_2560x1335.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:759,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:25756,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://newsletter.codecut.ai/i/199273666?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fec73727e-5c69-49f3-8ecb-f27364efbe90_2560x1335.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!PUDH!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fec73727e-5c69-49f3-8ecb-f27364efbe90_2560x1335.png 424w, https://substackcdn.com/image/fetch/$s_!PUDH!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fec73727e-5c69-49f3-8ecb-f27364efbe90_2560x1335.png 848w, https://substackcdn.com/image/fetch/$s_!PUDH!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fec73727e-5c69-49f3-8ecb-f27364efbe90_2560x1335.png 1272w, https://substackcdn.com/image/fetch/$s_!PUDH!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fec73727e-5c69-49f3-8ecb-f27364efbe90_2560x1335.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The runtime is mostly network upload time and server processing, so it depends on your internet speed and current LlamaCloud load rather than your local hardware.</p><h2>Summary</h2><p>In short:</p><ul><li><p><strong>LlamaParse</strong> wins on speed and accuracy. It&#8217;s the fastest overall and produces the cleanest output, but it requires sending PDFs to LlamaCloud.</p></li><li><p><strong>Marker</strong> is the best local option. It&#8217;s faster than Docling and handles tables with clear visual separation well, but it merges columns on dense layouts.</p></li><li><p><strong>Docling</strong> is the slowest of the three and prone to hallucinating values on dense tables.</p></li></ul><p>When to use each:</p><ul><li><p>Use <strong>LlamaParse</strong> if your documents aren&#8217;t sensitive and you want the best accuracy.</p></li><li><p>Use <strong>Marker</strong> if you must stay local.</p></li><li><p>Use <strong>Docling</strong> for its <a href="https://codecut.ai/docling-pdf-rag-document-processing/">broader document conversion features</a> beyond just table extraction like chunking and RAG.</p></li></ul><p>For the full comparison across other tables with different layouts, see <a href="https://codecut.ai/docling-vs-marker-vs-llamaparse/">PDF Table Extraction: Docling vs Marker vs LlamaParse Compared</a>.</p><div><hr></div><p><em>Originally published on <a href="https://codecut.ai/docling-vs-marker-vs-llamaparse-quick/">CodeCut</a>.</em></p>]]></content:encoded></item><item><title><![CDATA[Quick Tips: PP-OCRv6 - Extract Text from Receipts with Small OCR Models]]></title><description><![CDATA[Plus pick local LLMs that fit your machine]]></description><link>https://newsletter.codecut.ai/p/quick-tips-pp-ocrv6-extract-text</link><guid isPermaLink="false">https://newsletter.codecut.ai/p/quick-tips-pp-ocrv6-extract-text</guid><dc:creator><![CDATA[CodeCut]]></dc:creator><pubDate>Thu, 18 Jun 2026 16:01:22 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!2A_f!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4cae71b6-ee7a-4fdc-9706-49a02b5c926e_717x642.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Grab your coffee. Here are this week&#8217;s highlights.</p><div><hr></div><h2>&#128197; Today&#8217;s Picks</h2><h3>LM Studio - Pick Local Models That Fit Your Machine</h3><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!46M7!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0001e0e2-e71c-46c1-ab91-00b525887d85_1200x710.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!46M7!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0001e0e2-e71c-46c1-ab91-00b525887d85_1200x710.png 424w, https://substackcdn.com/image/fetch/$s_!46M7!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0001e0e2-e71c-46c1-ab91-00b525887d85_1200x710.png 848w, https://substackcdn.com/image/fetch/$s_!46M7!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0001e0e2-e71c-46c1-ab91-00b525887d85_1200x710.png 1272w, https://substackcdn.com/image/fetch/$s_!46M7!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0001e0e2-e71c-46c1-ab91-00b525887d85_1200x710.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!46M7!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0001e0e2-e71c-46c1-ab91-00b525887d85_1200x710.png" width="1200" height="710" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/0001e0e2-e71c-46c1-ab91-00b525887d85_1200x710.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:710,&quot;width&quot;:1200,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Code example: LM Studio - Pick Local Models That Fit Your Machine&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Code example: LM Studio - Pick Local Models That Fit Your Machine" title="Code example: LM Studio - Pick Local Models That Fit Your Machine" srcset="https://substackcdn.com/image/fetch/$s_!46M7!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0001e0e2-e71c-46c1-ab91-00b525887d85_1200x710.png 424w, https://substackcdn.com/image/fetch/$s_!46M7!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0001e0e2-e71c-46c1-ab91-00b525887d85_1200x710.png 848w, https://substackcdn.com/image/fetch/$s_!46M7!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0001e0e2-e71c-46c1-ab91-00b525887d85_1200x710.png 1272w, https://substackcdn.com/image/fetch/$s_!46M7!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0001e0e2-e71c-46c1-ab91-00b525887d85_1200x710.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h4>Problem</h4><p>Choosing a local LLM is not just about picking the smartest model.</p><p>You also need to know whether your machine can actually run it.</p><p>Model size, quantization, GPU memory, and system RAM all affect whether the model will load successfully.</p><h4>Solution</h4><p><strong><a href="https://lmstudio.ai/">LM Studio</a></strong> makes local model selection easier by showing the details that affect whether a model will run well.</p><p>What LM Studio shows:</p><ul><li><p>Download options: choose among model variants by comparing format, quantization, and download size.</p></li><li><p>Fit signal: see whether the model is likely to fit your machine before downloading.</p></li><li><p>README: review model-specific instructions and benchmarks from the model page.</p></li></ul><div><hr></div><h3>PP-OCRv6 - Extract Text from Receipts with Small OCR Models</h3><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!2A_f!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4cae71b6-ee7a-4fdc-9706-49a02b5c926e_717x642.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!2A_f!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4cae71b6-ee7a-4fdc-9706-49a02b5c926e_717x642.png 424w, https://substackcdn.com/image/fetch/$s_!2A_f!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4cae71b6-ee7a-4fdc-9706-49a02b5c926e_717x642.png 848w, https://substackcdn.com/image/fetch/$s_!2A_f!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4cae71b6-ee7a-4fdc-9706-49a02b5c926e_717x642.png 1272w, https://substackcdn.com/image/fetch/$s_!2A_f!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4cae71b6-ee7a-4fdc-9706-49a02b5c926e_717x642.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!2A_f!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4cae71b6-ee7a-4fdc-9706-49a02b5c926e_717x642.png" width="717" height="642" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/4cae71b6-ee7a-4fdc-9706-49a02b5c926e_717x642.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:642,&quot;width&quot;:717,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Code example: PP-OCRv6 - Extract Text from Receipts with Small OCR Models&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Code example: PP-OCRv6 - Extract Text from Receipts with Small OCR Models" title="Code example: PP-OCRv6 - Extract Text from Receipts with Small OCR Models" srcset="https://substackcdn.com/image/fetch/$s_!2A_f!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4cae71b6-ee7a-4fdc-9706-49a02b5c926e_717x642.png 424w, https://substackcdn.com/image/fetch/$s_!2A_f!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4cae71b6-ee7a-4fdc-9706-49a02b5c926e_717x642.png 848w, https://substackcdn.com/image/fetch/$s_!2A_f!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4cae71b6-ee7a-4fdc-9706-49a02b5c926e_717x642.png 1272w, https://substackcdn.com/image/fetch/$s_!2A_f!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4cae71b6-ee7a-4fdc-9706-49a02b5c926e_717x642.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h4>Problem</h4><p>When extracting text from messy documents, many teams now reach for large vision-language models.</p><p>But if you only need to find and read text, a smaller OCR-specific model may be faster and cheaper to run.</p><h4>Solution</h4><p><strong><a href="https://huggingface.co/collections/PaddlePaddle/pp-ocrv6">PP-OCRv6</a></strong> is designed for reading text from images, rather than broad vision-language reasoning, so it can focus on text extraction efficiently.</p><p>It delivers strong text detection and recognition while staying much smaller than billion-parameter VLMs.</p><p>With tiny, small, and medium variants, you can start lightweight and switch to a larger model only when the extracted text needs better accuracy.</p><blockquote><p>&#129514; <a href="https://bit.ly/4eDddLH">Run code</a></p></blockquote><div><hr></div><h3>&#128172; Rate Your Experience</h3><p>How would you rate your newsletter experience? <a href="https://r7yuob192zv.typeform.com/to/VODwGYMW">Share your feedback &#8594;</a></p><div><hr></div><h3>&#128269; Explore More on CodeCut</h3><ul><li><p><a href="https://codecut.ai/tool-selector/?utm_source=Substack&amp;utm_medium=email&amp;utm_campaign=article_tool-selector">Tool Selector</a> - Discover 70+ Python tools for AI and data science</p></li><li><p><a href="https://codecut.ai/production-ready-data-science/?utm_source=Substack&amp;utm_medium=email&amp;utm_campaign=article_production-ready-data-science">Production Ready Data Science</a> - A practical book for taking projects from prototype to production</p></li></ul>]]></content:encoded></item><item><title><![CDATA[Stop Hand-Tuning Prompts: Auto-Optimize an LLM Classifier with DSPy]]></title><description><![CDATA[Stop hand-tuning prompts with DSPy. Build an LLM classifier that evaluates, optimizes, and reuses better prompts with examples.]]></description><link>https://newsletter.codecut.ai/p/stop-hand-tuning-prompts-auto-optimize</link><guid isPermaLink="false">https://newsletter.codecut.ai/p/stop-hand-tuning-prompts-auto-optimize</guid><dc:creator><![CDATA[CodeCut]]></dc:creator><pubDate>Tue, 16 Jun 2026 16:03:09 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/51c0ebb7-787b-40a9-a3de-317076719e67_1200x628.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>The Problem with Hand-Written Prompts</h2><p>Model choice matters, but prompt quality matters too. If the prompt is vague or hard to maintain, the classifier can still produce wrong labels.</p><p>A typical example is a prompt written as one string:</p><pre><code><code>prompt = """
Classify this banking query as:
- card_arrival
- card_delivery_estimate
- card_not_working
- card_swallowed

Return only the label.

Query: My new card still has not arrived after two weeks.
Intent:
"""
</code></code></pre><p>This works for a simple demo, but real queries quickly reveal cases the prompt does not handle well.</p><p>For example:</p><pre><code><code>My new card arrived, but it does not work at the ATM.
</code></code></pre><p>Because the prompt does not clarify this edge case, the model may focus on &#8220;new card&#8221; and return:</p><pre><code><code>card_arrival
</code></code></pre><p>But the correct intent is:</p><pre><code><code>card_not_working
</code></code></pre><p>You can patch the prompt with another rule, but that creates a new problem: every change needs to be retested. A fix for one visible mistake can hide new failures elsewhere.</p><p>Without a dataset and metric, you cannot tell whether the classifier improved overall.</p><p>DSPy replaces manual prompt tweaking with four repeatable steps:</p><ol><li><p>Define the task as a program.</p></li><li><p>Evaluate the program with examples and a metric.</p></li><li><p>Let an optimizer improve the program.</p></li><li><p>Compare the score before and after.</p></li></ol><p>This article walks through that loop by building a small banking intent classifier.</p><blockquote><p><strong>Full version:</strong> This is a condensed version. For the complete walkthrough, see the <a href="https://codecut.ai/dspy-auto-optimize-llm-classifier/">full DSPy classifier tutorial</a>.</p></blockquote><h2>What Is DSPy?</h2><p><a href="https://dspy.ai/">DSPy</a> is a Python framework for programming LLM workflows instead of hand-writing prompts.</p><p>It breaks an LLM workflow into explicit parts:</p><ul><li><p><strong>Signatures</strong> define the inputs and outputs.</p></li><li><p><strong>Modules</strong> run the task with strategies such as <code>Predict</code> or <code>ChainOfThought</code>.</p></li><li><p><strong>Metrics</strong> score the outputs.</p></li><li><p><strong>Optimizers</strong> improve the program using examples and metrics.</p></li></ul><p>This structure makes prompt engineering measurable. You can compare versions, optimize against a metric, and reuse the improved program.</p><pre><code><code>Manual prompt                    DSPy program
-------------                    ------------
Task description       ---&gt;      Signature
Prompting style        ---&gt;      Module
Manual inspection      ---&gt;      Metric
Prompt tweaking        ---&gt;      Optimizer
</code></code></pre><h2>Setup: Banking Query Classification</h2><p>Install the libraries used in this tutorial:</p><pre><code><code>pip install -U dspy pandas python-dotenv
</code></code></pre><p><em>This article uses </em><code>dspy</code><em> v3.2.1, </em><code>pandas</code><em> v2.3.1, and </em><code>python-dotenv</code><em> v1.1.1.</em></p><p>This tutorial uses OpenAI&#8217;s <code>gpt-4o-mini</code> through DSPy&#8217;s language model interface. Store your API key in a <code>.env</code> file:</p><pre><code><code>OPENAI_API_KEY=your-openai-api-key
</code></code></pre><p>Then load the environment variables and configure DSPy:</p><pre><code><code>from typing import Literal

import dspy
import pandas as pd
from dotenv import load_dotenv

load_dotenv()

lm = dspy.LM("openai/gpt-4o-mini")
dspy.configure(lm=lm)
</code></code></pre><p>We will use <a href="https://huggingface.co/datasets/PolyAI/banking77">BANKING77</a>, a dataset of banking support questions labeled with customer intents. To keep loading simple, this tutorial reads the raw CSV files from the original <a href="https://github.com/PolyAI-LDN/task-specific-datasets">PolyAI repository</a>.</p><pre><code><code>TRAIN_URL = "https://raw.githubusercontent.com/PolyAI-LDN/task-specific-datasets/master/banking_data/train.csv"
TEST_URL = "https://raw.githubusercontent.com/PolyAI-LDN/task-specific-datasets/master/banking_data/test.csv"

train_df = pd.read_csv(TRAIN_URL)
test_df = pd.read_csv(TEST_URL)

print(train_df.head())
</code></code></pre><pre><code><code>                                                text      category
0                     I am still waiting on my card?  card_arrival
1  What can I do if my card still hasn't arrived ...  card_arrival
2  I have been waiting over a week. Is the card s...  card_arrival
3  Can I track my card while it is in the process...  card_arrival
4  How do I know if I will get my card, or if it ...  card_arrival
</code></code></pre><p>To keep the example small, we will use four card-support intents instead of all 77 labels. The subset is still useful because <code>card_arrival</code> and <code>card_delivery_estimate</code> are similar enough to create meaningful mistakes.</p><pre><code><code>INTENTS = [
    "card_arrival",
    "card_delivery_estimate",
    "card_not_working",
    "card_swallowed",
]


def sample_intents(data: pd.DataFrame, examples_per_intent: int) -&gt; pd.DataFrame:
    return (
        data[data["category"].isin(INTENTS)]
        .groupby("category", group_keys=False)
        .sample(n=examples_per_intent, random_state=42)
        .reset_index(drop=True)
    )


train_sample = sample_intents(train_df, examples_per_intent=8)
dev_sample = sample_intents(test_df, examples_per_intent=10)
</code></code></pre><p>Before evaluation, prepare the data for DSPy:</p><ul><li><p>Store each query-label pair as a <code>dspy.Example</code>.</p></li><li><p>Mark <code>query</code> as the input field with <code>.with_inputs("query")</code>.</p></li><li><p>Keep <code>intent</code> as the target label DSPy will compare against the prediction.</p></li></ul><pre><code><code>def to_dspy_examples(data: pd.DataFrame) -&gt; list[dspy.Example]:
    return [
        dspy.Example(query=row.text, intent=row.category).with_inputs("query")
        for row in data.itertuples(index=False)
    ]

trainset = to_dspy_examples(train_sample)
devset = to_dspy_examples(dev_sample)
</code></code></pre><h2>Define the Task with a Signature</h2><p>A DSPy signature makes the task explicit. Instead of hiding the task inside a prompt string, you define the input fields, output fields, and output constraints in code.</p><p>The signature below defines the task schema:</p><ul><li><p>Input field: <code>query</code></p></li><li><p>Output field: <code>intent</code></p></li><li><p>Allowed outputs: <code>card_arrival</code>, <code>card_delivery_estimate</code>, <code>card_not_working</code>, <code>card_swallowed</code></p></li><li><p>Field descriptions: short hints DSPy can use when prompting the model</p></li></ul><pre><code><code>class ClassifyBankingIntent(dspy.Signature):
    """Classify a banking support query into one of the allowed intents."""

    query: str = dspy.InputField(desc="Customer support query")
    intent: Literal[
        "card_arrival",
        "card_delivery_estimate",
        "card_not_working",
        "card_swallowed",
    ] = dspy.OutputField(desc="Predicted banking intent")
</code></code></pre><p>The typed <code>intent</code> field is how DSPy keeps outputs within the allowed labels. For a dedicated way to enforce and validate typed LLM outputs with Python types, see <a href="https://codecut.ai/enforce-structured-outputs-from-llms-with-pydanticai/">Enforce Structured Outputs from LLMs with PydanticAI</a>.</p><h2>Run the Task with DSPy Modules</h2><p>A DSPy module turns the signature into callable code.</p><p>Different modules run the same task in different ways:</p><ul><li><p><code>Predict</code> returns the output directly.</p></li><li><p><code>ChainOfThought</code> adds a reasoning step before the output.</p></li></ul><p>Because they can share the same signature, you can switch strategies without redefining the task.</p><h3><code>Predict</code>: Direct Prediction</h3><p><code>Predict</code> is the simplest module. It asks the model to return the output directly.</p><pre><code><code>predict_classifier = dspy.Predict(ClassifyBankingIntent)

prediction = predict_classifier(
    query="The ATM kept my card and did not return it. How do I get it back?"
)

print(f'Intent: {prediction.intent}')
</code></code></pre><pre><code><code>Intent: card_swallowed
</code></code></pre><p>This matches the query and stays within the allowed <code>intent</code> labels.</p><h3><code>ChainOfThought</code>: Reason Before Predicting</h3><p><code>ChainOfThought</code> keeps the same input and output fields, but adds a reasoning step before the prediction:</p><pre><code><code>cot_classifier = dspy.ChainOfThought(ClassifyBankingIntent)

prediction = cot_classifier(
    query="The ATM kept my card and did not return it. How do I get it back?"
)

print(f'Reasoning: {prediction.reasoning}')
print(f'Intent: {prediction.intent}')
</code></code></pre><pre><code><code>Reasoning: The customer's query indicates that their card was not returned by an ATM, which suggests that the card was likely swallowed by the machine. The customer is seeking information on how to retrieve their card, which aligns with the intent of a card being swallowed by the ATM.
Intent: card_swallowed
</code></code></pre><p>Unlike <code>Predict</code>, <code>ChainOfThought</code> exposes the reasoning before the final label. The predicted intent is still <code>card_swallowed</code>.</p><p>Now that the modules are defined, the next step is to score the classifier versions and optimize one of them.</p><h2>Evaluate the Baseline</h2><p>Before optimizing the classifier, we need to measure the baseline. Here, the metric is simple: a prediction is correct when the predicted intent matches the expected label.</p><pre><code><code>def intent_exact_match(example: dspy.Example, prediction: dspy.Prediction, trace=None) -&gt; bool:
    return example.intent == prediction.intent
</code></code></pre><p>Now create a DSPy evaluator:</p><pre><code><code>evaluate = dspy.Evaluate(
    devset=devset,  # examples to score
    metric=intent_exact_match,  # scoring function
    num_threads=4,  # parallel model calls
    display_progress=True,  # show progress bar
    display_table=5,  # show sample predictions
)
</code></code></pre><p>Use the same evaluator to compare <code>Predict</code> and <code>ChainOfThought</code> on the dev set:</p><pre><code><code>predict_score = evaluate(predict_classifier)
print(f"Predict score: {predict_score.score}")
</code></code></pre><p>Because <code>display_table=5</code> is set, the evaluator prints a sample of predictions before the score:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!YHsO!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff75d4a2e-9333-4730-8240-0c6bbd16058c_1994x820.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!YHsO!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff75d4a2e-9333-4730-8240-0c6bbd16058c_1994x820.png 424w, https://substackcdn.com/image/fetch/$s_!YHsO!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff75d4a2e-9333-4730-8240-0c6bbd16058c_1994x820.png 848w, https://substackcdn.com/image/fetch/$s_!YHsO!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff75d4a2e-9333-4730-8240-0c6bbd16058c_1994x820.png 1272w, https://substackcdn.com/image/fetch/$s_!YHsO!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff75d4a2e-9333-4730-8240-0c6bbd16058c_1994x820.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!YHsO!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff75d4a2e-9333-4730-8240-0c6bbd16058c_1994x820.png" width="1456" height="599" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f75d4a2e-9333-4730-8240-0c6bbd16058c_1994x820.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:599,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:114516,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://newsletter.codecut.ai/i/201828304?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff75d4a2e-9333-4730-8240-0c6bbd16058c_1994x820.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!YHsO!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff75d4a2e-9333-4730-8240-0c6bbd16058c_1994x820.png 424w, https://substackcdn.com/image/fetch/$s_!YHsO!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff75d4a2e-9333-4730-8240-0c6bbd16058c_1994x820.png 848w, https://substackcdn.com/image/fetch/$s_!YHsO!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff75d4a2e-9333-4730-8240-0c6bbd16058c_1994x820.png 1272w, https://substackcdn.com/image/fetch/$s_!YHsO!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff75d4a2e-9333-4730-8240-0c6bbd16058c_1994x820.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><em>... 35 more rows not displayed ...</em></p><pre><code><code>Predict score: 77.5</code></code></pre><p>Run the same evaluator on <code>ChainOfThought</code>:</p><pre><code><code>cot_score = evaluate(cot_classifier)
print(f"ChainOfThought score: {cot_score.score}")
</code></code></pre><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!88nN!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9d913094-513b-48e8-a318-402b93ec79f9_1984x834.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!88nN!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9d913094-513b-48e8-a318-402b93ec79f9_1984x834.png 424w, https://substackcdn.com/image/fetch/$s_!88nN!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9d913094-513b-48e8-a318-402b93ec79f9_1984x834.png 848w, https://substackcdn.com/image/fetch/$s_!88nN!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9d913094-513b-48e8-a318-402b93ec79f9_1984x834.png 1272w, https://substackcdn.com/image/fetch/$s_!88nN!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9d913094-513b-48e8-a318-402b93ec79f9_1984x834.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!88nN!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9d913094-513b-48e8-a318-402b93ec79f9_1984x834.png" width="1456" height="612" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/9d913094-513b-48e8-a318-402b93ec79f9_1984x834.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:612,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:112378,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://newsletter.codecut.ai/i/201828304?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9d913094-513b-48e8-a318-402b93ec79f9_1984x834.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!88nN!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9d913094-513b-48e8-a318-402b93ec79f9_1984x834.png 424w, https://substackcdn.com/image/fetch/$s_!88nN!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9d913094-513b-48e8-a318-402b93ec79f9_1984x834.png 848w, https://substackcdn.com/image/fetch/$s_!88nN!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9d913094-513b-48e8-a318-402b93ec79f9_1984x834.png 1272w, https://substackcdn.com/image/fetch/$s_!88nN!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9d913094-513b-48e8-a318-402b93ec79f9_1984x834.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><em>... 35 more rows not displayed ...</em></p><pre><code><code>ChainOfThought score: 80.0</code></code></pre><p>The displayed rows make the comparison easier to inspect: you can see which examples matched the expected intent and which ones failed. In this run, <code>ChainOfThought</code> scores higher because the reasoning step helps with some ambiguous delivery queries.</p><h2>Optimize the Classifier with Examples</h2><p>Once the metric shows where the baseline fails, DSPy can use training examples to search for a better version of the program.</p><p>DSPy provides several optimizer options depending on how much search you want:</p><ul><li><p><code>BootstrapFewShot</code> improves the prompt by adding better examples.</p></li><li><p><code>MIPROv2</code> improves the prompt by tuning both instructions and examples.</p></li><li><p><code>GEPA</code> improves the prompt by using feedback from previous attempts.</p></li></ul><p>This article uses <code>BootstrapFewShot</code> because it is the simplest optimizer for this setup. It uses the training set and metric to choose examples that make the prompt stronger.</p><p>Few-shot examples are useful when the label name alone is not enough.</p><p>For example, <code>card_arrival</code> could sound like a successful delivery, but this example shows what it means in the dataset:</p><pre><code><code>Query: My card has not arrived yet.
Intent: card_arrival
</code></code></pre><p>The label refers to questions or problems about card delivery. <code>BootstrapFewShot</code> helps find examples like this and add them to the prompt:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!KR4H!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0d2a8ed-050f-435e-b96b-8795c1879a94_1080x1192.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!KR4H!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0d2a8ed-050f-435e-b96b-8795c1879a94_1080x1192.png 424w, https://substackcdn.com/image/fetch/$s_!KR4H!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0d2a8ed-050f-435e-b96b-8795c1879a94_1080x1192.png 848w, https://substackcdn.com/image/fetch/$s_!KR4H!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0d2a8ed-050f-435e-b96b-8795c1879a94_1080x1192.png 1272w, https://substackcdn.com/image/fetch/$s_!KR4H!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0d2a8ed-050f-435e-b96b-8795c1879a94_1080x1192.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!KR4H!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0d2a8ed-050f-435e-b96b-8795c1879a94_1080x1192.png" width="1080" height="1192" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b0d2a8ed-050f-435e-b96b-8795c1879a94_1080x1192.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1192,&quot;width&quot;:1080,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;How BootstrapFewShot builds an optimized few-shot prompt: the training set splits into labeled demos and a bootstrap round, where the classifier predicts an intent, a metric keeps or discards the trace, and both paths merge into the final prompt.&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="How BootstrapFewShot builds an optimized few-shot prompt: the training set splits into labeled demos and a bootstrap round, where the classifier predicts an intent, a metric keeps or discards the trace, and both paths merge into the final prompt." title="How BootstrapFewShot builds an optimized few-shot prompt: the training set splits into labeled demos and a bootstrap round, where the classifier predicts an intent, a metric keeps or discards the trace, and both paths merge into the final prompt." srcset="https://substackcdn.com/image/fetch/$s_!KR4H!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0d2a8ed-050f-435e-b96b-8795c1879a94_1080x1192.png 424w, https://substackcdn.com/image/fetch/$s_!KR4H!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0d2a8ed-050f-435e-b96b-8795c1879a94_1080x1192.png 848w, https://substackcdn.com/image/fetch/$s_!KR4H!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0d2a8ed-050f-435e-b96b-8795c1879a94_1080x1192.png 1272w, https://substackcdn.com/image/fetch/$s_!KR4H!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0d2a8ed-050f-435e-b96b-8795c1879a94_1080x1192.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><pre><code><code>from dspy.teleprompt import BootstrapFewShot

optimizer = BootstrapFewShot(
    metric=intent_exact_match,  # score each candidate
    max_bootstrapped_demos=4,  # generated examples to keep
    max_labeled_demos=8,  # labeled examples to include
    max_rounds=1,  # bootstrap attempts per example
)

optimized_classifier = optimizer.compile(
    student=cot_classifier,
    trainset=trainset,
)
</code></code></pre><h2>Compare Before vs. After</h2><p>Evaluate the optimized classifier on the same dev set:</p><pre><code><code>optimized_score = evaluate(optimized_classifier)

scores = pd.DataFrame(
    [
        {"program": "Predict", "score": predict_score.score},
        {"program": "ChainOfThought", "score": cot_score.score},
        {"program": "BootstrapFewShot + ChainOfThought", "score": optimized_score.score},
    ]
)

print(scores)
</code></code></pre><pre><code><code>                             program  score
0                            Predict   77.5
1                     ChainOfThought   80.0
2  BootstrapFewShot + ChainOfThought   87.5
</code></code></pre><p>Nice! The optimized classifier performs best in this run, improving from <code>80.0</code> with <code>ChainOfThought</code> to <code>87.5</code> after adding optimized few-shot examples.</p><p>You can also inspect individual misses to understand what still fails:</p><pre><code><code>for example in devset:
    prediction = optimized_classifier(query=example.query)

    if prediction.intent != example.intent:
        print("Query:", example.query)
        print("Expected:", example.intent)
        print("Predicted:", prediction.intent)
        print()
</code></code></pre><pre><code><code>Query: Is there tracking info available?
Expected: card_arrival
Predicted: card_delivery_estimate

Query: Where is the tracking number for the card you sent me?
Expected: card_arrival
Predicted: card_delivery_estimate

Query: Do you know if there is a tracking number for the new card you sent me?
Expected: card_arrival
Predicted: card_delivery_estimate

Query: I'm just wondering when my card will get here.
Expected: card_delivery_estimate
Predicted: card_arrival

Query: I am waiting for my card to arrive.
Expected: card_delivery_estimate
Predicted: card_arrival
</code></code></pre><p>Most misses are between <code>card_arrival</code> and <code>card_delivery_estimate</code>. That makes sense: both intents mention waiting for a card, tracking, or delivery timing.</p><p>To improve this, we could add more labeled examples that separate &#8220;my card has not arrived&#8221; from &#8220;how long does delivery take?&#8221;</p><h2>Final Thoughts</h2><p>DSPy is worth using when an LLM workflow will run repeatedly and quality matters. It is especially useful when you have:</p><ul><li><p>Labeled examples</p></li><li><p>A metric</p></li><li><p>Several prompt or module versions to compare</p></li><li><p>A task that will evolve over time</p></li></ul><p>It is probably too much for one-off prompts, quick brainstorming, or tasks where you do not have examples to evaluate against.</p><p>For the complete walkthrough, see the <a href="https://codecut.ai/dspy-auto-optimize-llm-classifier/">full DSPy classifier tutorial</a>.</p><h2>Related Tutorials</h2><ul><li><p><strong><a href="https://codecut.ai/structured-llm-outputs-tools-comparison/">Structured Output Tools for LLMs: Instructor, PydanticAI, LangChain, Outlines, and Guidance Compared</a></strong>: Compares libraries that force LLMs to return valid, typed outputs, the same problem DSPy signatures solve.</p></li><li><p><strong><a href="https://codecut.ai/rag-evaluation-mlflow-quality-metrics/">Build Production-Ready RAG Systems with MLflow Quality Metrics</a></strong>: Measures LLM output quality with metrics, complementing DSPy&#8217;s evaluate-and-optimize loop.</p></li></ul><div><hr></div><p><em>Originally published on <a href="https://codecut.ai/dspy-auto-optimize-llm-classifier-quick/">CodeCut</a>.</em></p>]]></content:encoded></item><item><title><![CDATA[Quick Tips: Polars 1.41 - Speed Up Wide Parquet Scans Without Code Changes]]></title><description><![CDATA[Plus one interface for OpenAI, Anthropic, and Gemini]]></description><link>https://newsletter.codecut.ai/p/quick-tips-polars-141-speed-up-wide</link><guid isPermaLink="false">https://newsletter.codecut.ai/p/quick-tips-polars-141-speed-up-wide</guid><dc:creator><![CDATA[CodeCut]]></dc:creator><pubDate>Thu, 11 Jun 2026 16:01:05 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!rYU6!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F76bc99b1-f4ce-497f-b4e1-c604571497a0_1080x554.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Grab your coffee. Here are this week&#8217;s highlights.</p><div><hr></div><h2>&#128197; Today&#8217;s Picks</h2><h3>LiteLLM - One interface for OpenAI, Anthropic, and Gemini</h3><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!7Ak8!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ff11dfd-3a72-4455-b577-98d4019b5d2a_617x514.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!7Ak8!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ff11dfd-3a72-4455-b577-98d4019b5d2a_617x514.png 424w, https://substackcdn.com/image/fetch/$s_!7Ak8!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ff11dfd-3a72-4455-b577-98d4019b5d2a_617x514.png 848w, https://substackcdn.com/image/fetch/$s_!7Ak8!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ff11dfd-3a72-4455-b577-98d4019b5d2a_617x514.png 1272w, https://substackcdn.com/image/fetch/$s_!7Ak8!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ff11dfd-3a72-4455-b577-98d4019b5d2a_617x514.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!7Ak8!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ff11dfd-3a72-4455-b577-98d4019b5d2a_617x514.png" width="617" height="514" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/1ff11dfd-3a72-4455-b577-98d4019b5d2a_617x514.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:514,&quot;width&quot;:617,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Code example: LiteLLM - One interface for OpenAI, Anthropic, and Gemini&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Code example: LiteLLM - One interface for OpenAI, Anthropic, and Gemini" title="Code example: LiteLLM - One interface for OpenAI, Anthropic, and Gemini" srcset="https://substackcdn.com/image/fetch/$s_!7Ak8!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ff11dfd-3a72-4455-b577-98d4019b5d2a_617x514.png 424w, https://substackcdn.com/image/fetch/$s_!7Ak8!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ff11dfd-3a72-4455-b577-98d4019b5d2a_617x514.png 848w, https://substackcdn.com/image/fetch/$s_!7Ak8!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ff11dfd-3a72-4455-b577-98d4019b5d2a_617x514.png 1272w, https://substackcdn.com/image/fetch/$s_!7Ak8!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ff11dfd-3a72-4455-b577-98d4019b5d2a_617x514.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h4>Problem</h4><p>Frameworks like LangChain and LlamaIndex are useful when you need chains, agents, retrieval, memory, or orchestration.</p><p>But if you only need to call different providers through the same interface, you may not want to restructure your app around a full LLM framework.</p><h4>Solution</h4><p><strong><a href="https://github.com/BerriAI/litellm">LiteLLM</a></strong> solves this as a lightweight interface layer.</p><p>You can keep one <code>completion()</code> call across 100+ providers, then switch models by changing only the model string.</p><blockquote><p>&#128214; <a href="https://codecut.ai/customer-support-bot-memory-mem0/?utm_source=Substack&amp;utm_medium=email&amp;utm_campaign=article_customer-support-bot-memory-mem0">View Full Article</a></p></blockquote><div><hr></div><h3>Polars 1.41 - Speed Up Wide Parquet Scans Without Code Changes</h3><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!rYU6!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F76bc99b1-f4ce-497f-b4e1-c604571497a0_1080x554.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!rYU6!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F76bc99b1-f4ce-497f-b4e1-c604571497a0_1080x554.png 424w, https://substackcdn.com/image/fetch/$s_!rYU6!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F76bc99b1-f4ce-497f-b4e1-c604571497a0_1080x554.png 848w, https://substackcdn.com/image/fetch/$s_!rYU6!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F76bc99b1-f4ce-497f-b4e1-c604571497a0_1080x554.png 1272w, https://substackcdn.com/image/fetch/$s_!rYU6!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F76bc99b1-f4ce-497f-b4e1-c604571497a0_1080x554.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!rYU6!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F76bc99b1-f4ce-497f-b4e1-c604571497a0_1080x554.png" width="1080" height="554" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/76bc99b1-f4ce-497f-b4e1-c604571497a0_1080x554.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:554,&quot;width&quot;:1080,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Code example: Polars 1.41 - Speed Up Wide Parquet Scans Without Code Changes&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Code example: Polars 1.41 - Speed Up Wide Parquet Scans Without Code Changes" title="Code example: Polars 1.41 - Speed Up Wide Parquet Scans Without Code Changes" srcset="https://substackcdn.com/image/fetch/$s_!rYU6!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F76bc99b1-f4ce-497f-b4e1-c604571497a0_1080x554.png 424w, https://substackcdn.com/image/fetch/$s_!rYU6!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F76bc99b1-f4ce-497f-b4e1-c604571497a0_1080x554.png 848w, https://substackcdn.com/image/fetch/$s_!rYU6!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F76bc99b1-f4ce-497f-b4e1-c604571497a0_1080x554.png 1272w, https://substackcdn.com/image/fetch/$s_!rYU6!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F76bc99b1-f4ce-497f-b4e1-c604571497a0_1080x554.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h4>Problem</h4><p><code>scan_parquet</code> is Polars&#8217; lazy way to read a Parquet file, allowing it to optimize the query before loading data.</p><p>Before the query runs, Polars still needs to decode the Parquet footer to understand the schema, row groups, and column statistics. For very wide files, that setup step can take noticeable time.</p><h4>Solution</h4><p><strong><a href="https://github.com/pola-rs/polars">Polars</a></strong> 1.41 makes <code>scan_parquet</code> faster by replacing the old metadata decoder with one built specifically for Parquet files.</p><p>In the Polars 1.41 release benchmark, footer decoding was up to 3.29x faster on a 10,000-column file.</p><blockquote><p>&#128214; <a href="https://pola.rs/posts/polars-1-41/">Read the release</a></p></blockquote><div><hr></div><h3>&#128172; Rate Your Experience</h3><p>How would you rate your newsletter experience? <a href="https://r7yuob192zv.typeform.com/to/VODwGYMW">Share your feedback &#8594;</a></p><div><hr></div><h3>&#128269; Explore More on CodeCut</h3><ul><li><p><a href="https://codecut.ai/tool-selector/?utm_source=Substack&amp;utm_medium=email&amp;utm_campaign=article_tool-selector">Tool Selector</a> - Discover 70+ Python tools for AI and data science</p></li><li><p><a href="https://codecut.ai/production-ready-data-science/?utm_source=Substack&amp;utm_medium=email&amp;utm_campaign=article_production-ready-data-science">Production Ready Data Science</a> - A practical book for taking projects from prototype to production</p></li></ul>]]></content:encoded></item><item><title><![CDATA[Add Long-Term Memory to LLM Applications with Mem0]]></title><description><![CDATA[Build a memory-aware LLM workflow with Mem0 that extracts useful facts, retrieves relevant memories, and improves follow-up responses.]]></description><link>https://newsletter.codecut.ai/p/add-long-term-memory-to-llm-applications</link><guid isPermaLink="false">https://newsletter.codecut.ai/p/add-long-term-memory-to-llm-applications</guid><dc:creator><![CDATA[CodeCut]]></dc:creator><pubDate>Tue, 09 Jun 2026 16:01:38 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/ac1b64a9-7f5d-4071-9d2e-c33dd5756900_1200x628.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>Introduction</h2><p>LLM applications are often stateless by default. Each prompt only knows what you include at that moment, so useful context from earlier interactions can disappear between sessions.</p><p>For simple questions, that is often enough. For returning users, it can lead to repeated explanations, missing preferences, and less helpful responses.</p><p><a href="https://github.com/mem0ai/mem0">Mem0</a> helps by extracting important facts from conversations, storing them by user, and retrieving relevant memories when needed.</p><p>To see how Mem0 works, we will build a simple e-commerce support workflow, first without memory and then with Mem0, so you can compare how stored context changes the response.</p><blockquote><p>&#128214; <strong>Full version:</strong> This is a condensed version. For memory management operations like inspecting and deleting stored memories, see the <a href="https://codecut.ai/customer-support-bot-memory-mem0/">complete tutorial</a>.</p></blockquote><h2>Vector Search vs Memory</h2><p>A vector database is a good starting point for retrieval, but memory requires more than finding similar text. You still need a way to decide which details are worth saving for future interactions.</p><p>In this customer support example, the vector database may retrieve the entire message, including the coffee detail even though it is unrelated to the delayed order:</p><pre><code><code>My order #4821 still has not arrived. I contacted support last week and got no update. Also, I was checking this while making coffee this morning.
</code></code></pre><p>A memory layer should keep only the support-relevant facts:</p><pre><code><code>Order #4821 is delayed, and the customer contacted support last week without receiving an update.
</code></code></pre><p>To get from raw retrieval to useful memory, your application still needs logic for:</p><ul><li><p>Deciding which details from the message should be remembered for future replies.</p></li><li><p>Making sure each memory is tied to the right user or conversation.</p></li><li><p>Deciding how to handle newer memories that conflict with or supersede earlier ones.</p></li><li><p>Formatting retrieved messages into prompt-ready context.</p></li></ul><p>You can implement this yourself, but the extra extraction and memory-management logic adds up. Mem0 packages that workflow behind a simpler memory API, which we will use in the next section.</p><h2>Setup</h2><p>Install the libraries used in this tutorial:</p><ul><li><p><code>mem0ai</code>: Adds long-term memory for storing and retrieving user context.</p></li><li><p><code>litellm</code>: Provides a unified interface for calling LLM providers.</p></li><li><p><code>python-dotenv</code>: Loads API keys and configuration from a <code>.env</code> file.</p></li></ul><pre><code><code>pip install mem0ai litellm python-dotenv
</code></code></pre><p><em>This article uses </em><code>mem0ai</code><em> v2.0.4, </em><code>litellm</code><em> v1.87.0, and </em><code>python-dotenv</code><em> v1.2.2.</em></p><p>Since this tutorial uses OpenAI&#8217;s GPT-4o-mini, store your <code>OPENAI_API_KEY</code> in a <code>.env</code> file in your project folder so Mem0 and LiteLLM can access it.</p><pre><code><code>OPENAI_API_KEY=your-openai-api-key
</code></code></pre><p>Start by loading the environment variables and imports used throughout the tutorial:</p><pre><code><code>from dotenv import load_dotenv
from litellm import completion
from mem0 import Memory

load_dotenv()
</code></code></pre><p>Next, define a helper that sends a list of chat messages to GPT-4o-mini through LiteLLM and returns only the generated text:</p><pre><code><code>def ask_model(messages: list[dict[str, str]]) -&gt; str:
    response = completion(model="openai/gpt-4o-mini", messages=messages)
    return response.choices[0].message.content
</code></code></pre><p>To use another model, replace <code>"openai/gpt-4o-mini"</code> with a model name from the <a href="https://docs.litellm.ai/docs/providers">LiteLLM provider docs</a>.</p><p>Finally, configure Mem0 with an LLM for extracting memories from messages. This example uses GPT-4o-mini, but Mem0 and LiteLLM do not have to use the same model:</p><pre><code><code>memory = Memory.from_config(
    {
        "llm": {
            "provider": "openai",
            "config": {"model": "gpt-4o-mini"},
        },
    }
)
</code></code></pre><h2>Build a Stateless Support Bot</h2><p>First, create a support bot with no memory. It only sees the current message.</p><pre><code><code>SYSTEM_PROMPT = (
    "You are a helpful e-commerce customer support agent. "
    "Be polite, concise, and practical."
)


def ask_stateless_bot(question: str) -&gt; str:
    messages = [
        {"role": "system", "content": SYSTEM_PROMPT},
        {"role": "user", "content": question},
    ]
    return ask_model(messages)
</code></code></pre><p>The customer opens a support chat about a delayed order. We will also include an irrelevant detail in the message, so that we can later show that Mem0 can focus on the useful support facts:</p><pre><code><code>initial_message = (
    "My order #4821 still has not arrived. "
    "I contacted support last week and got no update. "
    "Also, I was checking this while making coffee this morning." # irrelevant detail
)

print(ask_stateless_bot(initial_message))
</code></code></pre><pre><code><code>I'm sorry to hear that your order #4821 hasn't arrived yet. I understand how frustrating this can be. Let me assist you by checking the status for you. Please give me a moment.
</code></code></pre><p>The reply is reasonable for a first message. The problem shows up when the customer returns later.</p><h2>Test the Stateless Support Bot</h2><p>Let&#8217;s see how the bot responds to a follow-up question without passing the earlier conversation:</p><pre><code><code>follow_up = "Hi, any update on my delayed order?"

print(ask_stateless_bot(follow_up))
</code></code></pre><pre><code><code>I&#8217;d be happy to help with your order. Could you please provide me with your order number? This will help me look up the status for you.
</code></code></pre><p>The response is polite, but it loses the context that matters most: the order number, the prior support contact, and the customer&#8217;s frustration.</p><h2>Store Support Interactions in Mem0</h2><p>To preserve the missing order context, add Mem0 as a memory layer. Store the customer message under a stable <code>customer_id</code> so it can be retrieved in later interactions.</p><pre><code><code>customer_id = "cust_4821"
order_id = "4821"
issue_type = "shipping"
sentiment = "frustrated"

memory_extraction_prompt = (
    "Extract only useful customer-support facts. Ignore unimportant information."
)

add_result = memory.add(
    messages=[{"role": "user", "content": initial_message}],
    user_id=customer_id,
    metadata={
        "issue_type": issue_type,
        "sentiment": sentiment,
        "order_id": order_id,
    },
    prompt=memory_extraction_prompt,
)

print(add_result["results"][0]["memory"])
</code></code></pre><pre><code><code>User's order #4821 has not arrived as of June 3, 2026, and they contacted support last week but received no update.</code></code></pre><p>Note that the saved memory keeps the support-relevant details: the order number, the delayed delivery, and the prior support contact. It does not include the unrelated coffee detail from the original message.</p><pre><code><code>Raw customer message
   |
   v
LLM-based extraction step
   |
   +--&gt; Save: order #4821 has not arrived
   +--&gt; Save: customer contacted support last week
   +--&gt; Drop: customer was making coffee
</code></code></pre><p>This has a few practical benefits:</p><ul><li><p>Cleaner retrieval: Future searches return useful support context instead of unrelated conversation details.</p></li><li><p>Lower API cost: Passing compact memories instead of noisy chat history can reduce the number of tokens sent to the model.</p></li><li><p>Better privacy boundaries: Incidental personal details are less likely to be retained when they are not needed.</p></li></ul><p>If you do not want Mem0 to infer or rewrite memories, set <code>infer=False</code>. In that mode, Mem0 stores the provided message directly instead of extracting selected facts from it.</p><h2>Retrieve Memories Before Responding</h2><p>Before answering a new message, define a helper that searches Mem0 for memories related to the current question and limits the search to the current <code>customer_id</code>:</p><pre><code><code>def retrieve_customer_memories(question: str, customer_id: str) -&gt; dict:
    return memory.search(
        query=question,
        filters={"user_id": customer_id},
        top_k=3,
    )


for item in retrieve_customer_memories(follow_up, customer_id)["results"]:
    print(item["memory"])
</code></code></pre><pre><code><code>User's order #4821 has not arrived and they contacted support last week but received no update.
</code></code></pre><p>Next, convert the dictionary returned by Mem0 into plain text so it can be inserted into the system prompt:</p><pre><code><code>def format_memories(search_result: dict) -&gt; str:
    memories = search_result["results"]
    if not memories:
        return "No relevant memories found."

    return "\n".join(f"- {item['memory']}" for item in memories)


print(format_memories(retrieve_customer_memories(follow_up, customer_id)))
</code></code></pre><pre><code><code>- User's order #4821 has not arrived and they contacted support last week but received no update.
</code></code></pre><h2>Compare Stateless vs Memory-Aware Responses</h2><p>Now we are ready to create a memory-aware support bot. The function below does three things:</p><ul><li><p>Retrieves relevant customer memories from Mem0.</p></li><li><p>Adds those memories to the system prompt.</p></li><li><p>Sends the updated prompt to the model.</p></li></ul><pre><code><code>def ask_memory_aware_bot(question: str, customer_id: str) -&gt; str:
    customer_memories = format_memories(
        retrieve_customer_memories(question, customer_id)
    )

    messages = [
        {
            "role": "system",
            "content": (
                f"{SYSTEM_PROMPT}\n\n"
                f"Relevant customer memories:\n{customer_memories}"
            ),
        },
        {"role": "user", "content": question},
    ]
    return ask_model(messages)
</code></code></pre><p>Ask the same follow-up again:</p><pre><code><code>print(ask_memory_aware_bot(follow_up, customer_id))
</code></code></pre><pre><code><code>Thanks for checking back on order #4821. I can see it is still delayed and that you contacted us last week without an update. I am escalating this with shipping now and will email you a new delivery estimate today.
</code></code></pre><p>This response now uses the stored memory. It remembers the order number, the earlier support contact, and the unresolved shipping issue without asking the customer to repeat them.</p><h2>Add a Status Update</h2><p>Next, add a new message that changes the order status. This lets us see how the memory-aware bot handles newer information:</p><pre><code><code>status_update_result = memory.add(
    messages=[{"role": "user", "content": "Order #4821 arrived today."}],
    user_id=customer_id,
    metadata={"order_id": order_id, "issue_type": issue_type},
)</code></code></pre><p>Mem0 stores this as a new memory. Ask the same follow-up again:</p><pre><code><code>print(ask_memory_aware_bot(follow_up, customer_id))
</code></code></pre><pre><code><code>Hello! I see that your order #4821 has actually arrived on June 3, 2026. If you need any further assistance or have questions about your order, feel free to let me know!
</code></code></pre><p>The answer changes because the model now receives both the earlier delay and the newer arrival status in the retrieved memories.</p><blockquote><p>For inspecting and deleting stored memories with <code>get_all()</code>, <code>delete()</code>, and <code>delete_all()</code>, see the <a href="https://codecut.ai/customer-support-bot-memory-mem0/">complete tutorial</a>.</p></blockquote><h2>Final Thoughts</h2><p>Stateless LLM applications are easy to build, but they only know what is included in the current prompt.</p><p>Mem0 adds a memory layer for applications that need continuity across sessions. The same pattern can support customer support bots, personal assistants, tutoring apps, healthcare intake tools, CRM workflows, and onboarding assistants.</p><p>With Mem0, applications can save important details from past interactions and retrieve the most relevant context for future responses.</p><p>Use memory when you don&#8217;t want returning users to have to repeat important details. Skip it when each request is self-contained or when storing user history adds unnecessary complexity.</p><h2>Related Tutorials</h2><ul><li><p><strong><a href="https://codecut.ai/semantic-search-postgres-pgvector-ollama/">Implement Semantic Search in Postgres Using pgvector and Ollama</a></strong>: Learn how to store embeddings in Postgres and retrieve semantically similar records with pgvector.</p></li><li><p><strong><a href="https://codecut.ai/enforce-structured-outputs-from-llms-with-pydanticai/">Enforce Structured Outputs from LLMs with PydanticAI</a></strong>: Learn how to validate extracted LLM outputs with typed Pydantic models.</p></li></ul><div><hr></div><p><em>Originally published on <a href="https://codecut.ai/customer-support-bot-memory-mem0-quick/">CodeCut</a>.</em></p>]]></content:encoded></item><item><title><![CDATA[Quick Tips: LM Studio - Try local LLMs without setup code]]></title><description><![CDATA[Plus refactor AI code with clean skills]]></description><link>https://newsletter.codecut.ai/p/quick-tips-lm-studio-try-local-llms</link><guid isPermaLink="false">https://newsletter.codecut.ai/p/quick-tips-lm-studio-try-local-llms</guid><dc:creator><![CDATA[CodeCut]]></dc:creator><pubDate>Thu, 04 Jun 2026 16:01:39 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!7dYz!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Febd21f39-78ab-4c13-8217-8eaee4d5ce65_1200x729.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Grab your coffee. Here are this week&#8217;s highlights.</p><div><hr></div><h2>&#128197; Today&#8217;s Picks</h2><h3>LM Studio - Try local LLMs without setup code</h3><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!7dYz!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Febd21f39-78ab-4c13-8217-8eaee4d5ce65_1200x729.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!7dYz!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Febd21f39-78ab-4c13-8217-8eaee4d5ce65_1200x729.png 424w, https://substackcdn.com/image/fetch/$s_!7dYz!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Febd21f39-78ab-4c13-8217-8eaee4d5ce65_1200x729.png 848w, https://substackcdn.com/image/fetch/$s_!7dYz!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Febd21f39-78ab-4c13-8217-8eaee4d5ce65_1200x729.png 1272w, https://substackcdn.com/image/fetch/$s_!7dYz!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Febd21f39-78ab-4c13-8217-8eaee4d5ce65_1200x729.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!7dYz!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Febd21f39-78ab-4c13-8217-8eaee4d5ce65_1200x729.png" width="1200" height="729" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ebd21f39-78ab-4c13-8217-8eaee4d5ce65_1200x729.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:729,&quot;width&quot;:1200,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Code example: LM Studio - Try local LLMs without setup code&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Code example: LM Studio - Try local LLMs without setup code" title="Code example: LM Studio - Try local LLMs without setup code" srcset="https://substackcdn.com/image/fetch/$s_!7dYz!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Febd21f39-78ab-4c13-8217-8eaee4d5ce65_1200x729.png 424w, https://substackcdn.com/image/fetch/$s_!7dYz!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Febd21f39-78ab-4c13-8217-8eaee4d5ce65_1200x729.png 848w, https://substackcdn.com/image/fetch/$s_!7dYz!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Febd21f39-78ab-4c13-8217-8eaee4d5ce65_1200x729.png 1272w, https://substackcdn.com/image/fetch/$s_!7dYz!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Febd21f39-78ab-4c13-8217-8eaee4d5ce65_1200x729.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h4>Problem</h4><p>Tools like Ollama and Hugging Face Transformers are powerful, but they can still require CLI commands, Python setup, model configuration, or server management before you send your first prompt.</p><p>That friction makes cloud APIs feel easier, even when local models are better for privacy, cost, or offline experimentation.</p><h4>Solution</h4><p><strong><a href="https://lmstudio.ai/">LM Studio</a></strong> gives you a clean desktop interface for browsing, downloading, and running local models without writing setup code.</p><blockquote></blockquote><div><hr></div><h3>Clean Code Skills - Turn AI-generated code into cleaner Python</h3><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!NeO-!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F39620424-459d-4bc9-ba12-837a637092c5_759x813.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!NeO-!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F39620424-459d-4bc9-ba12-837a637092c5_759x813.png 424w, https://substackcdn.com/image/fetch/$s_!NeO-!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F39620424-459d-4bc9-ba12-837a637092c5_759x813.png 848w, https://substackcdn.com/image/fetch/$s_!NeO-!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F39620424-459d-4bc9-ba12-837a637092c5_759x813.png 1272w, https://substackcdn.com/image/fetch/$s_!NeO-!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F39620424-459d-4bc9-ba12-837a637092c5_759x813.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!NeO-!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F39620424-459d-4bc9-ba12-837a637092c5_759x813.png" width="759" height="813" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/39620424-459d-4bc9-ba12-837a637092c5_759x813.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:813,&quot;width&quot;:759,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Code example: Clean Code Skills - Turn AI-generated code into cleaner Python&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Code example: Clean Code Skills - Turn AI-generated code into cleaner Python" title="Code example: Clean Code Skills - Turn AI-generated code into cleaner Python" srcset="https://substackcdn.com/image/fetch/$s_!NeO-!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F39620424-459d-4bc9-ba12-837a637092c5_759x813.png 424w, https://substackcdn.com/image/fetch/$s_!NeO-!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F39620424-459d-4bc9-ba12-837a637092c5_759x813.png 848w, https://substackcdn.com/image/fetch/$s_!NeO-!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F39620424-459d-4bc9-ba12-837a637092c5_759x813.png 1272w, https://substackcdn.com/image/fetch/$s_!NeO-!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F39620424-459d-4bc9-ba12-837a637092c5_759x813.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h4>Problem</h4><p>AI-generated code often works on the first run, but the structure can be hard to follow.</p><p>You might see long functions, duplicated logic, vague names, or deeply nested conditionals that make future changes slower.</p><h4>Solution</h4><p><strong><a href="https://github.com/ertugrul-dmr/clean-code-skills">Clean Code Skills</a></strong> gives your AI agent focused guidance based on Robert C. Martin&#8217;s Clean Code rules for Python and TypeScript.</p><p>Each skill targets a maintenance problem:</p><ul><li><p>boy-scout: improve the code it touches</p></li><li><p>clean-functions: keep functions small and focused</p></li><li><p>clean-names: choose names that explain intent</p></li><li><p>clean-tests: write tests around clear behavior</p></li><li><p>clean-general: reduce duplication, magic numbers, long branching paths, and more</p></li></ul><p>I use this skill when asking my agent to write Python code, refactor existing code, review a change, or add tests.</p><blockquote></blockquote><div><hr></div><h3>&#128172; Rate Your Experience</h3><p>How would you rate your newsletter experience? <a href="https://r7yuob192zv.typeform.com/to/VODwGYMW">Share your feedback &#8594;</a></p><div><hr></div><h3>&#128269; Explore More on CodeCut</h3><ul><li><p><a href="https://codecut.ai/tool-selector/?utm_source=Substack&amp;utm_medium=email&amp;utm_campaign=article_tool-selector">Tool Selector</a> - Discover 70+ Python tools for AI and data science</p></li><li><p><a href="https://codecut.ai/production-ready-data-science/?utm_source=Substack&amp;utm_medium=email&amp;utm_campaign=article_production-ready-data-science">Production Ready Data Science</a> - A practical book for taking projects from prototype to production</p></li></ul>]]></content:encoded></item><item><title><![CDATA[Extracting PDF Tables on Apple Silicon: olmOCR-2 vs PaddleOCR-VL]]></title><description><![CDATA[Compare olmOCR-2 and PaddleOCR-VL for PDF table extraction on a Mac. See which open-source VLM handles merged cells and numeric tables better.]]></description><link>https://newsletter.codecut.ai/p/extracting-pdf-tables-on-apple-silicon</link><guid isPermaLink="false">https://newsletter.codecut.ai/p/extracting-pdf-tables-on-apple-silicon</guid><dc:creator><![CDATA[CodeCut]]></dc:creator><pubDate>Tue, 02 Jun 2026 16:02:22 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/41bad61a-dc9b-40af-8ee8-a22898a92566_1200x628.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>Introduction</h2><p>In a <a href="https://codecut.ai/docling-vs-marker-vs-llamaparse/">previous article</a>, we tested three Python tools for PDF table extraction: Docling, Marker, and LlamaParse. None of them handled the test document perfectly: Docling hallucinated values, Marker merged columns on borderless rows, and LlamaParse added a duplicate empty column.</p><p>After publishing part 1, I came across two more tools that target the same problem and wanted to see how they perform compared to the ones we already tested:</p><ul><li><p><strong>olmOCR-2</strong> from Allen Institute for AI, a 7B fine-tune of Qwen2.5-VL</p></li><li><p><strong>PaddleOCR-VL 1.6</strong> from Baidu, a 1B model with a layout-detection pipeline</p></li></ul><p>Both claim state-of-the-art table extraction. We&#8217;ll test them on a Mac (Apple M5 Pro), using the same PDF as part 1, to see if they fix the failures we saw there. Both tools run on Apple Silicon via GGUF quantizations with <code>llama.cpp</code> or native CPU PaddlePaddle.</p><blockquote><p>&#128214; <strong>Full version:</strong> This is a condensed version focusing on the hardest table in the document. For the first-table walkthrough and a full discussion of the tradeoffs, see the <a href="https://codecut.ai/olmocr2-vs-paddleocr-vl/">complete comparison</a>.</p></blockquote><h2>The Test Document</h2><p>For a fair comparison, we will use the same PDF as part 1: the <a href="https://arxiv.org/pdf/2408.09869">Docling Technical Report</a> from arXiv:</p><pre><code><code>import urllib.request

source = "https://arxiv.org/pdf/2408.09869"
local_pdf = "docling_report.pdf"
urllib.request.urlretrieve(source, local_pdf)
</code></code></pre><h2>olmOCR-2: Qwen2.5-VL Fine-Tune</h2><p><a href="https://github.com/allenai/olmocr">olmOCR-2</a> is Allen AI&#8217;s open-weight OCR model. It stands out for three reasons:</p><ul><li><p><strong>A 7B fine-tune of Qwen2.5-VL</strong> reads each PDF page as an image</p></li><li><p><strong>Cheap to run at scale</strong>: on a rented NVIDIA H100, olmOCR-2 processes a few pages per second, working out to about $2 per 10,000 pages in cloud costs</p></li><li><p><strong>Strongest table benchmark</strong>: scores 84.9 on tables on its own olmOCR-Bench, the highest among open VLM-OCR models at release</p></li></ul><p>olmOCR-2 takes the whole PDF page as an image and produces structured output in a single step. This is the same architecture as Docling&#8217;s VLM pipeline from part 1, just with a different model.</p><pre><code><code>PDF page rendered as image
&#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;
&#9474; Text paragraph...   &#9474;
&#9474; Name  Score         &#9474;
&#9474; Alice  92           &#9474;
&#9474; Bob    85           &#9474;
&#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496;
         &#9474;
         &#9660;
One model reads the page
and writes the output
         &#9474;
         &#9660;
| Name  | Score |
|-------|-------|
| Alice | 92    |
| Bob   | 85    |
</code></code></pre><h3>Download the GGUF and vision projector</h3><p>To use olmOCR-2 with <code>llama.cpp</code> on Apple Silicon, install <code>llama.cpp</code> and download two files: the model weights and the vision projector (<code>mmproj</code>).</p><pre><code><code>brew install llama.cpp

# Language model (Q8_0, ~8 GB)
curl -L -O https://huggingface.co/lmstudio-community/olmOCR-2-7B-1025-GGUF/resolve/main/olmOCR-2-7B-1025-Q8_0.gguf

# Vision projector (F16, ~1.4 GB)
curl -L -O https://huggingface.co/lmstudio-community/olmOCR-2-7B-1025-GGUF/resolve/main/mmproj-olmOCR-2-7B-1025-F16.gguf
</code></code></pre><h3>Table extraction</h3><p>olmOCR-2 reads images, not PDFs, so we&#8217;ll extract tables in three steps:</p><ol><li><p>Convert each PDF page to an image</p></li><li><p>Run olmOCR-2 on each image and collect the output</p></li><li><p>Extract the tables from the combined output with a regex</p></li></ol><p>For step 1, we will use <code>pdf2image</code>, which depends on the <code>poppler</code> system binary. Install both:</p><pre><code><code>brew install poppler
pip install pdf2image
</code></code></pre><p>Now convert each page to a JPEG:</p><pre><code><code>import subprocess
from pathlib import Path
from pdf2image import convert_from_path

images_dir = Path("images")
images_dir.mkdir(exist_ok=True)

pages = convert_from_path(local_pdf, dpi=200)
for i, page in enumerate(pages):
    page.save(images_dir / f"page_{i}.jpg")
</code></code></pre><p>olmOCR-2 doesn&#8217;t have a pure-Python API that runs on Apple Silicon, so we shell out to <code>llama-mtmd-cli</code> via <code>subprocess</code> for each page. The command for one page looks like this:</p><pre><code><code>llama-mtmd-cli \
  -m olmOCR-2-7B-1025-Q8_0.gguf \
  --mmproj mmproj-olmOCR-2-7B-1025-F16.gguf \
  --image page_0.jpg \
  -p "Convert this page to markdown. Preserve tables exactly. Output tables in HTML format." \
  --n-predict 3072
</code></code></pre><p>What each flag does:</p><ul><li><p><code>-m</code>: the language model weights (the <code>.gguf</code> we downloaded)</p></li><li><p><code>--mmproj</code>: the vision encoder (the <code>mmproj</code> we downloaded)</p></li><li><p><code>--image</code>: the input image to process</p></li><li><p><code>-p</code>: the prompt sent to the model</p></li><li><p><code>--n-predict</code>: the maximum number of tokens to generate (3072 is enough for most table-heavy pages)</p></li></ul><p>Wrap it in a Python helper so we can loop over pages:</p><pre><code><code>import re

def extract_with_olmocr(page_path: str) -&gt; str:
    result = subprocess.run(
        [
            "llama-mtmd-cli",
            "-m", "olmOCR-2-7B-1025-Q8_0.gguf",
            "--mmproj", "mmproj-olmOCR-2-7B-1025-F16.gguf",
            "--image", page_path,
            "-p", "Convert this page to markdown. Preserve tables exactly. Output tables in HTML format.",
            "--n-predict", "3072",
        ],
        capture_output=True,
        text=True,
    )
    return result.stdout
</code></code></pre><p>Run the helper on every page and combine the outputs:</p><pre><code><code>%%time
olmocr_output = "\n".join(
    extract_with_olmocr(str(images_dir / f"page_{i}.jpg")) for i in range(len(pages))
)
</code></code></pre><pre><code><code>Wall time: 5min 34s
</code></code></pre><p>olmOCR-2&#8217;s output is mostly Markdown but tables come out as HTML blocks. Extract them with a regex:</p><pre><code><code>all_tables = re.findall(r"&lt;table&gt;.*?&lt;/table&gt;", olmocr_output, re.DOTALL)
print(f"Items tagged as table: {len(all_tables)}")
</code></code></pre><pre><code><code>Items tagged as table: 4
</code></code></pre><p>Not every block tagged <code>&lt;table&gt;</code> is actually a table. olmOCR-2 misreads the author block on the title page as a table and outputs two copies of it. We filter both out:</p><pre><code><code>incorrect_table_indices = (1, 2)

tables = [t for i, t in enumerate(all_tables) if i not in incorrect_table_indices]
print(f"Actual tables: {len(tables)}")
</code></code></pre><pre><code><code>Actual tables: 2
</code></code></pre><p>The output is HTML, so use <code>IPython.display.HTML</code> to see it rendered:</p><pre><code><code>from IPython.display import display, HTML
</code></code></pre><p>Let&#8217;s look at the hardest table in the document: 12 rows of similar-looking numbers and no cell borders to mark column boundaries. Here&#8217;s the original from the PDF:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!hMv2!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc2d36e6d-13b9-4582-85f9-4498873adba0_1064x758.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!hMv2!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc2d36e6d-13b9-4582-85f9-4498873adba0_1064x758.png 424w, https://substackcdn.com/image/fetch/$s_!hMv2!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc2d36e6d-13b9-4582-85f9-4498873adba0_1064x758.png 848w, https://substackcdn.com/image/fetch/$s_!hMv2!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc2d36e6d-13b9-4582-85f9-4498873adba0_1064x758.png 1272w, https://substackcdn.com/image/fetch/$s_!hMv2!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc2d36e6d-13b9-4582-85f9-4498873adba0_1064x758.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!hMv2!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc2d36e6d-13b9-4582-85f9-4498873adba0_1064x758.png" width="1064" height="758" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/c2d36e6d-13b9-4582-85f9-4498873adba0_1064x758.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:758,&quot;width&quot;:1064,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Second table from the original PDF&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Second table from the original PDF" title="Second table from the original PDF" srcset="https://substackcdn.com/image/fetch/$s_!hMv2!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc2d36e6d-13b9-4582-85f9-4498873adba0_1064x758.png 424w, https://substackcdn.com/image/fetch/$s_!hMv2!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc2d36e6d-13b9-4582-85f9-4498873adba0_1064x758.png 848w, https://substackcdn.com/image/fetch/$s_!hMv2!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc2d36e6d-13b9-4582-85f9-4498873adba0_1064x758.png 1272w, https://substackcdn.com/image/fetch/$s_!hMv2!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc2d36e6d-13b9-4582-85f9-4498873adba0_1064x758.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>And here&#8217;s what olmOCR-2 extracted:</p><pre><code><code>display(HTML(tables[1]))
</code></code></pre><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!axtI!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F052f3bcd-1c8c-4d11-a23e-8bebb6b29fcc_544x370.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!axtI!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F052f3bcd-1c8c-4d11-a23e-8bebb6b29fcc_544x370.png 424w, https://substackcdn.com/image/fetch/$s_!axtI!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F052f3bcd-1c8c-4d11-a23e-8bebb6b29fcc_544x370.png 848w, https://substackcdn.com/image/fetch/$s_!axtI!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F052f3bcd-1c8c-4d11-a23e-8bebb6b29fcc_544x370.png 1272w, https://substackcdn.com/image/fetch/$s_!axtI!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F052f3bcd-1c8c-4d11-a23e-8bebb6b29fcc_544x370.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!axtI!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F052f3bcd-1c8c-4d11-a23e-8bebb6b29fcc_544x370.png" width="544" height="370" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/052f3bcd-1c8c-4d11-a23e-8bebb6b29fcc_544x370.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:370,&quot;width&quot;:544,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Second table from olmOCR-2&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Second table from olmOCR-2" title="Second table from olmOCR-2" srcset="https://substackcdn.com/image/fetch/$s_!axtI!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F052f3bcd-1c8c-4d11-a23e-8bebb6b29fcc_544x370.png 424w, https://substackcdn.com/image/fetch/$s_!axtI!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F052f3bcd-1c8c-4d11-a23e-8bebb6b29fcc_544x370.png 848w, https://substackcdn.com/image/fetch/$s_!axtI!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F052f3bcd-1c8c-4d11-a23e-8bebb6b29fcc_544x370.png 1272w, https://substackcdn.com/image/fetch/$s_!axtI!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F052f3bcd-1c8c-4d11-a23e-8bebb6b29fcc_544x370.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><strong>Worked:</strong></p><ul><li><p>All 12 row labels (Caption, Footnote, ..., All) preserved</p></li><li><p>12 data rows extracted with one numeric value per cell</p></li></ul><p><strong>Didn&#8217;t work:</strong></p><ul><li><p><strong>Two column headers are missing:</strong> Only 4 of the 6 columns have headers, so the class-label column and one of the model columns appear unlabeled.</p></li><li><p><code>MRCNN R101</code><strong> is dropped from the header row:</strong> The numeric values in that column still appear, but they sit under the wrong header name.</p></li><li><p><strong>Hyphenated ranges become decimals:</strong> Every entry in the &#8220;human&#8221; range column is wrong: <code>84-89</code> becomes <code>84.89</code>, <code>83-91</code> becomes <code>83.91</code>, and so on.</p></li><li><p><strong>Numeric values drift in several cells:</strong> Most rows have at least one digit substitution (Page-footer <code>61.6</code> &#8594; <code>74.6</code>, List-item <code>81.2</code> &#8594; <code>81.6</code>, All-row <code>72.4</code> &#8594; <code>77.4</code>).</p></li></ul><p><strong>Conclusion</strong>: olmOCR-2&#8217;s output looks clean but can be quietly wrong. It introduces character-level errors on dense numeric tables. Verify numeric values before trusting them.</p><h3>Performance</h3><p>olmOCR-2 took 5 min 34 s for the 9-page PDF on an Apple M5 Pro (64 GB RAM), about 37 seconds per page through GGUF + llama.cpp.</p><p>For production on a Mac, switch to the native MLX build (<code>mlx-community/olmOCR-2-7B-1025-8bit</code>), which runs about 20% faster than GGUF.</p><h2>PaddleOCR-VL 1.6: Pipeline VLM</h2><p><a href="https://github.com/PaddlePaddle/PaddleOCR">PaddleOCR-VL</a> is Baidu&#8217;s open-weight document parser. It stands out for three reasons:</p><ul><li><p><strong>A 1B fine-tune of ERNIE-4.5</strong>, the smallest model of the new VLM-OCR generation</p></li><li><p><strong>Strong multilingual support</strong> including Chinese ancient documents, scans, and stamps (not tested in this article)</p></li><li><p><strong>Mature ecosystem</strong>: PaddleOCR has 78.9k stars on GitHub and a long history of production deployment</p></li></ul><p>Unlike olmOCR-2&#8217;s single-pass approach, PaddleOCR-VL splits table extraction into two stages:</p><ul><li><p><strong>Layout detection</strong> locates each text block, table, and figure on the page</p></li><li><p><strong>Element-level VL recognition</strong> reads each detected region and converts it to text or structured Markdown</p></li></ul><pre><code><code>PDF page
&#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;
&#9474; Text paragraph...   &#9474;
&#9474; Name  Score         &#9474;
&#9474; Alice  92           &#9474;
&#9474; Bob    85           &#9474;
&#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496;
         &#9474;
         &#9660;
1. Layout detection identifies [TABLE] region
         &#9474;
         &#9660;
2. Element-level VL reads only the table region
         &#9474;
         &#9660;
| Name  | Score |
|-------|-------|
| Alice | 92    |
| Bob   | 85    |
</code></code></pre><h3>Install</h3><p>Pick the install that matches your hardware.</p><p><strong>Apple Silicon (Mac):</strong></p><pre><code><code>pip install paddlepaddle
pip install -U "paddleocr[doc-parser]&gt;=3.6.0"
</code></code></pre><p><strong>Linux / Windows (NVIDIA):</strong></p><pre><code><code>pip install paddlepaddle-gpu==3.2.1
pip install -U "paddleocr[doc-parser]&gt;=3.6.0"
</code></code></pre><p><em>This article uses PaddleOCR v3.6.0.</em></p><h3>Table extraction</h3><p>Unlike olmOCR-2, PaddleOCR-VL accepts a PDF path directly and returns a result object per page. No PDF-to-image conversion or subprocess loop required:</p><pre><code><code>from paddleocr import PaddleOCRVL

pipeline = PaddleOCRVL(pipeline_version="v1.6")
</code></code></pre><p>Run the pipeline on the PDF:</p><pre><code><code>%%time
results = pipeline.predict(local_pdf)
</code></code></pre><pre><code><code>Wall time: 7min 56s
</code></code></pre><p>Each entry in <code>results</code> corresponds to one page of the PDF. Loop through them and collect the tables:</p><pre><code><code># Create an output directory for the per-page markdown files
paddle_output_dir = Path("paddle_output")
paddle_output_dir.mkdir(exist_ok=True)

# Save each page's markdown to disk
for res in results:
    res.save_to_markdown(save_path=str(paddle_output_dir))

# Find every HTML table block
all_paddle_tables = []
for md_file in sorted(paddle_output_dir.glob("*.md")):
    content = md_file.read_text()
    all_paddle_tables.extend(re.findall(r"&lt;table[^&gt;]*&gt;.*?&lt;/table&gt;", content, re.DOTALL))

print(f"Items tagged as table: {len(all_paddle_tables)}")
</code></code></pre><pre><code><code>Items tagged as table: 3
</code></code></pre><p>Not every block PaddleOCR-VL tagged as a table is a unique table. The third item is a malformed near-duplicate of the second. Let&#8217;s filter it out:</p><pre><code><code>incorrect_table_indices = (2,)

paddle_tables = [t for i, t in enumerate(all_paddle_tables) if i not in incorrect_table_indices]
print(f"Actual tables: {len(paddle_tables)}")
</code></code></pre><pre><code><code>Actual tables: 2
</code></code></pre><p>Now the same hard table. Here&#8217;s the original from the PDF:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!2Fng!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0b23bead-282b-4578-a2b7-e3b2e0cb37dc_1200x466.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!2Fng!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0b23bead-282b-4578-a2b7-e3b2e0cb37dc_1200x466.png 424w, https://substackcdn.com/image/fetch/$s_!2Fng!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0b23bead-282b-4578-a2b7-e3b2e0cb37dc_1200x466.png 848w, https://substackcdn.com/image/fetch/$s_!2Fng!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0b23bead-282b-4578-a2b7-e3b2e0cb37dc_1200x466.png 1272w, https://substackcdn.com/image/fetch/$s_!2Fng!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0b23bead-282b-4578-a2b7-e3b2e0cb37dc_1200x466.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!2Fng!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0b23bead-282b-4578-a2b7-e3b2e0cb37dc_1200x466.png" width="1200" height="466" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/0b23bead-282b-4578-a2b7-e3b2e0cb37dc_1200x466.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:466,&quot;width&quot;:1200,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Second table from the original PDF&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Second table from the original PDF" title="Second table from the original PDF" srcset="https://substackcdn.com/image/fetch/$s_!2Fng!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0b23bead-282b-4578-a2b7-e3b2e0cb37dc_1200x466.png 424w, https://substackcdn.com/image/fetch/$s_!2Fng!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0b23bead-282b-4578-a2b7-e3b2e0cb37dc_1200x466.png 848w, https://substackcdn.com/image/fetch/$s_!2Fng!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0b23bead-282b-4578-a2b7-e3b2e0cb37dc_1200x466.png 1272w, https://substackcdn.com/image/fetch/$s_!2Fng!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0b23bead-282b-4578-a2b7-e3b2e0cb37dc_1200x466.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>And here&#8217;s what PaddleOCR-VL extracted:</p><pre><code><code>display(HTML(paddle_tables[1]))
</code></code></pre><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!CdYX!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8adfdf48-1a5c-4b32-a9c3-416b4c8d6d49_864x389.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!CdYX!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8adfdf48-1a5c-4b32-a9c3-416b4c8d6d49_864x389.png 424w, https://substackcdn.com/image/fetch/$s_!CdYX!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8adfdf48-1a5c-4b32-a9c3-416b4c8d6d49_864x389.png 848w, https://substackcdn.com/image/fetch/$s_!CdYX!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8adfdf48-1a5c-4b32-a9c3-416b4c8d6d49_864x389.png 1272w, https://substackcdn.com/image/fetch/$s_!CdYX!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8adfdf48-1a5c-4b32-a9c3-416b4c8d6d49_864x389.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!CdYX!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8adfdf48-1a5c-4b32-a9c3-416b4c8d6d49_864x389.png" width="864" height="389" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/8adfdf48-1a5c-4b32-a9c3-416b4c8d6d49_864x389.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:389,&quot;width&quot;:864,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Second table from PaddleOCR-VL&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Second table from PaddleOCR-VL" title="Second table from PaddleOCR-VL" srcset="https://substackcdn.com/image/fetch/$s_!CdYX!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8adfdf48-1a5c-4b32-a9c3-416b4c8d6d49_864x389.png 424w, https://substackcdn.com/image/fetch/$s_!CdYX!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8adfdf48-1a5c-4b32-a9c3-416b4c8d6d49_864x389.png 848w, https://substackcdn.com/image/fetch/$s_!CdYX!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8adfdf48-1a5c-4b32-a9c3-416b4c8d6d49_864x389.png 1272w, https://substackcdn.com/image/fetch/$s_!CdYX!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8adfdf48-1a5c-4b32-a9c3-416b4c8d6d49_864x389.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><strong>Worked:</strong></p><ul><li><p>All 12 class-label rows plus the Total row are present (truncated above for space)</p></li><li><p>Hyphenated ranges preserved correctly as &#8220;84-89&#8221;, &#8220;40-61&#8221;, exactly where olmOCR-2 misread them as decimals</p></li><li><p>&#8220;n/a&#8221; entries preserved</p></li><li><p>All numeric values match the source</p></li></ul><p><strong>Didn&#8217;t work:</strong></p><ul><li><p><strong>Header grouping is wrong:</strong> The two parent headers in the original PDF get split into three in the extraction: &#8220;Count&#8221; is absorbed into &#8220;% of Total&#8221;, and &#8220;triple inter-annotator mAP @ 0.5-0.95 (%)&#8221; is split into two separate parents.</p></li></ul><p><strong>Conclusion</strong>: PaddleOCR-VL is 7x smaller than olmOCR-2 (1B vs 7B parameters) and still more accurate on this PDF. All numeric values match the source, and the only real flaw is the mis-grouped multi-tier headers.</p><h3>Performance</h3><p>PaddleOCR-VL 1.6 took about 7 min 56 s for the full 9-page PDF on an Apple M5 Pro running CPU PaddlePaddle, roughly 53 seconds per page.</p><p>Even though the model is smaller than olmOCR-2, the pipeline overhead (layout detection plus element-level recognition) makes it slower per page than olmOCR-2 on this hardware.</p><h2>Try It Yourself</h2><p>These benchmarks are based on a single academic PDF tested on an Apple M5 Pro (64 GB RAM) using GGUF Q8_0 quantizations via llama.cpp. Table complexity, document language, scan quality, and hardware all affect the results. The best way to pick the right tool is to run each one on a sample of your own PDFs.</p><p>For the first-table walkthrough on each tool and the full runtime setup, see the <a href="https://codecut.ai/olmocr2-vs-paddleocr-vl/">complete comparison</a>.</p><div><hr></div><p><em>Originally published on <a href="https://codecut.ai/olmocr2-vs-paddleocr-vl-quick/">CodeCut</a>.</em></p>]]></content:encoded></item><item><title><![CDATA[Quick Tips: Claude Code - Move Session Context Across Folders with /export]]></title><description><![CDATA[Plus reuse Python contextlib as decorators]]></description><link>https://newsletter.codecut.ai/p/quick-tips-claude-code-move-session</link><guid isPermaLink="false">https://newsletter.codecut.ai/p/quick-tips-claude-code-move-session</guid><dc:creator><![CDATA[CodeCut]]></dc:creator><pubDate>Thu, 28 May 2026 16:01:49 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!JeMp!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3021fc66-64d4-4eac-a951-6d1beec12864_1080x459.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Grab your coffee. Here are this week&#8217;s highlights.</p><div><hr></div><h2>&#128197; Today&#8217;s Picks</h2><h3>Python contextlib - Reuse Context Managers as Function Decorators</h3><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!WVA8!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff695a94e-92ea-40a7-b133-f768a4af65a2_592x745.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!WVA8!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff695a94e-92ea-40a7-b133-f768a4af65a2_592x745.png 424w, https://substackcdn.com/image/fetch/$s_!WVA8!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff695a94e-92ea-40a7-b133-f768a4af65a2_592x745.png 848w, https://substackcdn.com/image/fetch/$s_!WVA8!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff695a94e-92ea-40a7-b133-f768a4af65a2_592x745.png 1272w, https://substackcdn.com/image/fetch/$s_!WVA8!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff695a94e-92ea-40a7-b133-f768a4af65a2_592x745.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!WVA8!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff695a94e-92ea-40a7-b133-f768a4af65a2_592x745.png" width="592" height="745" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f695a94e-92ea-40a7-b133-f768a4af65a2_592x745.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:745,&quot;width&quot;:592,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Code example: Python contextlib - Reuse Context Managers as Function Decorators&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Code example: Python contextlib - Reuse Context Managers as Function Decorators" title="Code example: Python contextlib - Reuse Context Managers as Function Decorators" srcset="https://substackcdn.com/image/fetch/$s_!WVA8!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff695a94e-92ea-40a7-b133-f768a4af65a2_592x745.png 424w, https://substackcdn.com/image/fetch/$s_!WVA8!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff695a94e-92ea-40a7-b133-f768a4af65a2_592x745.png 848w, https://substackcdn.com/image/fetch/$s_!WVA8!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff695a94e-92ea-40a7-b133-f768a4af65a2_592x745.png 1272w, https://substackcdn.com/image/fetch/$s_!WVA8!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff695a94e-92ea-40a7-b133-f768a4af65a2_592x745.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h4>Problem</h4><p>Many developers know <code>@contextmanager</code> as a convenient way to add setup and teardown logic around a <code>with</code> block.</p><p>But fewer realize it can also be used as a function decorator.</p><p>A common pattern for timing or logging functions is to write a separate decorator using <code>functools.wraps</code>. But this means maintaining two versions of the same logic, which can drift over time.</p><h4>Solution</h4><p>Since Python 3.3, any function decorated with <code>@contextmanager</code> can be used both ways:</p><ul><li><p>Need to time a code block? <code>with duration("..."):</code></p></li><li><p>Need to time a whole function? <code>@duration("...")</code></p></li></ul><div><hr></div><h3>Claude Code - Move Session Context Across Folders with /export</h3><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!JeMp!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3021fc66-64d4-4eac-a951-6d1beec12864_1080x459.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!JeMp!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3021fc66-64d4-4eac-a951-6d1beec12864_1080x459.png 424w, https://substackcdn.com/image/fetch/$s_!JeMp!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3021fc66-64d4-4eac-a951-6d1beec12864_1080x459.png 848w, https://substackcdn.com/image/fetch/$s_!JeMp!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3021fc66-64d4-4eac-a951-6d1beec12864_1080x459.png 1272w, https://substackcdn.com/image/fetch/$s_!JeMp!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3021fc66-64d4-4eac-a951-6d1beec12864_1080x459.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!JeMp!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3021fc66-64d4-4eac-a951-6d1beec12864_1080x459.png" width="1080" height="459" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/3021fc66-64d4-4eac-a951-6d1beec12864_1080x459.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:459,&quot;width&quot;:1080,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Code example: Claude Code - Move Session Context Across Folders with /export&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Code example: Claude Code - Move Session Context Across Folders with /export" title="Code example: Claude Code - Move Session Context Across Folders with /export" srcset="https://substackcdn.com/image/fetch/$s_!JeMp!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3021fc66-64d4-4eac-a951-6d1beec12864_1080x459.png 424w, https://substackcdn.com/image/fetch/$s_!JeMp!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3021fc66-64d4-4eac-a951-6d1beec12864_1080x459.png 848w, https://substackcdn.com/image/fetch/$s_!JeMp!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3021fc66-64d4-4eac-a951-6d1beec12864_1080x459.png 1272w, https://substackcdn.com/image/fetch/$s_!JeMp!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3021fc66-64d4-4eac-a951-6d1beec12864_1080x459.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h4>Problem</h4><p>Each Claude session lives in the folder where it was started. When you open Claude somewhere else, the previous context does not carry over.</p><p>You could ask Claude to summarize the old session and paste it into the new one, but summaries can miss important details, decisions, or tool outputs.</p><h4>Solution</h4><p>Claude Code&#8217;s <code>/export</code> command gives you the full conversation as a plain text file, including tool calls.</p><p>How to use it:</p><ul><li><p>Run <code>/export filename.md</code> in the original folder before switching</p></li><li><p>Open a new Claude session in the other folder</p></li><li><p>Ask Claude to read filename.md and continue the task</p></li></ul><blockquote></blockquote><div><hr></div><h3>&#128172; Rate Your Experience</h3><p>How would you rate your newsletter experience? <a href="https://r7yuob192zv.typeform.com/to/VODwGYMW">Share your feedback &#8594;</a></p><div><hr></div><h3>&#128269; Explore More on CodeCut</h3><ul><li><p><a href="https://codecut.ai/tool-selector/?utm_source=Substack&amp;utm_medium=email&amp;utm_campaign=article_tool-selector">Tool Selector</a> - Discover 70+ Python tools for AI and data science</p></li><li><p><a href="https://codecut.ai/production-ready-data-science/?utm_source=Substack&amp;utm_medium=email&amp;utm_campaign=article_production-ready-data-science">Production Ready Data Science</a> - A practical book for taking projects from prototype to production</p></li></ul>]]></content:encoded></item><item><title><![CDATA[TimescaleDB - Time-Series Superpowers for PostgreSQL]]></title><description><![CDATA[TimescaleDB extends PostgreSQL with hypertables, continuous aggregates, and 90% compression for time-series data without changing your SQL queries.]]></description><link>https://newsletter.codecut.ai/p/timescaledb-time-series-superpowers</link><guid isPermaLink="false">https://newsletter.codecut.ai/p/timescaledb-time-series-superpowers</guid><dc:creator><![CDATA[CodeCut]]></dc:creator><pubDate>Tue, 26 May 2026 16:01:43 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!KQsc!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F54cd9b40-71ce-44ef-b00b-4bea452685b2_950x497.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!KQsc!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F54cd9b40-71ce-44ef-b00b-4bea452685b2_950x497.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!KQsc!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F54cd9b40-71ce-44ef-b00b-4bea452685b2_950x497.png 424w, https://substackcdn.com/image/fetch/$s_!KQsc!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F54cd9b40-71ce-44ef-b00b-4bea452685b2_950x497.png 848w, https://substackcdn.com/image/fetch/$s_!KQsc!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F54cd9b40-71ce-44ef-b00b-4bea452685b2_950x497.png 1272w, https://substackcdn.com/image/fetch/$s_!KQsc!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F54cd9b40-71ce-44ef-b00b-4bea452685b2_950x497.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!KQsc!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F54cd9b40-71ce-44ef-b00b-4bea452685b2_950x497.png" width="950" height="497" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/54cd9b40-71ce-44ef-b00b-4bea452685b2_950x497.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:497,&quot;width&quot;:950,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;TimescaleDB: Time-Series Superpowers for PostgreSQL&quot;,&quot;title&quot;:&quot;TimescaleDB: Time-Series Superpowers for PostgreSQL&quot;,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="TimescaleDB: Time-Series Superpowers for PostgreSQL" title="TimescaleDB: Time-Series Superpowers for PostgreSQL" srcset="https://substackcdn.com/image/fetch/$s_!KQsc!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F54cd9b40-71ce-44ef-b00b-4bea452685b2_950x497.png 424w, https://substackcdn.com/image/fetch/$s_!KQsc!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F54cd9b40-71ce-44ef-b00b-4bea452685b2_950x497.png 848w, https://substackcdn.com/image/fetch/$s_!KQsc!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F54cd9b40-71ce-44ef-b00b-4bea452685b2_950x497.png 1272w, https://substackcdn.com/image/fetch/$s_!KQsc!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F54cd9b40-71ce-44ef-b00b-4bea452685b2_950x497.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><blockquote><p>&#128161; <strong>Sponsored by <a href="https://www.tigerdata.com/">Tiger Data</a>.</strong> This article was produced in partnership with the team behind TimescaleDB. All technical content, examples, and opinions are my own.</p></blockquote><h2>Introduction</h2><p>PostgreSQL is optimized for transactional workloads where individual rows are read and updated. Common operations include updating user profiles, processing payments, or changing order states. Access is random and row-focused.</p><pre><code><code>PostgreSQL: designed for row-level access

&#9484;&#9472;&#9472;&#9472;&#9472;&#9488;
&#9474; r1 &#9474; &#9668;&#9472;&#9472; read/update
&#9500;&#9472;&#9472;&#9472;&#9472;&#9508;
&#9474; r2 &#9474;
&#9500;&#9472;&#9472;&#9472;&#9472;&#9508;
&#9474; r3 &#9474; &#9668;&#9472;&#9472; delete
&#9500;&#9472;&#9472;&#9472;&#9472;&#9508;
&#9474; r4 &#9474;
&#9500;&#9472;&#9472;&#9472;&#9472;&#9508;
&#9474; r5 &#9474; &#9668;&#9472;&#9472; read/update
&#9492;&#9472;&#9472;&#9472;&#9472;&#9496;
</code></code></pre><p>Time-series data is the opposite: you constantly append new readings, query by time range, and periodically delete old data in bulk. Instead of touching individual rows, every operation works on ranges of time.</p><pre><code><code>Time-series: needs range-level access

&#9484;&#9472;&#9472;&#9472;&#9472;&#9516;&#9472;&#9472;&#9472;&#9472;&#9516;&#9472;&#9472;&#9472;&#9472;&#9516;&#9472;&#9472;&#9472;&#9472;&#9488;
&#9474; t1 &#9474; t2 &#9474; t3 &#9474; t4 &#9474; &#9668;&#9472;&#9472; append
&#9500;&#9472;&#9472;&#9472;&#9472;&#9532;&#9472;&#9472;&#9472;&#9472;&#9532;&#9472;&#9472;&#9472;&#9472;&#9532;&#9472;&#9472;&#9472;&#9472;&#9508;
&#9474;    &#9474;    &#9474;    &#9474;    &#9474;
&#9474;    query range    &#9474;
&#9474;    &#9668;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9658;   &#9474;
&#9500;&#9472;&#9472;&#9472;&#9472;&#9532;&#9472;&#9472;&#9472;&#9472;&#9532;&#9472;&#9472;&#9472;&#9472;&#9532;&#9472;&#9472;&#9472;&#9472;&#9508;
&#9474; t1 &#9474; t2 &#9474;         &#9474;
&#9474; &#9660;  &#9474; &#9660;  &#9474;         &#9474;
&#9474;drop&#9474;drop&#9474;         &#9474;
&#9492;&#9472;&#9472;&#9472;&#9472;&#9524;&#9472;&#9472;&#9472;&#9472;&#9524;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496;
     &#9650;
     &#9492;&#9472;&#9472; bulk delete old chunks
</code></code></pre><p>PostgreSQL can handle this pattern at smaller scales, but time-series tables don&#8217;t remain small. They grow continuously, and operations that were efficient early on becomes less efficient over time.</p><p>As datasets scale into the hundreds of millions of rows, inserts, queries, and deletes all start to slow down.</p><h2>Why PostgreSQL Struggles with Time-Series Data</h2><h3>Writes: Every Insert Updates Every Index</h3><p>As a PostgreSQL table grows, inserts get progressively slower because every insert does two things:</p><ul><li><p><strong>Append the row to the table</strong>, which stores the actual data in insertion order (fast)</p></li><li><p><strong>Update every index</strong> by finding the correct sorted position for the new value, so queries can locate rows without scanning the full table (gets slower as the index grows)</p></li></ul><p>For example, inserting a sensor reading at 10:30:00 triggers both steps:</p><pre><code><code>INSERT INTO sensor_data VALUES ('10:30:00', 'sensor_3', 22.5)

Step 1: Append row to table
&#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9516;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9516;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;
&#9474; 10:00:01  &#9474; sensor_1 &#9474; 21.3 &#9474;
&#9474; 10:00:05  &#9474; sensor_2 &#9474; 22.1 &#9474;
&#9474; 10:29:58  &#9474; sensor_1 &#9474; 21.8 &#9474;
&#9474; 10:30:00  &#9474; sensor_3 &#9474; 22.5 &#9474; &#9668; append to end (fast)
&#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9524;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9524;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496;

Step 2: Update time index
&#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9516;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;
&#9474; 10:00:01 &#9474; row 1  &#9474;
&#9474; 10:00:05 &#9474; row 2  &#9474;
&#9474; 10:29:58 &#9474; row 3  &#9474;
&#9474; 10:30:00 &#9474; row 4  &#9474; &#9668; find sorted position, insert (slow)
&#9474; 10:30:02 &#9474; row 5  &#9474;
&#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9524;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496;
</code></code></pre><p>With more rows, the index grows larger and finding the correct position takes more work. Multiply this by every index on the table, and inserts get progressively slower.</p><h3>Reads: No Way to Skip Irrelevant Time Ranges</h3><p>Time-series queries almost always filter by time, but PostgreSQL stores all rows together with no time-based grouping. To find rows matching a time range, it must search through the entire index, even when you only need the most recent hour.</p><p>For example, if your table holds three months of sensor data and you query the last hour, PostgreSQL still searches from the beginning:</p><pre><code><code>SELECT * FROM sensor_data WHERE time &gt; '10:00:00'

&#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;
&#9474; Jan    Feb    Mar    Apr ... Apr 25 10:00+   &#9474;
&#9474; &#9668;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472; scanned &#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9658; &#9668;&#9472;&#9472; needed &#9472;&#9472;&#9658;   &#9474;
&#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496;
</code></code></pre><h3>Deletes: Row-by-Row Removal Locks the Table</h3><p>Time-series tables grow continuously, and at some point you need to delete old data. But PostgreSQL&#8217;s <code>DELETE</code> operates one row at a time, removing each entry from the table, updating every index, and logging the change for crash recovery.</p><p>For example, if you have three months of sensor data and decide to keep only April, PostgreSQL processes every row in January, February, and March one at a time:</p><pre><code><code>DELETE FROM sensor_data WHERE time &lt; '2026-04-01'

&#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9516;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9516;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9516;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;
&#9474;  Jan   &#9474;  Feb   &#9474;  Mar   &#9474;  Apr   &#9474;
&#9474;  drop  &#9474;  drop  &#9474;  drop  &#9474;  keep  &#9474;
&#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9524;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9524;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9524;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496;
 Millions of rows, deleted one at a time
</code></code></pre><p>TimescaleDB is a PostgreSQL extension designed to solve all three of these problems. Let&#8217;s see how it solves each of these problems.</p><h2>What Is TimescaleDB?</h2><p><a href="https://fandf.co/49LmheA">TimescaleDB</a> is a PostgreSQL <strong>extension</strong>. It installs into your existing PostgreSQL instance without replacing anything. Your queries, connections, backups, and monitoring all stay the same.</p><p>What it adds is a set of features designed specifically for time-series workloads:</p><ul><li><p><strong>Hypertables</strong>: Automatically split large tables into smaller time-based chunks</p></li><li><p><code>time_bucket()</code>: Group data by any time interval (15 minutes, 6 hours, 3 days)</p></li><li><p><strong>Continuous aggregates</strong>: Pre-computed views that only process new data on refresh</p></li><li><p><strong>Columnar compression</strong>: Reduce storage by 90-95% without changing how you query the data</p></li><li><p><strong>Retention policies</strong>: Drop old data instantly without row-by-row deletion</p></li></ul><p>To see how these features work in practice, let&#8217;s set up TimescaleDB and try each one.</p><h2>Setup</h2><p>The fastest way to start is with Docker. This runs TimescaleDB with PostgreSQL 18:</p><pre><code><code>docker run -d --name timescaledb \
  -p 5432:5432 \
  -e POSTGRES_PASSWORD=password \
  timescale/timescaledb-ha:pg18
</code></code></pre><p>Connect to the PostgreSQL instance running in the container:</p><pre><code><code>psql -d "postgres://postgres:password@localhost:5432/postgres"
</code></code></pre><p>Verify TimescaleDB is installed:</p><pre><code><code>SELECT default_version
FROM pg_available_extensions
WHERE name = 'timescaledb';
</code></code></pre><pre><code><code> default_version
-----------------
 2.26.3
</code></code></pre><blockquote><p><strong>Note:</strong> If you&#8217;re not using Docker, see <a href="https://www.tigerdata.com/docs/get-started/choose-your-path/install-timescaledb">Install TimescaleDB</a> for other setup options.</p></blockquote><h2>Hypertables: Automatic Time-Based Partitioning</h2><p>A hypertable looks and behaves like a regular PostgreSQL table. You insert, query, and join it the same way.</p><p>The difference is behind the scenes: TimescaleDB automatically splits the data into time-based &#8220;chunks&#8221; and skips irrelevant chunks during queries.</p><pre><code><code>SELECT * FROM sensor_data WHERE time &gt; 'Jan 13'

&#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;  &#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;  &#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;
&#9474; Chunk 1 &#9474;  &#9474; Chunk 2 &#9474;  &#9474; Chunk 3 &#9474;
&#9474; Jan 1-7 &#9474;  &#9474; Jan 8-14&#9474;  &#9474; Jan 15+ &#9474;
&#9474;  skip   &#9474;  &#9474; partial &#9474;  &#9474;  scan   &#9474;
&#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496;  &#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496;  &#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496;
     &#10007;            &#10003;            &#10003;

Only chunks overlapping the time range are read.
</code></code></pre><h3>Creating a Hypertable</h3><p>Creating a hypertable requires only one change to a standard <code>CREATE TABLE</code> statement: adding <code>WITH (tsdb.hypertable)</code>.</p><p>Let&#8217;s create one for storing sensor readings:</p><pre><code><code>CREATE TABLE sensor_data (
    time TIMESTAMPTZ NOT NULL,
    sensor_id TEXT NOT NULL,
    temperature DOUBLE PRECISION,
    humidity DOUBLE PRECISION
) WITH (tsdb.hypertable);
</code></code></pre><p>That&#8217;s it. TimescaleDB automatically:</p><ul><li><p>Uses the <code>time</code> column to decide how to split data into chunks</p></li><li><p>Creates time-based chunks (default: 7-day intervals)</p></li><li><p>Automatically indexes the <code>time</code> column for fast range queries</p></li></ul><p>You can customize the chunk interval and partition column if needed:</p><pre><code><code>CREATE TABLE metrics (
    time TIMESTAMPTZ NOT NULL,
    device_id TEXT NOT NULL,
    cpu_usage DOUBLE PRECISION,
    memory_usage DOUBLE PRECISION
) WITH (
    tsdb.hypertable,
    tsdb.partition_column = 'time',
    tsdb.chunk_interval = '1 day'
);
</code></code></pre><h3>Loading Data</h3><p>Let&#8217;s generate a realistic dataset: 5 sensors reporting temperature and humidity every 10 seconds for 7 days. This produces about 300,000 rows.</p><pre><code><code>INSERT INTO sensor_data (time, sensor_id, temperature, humidity)
SELECT
    ts,
    'sensor_' || device_num,
    20 + 10 * sin(extract(epoch FROM ts) / 43200) + random() * 2,
    40 + 20 * cos(extract(epoch FROM ts) / 43200) + random() * 5
FROM generate_series(
    '2026-04-21 00:00:00'::timestamptz,
    '2026-04-28 00:00:00'::timestamptz,
    INTERVAL '10 seconds'
) AS ts
CROSS JOIN generate_series(1, 5) AS device_num;
</code></code></pre><pre><code><code>INSERT 0 302405
</code></code></pre><p>The <code>sin()</code> and <code>cos()</code> functions create realistic daily temperature and humidity cycles, with <code>random()</code> adding noise.</p><h3>Inspecting Chunks</h3><p>Query the chunk metadata to see how TimescaleDB partitioned the data:</p><pre><code><code>SELECT chunk_name, range_start, range_end, is_compressed
FROM timescaledb_information.chunks
WHERE hypertable_name = 'sensor_data'
ORDER BY range_start;
</code></code></pre><pre><code><code>    chunk_name    |      range_start       |       range_end        | is_compressed
------------------+------------------------+------------------------+---------------
 _hyper_1_1_chunk | 2026-04-16 00:00:00+00 | 2026-04-23 00:00:00+00 | f
 _hyper_1_2_chunk | 2026-04-23 00:00:00+00 | 2026-04-30 00:00:00+00 | f
</code></code></pre><p>Each chunk spans exactly 7 days (<code>2026-04-16</code> to <code>2026-04-23</code>, then <code>2026-04-23</code> to <code>2026-04-30</code>). Let&#8217;s query the last 2 days and see which chunks get used:</p><pre><code><code>-- First 3 rows
SELECT * FROM sensor_data
WHERE time &gt; '2026-04-26 00:00:00'
ORDER BY time ASC LIMIT 3;

-- Last 3 rows
SELECT * FROM sensor_data
WHERE time &gt; '2026-04-26 00:00:00'
ORDER BY time DESC LIMIT 3;
</code></code></pre><pre><code><code>          time          | sensor_id |    temperature     |      humidity
------------------------+-----------+--------------------+--------------------
 2026-04-26 00:00:10+00 | sensor_5  | 31.067574603033847 |  32.42945016679141
 2026-04-26 00:00:10+00 | sensor_1  | 29.770821180002446 |  33.87789610251711
 2026-04-26 00:00:10+00 | sensor_2  |   30.5820979427979 | 36.640107625668165
(3 rows)

          time          | sensor_id |    temperature     |      humidity
------------------------+-----------+--------------------+--------------------
 2026-04-28 00:00:00+00 | sensor_1  |  18.82289094155276 | 61.290098283252824
 2026-04-28 00:00:00+00 | sensor_2  | 17.298999086810714 |  60.28357262461673
 2026-04-28 00:00:00+00 | sensor_3  |  18.31179563705799 | 61.943984006436224
(3 rows)
</code></code></pre><p>All rows fall between April 26 and April 28. To verify that only the second chunk was scanned, use <code>EXPLAIN</code> to explain the query plan:</p><pre><code><code>EXPLAIN SELECT * FROM sensor_data
WHERE time &gt; '2026-04-26 00:00:00';
</code></code></pre><pre><code><code>                                                     QUERY PLAN
-----------------------------------------------------------------------------------------------------------------------
 Index Scan using _hyper_1_2_chunk_sensor_data_time_idx on _hyper_1_2_chunk  (cost=0.42..3618.97 rows=85792 width=33)
   Index Cond: ("time" &gt; '2026-04-26 00:00:00+00')
</code></code></pre><p>The plan only references <code>_hyper_1_2_chunk</code> (Apr 23-30). The first chunk <code>_hyper_1_1_chunk</code> (Apr 16-23) doesn&#8217;t appear at all, meaning TimescaleDB skipped it entirely.</p><h2><code>time_bucket()</code>: Flexible Time-Based Aggregations</h2><p>Time-series analysis often requires grouping data into fixed time windows: average temperature every 15 minutes, total requests per 6-hour block, or peak usage per 3-day period.</p><p>PostgreSQL&#8217;s built-in <code>date_trunc()</code> only supports fixed calendar intervals like hour, day, or month. For custom intervals like 15 minutes, you have to manually calculate which bucket each timestamp falls into:</p><pre><code><code>-- PostgreSQL workaround for 15-minute buckets
SELECT
    date_trunc('hour', time)
        + INTERVAL '15 min' * FLOOR(EXTRACT(MINUTE FROM time) / 15)
        AS bucket,
    sensor_id,
    AVG(temperature) AS avg_temp
FROM sensor_data
WHERE sensor_id = 'sensor_1'
  AND time BETWEEN '2026-04-27 00:00:00' AND '2026-04-27 02:00:00'
GROUP BY bucket, sensor_id
ORDER BY bucket;
</code></code></pre><pre><code><code>         bucket         | sensor_id |      avg_temp
------------------------+-----------+--------------------
 2026-04-27 00:00:00+00 | sensor_1  | 13.542631344509111
 2026-04-27 00:15:00+00 | sensor_1  |  13.23800229831265
 2026-04-27 00:30:00+00 | sensor_1  |  13.20352104441286
 2026-04-27 00:45:00+00 | sensor_1  | 13.119135414712746
 2026-04-27 01:00:00+00 | sensor_1  | 12.889897733150352
 2026-04-27 01:15:00+00 | sensor_1  | 12.876690040717648
 2026-04-27 01:30:00+00 | sensor_1  |  12.72343003629354
 2026-04-27 01:45:00+00 | sensor_1  |  12.58139781130447
 2026-04-27 02:00:00+00 | sensor_1  |   11.8816092609178
</code></code></pre><p>TimescaleDB&#8217;s <code>time_bucket()</code> produces the same result with a single function call:</p><pre><code><code>SELECT
    time_bucket('15 minutes', time) AS bucket,
    sensor_id,
    AVG(temperature) AS avg_temp,
    AVG(humidity) AS avg_humidity
FROM sensor_data
WHERE sensor_id = 'sensor_1'
  AND time BETWEEN '2026-04-27 00:00:00' AND '2026-04-27 02:00:00'
GROUP BY bucket, sensor_id
ORDER BY bucket;
</code></code></pre><pre><code><code>         bucket         | sensor_id |      avg_temp      |    avg_humidity
------------------------+-----------+--------------------+--------------------
 2026-04-27 00:00:00+00 | sensor_1  | 13.542631344509111 | 29.297833301485014
 2026-04-27 00:15:00+00 | sensor_1  |  13.23800229831265 |  29.57904716854577
 2026-04-27 00:30:00+00 | sensor_1  |  13.20352104441286 |  30.02442153150598
 2026-04-27 00:45:00+00 | sensor_1  | 13.119135414712746 | 30.389413838577834
 2026-04-27 01:00:00+00 | sensor_1  | 12.889897733150352 |  30.57742656702146
 2026-04-27 01:15:00+00 | sensor_1  | 12.876690040717648 | 30.799536994969888
 2026-04-27 01:30:00+00 | sensor_1  |  12.72343003629354 | 31.520854251589334
 2026-04-27 01:45:00+00 | sensor_1  |  12.58139781130447 | 31.613018125020453
 2026-04-27 02:00:00+00 | sensor_1  |   11.8816092609178 |   33.0892996678552
</code></code></pre><h2>Continuous Aggregates: Pre-Computed Views That Refresh Incrementally</h2><p>When you run the same aggregation query repeatedly (e.g., hourly averages for a dashboard), PostgreSQL recomputes it from raw data every time. A materialized view solves this by storing the query result in a table, so future reads are instant.</p><p>The problem is that materialized views don&#8217;t update themselves. Every time new data arrives, you have to manually run <code>REFRESH MATERIALIZED VIEW</code> to keep the results current. This rebuilds the entire view from scratch, which can take minutes to hours on large tables.</p><p>TimescaleDB&#8217;s continuous aggregates take a smarter approach: they remember where they left off and only process new data on each refresh.</p><p>Let&#8217;s compare the two approaches with an example:</p><pre><code><code>Materialized view:    recompute ALL buckets on refresh
&#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9516;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9516;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9516;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9516;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9516;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;
&#9474; Jan  &#9474; Feb  &#9474; Mar  &#9474; Apr  &#9474; Apr  &#9474; Apr  &#9474;
&#9474; redo &#9474; redo &#9474; redo &#9474; redo &#9474; redo &#9474; redo &#9474;
&#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9524;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9524;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9524;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9524;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9524;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496;

Continuous aggregate: only recompute CHANGED buckets
&#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9516;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9516;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9516;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9516;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9516;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;
&#9474; Jan  &#9474; Feb  &#9474; Mar  &#9474; Apr  &#9474; Apr  &#9474; Apr  &#9474;
&#9474; skip &#9474; skip &#9474; skip &#9474; skip &#9474; skip &#9474; redo &#9474;
&#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9524;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9524;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9524;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9524;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9524;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496;
                                     &#9650;
                                 new data here
</code></code></pre><p>With a materialized view, all six buckets are recomputed even though only the last one has new data. With a continuous aggregate, the first five are skipped entirely.</p><h3>Creating a Continuous Aggregate</h3><p>Let&#8217;s create a continuous aggregate that computes hourly averages, min, max, and reading count per sensor. The syntax is similar to a materialized view, with <code>timescaledb.continuous</code> added:</p><pre><code><code>CREATE MATERIALIZED VIEW sensor_hourly
WITH (timescaledb.continuous) AS
SELECT
    time_bucket('1 hour', time) AS hour,
    sensor_id,
    AVG(temperature) AS avg_temp,
    MIN(temperature) AS min_temp,
    MAX(temperature) AS max_temp,
    AVG(humidity) AS avg_humidity,
    COUNT(*) AS readings
FROM sensor_data
GROUP BY hour, sensor_id;
</code></code></pre><p>This creates the aggregate and materializes the existing data. You can query it like any view:</p><pre><code><code>SELECT hour, sensor_id, avg_temp, readings
FROM sensor_hourly
WHERE sensor_id = 'sensor_1'
ORDER BY hour DESC
LIMIT 5;
</code></code></pre><pre><code><code>          hour          | sensor_id |      avg_temp      | readings
------------------------+-----------+--------------------+----------
 2026-04-28 00:00:00+00 | sensor_1  |  18.82289094155276 |        1
 2026-04-27 23:00:00+00 | sensor_1  |  17.62569258253398 |      360
 2026-04-27 22:00:00+00 | sensor_1  | 16.916727264064487 |      360
 2026-04-27 21:00:00+00 | sensor_1  | 16.140865785926938 |      360
 2026-04-27 20:00:00+00 | sensor_1  | 15.457800483137094 |      360
</code></code></pre><p>Each hour has 360 readings (one every 10 seconds = 6 per minute x 60 minutes). The midnight bucket has just 1 reading because that&#8217;s the last timestamp in our dataset.</p><h3>Refreshing After New Data Arrives</h3><p>Now let&#8217;s insert a new reading and see how the aggregate handles it:</p><pre><code><code>INSERT INTO sensor_data (time, sensor_id, temperature, humidity)
VALUES ('2026-04-28 00:10:00', 'sensor_1', 25.3, 51.2);
</code></code></pre><p>Then refresh only the time range that changed:</p><pre><code><code>CALL refresh_continuous_aggregate('sensor_hourly', '2026-04-28 00:00:00', '2026-04-28 02:00:00');
</code></code></pre><p>Now query the aggregate to see the update:</p><pre><code><code>SELECT hour, sensor_id, avg_temp, readings
FROM sensor_hourly
WHERE sensor_id = 'sensor_1'
ORDER BY hour DESC
LIMIT 5;
</code></code></pre><pre><code><code>          hour          | sensor_id |      avg_temp      | readings
------------------------+-----------+--------------------+----------
 2026-04-28 00:00:00+00 | sensor_1  |  22.06144547077638 |        2
 2026-04-27 23:00:00+00 | sensor_1  |  17.62569258253398 |      360
 2026-04-27 22:00:00+00 | sensor_1  | 16.916727264064487 |      360
 2026-04-27 21:00:00+00 | sensor_1  | 16.140865785926938 |      360
 2026-04-27 20:00:00+00 | sensor_1  | 15.457800483137094 |      360
</code></code></pre><p>The midnight bucket went from 1 reading to 2, with its average updated to <code>(18.82289 + 25.3) / 2 &#8776; 22.06</code>. The 20:00 through 23:00 buckets are unchanged because they fall outside the refresh window (<code>2026-04-28 00:00:00</code> to <code>2026-04-28 02:00:00</code>).</p><h3>Adding a Refresh Policy</h3><p>In practice, you wouldn&#8217;t refresh manually every time new data arrives. Instead, set up a policy that runs the refresh automatically:</p><pre><code><code>SELECT add_continuous_aggregate_policy(
    'sensor_hourly',
    start_offset  =&gt; INTERVAL '3 hours',
    end_offset    =&gt; INTERVAL '1 hour',
    schedule_interval =&gt; INTERVAL '1 hour'
);
</code></code></pre><p>This tells TimescaleDB:</p><ul><li><p><code>schedule_interval</code>: Run the refresh job every hour</p></li><li><p><code>start_offset</code>: Look back 3 hours for any data that changed</p></li><li><p><code>end_offset</code>: Don&#8217;t process the most recent hour (it&#8217;s likely still receiving data)</p></li></ul><p>The refresh processes only the buckets that fall within the <code>start_offset</code> to <code>end_offset</code> window. On a table with months of data, this takes seconds instead of minutes:</p><pre><code><code>Table with 3 months of data, policy runs at 10:00:

&#9474; Jan &#9474; Feb &#9474; Mar &#9474; ... &#9474; 7:00 &#9474; 8:00 &#9474; 9:00 &#9474;10:00&#9474;
&#9474;skip &#9474;skip &#9474;skip &#9474;     &#9474; redo &#9474; redo &#9474; redo &#9474;skip &#9474;
&#9474;&#9668;&#9472;&#9472;    untouched    &#9472;&#9472;&#9658;&#9474;&#9668;&#9472;&#9472;     3 hrs    &#9472;&#9472;&#9658;&#9474;     &#9474;
</code></code></pre><h3>Monitoring Refresh Jobs</h3><p>Check the status of refresh jobs:</p><pre><code><code>SELECT job_id, application_name, schedule_interval, next_start
FROM timescaledb_information.jobs
WHERE application_name LIKE '%Continuous%';
</code></code></pre><pre><code><code> job_id |              application_name              | schedule_interval |          next_start
--------+--------------------------------------------+-------------------+-------------------------------
   1002 | Refresh Continuous Aggregate Policy [1002] | 01:00:00          | 2026-04-30 05:37:23.960309+00
</code></code></pre><p>This confirms the policy is active: job 1002 refreshes the <code>sensor_hourly</code> aggregate every hour, with the next run scheduled for the time shown in <code>next_start</code>.</p><h2>Compression: Columnar Storage with 90%+ Reduction</h2><p>As time-series tables grow, storage becomes a concern. A sensor writing every 10 seconds across 5 devices generates over 300,000 rows per week. Over months, that adds up to gigabytes of data.</p><p>The good news is that time-series data compresses far better than transactional data. In a users table, every row has a different name, email, and address. In a sensor table, the same sensor IDs appear over and over, timestamps increment by the same interval, and readings barely change from one row to the next. This repetition means the database can store the data in a fraction of the space:</p><pre><code><code>10 rows of sensor data:

&#9474; sensor_1 &#9474; 10:00:10 &#9474; 22.1 &#9474;
&#9474; sensor_1 &#9474; 10:00:20 &#9474; 22.3 &#9474;
&#9474; sensor_1 &#9474; 10:00:30 &#9474; 22.2 &#9474;
&#9474; sensor_2 &#9474; 10:00:10 &#9474; 45.0 &#9474;
&#9474; sensor_2 &#9474; 10:00:20 &#9474; 45.2 &#9474;
&#9474; sensor_2 &#9474; 10:00:30 &#9474; 44.8 &#9474;

After compression:

  sensor_id: "sensor_1" x3, "sensor_2" x3
  time:      10:00:10, +10s, +10s, 10:00:10, +10s, +10s
  temp:      22.1, +0.2, -0.1, 45.0, +0.2, -0.4
</code></code></pre><p>TimescaleDB applies different compression strategies depending on the data type:</p><ul><li><p><strong>Timestamps</strong>: Stores the difference between consecutive values (like the <code>+10s, +10s, +10s</code> pattern above)</p></li><li><p><strong>Repeated strings</strong> (like sensor IDs): Stores the value once and references it across all rows</p></li><li><p><strong>Floating-point values</strong>: Stores the difference between consecutive readings (like the <code>+0.2, -0.1</code> pattern above)</p></li></ul><h3>When Compression Happens</h3><p>You wouldn&#8217;t want to compress data that&#8217;s still being written to. Each insert into a compressed chunk requires decompression, which is slow. TimescaleDB avoids this by default: hypertables created with <code>WITH (tsdb.hypertable)</code> automatically compress only chunks older than 7 days, leaving recent data uncompressed for fast writes.</p><pre><code><code>&#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488; &#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488; &#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;--&#9472;&#9488;
&#9474;  Apr 7-14   &#9474; &#9474;  Apr 14-21  &#9474; &#9474;  Apr 21-28    &#9474;
&#9474; compressed  &#9474; &#9474; compressed  &#9474; &#9474; uncompressed  &#9474;
&#9474; (storage &#8595;) &#9474; &#9474; (storage &#8595;) &#9474; &#9474; (fast writes) &#9474;
&#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496; &#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496; &#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;-&#9472;-&#9496;
&#9474;&#9668;&#9472;&#9472;   older than 7 days   &#9472;&#9472;&#9658;&#9474; &#9474;&#9668;&#9472;&#9472;  recent &#9472;&#9472;&#9658;&#9474;
        auto-compressed          still receiving
                                   new inserts
</code></code></pre><p>To use a different threshold, remove the default policy and add a new one. For example, to compress data older than 30 days instead of 7:</p><pre><code><code>-- Remove the default policy created with the hypertable
CALL remove_columnstore_policy('sensor_data');

-- Add a new policy with the desired interval
CALL add_columnstore_policy('sensor_data', after =&gt; INTERVAL '30 days');
</code></code></pre><h3>Configuring Compression</h3><p>Compression is already enabled by default when you create a hypertable. No extra setup is needed for basic compression. However, you can improve query performance on compressed data by telling TimescaleDB how to organize the rows:</p><pre><code><code>CREATE TABLE sensor_data_v2 (
    time TIMESTAMPTZ NOT NULL,
    sensor_id TEXT NOT NULL,
    temperature DOUBLE PRECISION,
    humidity DOUBLE PRECISION
) WITH (
    tsdb.hypertable,
    tsdb.segmentby = 'sensor_id',
    tsdb.orderby = 'time DESC'
);
</code></code></pre><ul><li><p><code>segmentby = 'sensor_id'</code>: Compresses each sensor&#8217;s data into a separate block. A query for <code>sensor_1</code> only reads that block, skipping all other sensors.</p></li><li><p><code>orderby = 'time DESC'</code>: Sorts each block newest first. A query for the latest reading finds it at the top without scanning the entire block.</p></li></ul><p>For example, if you query <code>WHERE sensor_id = 'sensor_1'</code>, here&#8217;s what happens with and without <code>segmentby</code>:</p><pre><code><code>Without segmentby (one block):

&#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;
&#9474; sensor_1, sensor_2 data mixed together   &#9474;
&#9474; query for sensor_1 decompresses ALL      &#9474;
&#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496;

With segmentby = 'sensor_id' (one block per sensor):

&#9484;&#9472; sensor_1 &#9472;&#9472;&#9472;&#9472;&#9472;&#9488;  &#9484;&#9472; sensor_2 &#9472;&#9472;&#9472;&#9472;&#9472;&#9488;
&#9474; time DESC      &#9474;  &#9474; time DESC      &#9474;
&#9474; 10:00:30 22.2  &#9474;  &#9474; 10:00:30 44.8  &#9474;
&#9474; 10:00:20 22.3  &#9474;  &#9474; 10:00:20 45.2  &#9474;
&#9474; 10:00:10 22.1  &#9474;  &#9474; 10:00:10 45.0  &#9474;
&#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496;  &#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496;
      read &#10003;              skip
</code></code></pre><p>With 100 sensors, this means a query for one sensor decompresses 1% of the data instead of all of it.</p><h3>Checking Compression Results</h3><p>The automatic policy runs daily, so let&#8217;s compress a chunk manually to see the results immediately:</p><pre><code><code>SELECT compress_chunk('_timescaledb_internal._hyper_1_1_chunk');
</code></code></pre><p>Now check the chunk status:</p><pre><code><code>SELECT chunk_name, range_start, range_end, is_compressed
FROM timescaledb_information.chunks
WHERE hypertable_name = 'sensor_data'
ORDER BY range_start;
</code></code></pre><pre><code><code>    chunk_name    |      range_start       |       range_end        | is_compressed
------------------+------------------------+------------------------+---------------
 _hyper_1_1_chunk | 2026-04-16 00:00:00+00 | 2026-04-23 00:00:00+00 | t
 _hyper_1_2_chunk | 2026-04-23 00:00:00+00 | 2026-04-30 00:00:00+00 | f
</code></code></pre><p><code>is_compressed = t</code> confirms the first chunk is now compressed.</p><p>Compression doesn&#8217;t change how you write queries. The same SQL works on both compressed and uncompressed data:</p><pre><code><code>-- This queries the compressed chunk (_hyper_1_1_chunk) directly
SELECT sensor_id, AVG(temperature) AS avg_temp, COUNT(*) AS readings
FROM sensor_data
WHERE time BETWEEN '2026-04-21 00:00:00' AND '2026-04-22 00:00:00'
  AND sensor_id = 'sensor_1'
GROUP BY sensor_id;
</code></code></pre><pre><code><code> sensor_id |      avg_temp      | readings
-----------+--------------------+----------
 sensor_1  | 15.388563698009191 |     8641
</code></code></pre><h2>Retention Policies: Automated Data Lifecycle Management</h2><p>Time-series tables grow continuously, and at some point old data needs to go. In plain PostgreSQL, <code>DELETE</code> removes rows one at a time, which is slow on large tables.</p><p>TimescaleDB solves this by dropping entire chunks instead of deleting individual rows. A chunk is a separate table internally, so dropping it is instant regardless of how many rows it contains.</p><pre><code><code>Retention policy: drop_after =&gt; 6 months

&#9474;  Oct   &#9474;  Nov   &#9474;  Dec   &#9474; ... &#9474;  Mar   &#9474;  Apr   &#9474;  May   &#9474;
&#9474;  DROP  &#9474;  DROP  &#9474;  DROP  &#9474;     &#9474;  keep  &#9474;  keep  &#9474;  keep  &#9474;
&#9474;&#9668;&#9472;&#9472;&#9472;&#9472; older than 6 months  &#9472;&#9472;&#9472;&#9472;&#9658;&#9474;&#9668;&#9472;&#9472;&#9472; within 6 months  &#9472;&#9472;&#9472;&#9658;&#9474;
        dropped instantly                 untouched
</code></code></pre><p>To have TimescaleDB drop old chunks automatically on a schedule, add a retention policy:</p><pre><code><code>SELECT add_retention_policy('sensor_data', drop_after =&gt; INTERVAL '6 months');
</code></code></pre><p>This runs a background job that automatically drops chunks containing data older than 6 months.</p><h3>Combining Retention with Continuous Aggregates</h3><p>Deleting all old data saves storage but means you can no longer answer questions like &#8220;how does this month compare to last year?&#8221; A common solution is to delete the raw readings after a short period, but keep the pre-computed summaries (hourly averages, daily totals) for much longer.</p><p>Here, raw data from <code>sensor_data</code> is deleted after 30 days to save storage, while the <code>sensor_hourly</code> continuous aggregate retains hourly averages for 1 year:</p><pre><code><code>-- Keep raw data for 30 days
SELECT add_retention_policy('sensor_data', drop_after =&gt; INTERVAL '30 days');

-- Keep hourly aggregates for 1 year
SELECT add_retention_policy('sensor_hourly', drop_after =&gt; INTERVAL '1 year');
</code></code></pre><p>This gives you detailed data for recent debugging and long-term trends for historical analysis, without the storage cost of keeping every raw reading forever.</p><h2>Key Takeaways</h2><p>TimescaleDB adds time-series capabilities to PostgreSQL without requiring you to switch databases or learn a new query language.</p><p>TimescaleDB is worth considering when your PostgreSQL tables are growing with timestamped data and you&#8217;re noticing slower inserts, expensive aggregation queries, or increasing storage costs. Since it&#8217;s an extension, you can try it on a single table without changing anything else in your database.</p><h2>Related Tutorials</h2><ul><li><p><strong><a href="https://codecut.ai/semantic-search-postgres-pgvector-ollama/">Implement Semantic Search in Postgres Using pgvector and Ollama</a></strong>: Another PostgreSQL extension that specializes Postgres for a workload it wasn&#8217;t originally built for, this time vector search for RAG.</p></li><li><p><strong><a href="https://codecut.ai/deep-dive-into-duckdb-data-scientists/">A Deep Dive into DuckDB for Data Scientists</a></strong>: An embedded analytical alternative for cases where you need fast aggregations on a single machine without running a database server.</p></li><li><p><strong><a href="https://codecut.ai/polars-vs-pandas-a-fast-multi-core-alternative-for-dataframes/">Polars vs. Pandas: A Fast, Multi-Core Alternative for DataFrames</a></strong>: A columnar processing engine in Python, useful for analyzing data pulled out of TimescaleDB.</p></li></ul><div><hr></div><p><em>Originally published on <a href="https://codecut.ai/deep-dive-timescaledb-postgresql/">CodeCut</a>.</em></p>]]></content:encoded></item><item><title><![CDATA[Quick Tips: smolagents - Build an AI Agent in 4 Lines of Code]]></title><description><![CDATA[Plus the Claude Code /goal command]]></description><link>https://newsletter.codecut.ai/p/smolagents-build-an-ai-agent-in-4</link><guid isPermaLink="false">https://newsletter.codecut.ai/p/smolagents-build-an-ai-agent-in-4</guid><dc:creator><![CDATA[CodeCut]]></dc:creator><pubDate>Thu, 21 May 2026 16:01:04 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!z3Ya!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee4bff33-1a4a-4a8e-980a-7a67e8a463af_617x661.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Grab your coffee. Here are this week&#8217;s highlights.</p><div><hr></div><h2>&#128197; Today&#8217;s Picks</h2><h3>Claude Code: Run Tasks to Completion with /goal</h3><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!I_uX!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff418cdec-1453-497f-bade-f18713093c6e_1080x818.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!I_uX!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff418cdec-1453-497f-bade-f18713093c6e_1080x818.png 424w, https://substackcdn.com/image/fetch/$s_!I_uX!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff418cdec-1453-497f-bade-f18713093c6e_1080x818.png 848w, https://substackcdn.com/image/fetch/$s_!I_uX!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff418cdec-1453-497f-bade-f18713093c6e_1080x818.png 1272w, https://substackcdn.com/image/fetch/$s_!I_uX!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff418cdec-1453-497f-bade-f18713093c6e_1080x818.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!I_uX!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff418cdec-1453-497f-bade-f18713093c6e_1080x818.png" width="1080" height="818" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f418cdec-1453-497f-bade-f18713093c6e_1080x818.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:818,&quot;width&quot;:1080,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Code example: Claude Code: Run Tasks to Completion with /goal&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Code example: Claude Code: Run Tasks to Completion with /goal" title="Code example: Claude Code: Run Tasks to Completion with /goal" srcset="https://substackcdn.com/image/fetch/$s_!I_uX!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff418cdec-1453-497f-bade-f18713093c6e_1080x818.png 424w, https://substackcdn.com/image/fetch/$s_!I_uX!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff418cdec-1453-497f-bade-f18713093c6e_1080x818.png 848w, https://substackcdn.com/image/fetch/$s_!I_uX!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff418cdec-1453-497f-bade-f18713093c6e_1080x818.png 1272w, https://substackcdn.com/image/fetch/$s_!I_uX!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff418cdec-1453-497f-bade-f18713093c6e_1080x818.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h4>Problem</h4><p>Starting a task in Claude Code often means spending multiple turns going back and forth before it finally reaches the result you wanted.</p><h4>Solution</h4><p><strong><a href="https://github.com/anthropics/claude-code">Claude Code</a></strong>&#8217;s <code>/goal</code> command lets you define a completion condition once, then continues working until that condition is met.</p><p>After each turn, a lightweight model evaluates the conversation against your condition. If the goal is not satisfied, Claude gets sent back for another turn with a short explanation of what still needs to be done.</p><p>To get the most out of <code>/goal</code>, write a condition the evaluator can actually verify.</p><ul><li><p>Name a measurable end state instead of a vague target (e.g. &#8220;all tests in <code>test/auth</code> pass&#8221; instead of &#8220;auth is working&#8221;)</p></li><li><p>List constraints that must hold along the way (e.g. &#8220;no other test file is modified&#8221; or &#8220;no tests are skipped or deleted&#8221;)</p></li><li><p>Add a safety limit like &#8220;stop after 20 turns&#8221; to avoid endless iterations.</p></li></ul><blockquote><p>&#128214; <a href="https://code.claude.com/docs/en/goal">View The Docs</a></p></blockquote><div><hr></div><h3>smolagents: Build an AI Agent in 4 Lines of Code</h3><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!z3Ya!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee4bff33-1a4a-4a8e-980a-7a67e8a463af_617x661.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!z3Ya!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee4bff33-1a4a-4a8e-980a-7a67e8a463af_617x661.png 424w, https://substackcdn.com/image/fetch/$s_!z3Ya!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee4bff33-1a4a-4a8e-980a-7a67e8a463af_617x661.png 848w, https://substackcdn.com/image/fetch/$s_!z3Ya!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee4bff33-1a4a-4a8e-980a-7a67e8a463af_617x661.png 1272w, https://substackcdn.com/image/fetch/$s_!z3Ya!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee4bff33-1a4a-4a8e-980a-7a67e8a463af_617x661.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!z3Ya!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee4bff33-1a4a-4a8e-980a-7a67e8a463af_617x661.png" width="617" height="661" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ee4bff33-1a4a-4a8e-980a-7a67e8a463af_617x661.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:661,&quot;width&quot;:617,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Code example: smolagents: Build an AI Agent in 4 Lines of Code&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Code example: smolagents: Build an AI Agent in 4 Lines of Code" title="Code example: smolagents: Build an AI Agent in 4 Lines of Code" srcset="https://substackcdn.com/image/fetch/$s_!z3Ya!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee4bff33-1a4a-4a8e-980a-7a67e8a463af_617x661.png 424w, https://substackcdn.com/image/fetch/$s_!z3Ya!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee4bff33-1a4a-4a8e-980a-7a67e8a463af_617x661.png 848w, https://substackcdn.com/image/fetch/$s_!z3Ya!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee4bff33-1a4a-4a8e-980a-7a67e8a463af_617x661.png 1272w, https://substackcdn.com/image/fetch/$s_!z3Ya!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee4bff33-1a4a-4a8e-980a-7a67e8a463af_617x661.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h4>Problem</h4><p>LangChain and LangGraph are powerful, but they also come with concepts like chains, runnables, and state graphs.</p><p>That is useful for complex workflows, but overkill when you just want to give a model a tool and let it solve a task.</p><h4>Solution</h4><p><strong><a href="https://github.com/huggingface/smolagents">smolagents</a></strong> from Hugging Face is a barebones agent library designed for simplicity.</p><p>In roughly 1,000 lines of code, it gives you the core pieces needed to build a working agent with a single import and three lines of setup.</p><p>Key features:</p><ul><li><p>Agents act by writing Python in a sandbox, the same way you&#8217;d script a task in a notebook</p></li><li><p>Model-agnostic design, swap between OpenAI, Anthropic, local Ollama, or Hugging Face Inference</p></li><li><p>Push agents to the Hugging Face Hub to share with the community</p></li></ul><blockquote></blockquote><div><hr></div><h3>&#128172; Rate Your Experience</h3><p>How would you rate your newsletter experience? <a href="https://r7yuob192zv.typeform.com/to/VODwGYMW">Share your feedback &#8594;</a></p><div><hr></div><h3>&#128269; Explore More on CodeCut</h3><ul><li><p><a href="https://codecut.ai/tool-selector/?utm_source=Substack&amp;utm_medium=email&amp;utm_campaign=article_tool-selector">Tool Selector</a> - Discover 70+ Python tools for AI and data science</p></li><li><p><a href="https://codecut.ai/production-ready-data-science/?utm_source=Substack&amp;utm_medium=email&amp;utm_campaign=article_production-ready-data-science">Production Ready Data Science</a> - A practical book for taking projects from prototype to production</p></li></ul>]]></content:encoded></item><item><title><![CDATA[Deep Dive: Shrink Your Python Container in One Command with SlimToolkit]]></title><description><![CDATA[Use SlimToolkit to shrink a Python container by half in one command. No Dockerfile changes. Walkthrough on a chatbot with common edge cases and fixes.]]></description><link>https://newsletter.codecut.ai/p/shrink-your-python-container-in-one</link><guid isPermaLink="false">https://newsletter.codecut.ai/p/shrink-your-python-container-in-one</guid><dc:creator><![CDATA[CodeCut]]></dc:creator><pubDate>Tue, 19 May 2026 16:01:04 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!Zr89!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb329f779-2dd1-4c93-858b-c53de6212372_1200x628.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Zr89!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb329f779-2dd1-4c93-858b-c53de6212372_1200x628.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Zr89!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb329f779-2dd1-4c93-858b-c53de6212372_1200x628.png 424w, https://substackcdn.com/image/fetch/$s_!Zr89!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb329f779-2dd1-4c93-858b-c53de6212372_1200x628.png 848w, https://substackcdn.com/image/fetch/$s_!Zr89!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb329f779-2dd1-4c93-858b-c53de6212372_1200x628.png 1272w, https://substackcdn.com/image/fetch/$s_!Zr89!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb329f779-2dd1-4c93-858b-c53de6212372_1200x628.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Zr89!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb329f779-2dd1-4c93-858b-c53de6212372_1200x628.png" width="1200" height="628" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b329f779-2dd1-4c93-858b-c53de6212372_1200x628.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:628,&quot;width&quot;:1200,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Shrink Your Python Container in One Command with SlimToolkit&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Shrink Your Python Container in One Command with SlimToolkit" title="Shrink Your Python Container in One Command with SlimToolkit" srcset="https://substackcdn.com/image/fetch/$s_!Zr89!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb329f779-2dd1-4c93-858b-c53de6212372_1200x628.png 424w, https://substackcdn.com/image/fetch/$s_!Zr89!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb329f779-2dd1-4c93-858b-c53de6212372_1200x628.png 848w, https://substackcdn.com/image/fetch/$s_!Zr89!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb329f779-2dd1-4c93-858b-c53de6212372_1200x628.png 1272w, https://substackcdn.com/image/fetch/$s_!Zr89!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb329f779-2dd1-4c93-858b-c53de6212372_1200x628.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h2>Introduction</h2><p>Most Docker images contain far more than a Python application actually needs at runtime. They include full OS layers with shells, compilers, and utilities that often go completely unused, leading to unnecessarily large images that consume storage and slow deployment pipelines.</p><p><a href="https://github.com/slimtoolkit/slim">SlimToolkit</a> analyzes your container at runtime, identifies which files are actually used, and builds a minimal image with only those dependencies.</p><p>This article walks through slimming a Chainlit LLM chatbot, but the same approach works on any Python container.</p><h2>What Is SlimToolkit?</h2><p><a href="https://github.com/slimtoolkit/slim">SlimToolkit</a> is a command-line tool that strips unused files from a container image <strong>without touching your Dockerfile</strong>. It works in two steps:</p><ol><li><p><strong>Static analysis.</strong> Looks at the image&#8217;s contents without running it.</p></li><li><p><strong>Dynamic analysis.</strong> Runs the image to see which files the app actually uses.</p></li></ol><p>The first step lists everything in the image. The second narrows that list to what the app actually needs. Everything outside the second list gets stripped.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!HvAZ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5860ab73-fd9a-46c0-93a7-6c92c06eb2fc_1080x614.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!HvAZ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5860ab73-fd9a-46c0-93a7-6c92c06eb2fc_1080x614.png 424w, https://substackcdn.com/image/fetch/$s_!HvAZ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5860ab73-fd9a-46c0-93a7-6c92c06eb2fc_1080x614.png 848w, https://substackcdn.com/image/fetch/$s_!HvAZ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5860ab73-fd9a-46c0-93a7-6c92c06eb2fc_1080x614.png 1272w, https://substackcdn.com/image/fetch/$s_!HvAZ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5860ab73-fd9a-46c0-93a7-6c92c06eb2fc_1080x614.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!HvAZ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5860ab73-fd9a-46c0-93a7-6c92c06eb2fc_1080x614.png" width="1080" height="614" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/5860ab73-fd9a-46c0-93a7-6c92c06eb2fc_1080x614.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:614,&quot;width&quot;:1080,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;How SlimToolkit decides what to keep: static analysis lists every file in the image, dynamic analysis narrows it down to the files the app uses, and the difference gets stripped.&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="How SlimToolkit decides what to keep: static analysis lists every file in the image, dynamic analysis narrows it down to the files the app uses, and the difference gets stripped." title="How SlimToolkit decides what to keep: static analysis lists every file in the image, dynamic analysis narrows it down to the files the app uses, and the difference gets stripped." srcset="https://substackcdn.com/image/fetch/$s_!HvAZ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5860ab73-fd9a-46c0-93a7-6c92c06eb2fc_1080x614.png 424w, https://substackcdn.com/image/fetch/$s_!HvAZ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5860ab73-fd9a-46c0-93a7-6c92c06eb2fc_1080x614.png 848w, https://substackcdn.com/image/fetch/$s_!HvAZ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5860ab73-fd9a-46c0-93a7-6c92c06eb2fc_1080x614.png 1272w, https://substackcdn.com/image/fetch/$s_!HvAZ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5860ab73-fd9a-46c0-93a7-6c92c06eb2fc_1080x614.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Install it via the official install script (works on Linux and macOS):</p><pre><code><code>curl -sL https://raw.githubusercontent.com/slimtoolkit/slim/master/scripts/install-slim.sh | sudo -E bash -
</code></code></pre><p>Or with Homebrew on macOS:</p><pre><code><code>brew install docker-slim
</code></code></pre><p>Verify the install:</p><pre><code><code>slim --version
</code></code></pre><p><strong>Output</strong></p><pre><code><code>mint version darwin/arm64|Aurora|1.41.8|latest|latest
</code></code></pre><blockquote><p>&#128187; <strong>Get the Code</strong>: The complete source code and Jupyter notebook for this tutorial are available on <a href="https://github.com/khuyentran1401/codecut-blog/blob/main/slimtoolkit_llm_chatbot">GitHub</a>. Clone it to follow along!</p></blockquote><h2>Build the Chatbot Image</h2><p>To test SlimToolkit, build a small Chainlit chatbot image first. We&#8217;ll write a small Chainlit chatbot, package it with a Dockerfile, and build it.</p><h3>The Chainlit App</h3><p>The chatbot app uses two libraries:</p><ul><li><p><a href="https://github.com/Chainlit/chainlit">Chainlit</a>: an open-source Python framework for building LLM chat UIs; provides the web interface and message handling</p></li><li><p><a href="https://github.com/openai/openai-python">OpenAI SDK</a>: calls <code>gpt-4o-mini</code> for responses</p></li></ul><pre><code><code># app.py
import os
import chainlit as cl
from openai import AsyncOpenAI

client = AsyncOpenAI(api_key=os.environ.get("OPENAI_API_KEY"))


@cl.on_chat_start
async def start():
    cl.user_session.set("messages", [])


@cl.on_message
async def main(message: cl.Message):
    messages = cl.user_session.get("messages")
    messages.append({"role": "user", "content": message.content})

    response = await client.chat.completions.create(
        model="gpt-4o-mini",
        messages=messages,
    )
    reply = response.choices[0].message.content

    messages.append({"role": "assistant", "content": reply})
    cl.user_session.set("messages", messages)

    await cl.Message(content=reply).send()
</code></code></pre><p>Here&#8217;s what happens when someone uses the chatbot:</p><ul><li><p>At import time, <code>AsyncOpenAI</code> reads the <code>OPENAI_API_KEY</code> environment variable and constructs the client</p></li><li><p>When a new chat session opens, <code>@cl.on_chat_start</code> initializes an empty message list for that session</p></li><li><p>When the user types something, <code>@cl.on_message</code> appends it to the history, sends the whole conversation to <code>gpt-4o-mini</code>, stores the reply, and displays it</p></li></ul><p>Before containerizing, let&#8217;s test it locally first. Export your OpenAI key:</p><pre><code><code>export OPENAI_API_KEY=sk-...
</code></code></pre><p>Then run the app:</p><pre><code><code>chainlit run app.py
</code></code></pre><p>Open http://localhost:8000, you should see the Chainlit welcome screen.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!wgnL!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F523eca70-fd4b-4251-8f43-22287915a192_960x572.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!wgnL!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F523eca70-fd4b-4251-8f43-22287915a192_960x572.png 424w, https://substackcdn.com/image/fetch/$s_!wgnL!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F523eca70-fd4b-4251-8f43-22287915a192_960x572.png 848w, https://substackcdn.com/image/fetch/$s_!wgnL!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F523eca70-fd4b-4251-8f43-22287915a192_960x572.png 1272w, https://substackcdn.com/image/fetch/$s_!wgnL!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F523eca70-fd4b-4251-8f43-22287915a192_960x572.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!wgnL!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F523eca70-fd4b-4251-8f43-22287915a192_960x572.png" width="960" height="572" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/523eca70-fd4b-4251-8f43-22287915a192_960x572.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:572,&quot;width&quot;:960,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;The Chainlit welcome screen&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="The Chainlit welcome screen" title="The Chainlit welcome screen" srcset="https://substackcdn.com/image/fetch/$s_!wgnL!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F523eca70-fd4b-4251-8f43-22287915a192_960x572.png 424w, https://substackcdn.com/image/fetch/$s_!wgnL!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F523eca70-fd4b-4251-8f43-22287915a192_960x572.png 848w, https://substackcdn.com/image/fetch/$s_!wgnL!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F523eca70-fd4b-4251-8f43-22287915a192_960x572.png 1272w, https://substackcdn.com/image/fetch/$s_!wgnL!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F523eca70-fd4b-4251-8f43-22287915a192_960x572.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h3>The Dockerfile</h3><p>Pin every dependency in a <code>requirements.txt</code> so the build is reproducible:</p><pre><code><code># requirements.txt
chainlit==2.11.1
openai==2.16.0
</code></code></pre><p>Create a Dockerfile to build the image:</p><pre><code><code># Dockerfile
FROM python:3.11-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY app.py .

EXPOSE 8000
CMD ["chainlit", "run", "app.py", "--host", "0.0.0.0", "--port", "8000", "-h"]
</code></code></pre><p>Here&#8217;s what each step does:</p><ul><li><p>Pulls <code>python:3.11-slim</code> as the base image</p></li><li><p>Installs the pinned dependencies from <code>requirements.txt</code></p></li><li><p>Copies in <code>app.py</code></p></li><li><p>Starts Chainlit on port 8000 when the container runs</p></li></ul><p>Build the image:</p><pre><code><code>docker build -t llm-chatbot:fat .
</code></code></pre><p>Verify the image size:</p><pre><code><code>docker images llm-chatbot:fat
</code></code></pre><p><strong>Output</strong></p><pre><code><code>IMAGE             ID             DISK USAGE   CONTENT SIZE   EXTRA
llm-chatbot:fat   e8de32dd85d4        308MB             0B
</code></code></pre><p>Around 300 MB. Let&#8217;s see if we can shrink it with SlimToolkit.</p><h2>Slim the Image</h2><p>To slim the image, start with the basic command:</p><pre><code><code>slim build \
    --target llm-chatbot:fat \
    --tag llm-chatbot:slim \
    --env OPENAI_API_KEY=$OPENAI_API_KEY
</code></code></pre><p>Each flag plays a role in the slim build:</p><ul><li><p><code>--target llm-chatbot:fat</code>: tells slim which image to minify</p></li><li><p><code>--tag llm-chatbot:slim</code>: names the output image</p></li><li><p><code>--env OPENAI_API_KEY=$OPENAI_API_KEY</code>: sets the env var inside slim&#8217;s probe container so the module-level <code>AsyncOpenAI()</code> can construct at import time</p></li></ul><p>Here&#8217;s what happens when you run the command:</p><ol><li><p>Slim inspects the fat image</p></li><li><p>Starts it in a sandbox</p></li><li><p>Sends a <code>GET /</code> probe and records every file the container touches</p></li><li><p>Builds a slim image containing only those files</p></li></ol><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!8mSa!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3225f7cb-1b0d-444b-8fac-a4b9747ef66f_1080x1146.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!8mSa!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3225f7cb-1b0d-444b-8fac-a4b9747ef66f_1080x1146.png 424w, https://substackcdn.com/image/fetch/$s_!8mSa!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3225f7cb-1b0d-444b-8fac-a4b9747ef66f_1080x1146.png 848w, https://substackcdn.com/image/fetch/$s_!8mSa!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3225f7cb-1b0d-444b-8fac-a4b9747ef66f_1080x1146.png 1272w, https://substackcdn.com/image/fetch/$s_!8mSa!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3225f7cb-1b0d-444b-8fac-a4b9747ef66f_1080x1146.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!8mSa!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3225f7cb-1b0d-444b-8fac-a4b9747ef66f_1080x1146.png" width="1080" height="1146" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/3225f7cb-1b0d-444b-8fac-a4b9747ef66f_1080x1146.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1146,&quot;width&quot;:1080,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;slim build flow: slim build sends GET / into a sandbox where the fat image responds; slim records every file the container touches and builds a slim image from only those files.&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="slim build flow: slim build sends GET / into a sandbox where the fat image responds; slim records every file the container touches and builds a slim image from only those files." title="slim build flow: slim build sends GET / into a sandbox where the fat image responds; slim records every file the container touches and builds a slim image from only those files." srcset="https://substackcdn.com/image/fetch/$s_!8mSa!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3225f7cb-1b0d-444b-8fac-a4b9747ef66f_1080x1146.png 424w, https://substackcdn.com/image/fetch/$s_!8mSa!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3225f7cb-1b0d-444b-8fac-a4b9747ef66f_1080x1146.png 848w, https://substackcdn.com/image/fetch/$s_!8mSa!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3225f7cb-1b0d-444b-8fac-a4b9747ef66f_1080x1146.png 1272w, https://substackcdn.com/image/fetch/$s_!8mSa!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3225f7cb-1b0d-444b-8fac-a4b9747ef66f_1080x1146.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>But the default <code>GET /</code> only loads the chat UI shell. It doesn&#8217;t actually send a chat message, so files needed for the chat path (OpenAI&#8217;s lazy submodules, httpcore, etc.) get stripped. To trace the chat path too, add <code>--continue-after enter</code>:</p><pre><code><code>slim build \
    --target llm-chatbot:fat \
    --tag llm-chatbot:slim \
    --env OPENAI_API_KEY=$OPENAI_API_KEY \
    --continue-after enter
</code></code></pre><p><code>--continue-after enter</code> pauses slim after the default probe so you can open the chatbot in a browser and send a message; this tells slim what files the chat actually needs.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!fwyW!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F030d0693-9468-4712-99b8-2e57151df0fa_921x251.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!fwyW!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F030d0693-9468-4712-99b8-2e57151df0fa_921x251.png 424w, https://substackcdn.com/image/fetch/$s_!fwyW!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F030d0693-9468-4712-99b8-2e57151df0fa_921x251.png 848w, https://substackcdn.com/image/fetch/$s_!fwyW!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F030d0693-9468-4712-99b8-2e57151df0fa_921x251.png 1272w, https://substackcdn.com/image/fetch/$s_!fwyW!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F030d0693-9468-4712-99b8-2e57151df0fa_921x251.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!fwyW!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F030d0693-9468-4712-99b8-2e57151df0fa_921x251.png" width="921" height="251" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/030d0693-9468-4712-99b8-2e57151df0fa_921x251.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:251,&quot;width&quot;:921,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Sending a message to the chatbot&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Sending a message to the chatbot" title="Sending a message to the chatbot" srcset="https://substackcdn.com/image/fetch/$s_!fwyW!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F030d0693-9468-4712-99b8-2e57151df0fa_921x251.png 424w, https://substackcdn.com/image/fetch/$s_!fwyW!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F030d0693-9468-4712-99b8-2e57151df0fa_921x251.png 848w, https://substackcdn.com/image/fetch/$s_!fwyW!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F030d0693-9468-4712-99b8-2e57151df0fa_921x251.png 1272w, https://substackcdn.com/image/fetch/$s_!fwyW!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F030d0693-9468-4712-99b8-2e57151df0fa_921x251.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Now that the chat path runs during the probe, slim keeps openai.resources and httpcore in the final image:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!dULL!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F789da874-06f2-475a-84f1-e8cd6468a4ff_1080x1210.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!dULL!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F789da874-06f2-475a-84f1-e8cd6468a4ff_1080x1210.png 424w, https://substackcdn.com/image/fetch/$s_!dULL!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F789da874-06f2-475a-84f1-e8cd6468a4ff_1080x1210.png 848w, https://substackcdn.com/image/fetch/$s_!dULL!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F789da874-06f2-475a-84f1-e8cd6468a4ff_1080x1210.png 1272w, https://substackcdn.com/image/fetch/$s_!dULL!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F789da874-06f2-475a-84f1-e8cd6468a4ff_1080x1210.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!dULL!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F789da874-06f2-475a-84f1-e8cd6468a4ff_1080x1210.png" width="1080" height="1210" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/789da874-06f2-475a-84f1-e8cd6468a4ff_1080x1210.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1210,&quot;width&quot;:1080,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;slim build flow with --continue-after enter: after the default GET / probe, slim pauses; you send a chat in the browser, which exercises openai.resources and httpcore; slim records those files and builds a slim image that includes them.&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="slim build flow with --continue-after enter: after the default GET / probe, slim pauses; you send a chat in the browser, which exercises openai.resources and httpcore; slim records those files and builds a slim image that includes them." title="slim build flow with --continue-after enter: after the default GET / probe, slim pauses; you send a chat in the browser, which exercises openai.resources and httpcore; slim records those files and builds a slim image that includes them." srcset="https://substackcdn.com/image/fetch/$s_!dULL!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F789da874-06f2-475a-84f1-e8cd6468a4ff_1080x1210.png 424w, https://substackcdn.com/image/fetch/$s_!dULL!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F789da874-06f2-475a-84f1-e8cd6468a4ff_1080x1210.png 848w, https://substackcdn.com/image/fetch/$s_!dULL!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F789da874-06f2-475a-84f1-e8cd6468a4ff_1080x1210.png 1272w, https://substackcdn.com/image/fetch/$s_!dULL!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F789da874-06f2-475a-84f1-e8cd6468a4ff_1080x1210.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Let&#8217;s compare both images:</p><pre><code><code>docker images llm-chatbot
</code></code></pre><p><strong>Output</strong></p><pre><code><code>IMAGE              ID             DISK USAGE   CONTENT SIZE   EXTRA
llm-chatbot:fat    e8de32dd85d4        308MB             0B    U
llm-chatbot:slim   e1f5e9b31e53        123MB             0B
</code></code></pre><p>Nice! We reduced the image size from 308 MB to 123 MB, about a 2.5x reduction.</p><p>Let&#8217;s run the slim image with the environment variable and see what happens.</p><pre><code><code>docker run -p 8000:8000 -e OPENAI_API_KEY=$OPENAI_API_KEY llm-chatbot:slim
</code></code></pre><p><strong>Output</strong></p><pre><code><code>File "/usr/local/lib/python3.11/site-packages/chainlit/server.py", line 181, in get_build_dir
    raise FileNotFoundError(f"{local_target} built UI dir not found")
FileNotFoundError: libs/copilot built UI dir not found
</code></code></pre><p>The error comes from a mismatch between Chainlit and slim:</p><ul><li><p>Chainlit expects <code>chainlit/copilot/</code> to exist when the server starts</p></li><li><p>Even with <code>--continue-after enter</code>, slim removed <code>chainlit/copilot/</code> because the chat you exercised in the browser only loaded the main chat UI. The Copilot widget is a separate Chainlit feature; no file inside <code>chainlit/copilot/</code> was opened during the probe</p></li><li><p>Now Chainlit&#8217;s startup check finds the directory missing and the container crashes</p></li></ul><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!fY20!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb79a89fc-dd25-459b-9bf0-6dbc687f87e5_1080x864.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!fY20!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb79a89fc-dd25-459b-9bf0-6dbc687f87e5_1080x864.png 424w, https://substackcdn.com/image/fetch/$s_!fY20!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb79a89fc-dd25-459b-9bf0-6dbc687f87e5_1080x864.png 848w, https://substackcdn.com/image/fetch/$s_!fY20!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb79a89fc-dd25-459b-9bf0-6dbc687f87e5_1080x864.png 1272w, https://substackcdn.com/image/fetch/$s_!fY20!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb79a89fc-dd25-459b-9bf0-6dbc687f87e5_1080x864.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!fY20!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb79a89fc-dd25-459b-9bf0-6dbc687f87e5_1080x864.png" width="1080" height="864" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b79a89fc-dd25-459b-9bf0-6dbc687f87e5_1080x864.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:864,&quot;width&quot;:1080,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Chainlit vs slim mismatch: the same GET / probe loads chainlit/main (kept by slim) but skips chainlit/copilot (stripped by slim). Chainlit's startup check for the stripped directory then crashes the container.&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Chainlit vs slim mismatch: the same GET / probe loads chainlit/main (kept by slim) but skips chainlit/copilot (stripped by slim). Chainlit's startup check for the stripped directory then crashes the container." title="Chainlit vs slim mismatch: the same GET / probe loads chainlit/main (kept by slim) but skips chainlit/copilot (stripped by slim). Chainlit's startup check for the stripped directory then crashes the container." srcset="https://substackcdn.com/image/fetch/$s_!fY20!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb79a89fc-dd25-459b-9bf0-6dbc687f87e5_1080x864.png 424w, https://substackcdn.com/image/fetch/$s_!fY20!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb79a89fc-dd25-459b-9bf0-6dbc687f87e5_1080x864.png 848w, https://substackcdn.com/image/fetch/$s_!fY20!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb79a89fc-dd25-459b-9bf0-6dbc687f87e5_1080x864.png 1272w, https://substackcdn.com/image/fetch/$s_!fY20!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb79a89fc-dd25-459b-9bf0-6dbc687f87e5_1080x864.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>This is not unique to Chainlit or chat-UI frameworks. Any framework that loads features lazily but checks for them at startup is vulnerable. Django&#8217;s admin, FastAPI apps with multiple routers, ML serving frameworks with embedded UIs, and plugin systems all hit the same problem.</p><h3>Add <code>--include-path</code> for the Chainlit Package</h3><p>The fix is <code>--include-path</code>, which tells SlimToolkit to preserve a path regardless of whether probing touched it. The path that matters is the whole Chainlit package directory, which contains every feature bundle Chainlit ships:</p><pre><code><code>slim build \
    --target llm-chatbot:fat \
    --tag llm-chatbot:slim \
    --include-path /usr/local/lib/python3.11/site-packages/chainlit \
    --continue-after enter \
    --env OPENAI_API_KEY=$OPENAI_API_KEY
</code></code></pre><p>Compare the images again:</p><pre><code><code>docker images llm-chatbot
</code></code></pre><p><strong>Output</strong></p><pre><code><code>IMAGE              ID             DISK USAGE   CONTENT SIZE   EXTRA
llm-chatbot:fat    e8de32dd85d4        308MB             0B    U
llm-chatbot:slim   952b6b44df9f        163MB             0B    U
</code></code></pre><p>Re-run the image to confirm it works:</p><pre><code><code>docker run -p 8000:8000 -e OPENAI_API_KEY=$OPENAI_API_KEY llm-chatbot:slim
</code></code></pre><p>With the <code>chainlit</code> directory preserved, the slim container starts and runs as expected.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!wgnL!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F523eca70-fd4b-4251-8f43-22287915a192_960x572.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!wgnL!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F523eca70-fd4b-4251-8f43-22287915a192_960x572.png 424w, https://substackcdn.com/image/fetch/$s_!wgnL!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F523eca70-fd4b-4251-8f43-22287915a192_960x572.png 848w, https://substackcdn.com/image/fetch/$s_!wgnL!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F523eca70-fd4b-4251-8f43-22287915a192_960x572.png 1272w, https://substackcdn.com/image/fetch/$s_!wgnL!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F523eca70-fd4b-4251-8f43-22287915a192_960x572.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!wgnL!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F523eca70-fd4b-4251-8f43-22287915a192_960x572.png" width="960" height="572" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/523eca70-fd4b-4251-8f43-22287915a192_960x572.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:572,&quot;width&quot;:960,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;The Chainlit chatbot's welcome screen, running from the slim container at 163 MB.&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="The Chainlit chatbot's welcome screen, running from the slim container at 163 MB." title="The Chainlit chatbot's welcome screen, running from the slim container at 163 MB." srcset="https://substackcdn.com/image/fetch/$s_!wgnL!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F523eca70-fd4b-4251-8f43-22287915a192_960x572.png 424w, https://substackcdn.com/image/fetch/$s_!wgnL!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F523eca70-fd4b-4251-8f43-22287915a192_960x572.png 848w, https://substackcdn.com/image/fetch/$s_!wgnL!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F523eca70-fd4b-4251-8f43-22287915a192_960x572.png 1272w, https://substackcdn.com/image/fetch/$s_!wgnL!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F523eca70-fd4b-4251-8f43-22287915a192_960x572.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h2>Inspecting the Result with <code>slim xray</code></h2><p>To see exactly which files slim stripped, run <code>slim xray</code> against both images. It reverse-engineers a built image into a JSON report listing every file, its size, and the layer it came from. Slim always writes its output to <code>slim.report.json</code>, so rename the first report before the second run overwrites it:</p><pre><code><code>slim xray --target llm-chatbot:fat
mv slim.report.json fat.report.json

slim xray --target llm-chatbot:slim
</code></code></pre><p>Each report is several megabytes of JSON, which makes manual comparison painful. To handle that, I packaged the diff and summary steps into <code>compare.sh</code>:</p><pre><code><code>bash compare.sh
</code></code></pre><p>Here are the biggest deletions, grouped by bucket:</p><pre><code><code>Removed                          Size    Bucket
/usr/bin/perl                    3.8 MB  OS cruft
libapt-pkg.so                    2.4 MB  OS cruft (apt)
ensurepip/pip-24.0.whl           2.1 MB  Python build leftover
libdb-5.3.so                     1.8 MB  OS cruft (apt)
/usr/bin/sqv                     1.6 MB  OS cruft (apt&#8217;s PGP verifier)
/usr/bin/bash                    1.4 MB  OS cruft
ensurepip/setuptools-79.0.1.whl  1.3 MB  Python build leftover
multidict/_multidict.so          923 kB  Unused part of aiohttp
jiter/jiter.so                   880 kB  Unused part of openai
pydoc_data/topics.py             775 kB  Python build leftover
aiohttp/_http_writer.so          600 kB  Unused part of aiohttp</code></code></pre><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!GbAY!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6712a248-1eba-4f58-a1f9-430bf66581fc_1080x828.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!GbAY!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6712a248-1eba-4f58-a1f9-430bf66581fc_1080x828.png 424w, https://substackcdn.com/image/fetch/$s_!GbAY!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6712a248-1eba-4f58-a1f9-430bf66581fc_1080x828.png 848w, https://substackcdn.com/image/fetch/$s_!GbAY!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6712a248-1eba-4f58-a1f9-430bf66581fc_1080x828.png 1272w, https://substackcdn.com/image/fetch/$s_!GbAY!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6712a248-1eba-4f58-a1f9-430bf66581fc_1080x828.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!GbAY!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6712a248-1eba-4f58-a1f9-430bf66581fc_1080x828.png" width="1080" height="828" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/6712a248-1eba-4f58-a1f9-430bf66581fc_1080x828.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:828,&quot;width&quot;:1080,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;The biggest deletions, grouped by bucket&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="The biggest deletions, grouped by bucket" title="The biggest deletions, grouped by bucket" srcset="https://substackcdn.com/image/fetch/$s_!GbAY!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6712a248-1eba-4f58-a1f9-430bf66581fc_1080x828.png 424w, https://substackcdn.com/image/fetch/$s_!GbAY!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6712a248-1eba-4f58-a1f9-430bf66581fc_1080x828.png 848w, https://substackcdn.com/image/fetch/$s_!GbAY!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6712a248-1eba-4f58-a1f9-430bf66581fc_1080x828.png 1272w, https://substackcdn.com/image/fetch/$s_!GbAY!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6712a248-1eba-4f58-a1f9-430bf66581fc_1080x828.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The biggest savings come from the Debian base image, not from the Python packages. Of the top 11 deletions:</p><ul><li><p><strong>Five</strong> are base-image binaries: <code>perl</code>, <code>bash</code>, <code>libapt-pkg</code>, <code>libdb-5.3</code>, <code>sqv</code></p></li><li><p><strong>Three</strong> are install-time leftovers: bundled pip and setuptools wheels, plus the <code>pydoc</code> help-text database</p></li><li><p><strong>Three</strong> are write-side or helper modules of runtime libraries the probe didn&#8217;t exercise: <code>aiohttp/_http_writer</code>, <code>multidict</code>, <code>jiter</code></p></li></ul><h2>Final Thoughts</h2><p>A single <code>slim build</code> command took this chatbot from 308 MB to 163 MB. That is one data point on one image. Your numbers will likely look different. The wider the gap between what your image installs and what it actually runs at runtime, the bigger the reduction tends to be. Give it a try on your own images and see what kind of improvement you get.</p><h2>Related Tutorials</h2><ul><li><p><strong><a href="https://codecut.ai/run-github-actions-locally-act/">How to Test GitHub Actions Locally with act</a></strong>: Iterate on a slim-build CI workflow locally before pushing.</p></li><li><p><strong><a href="https://codecut.ai/unregistry-direct-docker-deployment/">Unregistry: Skip the Registry, Deploy Docker Images Directly</a></strong>: Deploy the slimmed image straight to a server, no registry required.</p></li></ul><div><hr></div><blockquote><p>&#128218; <strong>Want to go deeper?</strong> Learning new techniques is the easy part. Knowing how to structure, test, and deploy them is what separates side projects from real work. My book shows you how to build data science projects that actually make it to production. <a href="https://codecut.ai/production-ready-data-science/?utm_source=blog&amp;utm_medium=article&amp;utm_campaign=book_cta_footer">Get the book &#8594;</a></p></blockquote><div><hr></div><p><em>Originally published on <a href="https://codecut.ai/shrink-python-container-slimtoolkit/">CodeCut</a>.</em></p>]]></content:encoded></item><item><title><![CDATA[Never Use yaml.load() in Python. Here’s Why.]]></title><description><![CDATA[Plus stop AI from hallucinating library APIs]]></description><link>https://newsletter.codecut.ai/p/never-use-yamlload-in-python-heres</link><guid isPermaLink="false">https://newsletter.codecut.ai/p/never-use-yamlload-in-python-heres</guid><dc:creator><![CDATA[CodeCut]]></dc:creator><pubDate>Thu, 14 May 2026 16:02:54 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!Zjm0!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb31b7086-55d6-45e3-ab6d-e3117ccd25bc_1080x824.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Grab your coffee. Here are this week&#8217;s highlights.</p><div><hr></div><h2>&#128197; Today&#8217;s Picks</h2><h3>Never Use yaml.load() in Python. Here&#8217;s Why.</h3><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Zjm0!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb31b7086-55d6-45e3-ab6d-e3117ccd25bc_1080x824.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Zjm0!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb31b7086-55d6-45e3-ab6d-e3117ccd25bc_1080x824.png 424w, https://substackcdn.com/image/fetch/$s_!Zjm0!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb31b7086-55d6-45e3-ab6d-e3117ccd25bc_1080x824.png 848w, https://substackcdn.com/image/fetch/$s_!Zjm0!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb31b7086-55d6-45e3-ab6d-e3117ccd25bc_1080x824.png 1272w, https://substackcdn.com/image/fetch/$s_!Zjm0!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb31b7086-55d6-45e3-ab6d-e3117ccd25bc_1080x824.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Zjm0!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb31b7086-55d6-45e3-ab6d-e3117ccd25bc_1080x824.png" width="1080" height="824" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b31b7086-55d6-45e3-ab6d-e3117ccd25bc_1080x824.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:824,&quot;width&quot;:1080,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Code example: Never Use yaml.load() in Python. Here's Why.&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Code example: Never Use yaml.load() in Python. Here's Why." title="Code example: Never Use yaml.load() in Python. Here's Why." srcset="https://substackcdn.com/image/fetch/$s_!Zjm0!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb31b7086-55d6-45e3-ab6d-e3117ccd25bc_1080x824.png 424w, https://substackcdn.com/image/fetch/$s_!Zjm0!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb31b7086-55d6-45e3-ab6d-e3117ccd25bc_1080x824.png 848w, https://substackcdn.com/image/fetch/$s_!Zjm0!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb31b7086-55d6-45e3-ab6d-e3117ccd25bc_1080x824.png 1272w, https://substackcdn.com/image/fetch/$s_!Zjm0!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb31b7086-55d6-45e3-ab6d-e3117ccd25bc_1080x824.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h4>Problem</h4><p>Has your AI assistant ever suggested yaml.load() to parse a config file?</p><p>It&#8217;s an easy mistake: yaml.load() looks like a clean way to turn YAML into a Python dict, and AI tools generate it freely because it appears in plenty of legacy code.</p><p>But <code>yaml.load()</code> can do more than parse text. It can execute Python objects embedded in the YAML, meaning anyone who controls that file could run shell commands on your machine.</p><h4>Solution</h4><p>Use <code>yaml.safe_load()</code> instead. It only supports standard YAML types like mappings, lists, strings, numbers, booleans, and null, and rejects anything that tries to execute code.</p><p>To catch unsafe <code>yaml.load()</code> calls automatically, scan your codebase with <strong><a href="https://github.com/PyCQA/bandit">Bandit</a></strong>.</p><blockquote><p>&#128214; <a href="https://codecut.ai/bandit-ai-generated-python-security/?utm_source=Substack&amp;utm_medium=email&amp;utm_campaign=article_bandit-ai-generated-python-security">View Full Article</a></p></blockquote><div><hr></div><h3>Context7: Stop AI Agents from Generating Deprecated Syntax</h3><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Bagp!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6ac6eb27-47f7-401b-ad36-87ee90957020_1080x1298.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Bagp!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6ac6eb27-47f7-401b-ad36-87ee90957020_1080x1298.png 424w, https://substackcdn.com/image/fetch/$s_!Bagp!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6ac6eb27-47f7-401b-ad36-87ee90957020_1080x1298.png 848w, https://substackcdn.com/image/fetch/$s_!Bagp!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6ac6eb27-47f7-401b-ad36-87ee90957020_1080x1298.png 1272w, https://substackcdn.com/image/fetch/$s_!Bagp!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6ac6eb27-47f7-401b-ad36-87ee90957020_1080x1298.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Bagp!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6ac6eb27-47f7-401b-ad36-87ee90957020_1080x1298.png" width="1080" height="1298" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/6ac6eb27-47f7-401b-ad36-87ee90957020_1080x1298.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1298,&quot;width&quot;:1080,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Code example: Context7: Stop AI Agents from Generating Deprecated Syntax&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Code example: Context7: Stop AI Agents from Generating Deprecated Syntax" title="Code example: Context7: Stop AI Agents from Generating Deprecated Syntax" srcset="https://substackcdn.com/image/fetch/$s_!Bagp!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6ac6eb27-47f7-401b-ad36-87ee90957020_1080x1298.png 424w, https://substackcdn.com/image/fetch/$s_!Bagp!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6ac6eb27-47f7-401b-ad36-87ee90957020_1080x1298.png 848w, https://substackcdn.com/image/fetch/$s_!Bagp!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6ac6eb27-47f7-401b-ad36-87ee90957020_1080x1298.png 1272w, https://substackcdn.com/image/fetch/$s_!Bagp!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6ac6eb27-47f7-401b-ad36-87ee90957020_1080x1298.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h4>Problem</h4><p>AI coding assistants often generate code using training data that may already be outdated.</p><p>That means if you ask for Polars 1.x code, the model may still generate deprecated 0.x APIs from older training data.</p><h4>Solution</h4><p><strong><a href="https://github.com/upstash/context7">Context7</a></strong> solves this by pulling the latest, version-specific library docs directly into the prompt before the assistant writes any code. That way, it generates code against the API that actually exists today.</p><p>Other capabilities:</p><ul><li><p>One command sets it up across Cursor, Claude Code, Copilot, and 30+ clients</p></li><li><p>Trigger with <code>use context7</code> in any prompt</p></li><li><p>Runs as either an MCP server or a CLI + skill, so it works with or without MCP support</p></li><li><p>Automatically loads version-matched docs like &#8220;Polars 1.0</p></li></ul><div><hr></div><h2>&#9749;&#65039; Weekly Finds</h2><p><strong><a href="https://github.com/beartype/beartype">beartype</a></strong> <em>[Code Quality]</em> - Near-real-time pure-Python runtime type-checker. Decorate a function and catch type violations the moment they happen, not after a stack trace.</p><p><strong><a href="https://github.com/jendrikseipp/vulture">vulture</a></strong> <em>[Code Quality]</em> - Find dead Python code. Scans your project for unused functions, classes, imports, and variables so you can safely delete them.</p><p><strong><a href="https://github.com/asottile/pyupgrade">pyupgrade</a></strong> <em>[Code Quality]</em> - A tool (and pre-commit hook) that automatically upgrades Python syntax to newer language versions. Drop f-strings, dict literals, and modern type hints in one sweep.</p><div><hr></div><h3>&#128172; Rate Your Experience</h3><p>How would you rate your newsletter experience? <a href="https://r7yuob192zv.typeform.com/to/VODwGYMW">Share your feedback &#8594;</a></p><div><hr></div><h3>&#128269; Explore More on CodeCut</h3><ul><li><p><a href="https://codecut.ai/tool-selector/?utm_source=Substack&amp;utm_medium=email&amp;utm_campaign=article_tool-selector">Tool Selector</a> - Discover 70+ Python tools for AI and data science</p></li><li><p><a href="https://codecut.ai/production-ready-data-science/?utm_source=Substack&amp;utm_medium=email&amp;utm_campaign=article_production-ready-data-science">Production Ready Data Science</a> - A practical book for taking projects from prototype to production</p></li></ul>]]></content:encoded></item><item><title><![CDATA[Bandit: Catch Insecure Patterns in AI-Generated Python Code]]></title><description><![CDATA[Plus run LLMs 70% faster with llama.cpp]]></description><link>https://newsletter.codecut.ai/p/bandit-catch-insecure-patterns-in</link><guid isPermaLink="false">https://newsletter.codecut.ai/p/bandit-catch-insecure-patterns-in</guid><dc:creator><![CDATA[CodeCut]]></dc:creator><pubDate>Tue, 12 May 2026 16:01:35 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!GRkv!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbedf982c-4840-457a-ae19-f11ed4f2b053_1024x753.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Grab your coffee. Here are this week&#8217;s highlights.</p><div><hr></div><h2>&#128197; Today&#8217;s Picks</h2><h3>Bandit: Catch Insecure Patterns in AI-Generated Python Code</h3><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!GRkv!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbedf982c-4840-457a-ae19-f11ed4f2b053_1024x753.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!GRkv!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbedf982c-4840-457a-ae19-f11ed4f2b053_1024x753.png 424w, https://substackcdn.com/image/fetch/$s_!GRkv!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbedf982c-4840-457a-ae19-f11ed4f2b053_1024x753.png 848w, https://substackcdn.com/image/fetch/$s_!GRkv!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbedf982c-4840-457a-ae19-f11ed4f2b053_1024x753.png 1272w, https://substackcdn.com/image/fetch/$s_!GRkv!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbedf982c-4840-457a-ae19-f11ed4f2b053_1024x753.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!GRkv!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbedf982c-4840-457a-ae19-f11ed4f2b053_1024x753.png" width="1024" height="753" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/bedf982c-4840-457a-ae19-f11ed4f2b053_1024x753.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:753,&quot;width&quot;:1024,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Code example: Bandit: Catch Insecure Patterns in AI-Generated Python Code&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Code example: Bandit: Catch Insecure Patterns in AI-Generated Python Code" title="Code example: Bandit: Catch Insecure Patterns in AI-Generated Python Code" srcset="https://substackcdn.com/image/fetch/$s_!GRkv!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbedf982c-4840-457a-ae19-f11ed4f2b053_1024x753.png 424w, https://substackcdn.com/image/fetch/$s_!GRkv!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbedf982c-4840-457a-ae19-f11ed4f2b053_1024x753.png 848w, https://substackcdn.com/image/fetch/$s_!GRkv!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbedf982c-4840-457a-ae19-f11ed4f2b053_1024x753.png 1272w, https://substackcdn.com/image/fetch/$s_!GRkv!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbedf982c-4840-457a-ae19-f11ed4f2b053_1024x753.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h4>Problem</h4><p>LLMs have become extremely good at generating syntactically valid Python, but security quality has barely improved.</p><p>Veracode&#8217;s Spring 2026 GenAI Code Security Report found that security pass rates have remained stuck near 55% since 2024.</p><p>That gap exists because models learn from public code full of insecure patterns and reproduce them when prompted.</p><p>At the same time, reviewers usually verify whether code works, not whether it introduces vulnerabilities.</p><h4>Solution</h4><p><strong><a href="https://github.com/PyCQA/bandit">Bandit</a></strong> is a static analyzer for Python that identifies insecure patterns by matching code against 60+ Common Weakness Enumeration (CWE) rules.</p><p>Key capabilities:</p><ul><li><p>Flags hardcoded secrets, weak hashes (MD5, SHA1), and unsafe deserialization</p></li><li><p>Detects SQL string concatenation, <code>eval</code>, and <code>exec</code> injection risks</p></li><li><p>Catches empty <code>except</code> blocks and unpinned Hugging Face downloads</p></li><li><p>Integrates with pre-commit hooks and GitHub Actions out of the box</p></li><li><p>Maps every finding to a CWE ID so issues stay auditable</p></li></ul><blockquote><p>&#128214; <a href="https://codecut.ai/bandit-ai-generated-python-security/?utm_source=Substack&amp;utm_medium=email&amp;utm_campaign=article_bandit-ai-generated-python-security">View Full Article</a></p></blockquote><div><hr></div><h3>llama.cpp: Run LLMs 70% Faster Than Ollama on the Same GPU</h3><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!wSdt!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9492d3c7-9010-4fec-9062-d44b85ac96d7_1080x738.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!wSdt!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9492d3c7-9010-4fec-9062-d44b85ac96d7_1080x738.png 424w, https://substackcdn.com/image/fetch/$s_!wSdt!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9492d3c7-9010-4fec-9062-d44b85ac96d7_1080x738.png 848w, https://substackcdn.com/image/fetch/$s_!wSdt!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9492d3c7-9010-4fec-9062-d44b85ac96d7_1080x738.png 1272w, https://substackcdn.com/image/fetch/$s_!wSdt!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9492d3c7-9010-4fec-9062-d44b85ac96d7_1080x738.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!wSdt!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9492d3c7-9010-4fec-9062-d44b85ac96d7_1080x738.png" width="1080" height="738" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/9492d3c7-9010-4fec-9062-d44b85ac96d7_1080x738.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:738,&quot;width&quot;:1080,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Code example: llama.cpp: Run LLMs 70% Faster Than Ollama on the Same GPU&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Code example: llama.cpp: Run LLMs 70% Faster Than Ollama on the Same GPU" title="Code example: llama.cpp: Run LLMs 70% Faster Than Ollama on the Same GPU" srcset="https://substackcdn.com/image/fetch/$s_!wSdt!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9492d3c7-9010-4fec-9062-d44b85ac96d7_1080x738.png 424w, https://substackcdn.com/image/fetch/$s_!wSdt!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9492d3c7-9010-4fec-9062-d44b85ac96d7_1080x738.png 848w, https://substackcdn.com/image/fetch/$s_!wSdt!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9492d3c7-9010-4fec-9062-d44b85ac96d7_1080x738.png 1272w, https://substackcdn.com/image/fetch/$s_!wSdt!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9492d3c7-9010-4fec-9062-d44b85ac96d7_1080x738.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h4>Problem</h4><p>An r/LocalLLaMA benchmark on Qwen-3 Coder 32B (FP16) measured Ollama at 30 tokens/sec and llama.cpp at 52 tokens/sec on the same model and hardware.</p><p>Ollama is built on top of llama.cpp, so the core inference engine is identical. The performance gap comes from configuration.</p><h4>Solution</h4><p>Ollama ships with conservative defaults designed to work reliably across many systems. <strong><a href="https://github.com/ggml-org/llama.cpp">llama.cpp</a></strong> exposes the low-level tuning flags so you can optimize specifically for your hardware.</p><p>Key flags to tune:</p><ul><li><p>GPU offload (<code>-ngl 99</code>): put the entire model on your GPU instead of partial offload</p></li><li><p>Throughput (<code>--batch-size</code>): bigger means faster prompts but more GPU memory</p></li><li><p>Context length (<code>--cache-type-k q4</code>): use less GPU memory per token so longer prompts fit</p></li><li><p>Generation speed (<code>-md</code>): run a small helper model alongside your main model for 2-3x faster generation</p></li></ul><blockquote><p>&#128214; <a href="https://www.reddit.com/r/LocalLLaMA/comments/1q64f26/llamacpp_vs_ollama_70_higher_code_generation/">View Source</a></p></blockquote><div><hr></div><h2>&#9749;&#65039; Weekly Finds</h2><p><strong><a href="https://github.com/plasma-umass/scalene">scalene</a></strong> <em>[Code Quality]</em> - A high-performance CPU, GPU, and memory profiler for Python with AI-powered optimization proposals.</p><p><strong><a href="https://github.com/pymc-labs/pymc-marketing">pymc-marketing</a></strong> <em>[Data Analysis]</em> - Bayesian marketing toolbox in PyMC. Includes Media Mix Modeling (MMM), customer lifetime value (CLV), and buy-till-you-die (BTYD) models.</p><p><strong><a href="https://github.com/antoniorodr/cronboard">cronboard</a></strong> <em>[Workflow Automation]</em> - A terminal-based dashboard for managing cron jobs locally and on remote servers. Add, edit, pause, resume, and search jobs from the terminal.</p><div><hr></div><h3>&#128172; Rate Your Experience</h3><p>How would you rate your newsletter experience? <a href="https://r7yuob192zv.typeform.com/to/VODwGYMW">Share your feedback &#8594;</a></p><div><hr></div><h3>&#128269; Explore More on CodeCut</h3><ul><li><p><a href="https://codecut.ai/tool-selector/?utm_source=Substack&amp;utm_medium=email&amp;utm_campaign=article_tool-selector">Tool Selector</a> - Discover 70+ Python tools for AI and data science</p></li><li><p><a href="https://codecut.ai/production-ready-data-science/?utm_source=Substack&amp;utm_medium=email&amp;utm_campaign=article_production-ready-data-science">Production Ready Data Science</a> - A practical book for taking projects from prototype to production</p></li></ul>]]></content:encoded></item><item><title><![CDATA[Slim: Shrink Docker Images by 30x with One Command]]></title><description><![CDATA[Plus run vectorized expressions on Polars lists]]></description><link>https://newsletter.codecut.ai/p/slim-shrink-docker-images-by-30x</link><guid isPermaLink="false">https://newsletter.codecut.ai/p/slim-shrink-docker-images-by-30x</guid><dc:creator><![CDATA[CodeCut]]></dc:creator><pubDate>Thu, 07 May 2026 16:01:20 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!pUW-!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2ce79fb5-4672-4bc7-ae0a-29087fb62def_1080x1244.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Grab your coffee. Here are this week&#8217;s highlights.</p><div><hr></div><h2>&#128197; Today&#8217;s Picks</h2><h3>Slim: Shrink Docker Images by 30x with One Command</h3><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!pUW-!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2ce79fb5-4672-4bc7-ae0a-29087fb62def_1080x1244.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!pUW-!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2ce79fb5-4672-4bc7-ae0a-29087fb62def_1080x1244.png 424w, https://substackcdn.com/image/fetch/$s_!pUW-!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2ce79fb5-4672-4bc7-ae0a-29087fb62def_1080x1244.png 848w, https://substackcdn.com/image/fetch/$s_!pUW-!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2ce79fb5-4672-4bc7-ae0a-29087fb62def_1080x1244.png 1272w, https://substackcdn.com/image/fetch/$s_!pUW-!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2ce79fb5-4672-4bc7-ae0a-29087fb62def_1080x1244.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!pUW-!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2ce79fb5-4672-4bc7-ae0a-29087fb62def_1080x1244.png" width="1080" height="1244" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/2ce79fb5-4672-4bc7-ae0a-29087fb62def_1080x1244.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1244,&quot;width&quot;:1080,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Code example: Slim: Shrink Docker Images by 30x with One Command&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Code example: Slim: Shrink Docker Images by 30x with One Command" title="Code example: Slim: Shrink Docker Images by 30x with One Command" srcset="https://substackcdn.com/image/fetch/$s_!pUW-!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2ce79fb5-4672-4bc7-ae0a-29087fb62def_1080x1244.png 424w, https://substackcdn.com/image/fetch/$s_!pUW-!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2ce79fb5-4672-4bc7-ae0a-29087fb62def_1080x1244.png 848w, https://substackcdn.com/image/fetch/$s_!pUW-!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2ce79fb5-4672-4bc7-ae0a-29087fb62def_1080x1244.png 1272w, https://substackcdn.com/image/fetch/$s_!pUW-!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2ce79fb5-4672-4bc7-ae0a-29087fb62def_1080x1244.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h4>Problem</h4><p>Docker images include the entire OS layer. For a simple Python app, this is unnecessary because it never touches the shells, compilers, and system utilities bundled inside.</p><p>This inflates images to hundreds of megabytes, wasting storage and adding time to every deploy.</p><h4>Solution</h4><p><strong><a href="https://github.com/slimtoolkit/slim">Slim</a></strong> automatically analyzes your container at runtime to identify which files are actually used, then builds a minimal image with only essential components.</p><p>Slim works alongside Docker, not instead of it:</p><ul><li><p>Step 1: Build your image with <code>docker build</code></p></li><li><p>Step 2: Minify with <code>slim build your-image</code></p></li><li><p>Step 3: Push the <code>.slim</code> image to your registry</p></li><li><p>Your Dockerfile and workflow stay the same</p></li></ul><blockquote></blockquote><div><hr></div><h3>Polars: Vectorize List Column Transformations with list.eval</h3><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!tU4K!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F80dab8d9-35ce-4211-979a-7e9b98aebda2_683x695.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!tU4K!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F80dab8d9-35ce-4211-979a-7e9b98aebda2_683x695.png 424w, https://substackcdn.com/image/fetch/$s_!tU4K!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F80dab8d9-35ce-4211-979a-7e9b98aebda2_683x695.png 848w, https://substackcdn.com/image/fetch/$s_!tU4K!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F80dab8d9-35ce-4211-979a-7e9b98aebda2_683x695.png 1272w, https://substackcdn.com/image/fetch/$s_!tU4K!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F80dab8d9-35ce-4211-979a-7e9b98aebda2_683x695.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!tU4K!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F80dab8d9-35ce-4211-979a-7e9b98aebda2_683x695.png" width="683" height="695" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/80dab8d9-35ce-4211-979a-7e9b98aebda2_683x695.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:695,&quot;width&quot;:683,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Code example: Polars: Vectorize List Column Transformations with list.eval&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Code example: Polars: Vectorize List Column Transformations with list.eval" title="Code example: Polars: Vectorize List Column Transformations with list.eval" srcset="https://substackcdn.com/image/fetch/$s_!tU4K!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F80dab8d9-35ce-4211-979a-7e9b98aebda2_683x695.png 424w, https://substackcdn.com/image/fetch/$s_!tU4K!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F80dab8d9-35ce-4211-979a-7e9b98aebda2_683x695.png 848w, https://substackcdn.com/image/fetch/$s_!tU4K!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F80dab8d9-35ce-4211-979a-7e9b98aebda2_683x695.png 1272w, https://substackcdn.com/image/fetch/$s_!tU4K!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F80dab8d9-35ce-4211-979a-7e9b98aebda2_683x695.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h4>Problem</h4><p>When working with list columns, most people reach for <code>apply</code> with a Python lambda that loops over every list row by row.</p><p>That approach breaks vectorization, so performance quickly degrades as the dataset grows.</p><h4>Solution</h4><p><strong><a href="https://github.com/pola-rs/polars">Polars</a></strong> solves this with <code>list.eval</code>, which runs a full expression against each list using <code>pl.element()</code> and stays fully vectorized.</p><p>Key benefits:</p><ul><li><p>Vectorized per-element transformations without Python loops</p></li><li><p>Support for a wide range of expressions, including aggregations like <code>max</code>, <code>mean</code>, and <code>sum</code></p></li><li><p>Composable with other Polars expressions for clean, readable pipelines</p></li></ul><blockquote><p>&#128214; <a href="https://codecut.ai/pandas-vs-polars-vs-duckdb-comparison/?utm_source=Substack&amp;utm_medium=email&amp;utm_campaign=article_pandas-vs-polars-vs-duckdb-comparison">View Full Article</a></p></blockquote><div><hr></div><h2>&#9749;&#65039; Weekly Finds</h2><p><strong><a href="https://github.com/asottile/pyupgrade">pyupgrade</a></strong> <em>[Code Quality]</em> - A tool (and pre-commit hook) to automatically upgrade syntax for newer versions of Python.</p><p><strong><a href="https://github.com/bgreenwell/doxx">doxx</a></strong> <em>[Python Utils]</em> - Expose the contents of .docx files without leaving your terminal. Fast, safe, and smart, with no Office required.</p><p><strong><a href="https://github.com/bruin-data/ingestr">ingestr</a></strong> <em>[Data Processing]</em> - ingestr is a CLI tool to copy data between any databases with a single command seamlessly.</p><div><hr></div><h3>&#128172; Rate Your Experience</h3><p>How would you rate your newsletter experience? <a href="https://r7yuob192zv.typeform.com/to/VODwGYMW">Share your feedback &#8594;</a></p><div><hr></div><h3>&#128269; Explore More on CodeCut</h3><ul><li><p><a href="https://codecut.ai/tool-selector/?utm_source=Substack&amp;utm_medium=email&amp;utm_campaign=article_tool-selector">Tool Selector</a> - Discover 70+ Python tools for AI and data science</p></li><li><p><a href="https://codecut.ai/production-ready-data-science/?utm_source=Substack&amp;utm_medium=email&amp;utm_campaign=article_production-ready-data-science">Production Ready Data Science</a> - A practical book for taking projects from prototype to production</p></li></ul>]]></content:encoded></item><item><title><![CDATA[Mem0: Turn AI Conversations into a Knowledge Graph]]></title><description><![CDATA[Plus Git-style versioning for cloud storage]]></description><link>https://newsletter.codecut.ai/p/mem0-turn-ai-conversations-into-a</link><guid isPermaLink="false">https://newsletter.codecut.ai/p/mem0-turn-ai-conversations-into-a</guid><dc:creator><![CDATA[CodeCut]]></dc:creator><pubDate>Tue, 05 May 2026 16:01:18 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!uyGL!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F42ac10a5-65f5-4a7b-9b8a-d2ba03fc160b_1080x1328.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Grab your coffee. Here are this week&#8217;s highlights.</p><div><hr></div><h2>&#128197; Today&#8217;s Picks</h2><h3>Mem0: Turn AI Conversations into a Knowledge Graph</h3><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!uyGL!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F42ac10a5-65f5-4a7b-9b8a-d2ba03fc160b_1080x1328.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!uyGL!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F42ac10a5-65f5-4a7b-9b8a-d2ba03fc160b_1080x1328.png 424w, https://substackcdn.com/image/fetch/$s_!uyGL!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F42ac10a5-65f5-4a7b-9b8a-d2ba03fc160b_1080x1328.png 848w, https://substackcdn.com/image/fetch/$s_!uyGL!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F42ac10a5-65f5-4a7b-9b8a-d2ba03fc160b_1080x1328.png 1272w, https://substackcdn.com/image/fetch/$s_!uyGL!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F42ac10a5-65f5-4a7b-9b8a-d2ba03fc160b_1080x1328.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!uyGL!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F42ac10a5-65f5-4a7b-9b8a-d2ba03fc160b_1080x1328.png" width="1080" height="1328" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/42ac10a5-65f5-4a7b-9b8a-d2ba03fc160b_1080x1328.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1328,&quot;width&quot;:1080,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Code example: Mem0: Turn AI Conversations into a Knowledge Graph&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Code example: Mem0: Turn AI Conversations into a Knowledge Graph" title="Code example: Mem0: Turn AI Conversations into a Knowledge Graph" srcset="https://substackcdn.com/image/fetch/$s_!uyGL!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F42ac10a5-65f5-4a7b-9b8a-d2ba03fc160b_1080x1328.png 424w, https://substackcdn.com/image/fetch/$s_!uyGL!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F42ac10a5-65f5-4a7b-9b8a-d2ba03fc160b_1080x1328.png 848w, https://substackcdn.com/image/fetch/$s_!uyGL!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F42ac10a5-65f5-4a7b-9b8a-d2ba03fc160b_1080x1328.png 1272w, https://substackcdn.com/image/fetch/$s_!uyGL!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F42ac10a5-65f5-4a7b-9b8a-d2ba03fc160b_1080x1328.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h4>Problem</h4><p>Vector databases store each memory as an independent embedding. You can search for similar text, but they cannot link entities across facts or follow relationships between them.</p><p>Store &#8220;Alice and Bob are collaborating&#8221; and &#8220;Alice leads payments&#8221; separately, and a vector store cannot recognize that both facts refer to the same Alice.</p><h4>Solution</h4><p><strong><a href="https://github.com/mem0ai/mem0">Mem0</a></strong> fixes this by extracting entities and relationships from each conversation.</p><p>When you store &#8220;Alice and Bob are collaborating&#8221; and &#8220;Alice leads payments,&#8221; Mem0 recognizes that both facts refer to the same Alice and links them in a knowledge graph.</p><p>Key capabilities:</p><ul><li><p>Reads raw conversations and identifies the entities for you</p></li><li><p>Relationship tracking between people, projects, and concepts</p></li><li><p>A memory layer that grows smarter with every interaction</p></li></ul><blockquote><p>&#129514; <a href="https://bit.ly/4eXJRYY">Run code</a></p></blockquote><div><hr></div><h3>lakeFS: Bring Git-Style Branches and Commits to Cloud Storage</h3><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!jBzo!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb307b68b-3ae0-4497-94ab-f933d35df7ce_626x619.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!jBzo!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb307b68b-3ae0-4497-94ab-f933d35df7ce_626x619.png 424w, https://substackcdn.com/image/fetch/$s_!jBzo!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb307b68b-3ae0-4497-94ab-f933d35df7ce_626x619.png 848w, https://substackcdn.com/image/fetch/$s_!jBzo!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb307b68b-3ae0-4497-94ab-f933d35df7ce_626x619.png 1272w, https://substackcdn.com/image/fetch/$s_!jBzo!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb307b68b-3ae0-4497-94ab-f933d35df7ce_626x619.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!jBzo!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb307b68b-3ae0-4497-94ab-f933d35df7ce_626x619.png" width="626" height="619" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b307b68b-3ae0-4497-94ab-f933d35df7ce_626x619.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:619,&quot;width&quot;:626,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Code example: lakeFS: Bring Git-Style Branches and Commits to Cloud Storage&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Code example: lakeFS: Bring Git-Style Branches and Commits to Cloud Storage" title="Code example: lakeFS: Bring Git-Style Branches and Commits to Cloud Storage" srcset="https://substackcdn.com/image/fetch/$s_!jBzo!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb307b68b-3ae0-4497-94ab-f933d35df7ce_626x619.png 424w, https://substackcdn.com/image/fetch/$s_!jBzo!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb307b68b-3ae0-4497-94ab-f933d35df7ce_626x619.png 848w, https://substackcdn.com/image/fetch/$s_!jBzo!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb307b68b-3ae0-4497-94ab-f933d35df7ce_626x619.png 1272w, https://substackcdn.com/image/fetch/$s_!jBzo!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb307b68b-3ae0-4497-94ab-f933d35df7ce_626x619.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h4>Problem</h4><p>Cloud storage like S3, Google Cloud Storage, and Azure Blob has no concept of versions, branches, or commits like Git does.</p><p>As a result, testing changes can mean copying terabytes of data, and fixing mistakes requires tedious file-by-file restores.</p><h4>Solution</h4><p><strong><a href="https://github.com/treeverse/lakeFS">lakeFS</a></strong> turns your cloud storage into a Git-like repository. You get branches, commits, merges, and instant rollback, all without copying data.</p><blockquote></blockquote><div><hr></div><h2>&#9749;&#65039; Weekly Finds</h2><p><strong><a href="https://github.com/unslothai/unsloth">unsloth</a></strong> <em>[LLM]</em> - Fine-tune LLMs 5x faster with 60% less memory using QLoRA, no accuracy loss.</p><p><strong><a href="https://github.com/upstash/context7">context7</a></strong> <em>[RAG]</em> - Pulls up-to-date, version-specific docs and code examples straight into your LLM prompts and AI code editors.</p><p><strong><a href="https://github.com/yamadashy/repomix">Repomix</a></strong> <em>[LLM]</em> - Packs your entire repository into a single AI-friendly file you can paste into Claude, ChatGPT, or Gemini.</p><div><hr></div><h3>&#128172; Rate Your Experience</h3><p>How would you rate your newsletter experience? <a href="https://r7yuob192zv.typeform.com/to/VODwGYMW">Share your feedback &#8594;</a></p><div><hr></div><h3>&#128269; Explore More on CodeCut</h3><ul><li><p><a href="https://codecut.ai/tool-selector/?utm_source=Substack&amp;utm_medium=email&amp;utm_campaign=article_tool-selector">Tool Selector</a> - Discover 70+ Python tools for AI and data science</p></li><li><p><a href="https://codecut.ai/production-ready-data-science/?utm_source=Substack&amp;utm_medium=email&amp;utm_campaign=article_production-ready-data-science">Production Ready Data Science</a> - A practical book for taking projects from prototype to production</p></li></ul>]]></content:encoded></item><item><title><![CDATA[Generate Time-Sortable IDs with Python 3.14’s UUID v7]]></title><description><![CDATA[Plus auto-infer Python types with Pyrefly]]></description><link>https://newsletter.codecut.ai/p/generate-time-sortable-ids-with-python</link><guid isPermaLink="false">https://newsletter.codecut.ai/p/generate-time-sortable-ids-with-python</guid><dc:creator><![CDATA[CodeCut]]></dc:creator><pubDate>Thu, 30 Apr 2026 16:01:47 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!AC9Z!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F174270cf-93f4-46be-8cfe-ec631907a67b_474x535.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Grab your coffee. Here are this week&#8217;s highlights.</p><div><hr></div><h2>&#128197; Today&#8217;s Picks</h2><h3>Generate Time-Sortable IDs with Python 3.14&#8217;s UUID v7</h3><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!AC9Z!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F174270cf-93f4-46be-8cfe-ec631907a67b_474x535.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!AC9Z!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F174270cf-93f4-46be-8cfe-ec631907a67b_474x535.png 424w, https://substackcdn.com/image/fetch/$s_!AC9Z!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F174270cf-93f4-46be-8cfe-ec631907a67b_474x535.png 848w, https://substackcdn.com/image/fetch/$s_!AC9Z!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F174270cf-93f4-46be-8cfe-ec631907a67b_474x535.png 1272w, https://substackcdn.com/image/fetch/$s_!AC9Z!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F174270cf-93f4-46be-8cfe-ec631907a67b_474x535.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!AC9Z!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F174270cf-93f4-46be-8cfe-ec631907a67b_474x535.png" width="474" height="535" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/174270cf-93f4-46be-8cfe-ec631907a67b_474x535.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:535,&quot;width&quot;:474,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Code example: Generate Time-Sortable IDs with Python 3.14's UUID v7&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Code example: Generate Time-Sortable IDs with Python 3.14's UUID v7" title="Code example: Generate Time-Sortable IDs with Python 3.14's UUID v7" srcset="https://substackcdn.com/image/fetch/$s_!AC9Z!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F174270cf-93f4-46be-8cfe-ec631907a67b_474x535.png 424w, https://substackcdn.com/image/fetch/$s_!AC9Z!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F174270cf-93f4-46be-8cfe-ec631907a67b_474x535.png 848w, https://substackcdn.com/image/fetch/$s_!AC9Z!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F174270cf-93f4-46be-8cfe-ec631907a67b_474x535.png 1272w, https://substackcdn.com/image/fetch/$s_!AC9Z!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F174270cf-93f4-46be-8cfe-ec631907a67b_474x535.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h4>Problem</h4><p>UUID4 generates purely random identifiers that lack chronological ordering.</p><p>Without embedded timestamps, you need separate timestamp fields and custom sorting logic to organize records by creation time.</p><h4>Solution</h4><p>Python 3.14 introduces UUID version 7 with built-in timestamp ordering.</p><p>Key features:</p><ul><li><p>Determine creation order by comparing two UUIDs directly</p></li><li><p>Retrieve exact creation time by extracting the embedded timestamp</p></li></ul><blockquote></blockquote><div><hr></div><h3>Pyrefly: Automatic Type Inference for Python</h3><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!2Ucg!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F812a4bae-301f-4fb8-adcc-43769828f2c2_662x446.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!2Ucg!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F812a4bae-301f-4fb8-adcc-43769828f2c2_662x446.png 424w, https://substackcdn.com/image/fetch/$s_!2Ucg!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F812a4bae-301f-4fb8-adcc-43769828f2c2_662x446.png 848w, https://substackcdn.com/image/fetch/$s_!2Ucg!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F812a4bae-301f-4fb8-adcc-43769828f2c2_662x446.png 1272w, https://substackcdn.com/image/fetch/$s_!2Ucg!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F812a4bae-301f-4fb8-adcc-43769828f2c2_662x446.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!2Ucg!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F812a4bae-301f-4fb8-adcc-43769828f2c2_662x446.png" width="662" height="446" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/812a4bae-301f-4fb8-adcc-43769828f2c2_662x446.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:446,&quot;width&quot;:662,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Code example: Pyrefly: Automatic Type Inference for Python&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Code example: Pyrefly: Automatic Type Inference for Python" title="Code example: Pyrefly: Automatic Type Inference for Python" srcset="https://substackcdn.com/image/fetch/$s_!2Ucg!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F812a4bae-301f-4fb8-adcc-43769828f2c2_662x446.png 424w, https://substackcdn.com/image/fetch/$s_!2Ucg!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F812a4bae-301f-4fb8-adcc-43769828f2c2_662x446.png 848w, https://substackcdn.com/image/fetch/$s_!2Ucg!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F812a4bae-301f-4fb8-adcc-43769828f2c2_662x446.png 1272w, https://substackcdn.com/image/fetch/$s_!2Ucg!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F812a4bae-301f-4fb8-adcc-43769828f2c2_662x446.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h4>Problem</h4><p>By default, MyPy ignores untyped function bodies, forcing you to manually annotate variables and return types to get meaningful type checking.</p><p>This adds overhead and clutters your code with types that don&#8217;t add much value.</p><h4>Solution</h4><p><strong><a href="https://github.com/facebook/pyrefly">Pyrefly</a></strong> automatically infers return types and local variable types from usage, so you only annotate function parameters and public APIs where it matters.</p><p>Other features:</p><ul><li><p>Lightning-fast type checking built in Rust</p></li><li><p>Works with VSCode, Neovim, Helix, Emacs, and Zed</p></li><li><p>Recognizes dataclass transforms from attrs, Pydantic, and similar libraries</p></li></ul><blockquote></blockquote><div><hr></div><h2>&#9749;&#65039; Weekly Finds</h2><p><strong><a href="https://github.com/tobgu/pyrsistent">pyrsistent</a></strong> <em>[Python Utils]</em> - Persistent/Immutable/Functional data structures for Python</p><p><strong><a href="https://github.com/GrahamDumpleton/wrapt">wrapt</a></strong> <em>[Python Utils]</em> - A Python module for decorators, wrappers and monkey patching</p><p><strong><a href="https://github.com/posit-dev/py-shiny">Shiny for Python</a></strong> <em>[Dashboard]</em> - Build fast, beautiful reactive web applications in Python</p><div><hr></div><h3>&#128172; Rate Your Experience</h3><p>How would you rate your newsletter experience? <a href="https://r7yuob192zv.typeform.com/to/VODwGYMW">Share your feedback &#8594;</a></p><div><hr></div><h3>&#128269; Explore More on CodeCut</h3><ul><li><p><a href="https://codecut.ai/tool-selector/?utm_source=Substack&amp;utm_medium=email&amp;utm_campaign=article_tool-selector">Tool Selector</a> - Discover 70+ Python tools for AI and data science</p></li><li><p><a href="https://codecut.ai/production-ready-data-science/?utm_source=Substack&amp;utm_medium=email&amp;utm_campaign=article_production-ready-data-science">Production Ready Data Science</a> - A practical book for taking projects from prototype to production</p></li></ul>]]></content:encoded></item><item><title><![CDATA[kotaemon: Self-Hosted Document QA with Citations in One Command]]></title><description><![CDATA[Plus replace MCP tools with modular skill files]]></description><link>https://newsletter.codecut.ai/p/kotaemon-self-hosted-document-qa</link><guid isPermaLink="false">https://newsletter.codecut.ai/p/kotaemon-self-hosted-document-qa</guid><dc:creator><![CDATA[CodeCut]]></dc:creator><pubDate>Tue, 28 Apr 2026 16:01:41 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!_mBQ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3e64a13e-0bd5-48e0-914c-e7357f1e2547_1200x792.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Grab your coffee. Here are this week&#8217;s highlights.</p><div><hr></div><h2>&#128197; Today&#8217;s Picks</h2><h3>kotaemon: Self-Hosted Document QA with Citations in One Command</h3><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!_mBQ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3e64a13e-0bd5-48e0-914c-e7357f1e2547_1200x792.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!_mBQ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3e64a13e-0bd5-48e0-914c-e7357f1e2547_1200x792.png 424w, https://substackcdn.com/image/fetch/$s_!_mBQ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3e64a13e-0bd5-48e0-914c-e7357f1e2547_1200x792.png 848w, https://substackcdn.com/image/fetch/$s_!_mBQ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3e64a13e-0bd5-48e0-914c-e7357f1e2547_1200x792.png 1272w, https://substackcdn.com/image/fetch/$s_!_mBQ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3e64a13e-0bd5-48e0-914c-e7357f1e2547_1200x792.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!_mBQ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3e64a13e-0bd5-48e0-914c-e7357f1e2547_1200x792.png" width="1200" height="792" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/3e64a13e-0bd5-48e0-914c-e7357f1e2547_1200x792.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:792,&quot;width&quot;:1200,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Code example: kotaemon: Self-Hosted Document QA with Citations in One Command&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Code example: kotaemon: Self-Hosted Document QA with Citations in One Command" title="Code example: kotaemon: Self-Hosted Document QA with Citations in One Command" srcset="https://substackcdn.com/image/fetch/$s_!_mBQ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3e64a13e-0bd5-48e0-914c-e7357f1e2547_1200x792.png 424w, https://substackcdn.com/image/fetch/$s_!_mBQ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3e64a13e-0bd5-48e0-914c-e7357f1e2547_1200x792.png 848w, https://substackcdn.com/image/fetch/$s_!_mBQ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3e64a13e-0bd5-48e0-914c-e7357f1e2547_1200x792.png 1272w, https://substackcdn.com/image/fetch/$s_!_mBQ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3e64a13e-0bd5-48e0-914c-e7357f1e2547_1200x792.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h4>Problem</h4><p>Building a RAG app for document Q&amp;A usually means assembling a parser, vector database, retrieval pipeline, and UI from scratch.</p><p>Each piece has its own setup, and getting everything to work together can take hours of debugging.</p><h4>Solution</h4><p><strong><a href="https://github.com/Cinnamon/kotaemon">kotaemon</a></strong> packages the entire RAG stack into a single Docker image, letting you skip the setup and go straight to asking questions.</p><p>Key features:</p><ul><li><p>Citations linked to exact PDF pages for verifiable answers</p></li><li><p>Question answering across multiple documents with figures and tables</p></li><li><p>Works with local models or cloud APIs like OpenAI, Azure, and Groq</p></li><li><p>Extensible Gradio-based UI with multi-user document management</p></li></ul><blockquote></blockquote><div><hr></div><h3>gws: Replace Bulky MCP Tools with 100+ Modular Skill Files</h3><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!cZVv!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F839a4917-12c5-4cf3-919c-4c16182e9df2_1200x801.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!cZVv!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F839a4917-12c5-4cf3-919c-4c16182e9df2_1200x801.png 424w, https://substackcdn.com/image/fetch/$s_!cZVv!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F839a4917-12c5-4cf3-919c-4c16182e9df2_1200x801.png 848w, https://substackcdn.com/image/fetch/$s_!cZVv!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F839a4917-12c5-4cf3-919c-4c16182e9df2_1200x801.png 1272w, https://substackcdn.com/image/fetch/$s_!cZVv!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F839a4917-12c5-4cf3-919c-4c16182e9df2_1200x801.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!cZVv!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F839a4917-12c5-4cf3-919c-4c16182e9df2_1200x801.png" width="1200" height="801" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/839a4917-12c5-4cf3-919c-4c16182e9df2_1200x801.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:801,&quot;width&quot;:1200,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Code example: gws: Replace Bulky MCP Tools with 100+ Modular Skill Files&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Code example: gws: Replace Bulky MCP Tools with 100+ Modular Skill Files" title="Code example: gws: Replace Bulky MCP Tools with 100+ Modular Skill Files" srcset="https://substackcdn.com/image/fetch/$s_!cZVv!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F839a4917-12c5-4cf3-919c-4c16182e9df2_1200x801.png 424w, https://substackcdn.com/image/fetch/$s_!cZVv!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F839a4917-12c5-4cf3-919c-4c16182e9df2_1200x801.png 848w, https://substackcdn.com/image/fetch/$s_!cZVv!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F839a4917-12c5-4cf3-919c-4c16182e9df2_1200x801.png 1272w, https://substackcdn.com/image/fetch/$s_!cZVv!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F839a4917-12c5-4cf3-919c-4c16182e9df2_1200x801.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h4>Problem</h4><p>Connecting AI agents to Google Workspace through MCP often means injecting every tool definition into each request, even if only a couple are needed.</p><p>That overhead quickly eats into the token budget, leaving less room for reasoning and task execution.</p><h4>Solution</h4><p><strong><a href="https://github.com/googleworkspace/cli">gws</a></strong> solves this by replacing bulky tool definitions with 100+ modular SKILL.md files.</p><p>Agents load only the skills they need, keeping the context lean and efficient.</p><p>Key features:</p><ul><li><p>Works with Claude Code, Cursor, Gemini CLI, and other AI agents out of the box</p></li><li><p>100+ skill files covering Google Docs, Sheets, Drive, Calendar, and more</p></li><li><p>Agents load only relevant skills instead of full tool definitions</p></li></ul><blockquote></blockquote><div><hr></div><h2>&#9749;&#65039; Weekly Finds</h2><p><strong><a href="https://github.com/Unstructured-IO/unstructured">unstructured</a></strong> <em>[RAG]</em> - Turn any document into clean, structured data ready for RAG pipelines and LLM applications.</p><p><strong><a href="https://github.com/mangiucugna/json_repair">json_repair</a></strong> <em>[LLM]</em> - Repair malformed JSON from LLMs, APIs, and logs. A drop-in replacement for json.loads() that auto-fixes broken output.</p><p><strong><a href="https://github.com/mindsdb/mindsdb">MindsDB</a></strong> <em>[AI Agents]</em> - Query AI models directly from your database using SQL. Connect 200+ data sources to LLMs, ML, and vector operations.</p><div><hr></div><h3>&#128172; Rate Your Experience</h3><p>How would you rate your newsletter experience? <a href="https://r7yuob192zv.typeform.com/to/VODwGYMW">Share your feedback &#8594;</a></p><div><hr></div><h3>&#128269; Explore More on CodeCut</h3><ul><li><p><a href="https://codecut.ai/tool-selector/?utm_source=Substack&amp;utm_medium=email&amp;utm_campaign=article_tool-selector">Tool Selector</a> - Discover 70+ Python tools for AI and data science</p></li><li><p><a href="https://codecut.ai/production-ready-data-science/?utm_source=Substack&amp;utm_medium=email&amp;utm_campaign=article_production-ready-data-science">Production Ready Data Science</a> - A practical book for taking projects from prototype to production</p></li></ul>]]></content:encoded></item></channel></rss>