<?xml version="1.0" encoding="utf-8"?>
<feed xml:lang="en-us" xmlns="http://www.w3.org/2005/Atom"><title>Simon Willison's Weblog: python</title><link href="http://simonwillison.net/" rel="alternate"/><link href="http://simonwillison.net/tags/python.atom" rel="self"/><id>http://simonwillison.net/</id><updated>2026-04-15T23:16:34+00:00</updated><author><name>Simon Willison</name></author><entry><title>datasette 1.0a27</title><link href="https://simonwillison.net/2026/Apr/15/datasette/#atom-tag" rel="alternate"/><published>2026-04-15T23:16:34+00:00</published><updated>2026-04-15T23:16:34+00:00</updated><id>https://simonwillison.net/2026/Apr/15/datasette/#atom-tag</id><summary type="html">
    &lt;p&gt;&lt;strong&gt;Release:&lt;/strong&gt; &lt;a href="https://github.com/simonw/datasette/releases/tag/1.0a27"&gt;datasette 1.0a27&lt;/a&gt;&lt;/p&gt;
    &lt;p&gt;Two major changes in this new Datasette alpha. I covered the first of those &lt;a href="https://simonwillison.net/2026/Apr/14/replace-token-based-csrf/"&gt;in detail yesterday&lt;/a&gt; - Datasette no longer uses Django-style CSRF form tokens, instead using modern browser headers &lt;a href="https://words.filippo.io/csrf"&gt;as described by Filippo Valsorda&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The second big change is that Datasette now fires a new &lt;a href="https://docs.datasette.io/en/latest/events.html#datasette.events.RenameTableEvent"&gt;RenameTableEvent&lt;/a&gt; any time a table is renamed during a SQLite transaction. This is useful because some plugins (like &lt;a href="https://github.com/datasette/datasette-comments"&gt;datasette-comments&lt;/a&gt;) attach additional data to table records by name, so a renamed table requires them to react in appropriate ways.&lt;/p&gt;
&lt;p&gt;Here are the rest of the changes in the alpha:&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;New &lt;a href="https://docs.datasette.io/en/latest/internals.html#internals-datasette-client-actor"&gt;actor= parameter&lt;/a&gt; for &lt;code&gt;datasette.client&lt;/code&gt; methods, allowing internal requests to be made as a specific actor. This is particularly useful for writing automated tests. (&lt;a href="https://github.com/simonw/datasette/pull/2688"&gt;#2688&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;New &lt;code&gt;Database(is_temp_disk=True)&lt;/code&gt; option, used internally for the internal database. This helps resolve intermittent database locked errors caused by the internal database being in-memory as opposed to on-disk. (&lt;a href="https://github.com/simonw/datasette/issues/2683"&gt;#2683&lt;/a&gt;) (&lt;a href="https://github.com/simonw/datasette/pull/2684"&gt;#2684&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;/&amp;lt;database&amp;gt;/&amp;lt;table&amp;gt;/-/upsert&lt;/code&gt; API (&lt;a href="https://docs.datasette.io/en/latest/json_api.html#tableupsertview"&gt;docs&lt;/a&gt;) now rejects rows with &lt;code&gt;null&lt;/code&gt; primary key values. (&lt;a href="https://github.com/simonw/datasette/issues/1936"&gt;#1936&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Improved example in the API explorer for the &lt;code&gt;/-/upsert&lt;/code&gt; endpoint (&lt;a href="https://docs.datasette.io/en/latest/json_api.html#tableupsertview"&gt;docs&lt;/a&gt;). (&lt;a href="https://github.com/simonw/datasette/issues/1936"&gt;#1936&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;/&amp;lt;database&amp;gt;.json&lt;/code&gt; endpoint now includes an &lt;code&gt;"ok": true&lt;/code&gt; key, for consistency with other JSON API responses.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.datasette.io/en/latest/internals.html#internals-utils-call-with-supported-arguments"&gt;call_with_supported_arguments()&lt;/a&gt; is now documented as a supported public API. (&lt;a href="https://github.com/simonw/datasette/pull/2678"&gt;#2678&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/annotated-release-notes"&gt;annotated-release-notes&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="annotated-release-notes"/><category term="datasette"/><category term="python"/></entry><entry><title>Gemma 4 audio with MLX</title><link href="https://simonwillison.net/2026/Apr/12/mlx-audio/#atom-tag" rel="alternate"/><published>2026-04-12T23:57:53+00:00</published><updated>2026-04-12T23:57:53+00:00</updated><id>https://simonwillison.net/2026/Apr/12/mlx-audio/#atom-tag</id><summary type="html">
    &lt;p&gt;Thanks to a &lt;a href="https://twitter.com/RahimNathwani/status/2039961945613209852"&gt;tip from Rahim Nathwani&lt;/a&gt;, here's a &lt;code&gt;uv run&lt;/code&gt; recipe for transcribing an audio file on macOS using the 10.28 GB &lt;a href="https://huggingface.co/google/gemma-4-E2B"&gt;Gemma 4 E2B model&lt;/a&gt; with MLX and &lt;a href="https://github.com/Blaizzy/mlx-vlm"&gt;mlx-vlm&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uv run --python 3.13 --with mlx_vlm --with torchvision --with gradio \
  mlx_vlm.generate \
  --model google/gemma-4-e2b-it \
  --audio file.wav \
  --prompt "Transcribe this audio" \
  --max-tokens 500 \
  --temperature 1.0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;audio controls style="width: 100%"&gt;
  &lt;source src="https://static.simonwillison.net/static/2026/demo-audio-for-gemma.wav" type="audio/wav"&gt;
  Your browser does not support the audio element.
&lt;/audio&gt;&lt;/p&gt;
&lt;p&gt;I tried it on &lt;a href="https://static.simonwillison.net/static/2026/demo-audio-for-gemma.wav"&gt;this 14 second &lt;code&gt;.wav&lt;/code&gt; file&lt;/a&gt; and it output the following:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;This front here is a quick voice memo. I want to try it out with MLX VLM. Just going to see if it can be transcribed by Gemma and how that works.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;(That was supposed to be "This right here..." and "... how well that works" but I can hear why it misinterpreted that as "front" and "how that works".)&lt;/p&gt;

    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/uv"&gt;uv&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/mlx"&gt;mlx&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/gemma"&gt;gemma&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llms"&gt;llms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/speech-to-text"&gt;speech-to-text&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/generative-ai"&gt;generative-ai&lt;/a&gt;&lt;/p&gt;



</summary><category term="uv"/><category term="mlx"/><category term="ai"/><category term="gemma"/><category term="llms"/><category term="speech-to-text"/><category term="python"/><category term="generative-ai"/></entry><entry><title>asgi-gzip 0.3</title><link href="https://simonwillison.net/2026/Apr/9/asgi-gzip/#atom-tag" rel="alternate"/><published>2026-04-09T03:54:40+00:00</published><updated>2026-04-09T03:54:40+00:00</updated><id>https://simonwillison.net/2026/Apr/9/asgi-gzip/#atom-tag</id><summary type="html">
    &lt;p&gt;&lt;strong&gt;Release:&lt;/strong&gt; &lt;a href="https://github.com/simonw/asgi-gzip/releases/tag/0.3"&gt;asgi-gzip 0.3&lt;/a&gt;&lt;/p&gt;
    &lt;p&gt;I ran into trouble deploying a new feature using &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events"&gt;SSE&lt;/a&gt; to a production Datasette instance, and it turned out that instance was using &lt;a href="https://github.com/simonw/datasette-gzip"&gt;datasette-gzip&lt;/a&gt; which uses &lt;a href="https://github.com/simonw/asgi-gzip"&gt;asgi-gzip&lt;/a&gt; which was incorrectly compressing &lt;code&gt;event/text-stream&lt;/code&gt; responses.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;asgi-gzip&lt;/code&gt; was extracted from Starlette, and has &lt;a href="https://simonwillison.net/2022/Apr/28/issue-on-changes/"&gt;a GitHub Actions scheduled workflow&lt;/a&gt; to check Starlette for updates that need to be ported to the library... but that action had stopped running and hence had missed &lt;a href="https://github.com/Kludex/starlette/commit/a9a8dab0cc3cbd05dca37650fc392717b9fe5bbf"&gt;Starlette's own fix&lt;/a&gt; for this issue.&lt;/p&gt;
&lt;p&gt;I ran the workflow and integrated the new fix, and now &lt;code&gt;datasette-gzip&lt;/code&gt; and &lt;code&gt;asgi-gzip&lt;/code&gt; both correctly handle &lt;code&gt;text/event-stream&lt;/code&gt; in SSE responses.&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/gzip"&gt;gzip&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/asgi"&gt;asgi&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="gzip"/><category term="asgi"/><category term="python"/></entry><entry><title>llm-all-models-async 0.1</title><link href="https://simonwillison.net/2026/Mar/31/llm-all-models-async/#atom-tag" rel="alternate"/><published>2026-03-31T20:52:02+00:00</published><updated>2026-03-31T20:52:02+00:00</updated><id>https://simonwillison.net/2026/Mar/31/llm-all-models-async/#atom-tag</id><summary type="html">
    &lt;p&gt;&lt;strong&gt;Release:&lt;/strong&gt; &lt;a href="https://github.com/simonw/llm-all-models-async/releases/tag/0.1"&gt;llm-all-models-async 0.1&lt;/a&gt;&lt;/p&gt;
    &lt;p&gt;LLM plugins can define new models in both &lt;a href="https://llm.datasette.io/en/stable/plugins/tutorial-model-plugin.html"&gt;sync&lt;/a&gt; and &lt;a href="https://llm.datasette.io/en/stable/plugins/advanced-model-plugins.html#async-models"&gt;async&lt;/a&gt; varieties. The async variants are most common for API-backed models - sync variants tend to be things that run the model directly within the plugin.&lt;/p&gt;
&lt;p&gt;My &lt;a href="https://simonwillison.net/2026/Mar/30/mr-chatterbox/#running-it-locally-with-llm"&gt;llm-mrchatterbox&lt;/a&gt; plugin is sync only. I wanted to try it out with various Datasette LLM features (specifically &lt;a href="https://github.com/datasette/datasette-enrichments-llm"&gt;datasette-enrichments-llm&lt;/a&gt;) but Datasette can only use async models.&lt;/p&gt;
&lt;p&gt;So... I had Claude spin up this plugin that turns sync models into async models using a thread pool. This ended up needing an extra plugin hook mechanism in LLM itself, which I shipped just now in &lt;a href="https://llm.datasette.io/en/stable/changelog.html#v0-30"&gt;LLM 0.30&lt;/a&gt;.&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/llm"&gt;llm&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/async"&gt;async&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="llm"/><category term="async"/><category term="python"/></entry><entry><title>Python Vulnerability Lookup</title><link href="https://simonwillison.net/2026/Mar/29/python-vulnerability-lookup/#atom-tag" rel="alternate"/><published>2026-03-29T18:46:16+00:00</published><updated>2026-03-29T18:46:16+00:00</updated><id>https://simonwillison.net/2026/Mar/29/python-vulnerability-lookup/#atom-tag</id><summary type="html">
    &lt;p&gt;&lt;strong&gt;Tool:&lt;/strong&gt; &lt;a href="https://tools.simonwillison.net/python-vulnerability-lookup"&gt;Python Vulnerability Lookup&lt;/a&gt;&lt;/p&gt;
    &lt;p&gt;I learned that the &lt;a href="https://osv.dev/"&gt;OSV.dev&lt;/a&gt; open source vulnerability database has an open CORS &lt;a href="https://google.github.io/osv.dev/api/"&gt;JSON API&lt;/a&gt;, so I had Claude Code build this &lt;a href="https://simonwillison.net/2025/Dec/10/html-tools/"&gt;HTML tool&lt;/a&gt; for pasting in a &lt;code&gt;pyproject.toml&lt;/code&gt; or &lt;code&gt;requirements.txt&lt;/code&gt; file (or name of a GitHub repo containing those) and seeing a list of all reported vulnerabilities from that API.&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/tools"&gt;tools&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/supply-chain"&gt;supply-chain&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/vibe-coding"&gt;vibe-coding&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/security"&gt;security&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="tools"/><category term="python"/><category term="supply-chain"/><category term="vibe-coding"/><category term="security"/></entry><entry><title>LiteLLM Hack: Were You One of the 47,000?</title><link href="https://simonwillison.net/2026/Mar/25/litellm-hack/#atom-tag" rel="alternate"/><published>2026-03-25T17:21:04+00:00</published><updated>2026-03-25T17:21:04+00:00</updated><id>https://simonwillison.net/2026/Mar/25/litellm-hack/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://futuresearch.ai/blog/litellm-hack-were-you-one-of-the-47000/"&gt;LiteLLM Hack: Were You One of the 47,000?&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Daniel Hnyk used the &lt;a href="https://console.cloud.google.com/bigquery?p=bigquery-public-data&amp;amp;d=pypi"&gt;BigQuery PyPI dataset&lt;/a&gt; to determine how many downloads there were of &lt;a href="https://simonwillison.net/2026/Mar/24/malicious-litellm/"&gt;the exploited LiteLLM packages&lt;/a&gt; during the 46 minute period they were live on PyPI. The answer was 46,996 across the two compromised release versions (1.82.7 and 1.82.8).&lt;/p&gt;
&lt;p&gt;They also identified 2,337 packages that depended on LiteLLM - 88% of which did not pin versions in a way that would have avoided the exploited version.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://twitter.com/hnykda/status/2036834100342825369"&gt;@hnykda&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/supply-chain"&gt;supply-chain&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/security"&gt;security&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/pypi"&gt;pypi&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/packaging"&gt;packaging&lt;/a&gt;&lt;/p&gt;



</summary><category term="supply-chain"/><category term="security"/><category term="pypi"/><category term="python"/><category term="packaging"/></entry><entry><title>Package Managers Need to Cool Down</title><link href="https://simonwillison.net/2026/Mar/24/package-managers-need-to-cool-down/#atom-tag" rel="alternate"/><published>2026-03-24T21:11:38+00:00</published><updated>2026-03-24T21:11:38+00:00</updated><id>https://simonwillison.net/2026/Mar/24/package-managers-need-to-cool-down/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://nesbitt.io/2026/03/04/package-managers-need-to-cool-down.html"&gt;Package Managers Need to Cool Down&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Today's &lt;a href="https://simonwillison.net/2026/Mar/24/malicious-litellm/"&gt;LiteLLM supply chain attack&lt;/a&gt; inspired me to revisit the idea of &lt;a href="https://simonwillison.net/2025/Nov/21/dependency-cooldowns/"&gt;dependency cooldowns&lt;/a&gt;, the practice of only installing updated dependencies once they've been out in the wild for a few days to give the community a chance to spot if they've been subverted in some way.&lt;/p&gt;
&lt;p&gt;This recent piece (March 4th) piece by Andrew Nesbitt reviews the current state of dependency cooldown mechanisms across different packaging tools. It's surprisingly well supported! There's been a flurry of activity across major packaging tools, including:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://pnpm.io/blog/releases/10.16#new-setting-for-delayed-dependency-updates"&gt;pnpm 10.16&lt;/a&gt; (September 2025) — &lt;code&gt;minimumReleaseAge&lt;/code&gt; with &lt;code&gt;minimumReleaseAgeExclude&lt;/code&gt; for trusted packages&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/yarnpkg/berry/releases/tag/%40yarnpkg%2Fcli%2F4.10.0"&gt;Yarn 4.10.0&lt;/a&gt; (September 2025) — &lt;code&gt;npmMinimalAgeGate&lt;/code&gt; (in minutes) with &lt;code&gt;npmPreapprovedPackages&lt;/code&gt; for exemptions&lt;/li&gt;
&lt;li&gt;&lt;a href="https://bun.com/blog/bun-v1.3#minimum-release-age"&gt;Bun 1.3&lt;/a&gt; (October 2025) — &lt;code&gt;minimumReleaseAge&lt;/code&gt; via &lt;code&gt;bunfig.toml&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://deno.com/blog/v2.6#controlling-dependency-stability"&gt;Deno 2.6&lt;/a&gt; (December 2025) — &lt;code&gt;--minimum-dependency-age&lt;/code&gt; for &lt;code&gt;deno update&lt;/code&gt; and &lt;code&gt;deno outdated&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/astral-sh/uv/releases/tag/0.9.17"&gt;uv 0.9.17&lt;/a&gt; (December 2025) — added relative duration support to existing &lt;code&gt;--exclude-newer&lt;/code&gt;, plus per-package overrides via &lt;code&gt;exclude-newer-package&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ichard26.github.io/blog/2026/01/whats-new-in-pip-26.0/"&gt;pip 26.0&lt;/a&gt; (January 2026) — &lt;code&gt;--uploaded-prior-to&lt;/code&gt; (absolute timestamps only; &lt;a href="https://github.com/pypa/pip/issues/13674"&gt;relative duration support requested&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://socket.dev/blog/npm-introduces-minimumreleaseage-and-bulk-oidc-configuration"&gt;npm 11.10.0&lt;/a&gt; (February 2026) — &lt;code&gt;min-release-age&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;pip&lt;/code&gt; currently only supports absolute rather than relative dates but Seth Larson &lt;a href="https://sethmlarson.dev/pip-relative-dependency-cooling-with-crontab"&gt;has a workaround for that&lt;/a&gt; using a scheduled cron to update the absolute date in the &lt;code&gt;pip.conf&lt;/code&gt; config file.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/deno"&gt;deno&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/uv"&gt;uv&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/supply-chain"&gt;supply-chain&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/pip"&gt;pip&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/pypi"&gt;pypi&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/npm"&gt;npm&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/packaging"&gt;packaging&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/security"&gt;security&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/javascript"&gt;javascript&lt;/a&gt;&lt;/p&gt;



</summary><category term="deno"/><category term="uv"/><category term="supply-chain"/><category term="pip"/><category term="pypi"/><category term="npm"/><category term="packaging"/><category term="security"/><category term="python"/><category term="javascript"/></entry><entry><title>Malicious litellm_init.pth in litellm 1.82.8 — credential stealer</title><link href="https://simonwillison.net/2026/Mar/24/malicious-litellm/#atom-tag" rel="alternate"/><published>2026-03-24T15:07:31+00:00</published><updated>2026-03-24T15:07:31+00:00</updated><id>https://simonwillison.net/2026/Mar/24/malicious-litellm/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/BerriAI/litellm/issues/24512"&gt;Malicious litellm_init.pth in litellm 1.82.8 — credential stealer&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
The LiteLLM v1.82.8 package published to PyPI was compromised with a particularly nasty credential stealer hidden in base64 in a &lt;code&gt;litellm_init.pth&lt;/code&gt; file, which means installing the package is enough to trigger it even without running &lt;code&gt;import litellm&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;(1.82.7 had the exploit as well but it was in the &lt;code&gt;proxy/proxy_server.py&lt;/code&gt; file so the package had to be imported for it to take effect.)&lt;/p&gt;
&lt;p&gt;This issue has a very detailed description of what the credential stealer does. There's more information about the timeline of the exploit &lt;a href="https://github.com/BerriAI/litellm/issues/24518"&gt;over here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;PyPI has already &lt;a href="https://pypi.org/help/#project_in_quarantine"&gt;quarantined&lt;/a&gt; the &lt;a href="https://pypi.org/project/litellm/"&gt;litellm package&lt;/a&gt; so the window for compromise was just a few hours, but if you DID install the package it would have hoovered up a bewildering array of secrets, including &lt;code&gt;~/.ssh/&lt;/code&gt;, &lt;code&gt;~/.gitconfig&lt;/code&gt;, &lt;code&gt;~/.git-credentials&lt;/code&gt;, &lt;code&gt;~/.aws/&lt;/code&gt;, &lt;code&gt;~/.kube/&lt;/code&gt;, &lt;code&gt;~/.config/&lt;/code&gt;, &lt;code&gt;~/.azure/&lt;/code&gt;, &lt;code&gt;~/.docker/&lt;/code&gt;, &lt;code&gt;~/.npmrc&lt;/code&gt;, &lt;code&gt;~/.vault-token&lt;/code&gt;, &lt;code&gt;~/.netrc&lt;/code&gt;, &lt;code&gt;~/.lftprc&lt;/code&gt;, &lt;code&gt;~/.msmtprc&lt;/code&gt;, &lt;code&gt;~/.my.cnf&lt;/code&gt;, &lt;code&gt;~/.pgpass&lt;/code&gt;, &lt;code&gt;~/.mongorc.js&lt;/code&gt;, &lt;code&gt;~/.bash_history&lt;/code&gt;, &lt;code&gt;~/.zsh_history&lt;/code&gt;, &lt;code&gt;~/.sh_history&lt;/code&gt;, &lt;code&gt;~/.mysql_history&lt;/code&gt;, &lt;code&gt;~/.psql_history&lt;/code&gt;, &lt;code&gt;~/.rediscli_history&lt;/code&gt;, &lt;code&gt;~/.bitcoin/&lt;/code&gt;, &lt;code&gt;~/.litecoin/&lt;/code&gt;, &lt;code&gt;~/.dogecoin/&lt;/code&gt;, &lt;code&gt;~/.zcash/&lt;/code&gt;, &lt;code&gt;~/.dashcore/&lt;/code&gt;, &lt;code&gt;~/.ripple/&lt;/code&gt;, &lt;code&gt;~/.bitmonero/&lt;/code&gt;, &lt;code&gt;~/.ethereum/&lt;/code&gt;, &lt;code&gt;~/.cardano/&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;It looks like this supply chain attack started with the &lt;a href="https://www.crowdstrike.com/en-us/blog/from-scanner-to-stealer-inside-the-trivy-action-supply-chain-compromise/"&gt;recent exploit&lt;/a&gt; against &lt;a href="https://trivy.dev/"&gt;Trivy&lt;/a&gt;, ironically a security scanner tool that was used in CI &lt;a href="https://github.com/BerriAI/litellm/blob/9343aeefca37aa49a6ea54397d7615adae5c72c9/ci_cd/security_scans.sh#L16"&gt;by LiteLLM&lt;/a&gt;. The Trivy exploit likely resulted in stolen PyPI credentials which were then used to directly publish the vulnerable packages.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/supply-chain"&gt;supply-chain&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/open-source"&gt;open-source&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/pypi"&gt;pypi&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/security"&gt;security&lt;/a&gt;&lt;/p&gt;



</summary><category term="supply-chain"/><category term="open-source"/><category term="pypi"/><category term="python"/><category term="security"/></entry><entry><title>Experimenting with Starlette 1.0 with Claude skills</title><link href="https://simonwillison.net/2026/Mar/22/starlette/#atom-tag" rel="alternate"/><published>2026-03-22T23:57:44+00:00</published><updated>2026-03-22T23:57:44+00:00</updated><id>https://simonwillison.net/2026/Mar/22/starlette/#atom-tag</id><summary type="html">
    &lt;p&gt;&lt;a href="https://marcelotryle.com/blog/2026/03/22/starlette-10-is-here/"&gt;Starlette 1.0 is out&lt;/a&gt;! This is a really big deal. I think Starlette may be the Python framework with the most usage compared to its relatively low brand recognition because Starlette is the foundation of &lt;a href="https://fastapi.tiangolo.com/"&gt;FastAPI&lt;/a&gt;, which has attracted a huge amount of buzz that seems to have overshadowed Starlette itself.&lt;/p&gt;
&lt;p&gt;Kim Christie started working on Starlette in 2018 and it quickly became my favorite out of the new breed of Python ASGI frameworks. The only reason I didn't use it as the basis for my own &lt;a href="https://datasette.io/"&gt;Datasette&lt;/a&gt; project was that it didn't yet promise stability, and I was determined to provide a stable API for Datasette's own plugins... albeit I still haven't been brave enough to ship my own 1.0 release (after 26 alphas and counting)!&lt;/p&gt;
&lt;p&gt;Then in September 2025 Marcelo Trylesinski &lt;a href="https://github.com/Kludex/starlette/discussions/2997"&gt;announced that Starlette and Uvicorn were transferring to their GitHub account&lt;/a&gt;, in recognition of their many years of contributions and to make it easier for them to receive sponsorship against those projects.&lt;/p&gt;
&lt;p&gt;The 1.0 version has a few breaking changes compared to the 0.x series, described in &lt;a href="https://starlette.dev/release-notes/#100rc1-february-23-2026"&gt;the release notes for 1.0.0rc1&lt;/a&gt; that came out in February.&lt;/p&gt;
&lt;p&gt;The most notable of these is a change to how code runs on startup and shutdown. Previously that was handled by &lt;code&gt;on_startup&lt;/code&gt; and &lt;code&gt;on_shutdown&lt;/code&gt; parameters, but the new system uses a neat &lt;a href="https://starlette.dev/lifespan/"&gt;lifespan&lt;/a&gt; mechanism instead based around an &lt;a href="https://docs.python.org/3/library/contextlib.html#contextlib.asynccontextmanager"&gt;async context manager&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;span class="pl-en"&gt;@&lt;span class="pl-s1"&gt;contextlib&lt;/span&gt;.&lt;span class="pl-c1"&gt;asynccontextmanager&lt;/span&gt;&lt;/span&gt;
&lt;span class="pl-k"&gt;async&lt;/span&gt; &lt;span class="pl-k"&gt;def&lt;/span&gt; &lt;span class="pl-en"&gt;lifespan&lt;/span&gt;(&lt;span class="pl-s1"&gt;app&lt;/span&gt;):
    &lt;span class="pl-k"&gt;async&lt;/span&gt; &lt;span class="pl-k"&gt;with&lt;/span&gt; &lt;span class="pl-en"&gt;some_async_resource&lt;/span&gt;():
        &lt;span class="pl-en"&gt;print&lt;/span&gt;(&lt;span class="pl-s"&gt;"Run at startup!"&lt;/span&gt;)
        &lt;span class="pl-k"&gt;yield&lt;/span&gt;
        &lt;span class="pl-en"&gt;print&lt;/span&gt;(&lt;span class="pl-s"&gt;"Run on shutdown!"&lt;/span&gt;)

&lt;span class="pl-s1"&gt;app&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-en"&gt;Starlette&lt;/span&gt;(
    &lt;span class="pl-s1"&gt;routes&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-s1"&gt;routes&lt;/span&gt;,
    &lt;span class="pl-s1"&gt;lifespan&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-s1"&gt;lifespan&lt;/span&gt;
)&lt;/pre&gt;
&lt;p&gt;If you haven't tried Starlette before it feels to me like an asyncio-native cross between Flask and Django, unsurprising since creator Kim Christie is also responsible for Django REST Framework. Crucially, this means you can write most apps as a single Python file, Flask style.&lt;/p&gt;
&lt;p&gt;This makes it &lt;em&gt;really&lt;/em&gt; easy for LLMs to spit out a working Starlette app from a single prompt.&lt;/p&gt;
&lt;p&gt;There's just one problem there: if 1.0 breaks compatibility with the Starlette code that the models have been trained on, how can we have them generate code that works with 1.0?&lt;/p&gt;
&lt;p&gt;I decided to see if I could get this working &lt;a href="https://simonwillison.net/2025/Oct/16/claude-skills/"&gt;with a Skill&lt;/a&gt;.&lt;/p&gt;
&lt;h4 id="building-a-skill-with-claude"&gt;Building a Skill with Claude&lt;/h4&gt;
&lt;p&gt;Regular Claude Chat on &lt;a href="https://claude.ai/"&gt;claude.ai&lt;/a&gt; has skills, and one of those default skills is the &lt;a href="https://github.com/anthropics/skills/blob/main/skills/skill-creator/SKILL.md"&gt;skill-creator skill&lt;/a&gt;. This means Claude knows how to build its own skills.&lt;/p&gt;
&lt;p&gt;So I started &lt;a href="https://claude.ai/share/b537c340-aea7-49d6-a14d-3134aa1bd957"&gt;a chat session&lt;/a&gt; and told it:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Clone Starlette from GitHub - it just had its 1.0 release. Build a skill markdown document for this release which includes code examples of every feature.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I didn't even tell it where to find the repo, Starlette is widely enough known that I expected it could find it on its own.&lt;/p&gt;
&lt;p&gt;It ran &lt;code&gt;git clone https://github.com/encode/starlette.git&lt;/code&gt; which is actually the old repository name, but GitHub handles redirects automatically so this worked just fine.&lt;/p&gt;
&lt;p&gt;The &lt;a href="https://github.com/simonw/research/blob/main/starlette-1-skill/SKILL.md"&gt;resulting skill document&lt;/a&gt; looked very thorough to me... and then I noticed a new button at the top I hadn't seen before labelled "Copy to your skills". So I clicked it:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2026/skill-button.jpg" alt="Screenshot of the Claude.ai interface showing a conversation titled &amp;quot;Starlette 1.0 skill document with code examples.&amp;quot; The left panel shows a chat where the user prompted: &amp;quot;Clone Starlette from GitHub - it just had its 1.0 release. Build a skill markdown document for this release which includes code examples of every feature.&amp;quot; Claude's responses include collapsed sections labeled &amp;quot;Strategized cloning repository and documenting comprehensive feature examples,&amp;quot; &amp;quot;Examined version details and surveyed source documentation comprehensively,&amp;quot; and &amp;quot;Synthesized Starlette 1.0 knowledge to construct comprehensive skill documentation,&amp;quot; with intermediate messages like &amp;quot;I'll clone Starlette from GitHub and build a comprehensive skill document. Let me start by reading the skill-creator guide and then cloning the repo,&amp;quot; &amp;quot;Now let me read through all the documentation files to capture every feature:&amp;quot; and &amp;quot;Now I have a thorough understanding of the entire codebase. Let me build the comprehensive skill document.&amp;quot; The right panel shows a skill preview pane with buttons &amp;quot;Copy to your skills&amp;quot; and &amp;quot;Copy&amp;quot; at the top, and a Description section reading: &amp;quot;Build async web applications and APIs with Starlette 1.0, the lightweight ASGI framework for Python. Use this skill whenever a user wants to create an async Python web app, REST API, WebSocket server, or ASGI application using Starlette. Triggers include mentions of 'Starlette', 'ASGI', async Python web frameworks, or requests to build lightweight async APIs, WebSocket services, streaming responses, or middleware pipelines. Also use when the user is working with FastAPI internals (which is built on Starlette), needs ASGI middleware patterns, or wants a minimal async web server&amp;quot; (text truncated)." style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;And now my regular Claude chat has access to that skill!&lt;/p&gt;
&lt;h4 id="a-task-management-demo-app"&gt;A task management demo app&lt;/h4&gt;
&lt;p&gt;I started &lt;a href="https://claude.ai/share/b5285fbc-5849-4939-b473-dcb66f73503b"&gt;a new conversation&lt;/a&gt; and prompted:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Build a task management app with Starlette, it should have projects and tasks and comments and labels&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;And Claude did exactly that, producing a simple GitHub Issues clone using Starlette 1.0, a SQLite database (via &lt;a href="https://github.com/omnilib/aiosqlite"&gt;aiosqlite&lt;/a&gt;) and a Jinja2 template.&lt;/p&gt;
&lt;p&gt;Claude even tested the app manually like this:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;&lt;span class="pl-c1"&gt;cd&lt;/span&gt; /home/claude/taskflow &lt;span class="pl-k"&gt;&amp;amp;&amp;amp;&lt;/span&gt; timeout 5 python -c &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
&lt;span class="pl-s"&gt;import asyncio&lt;/span&gt;
&lt;span class="pl-s"&gt;from database import init_db&lt;/span&gt;
&lt;span class="pl-s"&gt;asyncio.run(init_db())&lt;/span&gt;
&lt;span class="pl-s"&gt;print('DB initialized successfully')&lt;/span&gt;
&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-k"&gt;2&amp;gt;&amp;amp;1&lt;/span&gt;

pip install httpx --break-system-packages -q \
  &lt;span class="pl-k"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="pl-c1"&gt;cd&lt;/span&gt; /home/claude/taskflow &lt;span class="pl-k"&gt;&amp;amp;&amp;amp;&lt;/span&gt; \
  python -c &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
&lt;span class="pl-s"&gt;from starlette.testclient import TestClient&lt;/span&gt;
&lt;span class="pl-s"&gt;from main import app&lt;/span&gt;
&lt;span class="pl-s"&gt;&lt;/span&gt;
&lt;span class="pl-s"&gt;client = TestClient(app)&lt;/span&gt;
&lt;span class="pl-s"&gt;&lt;/span&gt;
&lt;span class="pl-s"&gt;r = client.get('/api/stats')&lt;/span&gt;
&lt;span class="pl-s"&gt;print('Stats:', r.json())&lt;/span&gt;
&lt;span class="pl-s"&gt;&lt;/span&gt;
&lt;span class="pl-s"&gt;r = client.get('/api/projects')&lt;/span&gt;
&lt;span class="pl-s"&gt;print('Projects:', len(r.json()), 'found')&lt;/span&gt;
&lt;span class="pl-s"&gt;&lt;/span&gt;
&lt;span class="pl-s"&gt;r = client.get('/api/tasks')&lt;/span&gt;
&lt;span class="pl-s"&gt;print('Tasks:', len(r.json()), 'found')&lt;/span&gt;
&lt;span class="pl-s"&gt;&lt;/span&gt;
&lt;span class="pl-s"&gt;r = client.get('/api/labels')&lt;/span&gt;
&lt;span class="pl-s"&gt;print('Labels:', len(r.json()), 'found')&lt;/span&gt;
&lt;span class="pl-s"&gt;&lt;/span&gt;
&lt;span class="pl-s"&gt;r = client.get('/api/tasks/1')&lt;/span&gt;
&lt;span class="pl-s"&gt;t = r.json()&lt;/span&gt;
&lt;span class="pl-s"&gt;print(f'Task 1: &lt;span class="pl-cce"&gt;\"&lt;/span&gt;{t[&lt;span class="pl-cce"&gt;\"&lt;/span&gt;title&lt;span class="pl-cce"&gt;\"&lt;/span&gt;]}&lt;span class="pl-cce"&gt;\"&lt;/span&gt; - {len(t[&lt;span class="pl-cce"&gt;\"&lt;/span&gt;comments&lt;span class="pl-cce"&gt;\"&lt;/span&gt;])} comments, {len(t[&lt;span class="pl-cce"&gt;\"&lt;/span&gt;labels&lt;span class="pl-cce"&gt;\"&lt;/span&gt;])} labels')&lt;/span&gt;
&lt;span class="pl-s"&gt;&lt;/span&gt;
&lt;span class="pl-s"&gt;r = client.post('/api/tasks', json={'title':'Test task','project_id':1,'priority':'high','label_ids':[1,2]})&lt;/span&gt;
&lt;span class="pl-s"&gt;print('Created task:', r.status_code, r.json()['title'])&lt;/span&gt;
&lt;span class="pl-s"&gt;&lt;/span&gt;
&lt;span class="pl-s"&gt;r = client.post('/api/comments', json={'task_id':1,'content':'Test comment'})&lt;/span&gt;
&lt;span class="pl-s"&gt;print('Created comment:', r.status_code)&lt;/span&gt;
&lt;span class="pl-s"&gt;&lt;/span&gt;
&lt;span class="pl-s"&gt;r = client.get('/')&lt;/span&gt;
&lt;span class="pl-s"&gt;print('Homepage:', r.status_code, '- length:', len(r.text))&lt;/span&gt;
&lt;span class="pl-s"&gt;&lt;/span&gt;
&lt;span class="pl-s"&gt;print('\nAll tests passed!')&lt;/span&gt;
&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;For all of the buzz about Claude Code, it's easy to overlook that Claude itself counts as a coding agent now, fully able to both write and then test the code that it is writing.&lt;/p&gt;
&lt;p&gt;Here's what the resulting app looked like. The code is &lt;a href="https://github.com/simonw/research/blob/main/starlette-1-skill/taskflow"&gt;here in my research repository&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2026/taskflow.jpg" alt="Screenshot of a dark-themed Kanban board app called &amp;quot;TaskFlow&amp;quot; showing the &amp;quot;Website Redesign&amp;quot; project. The left sidebar has sections &amp;quot;OVERVIEW&amp;quot; with &amp;quot;Dashboard&amp;quot;, &amp;quot;All Tasks&amp;quot;, and &amp;quot;Labels&amp;quot;, and &amp;quot;PROJECTS&amp;quot; with &amp;quot;Website Redesign&amp;quot; (1) and &amp;quot;API Platform&amp;quot; (0). The main area has three columns: &amp;quot;TO DO&amp;quot; (0) showing &amp;quot;No tasks&amp;quot;, &amp;quot;IN PROGRESS&amp;quot; (1) with a card titled &amp;quot;Blog about Starlette 1.0&amp;quot; tagged &amp;quot;MEDIUM&amp;quot; and &amp;quot;Documentation&amp;quot;, and &amp;quot;DONE&amp;quot; (0) showing &amp;quot;No tasks&amp;quot;. Top-right buttons read &amp;quot;+ New Task&amp;quot; and &amp;quot;Delete&amp;quot;." style="max-width: 100%;" /&gt;&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/skills"&gt;skills&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/claude"&gt;claude&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/asgi"&gt;asgi&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llms"&gt;llms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/open-source"&gt;open-source&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/coding-agents"&gt;coding-agents&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai-assisted-programming"&gt;ai-assisted-programming&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/generative-ai"&gt;generative-ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/agentic-engineering"&gt;agentic-engineering&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/kim-christie"&gt;kim-christie&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/starlette"&gt;starlette&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="skills"/><category term="claude"/><category term="asgi"/><category term="ai"/><category term="llms"/><category term="open-source"/><category term="coding-agents"/><category term="ai-assisted-programming"/><category term="python"/><category term="generative-ai"/><category term="agentic-engineering"/><category term="kim-christie"/><category term="starlette"/></entry><entry><title>Thoughts on OpenAI acquiring Astral and uv/ruff/ty</title><link href="https://simonwillison.net/2026/Mar/19/openai-acquiring-astral/#atom-tag" rel="alternate"/><published>2026-03-19T16:45:15+00:00</published><updated>2026-03-19T16:45:15+00:00</updated><id>https://simonwillison.net/2026/Mar/19/openai-acquiring-astral/#atom-tag</id><summary type="html">
    &lt;p&gt;The big news this morning: &lt;a href="https://astral.sh/blog/openai"&gt;Astral to join OpenAI&lt;/a&gt; (on the Astral blog) and &lt;a href="https://openai.com/index/openai-to-acquire-astral/"&gt;OpenAI to acquire Astral&lt;/a&gt; (the OpenAI announcement). Astral are the company behind &lt;a href="https://simonwillison.net/tags/uv/"&gt;uv&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ruff/"&gt;ruff&lt;/a&gt;, and &lt;a href="https://simonwillison.net/tags/ty/"&gt;ty&lt;/a&gt; - three increasingly load-bearing open source projects in the Python ecosystem. I have thoughts!&lt;/p&gt;
&lt;h4 id="the-official-line-from-openai-and-astral"&gt;The official line from OpenAI and Astral&lt;/h4&gt;
&lt;p&gt;The Astral team will become part of the Codex team at OpenAI.&lt;/p&gt;
&lt;p&gt;Charlie Marsh &lt;a href="https://astral.sh/blog/openai"&gt;has this to say&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Open source is at the heart of that impact and the heart of that story; it sits at the center of everything we do. In line with our philosophy and &lt;a href="https://openai.com/index/openai-to-acquire-astral/"&gt;OpenAI's own announcement&lt;/a&gt;, OpenAI will continue supporting our open source tools after the deal closes. We'll keep building in the open, alongside our community -- and for the broader Python ecosystem -- just as we have from the start. [...]&lt;/p&gt;
&lt;p&gt;After joining the Codex team, we'll continue building our open source tools, explore ways they can work more seamlessly with Codex, and expand our reach to think more broadly about the future of software development.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;OpenAI's message &lt;a href="https://openai.com/index/openai-to-acquire-astral/"&gt;has a slightly different focus&lt;/a&gt; (highlights mine):&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;As part of our developer-first philosophy, after closing OpenAI plans to support Astral’s open source products. &lt;strong&gt;By bringing Astral’s tooling and engineering expertise to OpenAI, we will accelerate our work on Codex&lt;/strong&gt; and expand what AI can do across the software development lifecycle.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This is a slightly confusing message. The &lt;a href="https://github.com/openai/codex"&gt;Codex CLI&lt;/a&gt; is a Rust application, and Astral have some of the best Rust engineers in the industry - &lt;a href="https://github.com/burntsushi"&gt;BurntSushi&lt;/a&gt; alone (&lt;a href="https://github.com/rust-lang/regex"&gt;Rust regex&lt;/a&gt;, &lt;a href="https://github.com/BurntSushi/ripgrep"&gt;ripgrep&lt;/a&gt;, &lt;a href="https://github.com/BurntSushi/jiff"&gt;jiff&lt;/a&gt;) may be worth the price of acquisition!&lt;/p&gt;
&lt;p&gt;So is this about the talent or about the product? I expect both, but I know from past experience that a product+talent acquisition can turn into a talent-only acquisition later on.&lt;/p&gt;
&lt;h4 id="uv-is-the-big-one"&gt;uv is the big one&lt;/h4&gt;
&lt;p&gt;Of Astral's projects the most impactful is &lt;a href="https://github.com/astral-sh/uv"&gt;uv&lt;/a&gt;. If you're not familiar with it, &lt;code&gt;uv&lt;/code&gt; is by far the most convincing solution to Python's environment management problems, best illustrated by &lt;a href="https://xkcd.com/1987/"&gt;this classic XKCD&lt;/a&gt;:&lt;/p&gt;
&lt;p style="text-align: center"&gt;&lt;img src="https://imgs.xkcd.com/comics/python_environment.png" alt="xkcd comic showing a tangled, chaotic flowchart of Python environment paths and installations. Nodes include &amp;quot;PIP&amp;quot;, &amp;quot;EASY_INSTALL&amp;quot;, &amp;quot;$PYTHONPATH&amp;quot;, &amp;quot;ANACONDA PYTHON&amp;quot;, &amp;quot;ANOTHER PIP??&amp;quot;, &amp;quot;HOMEBREW PYTHON (2.7)&amp;quot;, &amp;quot;OS PYTHON&amp;quot;, &amp;quot;HOMEBREW PYTHON (3.6)&amp;quot;, &amp;quot;PYTHON.ORG BINARY (2.6)&amp;quot;, and &amp;quot;(MISC FOLDERS OWNED BY ROOT)&amp;quot; connected by a mess of overlapping arrows. A stick figure with a &amp;quot;?&amp;quot; stands at the top left. Paths at the bottom include &amp;quot;/usr/local/Cellar&amp;quot;, &amp;quot;/usr/local/opt&amp;quot;, &amp;quot;/usr/local/lib/python3.6&amp;quot;, &amp;quot;/usr/local/lib/python2.7&amp;quot;, &amp;quot;/python/&amp;quot;, &amp;quot;/newenv/&amp;quot;, &amp;quot;$PATH&amp;quot;, &amp;quot;????&amp;quot;, and &amp;quot;/(A BUNCH OF PATHS WITH &amp;quot;FRAMEWORKS&amp;quot; IN THEM SOMEWHERE)/&amp;quot;. Caption reads: &amp;quot;MY PYTHON ENVIRONMENT HAS BECOME SO DEGRADED THAT MY LAPTOP HAS BEEN DECLARED A SUPERFUND SITE.&amp;quot;" style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;Switch from &lt;code&gt;python&lt;/code&gt; to &lt;code&gt;uv run&lt;/code&gt; and most of these problems go away. I've been using it extensively for the past couple of years and it's become an essential part of my workflow.&lt;/p&gt;
&lt;p&gt;I'm not alone in this. According to PyPI Stats &lt;a href="https://pypistats.org/packages/uv"&gt;uv was downloaded&lt;/a&gt; more than 126 million times last month! Since its release in February 2024 - just two years ago - it's become one of the most popular tools for running Python code.&lt;/p&gt;
&lt;h4 id="ruff-and-ty"&gt;Ruff and ty&lt;/h4&gt;
&lt;p&gt;Astral's two other big projects are &lt;a href="https://github.com/astral-sh/ruff"&gt;ruff&lt;/a&gt; - a Python linter and formatter - and &lt;a href="https://github.com/astral-sh/ty"&gt;ty&lt;/a&gt; - a fast Python type checker.&lt;/p&gt;
&lt;p&gt;These are popular tools that provide a great developer experience but they aren't load-bearing in the same way that &lt;code&gt;uv&lt;/code&gt; is.&lt;/p&gt;
&lt;p&gt;They do however resonate well with coding agent tools like Codex - giving an agent access to fast linting and type checking tools can help improve the quality of the code they generate.&lt;/p&gt;
&lt;p&gt;I'm not convinced that integrating them &lt;em&gt;into&lt;/em&gt; the coding agent itself as opposed to telling it when to run them will make a meaningful difference, but I may just not be imaginative enough here.&lt;/p&gt;
&lt;h4 id="what-of-pyx-"&gt;What of pyx?&lt;/h4&gt;
&lt;p&gt;Ever since &lt;code&gt;uv&lt;/code&gt; started to gain traction the Python community has been worrying about the strategic risk of a single VC-backed company owning a key piece of Python infrastructure. I &lt;a href="https://simonwillison.net/2024/Sep/8/uv-under-discussion-on-mastodon/"&gt;wrote about&lt;/a&gt; one of those conversations in detail back in September 2024.&lt;/p&gt;
&lt;p&gt;The conversation back then focused on what Astral's business plan could be, which started to take form &lt;a href="https://simonwillison.net/2025/Aug/13/pyx/"&gt;in August 2025&lt;/a&gt; when they announced &lt;a href="https://astral.sh/pyx"&gt;pyx&lt;/a&gt;, their private PyPI-style package registry for organizations.&lt;/p&gt;
&lt;p&gt;I'm less convinced that pyx makes sense within OpenAI, and it's notably absent from both the Astral and OpenAI announcement posts.&lt;/p&gt;
&lt;h4 id="competitive-dynamics"&gt;Competitive dynamics&lt;/h4&gt;
&lt;p&gt;An interesting aspect of this deal is how it might impact the competition between Anthropic and OpenAI.&lt;/p&gt;
&lt;p&gt;Both companies spent most of 2025 focused on improving the coding ability of their models, resulting in the &lt;a href="https://simonwillison.net/tags/november-2025-inflection/"&gt;November 2025 inflection point&lt;/a&gt; when coding agents went from often-useful to almost-indispensable tools for software development.&lt;/p&gt;
&lt;p&gt;The competition between Anthropic's Claude Code and OpenAI's Codex is &lt;em&gt;fierce&lt;/em&gt;. Those $200/month subscriptions add up to billions of dollars a year in revenue, for companies that very much need that money.&lt;/p&gt;
&lt;p&gt;Anthropic &lt;a href="https://www.anthropic.com/news/anthropic-acquires-bun-as-claude-code-reaches-usd1b-milestone"&gt;acquired the Bun JavaScript runtime&lt;/a&gt; in December 2025, an acquisition that looks somewhat similar in shape to Astral.&lt;/p&gt;
&lt;p&gt;Bun was already a core component of Claude Code and that acquisition looked to mainly be about ensuring that a crucial dependency stayed actively maintained. Claude Code's performance has increased significantly since then thanks to the efforts of Bun's Jarred Sumner.&lt;/p&gt;
&lt;p&gt;One bad version of this deal would be if OpenAI start using their ownership of &lt;code&gt;uv&lt;/code&gt; as leverage in their competition with Anthropic.&lt;/p&gt;
&lt;h4 id="astral-s-quiet-series-a-and-b"&gt;Astral's quiet series A and B&lt;/h4&gt;
&lt;p&gt;One detail that caught my eye from Astral's announcement, in the section thanking the team, investors, and community:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Second, to our investors, especially &lt;a href="https://www.accel.com/team/casey-aylward#bay-area"&gt;Casey Aylward&lt;/a&gt; from Accel, who led our Seed and Series A, and &lt;a href="https://a16z.com/author/jennifer-li/"&gt;Jennifer Li&lt;/a&gt; from Andreessen Horowitz, who led our Series B. As a first-time, technical, solo founder, you showed far more belief in me than I ever showed in myself, and I will never forget that.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;As far as I can tell neither the Series A nor the Series B were previously announced - I've only been able to find coverage of the original seed round &lt;a href="https://astral.sh/blog/announcing-astral-the-company-behind-ruff"&gt;from April 2023&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Those investors presumably now get to exchange their stake in Astral for a piece of OpenAI. I wonder how much influence they had on Astral's decision to sell.&lt;/p&gt;
&lt;h4 id="forking-as-a-credible-exit-"&gt;Forking as a credible exit?&lt;/h4&gt;
&lt;p&gt;Armin Ronacher built &lt;a href="https://til.simonwillison.net/python/rye"&gt;Rye&lt;/a&gt;, which was later taken over by Astral and effectively merged with uv. In &lt;a href="https://lucumr.pocoo.org/2024/8/21/harvest-season/"&gt;August 2024&lt;/a&gt; he wrote about the risk involved in a VC-backed company owning a key piece of open source infrastructure and said the following (highlight mine):&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;However having seen the code and what uv is doing, &lt;strong&gt;even in the worst possible future this is a very forkable and maintainable thing&lt;/strong&gt;. I believe that even in case Astral shuts down or were to do something incredibly dodgy licensing wise, the community would be better off than before uv existed.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Astral's own Douglas Creager &lt;a href="https://news.ycombinator.com/item?id=47438723#47439974"&gt;emphasized this angle on Hacker News today&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;All I can say is that &lt;em&gt;right now&lt;/em&gt;, we're committed to maintaining our open-source tools with the same level of effort, care, and attention to detail as before. That does not change with this acquisition. No one can guarantee how motives, incentives, and decisions might change years down the line. But that's why we bake optionality into it with the tools being permissively licensed. That makes the worst-case scenarios have the shape of "fork and move on", and not "software disappears forever".&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I like and trust the Astral team and I'm optimistic that their projects will be well-maintained in their new home.&lt;/p&gt;
&lt;p&gt;OpenAI don't yet have much of a track record with respect to acquiring and maintaining open source projects. They've been on a bit of an acquisition spree over the past three months though, snapping up &lt;a href="https://openai.com/index/openai-to-acquire-promptfoo/"&gt;Promptfoo&lt;/a&gt; and &lt;a href="https://steipete.me/posts/2026/openclaw"&gt;OpenClaw&lt;/a&gt; (sort-of, they hired creator Peter Steinberger and are spinning OpenClaw off to a foundation), plus closed source LaTeX platform &lt;a href="https://openai.com/index/introducing-prism/"&gt;Crixet (now Prism)&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;If things do go south for &lt;code&gt;uv&lt;/code&gt; and the other Astral projects we'll get to see how credible the forking exit strategy turns out to be.&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/ty"&gt;ty&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/uv"&gt;uv&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/openai"&gt;openai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/astral"&gt;astral&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ruff"&gt;ruff&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/codex-cli"&gt;codex-cli&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/rust"&gt;rust&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/coding-agents"&gt;coding-agents&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/charlie-marsh"&gt;charlie-marsh&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="ty"/><category term="uv"/><category term="openai"/><category term="astral"/><category term="ai"/><category term="ruff"/><category term="codex-cli"/><category term="rust"/><category term="coding-agents"/><category term="python"/><category term="charlie-marsh"/></entry><entry><title>Quoting Ken Jin</title><link href="https://simonwillison.net/2026/Mar/17/ken-jin/#atom-tag" rel="alternate"/><published>2026-03-17T21:48:26+00:00</published><updated>2026-03-17T21:48:26+00:00</updated><id>https://simonwillison.net/2026/Mar/17/ken-jin/#atom-tag</id><summary type="html">
    &lt;blockquote cite="https://fidget-spinner.github.io/posts/jit-on-track.html"&gt;&lt;p&gt;Great news—we’ve hit our (very modest) performance goals for the CPython JIT over a year early for macOS AArch64, and a few months early for x86_64 Linux. The 3.15 alpha JIT is about &lt;strong&gt;11-12%&lt;/strong&gt; faster on macOS AArch64 than the tail calling interpreter, and &lt;strong&gt;5-6%&lt;/strong&gt;faster than the standard interpreter on x86_64 Linux.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p class="cite"&gt;&amp;mdash; &lt;a href="https://fidget-spinner.github.io/posts/jit-on-track.html"&gt;Ken Jin&lt;/a&gt;, Python 3.15’s JIT is now back on track&lt;/p&gt;

    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;&lt;/p&gt;



</summary><category term="python"/></entry><entry><title>Coding agents for data analysis</title><link href="https://simonwillison.net/2026/Mar/16/coding-agents-for-data-analysis/#atom-tag" rel="alternate"/><published>2026-03-16T20:12:32+00:00</published><updated>2026-03-16T20:12:32+00:00</updated><id>https://simonwillison.net/2026/Mar/16/coding-agents-for-data-analysis/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://simonw.github.io/nicar-2026-coding-agents/"&gt;Coding agents for data analysis&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Here's the handout I prepared for my NICAR 2026 workshop "Coding agents for data analysis" - a three hour session aimed at data journalists demonstrating ways that tools like Claude Code and OpenAI Codex can be used to explore, analyze and clean data.&lt;/p&gt;
&lt;p&gt;Here's the table of contents:&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://simonw.github.io/nicar-2026-coding-agents/coding-agents.html"&gt;Coding agents&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://simonw.github.io/nicar-2026-coding-agents/warmup.html"&gt;Warmup: ChatGPT and Claude&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://simonw.github.io/nicar-2026-coding-agents/setup.html"&gt;Setup Claude Code and Codex&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://simonw.github.io/nicar-2026-coding-agents/asking-questions.html"&gt;Asking questions against a database&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://simonw.github.io/nicar-2026-coding-agents/exploring-data.html"&gt;Exploring data with agents&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://simonw.github.io/nicar-2026-coding-agents/cleaning-trees.html"&gt;Cleaning data: decoding neighborhood codes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://simonw.github.io/nicar-2026-coding-agents/visualizations.html"&gt;Creating visualizations with agents&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://simonw.github.io/nicar-2026-coding-agents/scraping.html"&gt;Scraping data with agents&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;I ran the workshop using GitHub Codespaces and OpenAI Codex, since it was easy (and inexpensive) to distribute a budget-restricted API key for Codex that attendees could use during the class. Participants ended up burning $23 of Codex tokens.&lt;/p&gt;
&lt;p&gt;The exercises all used Python and SQLite and some of them used Datasette.&lt;/p&gt;
&lt;p&gt;One highlight of the workshop was when we started &lt;a href="https://simonw.github.io/nicar-2026-coding-agents/visualizations.html#javascript-visualizations"&gt;running Datasette&lt;/a&gt; such that it served static content from a &lt;code&gt;viz/&lt;/code&gt; folder, then had Claude Code start vibe coding new interactive visualizations directly in that folder. Here's a heat map it created for my trees database using Leaflet and &lt;a href="https://github.com/Leaflet/Leaflet.heat"&gt;Leaflet.heat&lt;/a&gt;, &lt;a href="https://gist.github.com/simonw/985ae2a6a3cd3df3fd375eb58dabea0f"&gt;source code here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Screenshot of a &amp;quot;Trees SQL Map&amp;quot; web application with the heading &amp;quot;Trees SQL Map&amp;quot; and subheading &amp;quot;Run a query and render all returned points as a heat map. The default query targets roughly 200,000 trees.&amp;quot; Below is an input field containing &amp;quot;/trees/-/query.json&amp;quot;, a &amp;quot;Run Query&amp;quot; button, and a SQL query editor with the text &amp;quot;SELECT cast(Latitude AS float) AS latitude, cast(Longitude AS float) AS longitude, CASE WHEN DBH IS NULL OR DBH = '' THEN 0.3 WHEN cast(DBH AS float) &amp;lt;= 0 THEN 0.3 WHEN cast(DBH AS float) &amp;gt;= 80 THEN 1.0&amp;quot; (query is truncated). A status message reads &amp;quot;Loaded 1,000 rows and plotted 1,000 points as heat map.&amp;quot; Below is a Leaflet/OpenStreetMap interactive map of San Francisco showing a heat map overlay of tree locations, with blue/green clusters concentrated in areas like the Richmond District, Sunset District, and other neighborhoods. Map includes zoom controls and a &amp;quot;Leaflet | © OpenStreetMap contributors&amp;quot; attribution." src="https://static.simonwillison.net/static/2026/tree-sql-map.jpg" /&gt;&lt;/p&gt;
&lt;p&gt;I designed the handout to also be useful for people who weren't able to attend the session in person. As is usually the case, material aimed at data journalists is equally applicable to anyone else with data to explore.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/nicar"&gt;nicar&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sqlite"&gt;sqlite&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/speaking"&gt;speaking&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llms"&gt;llms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/coding-agents"&gt;coding-agents&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/generative-ai"&gt;generative-ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/data-journalism"&gt;data-journalism&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/github-codespaces"&gt;github-codespaces&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/codex-cli"&gt;codex-cli&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/claude-code"&gt;claude-code&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/leaflet"&gt;leaflet&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/geospatial"&gt;geospatial&lt;/a&gt;&lt;/p&gt;



</summary><category term="nicar"/><category term="sqlite"/><category term="ai"/><category term="speaking"/><category term="llms"/><category term="coding-agents"/><category term="generative-ai"/><category term="data-journalism"/><category term="github-codespaces"/><category term="codex-cli"/><category term="datasette"/><category term="claude-code"/><category term="python"/><category term="leaflet"/><category term="geospatial"/></entry><entry><title>Quoting Jannis Leidel</title><link href="https://simonwillison.net/2026/Mar/14/jannis-leidel/#atom-tag" rel="alternate"/><published>2026-03-14T18:41:25+00:00</published><updated>2026-03-14T18:41:25+00:00</updated><id>https://simonwillison.net/2026/Mar/14/jannis-leidel/#atom-tag</id><summary type="html">
    &lt;blockquote cite="https://jazzband.co/news/2026/03/14/sunsetting-jazzband"&gt;&lt;p&gt;GitHub’s &lt;a href="https://www.theregister.com/2026/02/18/godot_maintainers_struggle_with_draining/"&gt;slopocalypse&lt;/a&gt; – the flood of AI-generated spam PRs and issues – has made Jazzband’s model of open membership and shared push access untenable.&lt;/p&gt;
&lt;p&gt;Jazzband was designed for a world where the worst case was someone accidentally merging the wrong PR. In a world where &lt;a href="https://www.devclass.com/ai-ml/2026/02/19/github-itself-to-blame-for-ai-slop-prs-say-devs/4091420"&gt;only 1 in 10 AI-generated PRs meets project standards&lt;/a&gt;, where curl had to &lt;a href="https://daniel.haxx.se/blog/2026/01/26/the-end-of-the-curl-bug-bounty/"&gt;shut down its bug bounty&lt;/a&gt; because confirmation rates dropped below 5%, and where GitHub’s own response was a &lt;a href="https://www.theregister.com/2026/02/03/github_kill_switch_pull_requests_ai"&gt;kill switch to disable pull requests entirely&lt;/a&gt; – an organization that gives push access to everyone who joins simply can’t operate safely anymore.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p class="cite"&gt;&amp;mdash; &lt;a href="https://jazzband.co/news/2026/03/14/sunsetting-jazzband"&gt;Jannis Leidel&lt;/a&gt;, Sunsetting Jazzband&lt;/p&gt;

    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/ai-ethics"&gt;ai-ethics&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/open-source"&gt;open-source&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/github"&gt;github&lt;/a&gt;&lt;/p&gt;



</summary><category term="ai-ethics"/><category term="open-source"/><category term="python"/><category term="ai"/><category term="github"/></entry><entry><title>An AI agent coding skeptic tries AI agent coding, in excessive detail</title><link href="https://simonwillison.net/2026/Feb/27/ai-agent-coding-in-excessive-detail/#atom-tag" rel="alternate"/><published>2026-02-27T20:43:41+00:00</published><updated>2026-02-27T20:43:41+00:00</updated><id>https://simonwillison.net/2026/Feb/27/ai-agent-coding-in-excessive-detail/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://minimaxir.com/2026/02/ai-agent-coding/"&gt;An AI agent coding skeptic tries AI agent coding, in excessive detail&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Another in the genre of "OK, coding agents got good in November" posts, this one is by Max Woolf and is very much worth your time. He describes a sequence of coding agent projects, each more ambitious than the last - starting with simple YouTube metadata scrapers and eventually evolving to this:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;It would be arrogant to port Python's &lt;a href="https://scikit-learn.org/stable/"&gt;scikit-learn&lt;/a&gt; — the gold standard of data science and machine learning libraries — to Rust with all the features that implies.&lt;/p&gt;
&lt;p&gt;But that's unironically a good idea so I decided to try and do it anyways. With the use of agents, I am now developing &lt;code&gt;rustlearn&lt;/code&gt; (extreme placeholder name), a Rust crate that implements not only the fast implementations of the standard machine learning algorithms such as &lt;a href="https://en.wikipedia.org/wiki/Logistic_regression"&gt;logistic regression&lt;/a&gt; and &lt;a href="https://en.wikipedia.org/wiki/K-means_clustering"&gt;k-means clustering&lt;/a&gt;, but also includes the fast implementations of the algorithms above: the same three step pipeline I describe above still works even with the more simple algorithms to beat scikit-learn's implementations.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Max also captures the frustration of trying to explain how good the models have got to an existing skeptical audience:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The real annoying thing about Opus 4.6/Codex 5.3 is that it’s impossible to publicly say “Opus 4.5 (and the models that came after it) are an order of magnitude better than coding LLMs released just months before it” without sounding like an AI hype booster clickbaiting, but it’s the counterintuitive truth to my personal frustration. I have been trying to break this damn model by giving it complex tasks that would take me months to do by myself despite my coding pedigree but Opus and Codex keep doing them correctly.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;A throwaway remark in this post inspired me to &lt;a href="https://github.com/simonw/research/tree/main/rust-wordcloud#readme"&gt;ask Claude Code to build a Rust word cloud CLI tool&lt;/a&gt;, which it happily did.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/max-woolf"&gt;max-woolf&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/coding-agents"&gt;coding-agents&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai-assisted-programming"&gt;ai-assisted-programming&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/generative-ai"&gt;generative-ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/agentic-engineering"&gt;agentic-engineering&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llms"&gt;llms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/november-2025-inflection"&gt;november-2025-inflection&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/rust"&gt;rust&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;&lt;/p&gt;



</summary><category term="max-woolf"/><category term="coding-agents"/><category term="ai-assisted-programming"/><category term="generative-ai"/><category term="agentic-engineering"/><category term="ai"/><category term="llms"/><category term="november-2025-inflection"/><category term="rust"/><category term="python"/></entry><entry><title>Em dash</title><link href="https://simonwillison.net/2026/Feb/15/em-dashes/#atom-tag" rel="alternate"/><published>2026-02-15T21:40:46+00:00</published><updated>2026-02-15T21:40:46+00:00</updated><id>https://simonwillison.net/2026/Feb/15/em-dashes/#atom-tag</id><summary type="html">
    &lt;p&gt;I'm occasionally accused of using LLMs to write the content on my blog. I don't do that, and I don't think my writing has much of an LLM smell to it... with one notable exception:&lt;/p&gt;
&lt;pre&gt;    &lt;span class="pl-c"&gt;# Finally, do em dashes&lt;/span&gt;
    &lt;span class="pl-s1"&gt;s&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;s&lt;/span&gt;.&lt;span class="pl-c1"&gt;replace&lt;/span&gt;(&lt;span class="pl-s"&gt;' - '&lt;/span&gt;, &lt;span class="pl-s"&gt;u'&lt;span class="pl-cce"&gt;\u2014&lt;/span&gt;'&lt;/span&gt;)&lt;/pre&gt;

&lt;p&gt;That code to add em dashes to my posts dates back to &lt;a href="https://github.com/simonw/simonwillisonblog/blob/e6d0327b37debdf820b5cfef4fb7d09a9624cea9/blog/templatetags/entry_tags.py#L145-L146"&gt;at least 2015&lt;/a&gt; when I ported my blog from an older version of Django (in a long-lost Mercurial repository) and started afresh on GitHub.&lt;/p&gt;

    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/generative-ai"&gt;generative-ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/typography"&gt;typography&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/blogging"&gt;blogging&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llms"&gt;llms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;&lt;/p&gt;



</summary><category term="generative-ai"/><category term="typography"/><category term="blogging"/><category term="ai"/><category term="llms"/><category term="python"/></entry><entry><title>cysqlite - a new sqlite driver</title><link href="https://simonwillison.net/2026/Feb/11/cysqlite/#atom-tag" rel="alternate"/><published>2026-02-11T17:34:40+00:00</published><updated>2026-02-11T17:34:40+00:00</updated><id>https://simonwillison.net/2026/Feb/11/cysqlite/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://charlesleifer.com/blog/cysqlite---a-new-sqlite-driver/"&gt;cysqlite - a new sqlite driver&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Charles Leifer has been maintaining &lt;a href="https://github.com/coleifer/pysqlite3"&gt;pysqlite3&lt;/a&gt; - a fork of the Python standard library's &lt;code&gt;sqlite3&lt;/code&gt; module that makes it much easier to run upgraded SQLite versions - since 2018.&lt;/p&gt;
&lt;p&gt;He's been working on a ground-up &lt;a href="https://cython.org/"&gt;Cython&lt;/a&gt; rewrite called &lt;a href="https://github.com/coleifer/cysqlite"&gt;cysqlite&lt;/a&gt; for almost as long, but it's finally at a stage where it's ready for people to try out.&lt;/p&gt;
&lt;p&gt;The biggest change from the &lt;code&gt;sqlite3&lt;/code&gt; module involves transactions. Charles explains his discomfort with the &lt;code&gt;sqlite3&lt;/code&gt; implementation at length - that library provides two different variants neither of which exactly match the autocommit mechanism in SQLite itself.&lt;/p&gt;
&lt;p&gt;I'm particularly excited about the support for &lt;a href="https://cysqlite.readthedocs.io/en/latest/api.html#tablefunction"&gt;custom virtual tables&lt;/a&gt;, a feature I'd love to see in &lt;code&gt;sqlite3&lt;/code&gt; itself.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;cysqlite&lt;/code&gt; provides a Python extension compiled from C, which means it normally wouldn't be available in Pyodide. I &lt;a href="https://github.com/simonw/research/tree/main/cysqlite-wasm-wheel"&gt;set Claude Code on it&lt;/a&gt; (here's &lt;a href="https://github.com/simonw/research/pull/79#issue-3923792518"&gt;the prompt&lt;/a&gt;) and it built me &lt;a href="https://github.com/simonw/research/blob/main/cysqlite-wasm-wheel/cysqlite-0.1.4-cp311-cp311-emscripten_3_1_46_wasm32.whl"&gt;cysqlite-0.1.4-cp311-cp311-emscripten_3_1_46_wasm32.whl&lt;/a&gt;, a 688KB wheel file with a WASM build of the library that can be loaded into Pyodide like this:&lt;/p&gt;
&lt;pre&gt;&lt;span class="pl-k"&gt;import&lt;/span&gt; &lt;span class="pl-s1"&gt;micropip&lt;/span&gt;
&lt;span class="pl-k"&gt;await&lt;/span&gt; &lt;span class="pl-s1"&gt;micropip&lt;/span&gt;.&lt;span class="pl-c1"&gt;install&lt;/span&gt;(
    &lt;span class="pl-s"&gt;"https://simonw.github.io/research/cysqlite-wasm-wheel/cysqlite-0.1.4-cp311-cp311-emscripten_3_1_46_wasm32.whl"&lt;/span&gt;
)
&lt;span class="pl-k"&gt;import&lt;/span&gt; &lt;span class="pl-s1"&gt;cysqlite&lt;/span&gt;
&lt;span class="pl-en"&gt;print&lt;/span&gt;(&lt;span class="pl-s1"&gt;cysqlite&lt;/span&gt;.&lt;span class="pl-c1"&gt;connect&lt;/span&gt;(&lt;span class="pl-s"&gt;":memory:"&lt;/span&gt;).&lt;span class="pl-c1"&gt;execute&lt;/span&gt;(
    &lt;span class="pl-s"&gt;"select sqlite_version()"&lt;/span&gt;
).&lt;span class="pl-c1"&gt;fetchone&lt;/span&gt;())&lt;/pre&gt;

&lt;p&gt;(I also learned that wheels like this have to be built for the emscripten version used by that edition of Pyodide - my experimental wheel loads in Pyodide 0.25.1 but fails in 0.27.5 with a &lt;code&gt;Wheel was built with Emscripten v3.1.46 but Pyodide was built with Emscripten v3.1.58&lt;/code&gt; error.)&lt;/p&gt;
&lt;p&gt;You can try my wheel in &lt;a href="https://7ebbff98.tools-b1q.pages.dev/pyodide-repl"&gt;this new Pyodide REPL&lt;/a&gt; i had Claude build as a mobile-friendly alternative to Pyodide's &lt;a href="https://pyodide.org/en/stable/console.html"&gt;own hosted console&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I also had Claude build &lt;a href="https://simonw.github.io/research/cysqlite-wasm-wheel/demo.html"&gt;this demo page&lt;/a&gt; that executes the original test suite in the browser and displays the results:&lt;/p&gt;
&lt;p&gt;&lt;img alt="Screenshot of the cysqlite WebAssembly Demo page with a dark theme. Title reads &amp;quot;cysqlite — WebAssembly Demo&amp;quot; with subtitle &amp;quot;Testing cysqlite compiled to WebAssembly via Emscripten, running in Pyodide in the browser.&amp;quot; Environment section shows Pyodide 0.25.1, Python 3.11.3, cysqlite 0.1.4, SQLite 3.51.2, Platform Emscripten-3.1.46-wasm32-32bit, Wheel file cysqlite-0.1.4-cp311-cp311-emscripten_3_1_46_wasm32.wh (truncated). A green progress bar shows &amp;quot;All 115 tests passed! (1 skipped)&amp;quot; at 100%, with Passed: 115, Failed: 0, Errors: 0, Skipped: 1, Total: 116. Test Results section lists TestBackup 1/1 passed, TestBlob 6/6 passed, TestCheckConnection 4/4 passed, TestDataTypesTableFunction 1/1 passed, all with green badges." src="https://static.simonwillison.net/static/2026/cysqlite-tests.jpg" /&gt;

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://lobste.rs/s/gipvta/cysqlite_new_sqlite_driver"&gt;lobste.rs&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/charles-leifer"&gt;charles-leifer&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/pyodide"&gt;pyodide&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/webassembly"&gt;webassembly&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sqlite"&gt;sqlite&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai-assisted-programming"&gt;ai-assisted-programming&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/claude-code"&gt;claude-code&lt;/a&gt;&lt;/p&gt;



</summary><category term="charles-leifer"/><category term="pyodide"/><category term="webassembly"/><category term="sqlite"/><category term="python"/><category term="ai-assisted-programming"/><category term="claude-code"/></entry><entry><title>Running Pydantic's Monty Rust sandboxed Python subset in WebAssembly</title><link href="https://simonwillison.net/2026/Feb/6/pydantic-monty/#atom-tag" rel="alternate"/><published>2026-02-06T22:31:31+00:00</published><updated>2026-02-06T22:31:31+00:00</updated><id>https://simonwillison.net/2026/Feb/6/pydantic-monty/#atom-tag</id><summary type="html">
    &lt;p&gt;There's a jargon-filled headline for you! Everyone's &lt;a href="https://simonwillison.net/2026/Jan/8/llm-predictions-for-2026/#1-year-we-re-finally-going-to-solve-sandboxing"&gt;building sandboxes&lt;/a&gt; for running untrusted code right now, and Pydantic's latest attempt, &lt;a href="https://github.com/pydantic/monty"&gt;Monty&lt;/a&gt;, provides a custom Python-like language (a subset of Python) in Rust and makes it available as both a Rust library and a Python package. I got it working in WebAssembly, providing a sandbox-in-a-sandbox.&lt;/p&gt;
&lt;p&gt;Here's &lt;a href="https://github.com/pydantic/monty"&gt;how they describe Monty&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Monty avoids the cost, latency, complexity and general faff of using full container based sandbox for running LLM generated code.&lt;/p&gt;
&lt;p&gt;Instead, it let's you safely run Python code written by an LLM embedded in your agent, with startup times measured in single digit microseconds not hundreds of milliseconds.&lt;/p&gt;
&lt;p&gt;What Monty &lt;strong&gt;can&lt;/strong&gt; do:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Run a reasonable subset of Python code - enough for your agent to express what it wants to do&lt;/li&gt;
&lt;li&gt;Completely block access to the host environment: filesystem, env variables and network access are all implemented via external function calls the developer can control&lt;/li&gt;
&lt;li&gt;Call functions on the host - only functions you give it access to [...]&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;A quick way to try it out is via &lt;a href="https://github.com/astral-sh/uv"&gt;uv&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uv run --with pydantic-monty python -m asyncio
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then paste this into the Python interactive prompt - the &lt;code&gt;-m asyncio&lt;/code&gt; enables top-level await:&lt;/p&gt;
&lt;pre&gt;&lt;span&gt;import&lt;/span&gt; &lt;span&gt;pydantic_monty&lt;/span&gt;
&lt;span&gt;code&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;pydantic_monty&lt;/span&gt;.&lt;span&gt;Monty&lt;/span&gt;(&lt;span&gt;'print("hello " + str(4 * 5))'&lt;/span&gt;)
&lt;span&gt;await&lt;/span&gt; &lt;span&gt;pydantic_monty&lt;/span&gt;.&lt;span&gt;run_monty_async&lt;/span&gt;(&lt;span&gt;code&lt;/span&gt;)&lt;/pre&gt;
&lt;p&gt;Monty supports a &lt;em&gt;very&lt;/em&gt; small subset of Python - it doesn't even support class declarations yet!&lt;/p&gt;
&lt;p&gt;But, given its target use-case, that's not actually a problem.&lt;/p&gt;
&lt;p&gt;The neat thing about providing tools like this for LLMs is that they're really good at iterating against error messages. A coding agent can run some Python code, get an error message telling it that classes aren't supported and then try again with a different approach.&lt;/p&gt;
&lt;p&gt;I wanted to try this in a browser, so I fired up &lt;a href="https://simonwillison.net/2025/Nov/6/async-code-research/"&gt;a code research task&lt;/a&gt; in Claude Code for web and kicked it off with the following:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Clone &lt;a href="https://github.com/pydantic/monty"&gt;https://github.com/pydantic/monty&lt;/a&gt; to /tmp and figure out how to compile it into a python WebAssembly wheel that can then be loaded in Pyodide. The wheel file itself should be checked into the repo along with build scripts and passing pytest playwright test scripts that load Pyodide from a CDN and the wheel from a “python -m http.server” localhost and demonstrate it working&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Then a little later:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I want an additional WASM file that works independently of Pyodide, which is also usable in a web browser - build that too along with playwright tests that show it working. Also build two HTML files - one called demo.html and one called pyodide-demo.html - these should work similar to &lt;a href="https://tools.simonwillison.net/micropython"&gt;https://tools.simonwillison.net/micropython&lt;/a&gt; (download that code with curl to inspect it) - one should load the WASM build, the other should load Pyodide and have it use the WASM wheel. These will be served by GitHub Pages so they can load the WASM and wheel from a relative path since the .html files will be served from the same folder as the wheel and WASM file&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Here's &lt;a href="https://gisthost.github.io/?22d88e6367d7e002c4fb383c213c2df2/page-001.html"&gt;the transcript&lt;/a&gt;, and the &lt;a href="https://github.com/simonw/research/tree/main/monty-wasm-pyodide"&gt;final research report&lt;/a&gt; it produced.&lt;/p&gt;
&lt;p&gt;I now have the Monty Rust code compiled to WebAssembly in two different shapes - as a &lt;code&gt;.wasm&lt;/code&gt; bundle you can load and call from JavaScript, and as a &lt;code&gt;monty-wasm-pyodide/pydantic_monty-0.0.3-cp313-cp313-emscripten_4_0_9_wasm32.whl&lt;/code&gt; wheel file which can be loaded into &lt;a href="https://pyodide.org/"&gt;Pyodide&lt;/a&gt; and then called from Python in Pyodide in WebAssembly in a browser.&lt;/p&gt;
&lt;p&gt;Here are those two demos, hosted on GitHub Pages:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://simonw.github.io/research/monty-wasm-pyodide/demo.html"&gt;Monty WASM demo&lt;/a&gt; - a UI over JavaScript that loads the Rust WASM module directly.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://simonw.github.io/research/monty-wasm-pyodide/pyodide-demo.html"&gt;Monty Pyodide demo&lt;/a&gt; - this one provides an identical interface but here the code is &lt;a href="https://github.com/simonw/research/blob/3add1ffec70b530711fa237d91f546da5bcf1f1c/monty-wasm-pyodide/pyodide-demo.html#L257-L280"&gt;loading Pyodide and then installing the Monty WASM wheel&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2026/monty-pyodide.jpg" alt="Screenshot of a web app titled &amp;quot;Monty via Pyodide&amp;quot; with description &amp;quot;Run Monty (a sandboxed Python interpreter by Pydantic) inside Pyodide (CPython compiled to WebAssembly). This loads the pydantic-monty wheel and uses its full Python API. Code is saved in the URL for sharing.&amp;quot; A green banner reads &amp;quot;Code executed successfully!&amp;quot; Below are example buttons labeled &amp;quot;Basic&amp;quot;, &amp;quot;Inputs&amp;quot;, &amp;quot;Reuse&amp;quot;, &amp;quot;Error Handling&amp;quot;, &amp;quot;Fibonacci&amp;quot;, and &amp;quot;Classes&amp;quot;. A code editor labeled &amp;quot;Python Code (runs inside Monty sandbox via Pyodide):&amp;quot; contains: &amp;quot;import pydantic_monty\n\n# Create interpreter with input variables\nm = pydantic_monty.Monty('x + y', inputs=['x', 'y'])\n\n# Run with different inputs\nresult1 = m.run(inputs={&amp;quot;x&amp;quot;: 10, &amp;quot;y&amp;quot;: 20})\nprint(f&amp;quot;10 + 20 = {result1}&amp;quot;)\n\nresult2 = m.run(inputs={&amp;quot;x&amp;quot;: 100, &amp;quot;y&amp;quot;: 200})&amp;quot; with &amp;quot;Run Code&amp;quot; and &amp;quot;Clear&amp;quot; buttons. The Output section shows &amp;quot;10 + 20 = 30&amp;quot; and &amp;quot;100 + 200 = 300&amp;quot; with a &amp;quot;Copy&amp;quot; button. Footer reads &amp;quot;Executed in 4.0ms&amp;quot;." style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;As a connoisseur of sandboxes - the more options the better! - this new entry from Pydantic ticks a lot of my boxes. It's small, fast, widely available (thanks to Rust and WebAssembly) and provides strict limits on memory usage, CPU time and access to disk and network.&lt;/p&gt;
&lt;p&gt;It was also a great excuse to spin up another demo showing how easy it is these days to turn compiled code like C or Rust into WebAssembly that runs in both a browser and a Pyodide environment.&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/pydantic"&gt;pydantic&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sandboxing"&gt;sandboxing&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/claude-code"&gt;claude-code&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llms"&gt;llms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/rust"&gt;rust&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/webassembly"&gt;webassembly&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/javascript"&gt;javascript&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai-assisted-programming"&gt;ai-assisted-programming&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/coding-agents"&gt;coding-agents&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/generative-ai"&gt;generative-ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/pyodide"&gt;pyodide&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="pydantic"/><category term="sandboxing"/><category term="ai"/><category term="claude-code"/><category term="llms"/><category term="rust"/><category term="webassembly"/><category term="python"/><category term="javascript"/><category term="ai-assisted-programming"/><category term="coding-agents"/><category term="generative-ai"/><category term="pyodide"/></entry><entry><title>Distributing Go binaries like sqlite-scanner through PyPI using go-to-wheel</title><link href="https://simonwillison.net/2026/Feb/4/distributing-go-binaries/#atom-tag" rel="alternate"/><published>2026-02-04T14:59:47+00:00</published><updated>2026-02-04T14:59:47+00:00</updated><id>https://simonwillison.net/2026/Feb/4/distributing-go-binaries/#atom-tag</id><summary type="html">
    &lt;p&gt;I've been exploring Go for building small, fast and self-contained binary applications recently. I'm enjoying how there's generally one obvious way to do things and the resulting code is boring and readable - and something that LLMs are very competent at writing. The one catch is distribution, but it turns out publishing Go binaries to PyPI means any Go binary can be just a &lt;code&gt;uvx package-name&lt;/code&gt; call away.&lt;/p&gt;
&lt;h4 id="sqlite-scanner"&gt;sqlite-scanner&lt;/h4&gt;
&lt;p&gt;&lt;a href="https://github.com/simonw/sqlite-scanner"&gt;sqlite-scanner&lt;/a&gt; is my new Go CLI tool for scanning a filesystem for SQLite database files.&lt;/p&gt;
&lt;p&gt;It works by checking if the first 16 bytes of the file exactly match the SQLite magic number sequence &lt;code&gt;SQLite format 3\x00&lt;/code&gt;. It can search one or more folders recursively, spinning up concurrent goroutines to accelerate the scan. It streams out results as it finds them in plain text, JSON or newline-delimited JSON. It can optionally display the file sizes as well.&lt;/p&gt;
&lt;p&gt;To try it out you can download a release from the &lt;a href="https://github.com/simonw/sqlite-scanner/releases"&gt;GitHub releases&lt;/a&gt; - and then &lt;a href="https://support.apple.com/en-us/102445"&gt;jump through macOS hoops&lt;/a&gt; to execute an "unsafe" binary. Or you can clone the repo and compile it with Go. Or... you can run the binary like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uvx sqlite-scanner
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;By default this will search your current directory for SQLite databases. You can pass one or more directories as arguments:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uvx sqlite-scanner ~ /tmp
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Add &lt;code&gt;--json&lt;/code&gt; for JSON output, &lt;code&gt;--size&lt;/code&gt; to include file sizes or &lt;code&gt;--jsonl&lt;/code&gt; for newline-delimited JSON. Here's a demo:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uvx sqlite-scanner ~ --jsonl --size
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2025/sqlite-scanner-demo.gif" alt="running that command produces a sequence of JSON objects, each with a path and a size key" style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;If you haven't been uv-pilled yet you can instead install &lt;code&gt;sqlite-scanner&lt;/code&gt; using &lt;code&gt;pip install sqlite-scanner&lt;/code&gt; and then run &lt;code&gt;sqlite-scanner&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;To get a permanent copy with &lt;code&gt;uv&lt;/code&gt; use &lt;code&gt;uv tool install sqlite-scanner&lt;/code&gt;.&lt;/p&gt;
&lt;h4 id="how-the-python-package-works"&gt;How the Python package works&lt;/h4&gt;
&lt;p&gt;The reason this is worth doing is that &lt;code&gt;pip&lt;/code&gt;, &lt;code&gt;uv&lt;/code&gt; and &lt;a href="https://pypi.org/"&gt;PyPI&lt;/a&gt; will work together to identify the correct compiled binary for your operating system and architecture.&lt;/p&gt;
&lt;p&gt;This is driven by file names. If you visit &lt;a href="https://pypi.org/project/sqlite-scanner/#files"&gt;the PyPI downloads for sqlite-scanner&lt;/a&gt; you'll see the following files:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;sqlite_scanner-0.1.1-py3-none-win_arm64.whl&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sqlite_scanner-0.1.1-py3-none-win_amd64.whl&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sqlite_scanner-0.1.1-py3-none-musllinux_1_2_x86_64.whl&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sqlite_scanner-0.1.1-py3-none-musllinux_1_2_aarch64.whl&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sqlite_scanner-0.1.1-py3-none-manylinux_2_17_x86_64.whl&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sqlite_scanner-0.1.1-py3-none-manylinux_2_17_aarch64.whl&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sqlite_scanner-0.1.1-py3-none-macosx_11_0_arm64.whl&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sqlite_scanner-0.1.1-py3-none-macosx_10_9_x86_64.whl&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When I run &lt;code&gt;pip install sqlite-scanner&lt;/code&gt; or &lt;code&gt;uvx sqlite-scanner&lt;/code&gt; on my Apple Silicon Mac laptop Python's packaging magic ensures I get that &lt;code&gt;macosx_11_0_arm64.whl&lt;/code&gt; variant.&lt;/p&gt;
&lt;p&gt;Here's &lt;a href="https://tools.simonwillison.net/zip-wheel-explorer?url=https%3A%2F%2Ffiles.pythonhosted.org%2Fpackages%2F88%2Fb1%2F17a716635d2733fec53ba0a8267f85bd6b6cf882c6b29301bc711fba212c%2Fsqlite_scanner-0.1.1-py3-none-macosx_11_0_arm64.whl#sqlite_scanner/__init__.py"&gt;what's in the wheel&lt;/a&gt;, which is a zip file with a &lt;code&gt;.whl&lt;/code&gt; extension.&lt;/p&gt;
&lt;p&gt;In addition to the &lt;code&gt;bin/sqlite-scanner&lt;/code&gt; the most important file is &lt;code&gt;sqlite_scanner/__init__.py&lt;/code&gt; which includes the following:&lt;/p&gt;
&lt;pre&gt;&lt;span class="pl-k"&gt;def&lt;/span&gt; &lt;span class="pl-en"&gt;get_binary_path&lt;/span&gt;():
    &lt;span class="pl-s"&gt;"""Return the path to the bundled binary."""&lt;/span&gt;
    &lt;span class="pl-s1"&gt;binary&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;os&lt;/span&gt;.&lt;span class="pl-c1"&gt;path&lt;/span&gt;.&lt;span class="pl-c1"&gt;join&lt;/span&gt;(&lt;span class="pl-s1"&gt;os&lt;/span&gt;.&lt;span class="pl-c1"&gt;path&lt;/span&gt;.&lt;span class="pl-c1"&gt;dirname&lt;/span&gt;(&lt;span class="pl-s1"&gt;__file__&lt;/span&gt;), &lt;span class="pl-s"&gt;"bin"&lt;/span&gt;, &lt;span class="pl-s"&gt;"sqlite-scanner"&lt;/span&gt;)
 
    &lt;span class="pl-c"&gt;# Ensure binary is executable on Unix&lt;/span&gt;
    &lt;span class="pl-k"&gt;if&lt;/span&gt; &lt;span class="pl-s1"&gt;sys&lt;/span&gt;.&lt;span class="pl-c1"&gt;platform&lt;/span&gt; &lt;span class="pl-c1"&gt;!=&lt;/span&gt; &lt;span class="pl-s"&gt;"win32"&lt;/span&gt;:
        &lt;span class="pl-s1"&gt;current_mode&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;os&lt;/span&gt;.&lt;span class="pl-c1"&gt;stat&lt;/span&gt;(&lt;span class="pl-s1"&gt;binary&lt;/span&gt;).&lt;span class="pl-c1"&gt;st_mode&lt;/span&gt;
        &lt;span class="pl-k"&gt;if&lt;/span&gt; &lt;span class="pl-c1"&gt;not&lt;/span&gt; (&lt;span class="pl-s1"&gt;current_mode&lt;/span&gt; &lt;span class="pl-c1"&gt;&amp;amp;&lt;/span&gt; &lt;span class="pl-s1"&gt;stat&lt;/span&gt;.&lt;span class="pl-c1"&gt;S_IXUSR&lt;/span&gt;):
            &lt;span class="pl-s1"&gt;os&lt;/span&gt;.&lt;span class="pl-c1"&gt;chmod&lt;/span&gt;(&lt;span class="pl-s1"&gt;binary&lt;/span&gt;, &lt;span class="pl-s1"&gt;current_mode&lt;/span&gt; &lt;span class="pl-c1"&gt;|&lt;/span&gt; &lt;span class="pl-s1"&gt;stat&lt;/span&gt;.&lt;span class="pl-c1"&gt;S_IXUSR&lt;/span&gt; &lt;span class="pl-c1"&gt;|&lt;/span&gt; &lt;span class="pl-s1"&gt;stat&lt;/span&gt;.&lt;span class="pl-c1"&gt;S_IXGRP&lt;/span&gt; &lt;span class="pl-c1"&gt;|&lt;/span&gt; &lt;span class="pl-s1"&gt;stat&lt;/span&gt;.&lt;span class="pl-c1"&gt;S_IXOTH&lt;/span&gt;)
 
    &lt;span class="pl-k"&gt;return&lt;/span&gt; &lt;span class="pl-s1"&gt;binary&lt;/span&gt;
 
 
&lt;span class="pl-k"&gt;def&lt;/span&gt; &lt;span class="pl-en"&gt;main&lt;/span&gt;():
    &lt;span class="pl-s"&gt;"""Execute the bundled binary."""&lt;/span&gt;
    &lt;span class="pl-s1"&gt;binary&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-en"&gt;get_binary_path&lt;/span&gt;()
 
    &lt;span class="pl-k"&gt;if&lt;/span&gt; &lt;span class="pl-s1"&gt;sys&lt;/span&gt;.&lt;span class="pl-c1"&gt;platform&lt;/span&gt; &lt;span class="pl-c1"&gt;==&lt;/span&gt; &lt;span class="pl-s"&gt;"win32"&lt;/span&gt;:
        &lt;span class="pl-c"&gt;# On Windows, use subprocess to properly handle signals&lt;/span&gt;
        &lt;span class="pl-s1"&gt;sys&lt;/span&gt;.&lt;span class="pl-c1"&gt;exit&lt;/span&gt;(&lt;span class="pl-s1"&gt;subprocess&lt;/span&gt;.&lt;span class="pl-c1"&gt;call&lt;/span&gt;([&lt;span class="pl-s1"&gt;binary&lt;/span&gt;] &lt;span class="pl-c1"&gt;+&lt;/span&gt; &lt;span class="pl-s1"&gt;sys&lt;/span&gt;.&lt;span class="pl-c1"&gt;argv&lt;/span&gt;[&lt;span class="pl-c1"&gt;1&lt;/span&gt;:]))
    &lt;span class="pl-k"&gt;else&lt;/span&gt;:
        &lt;span class="pl-c"&gt;# On Unix, exec replaces the process&lt;/span&gt;
        &lt;span class="pl-s1"&gt;os&lt;/span&gt;.&lt;span class="pl-c1"&gt;execvp&lt;/span&gt;(&lt;span class="pl-s1"&gt;binary&lt;/span&gt;, [&lt;span class="pl-s1"&gt;binary&lt;/span&gt;] &lt;span class="pl-c1"&gt;+&lt;/span&gt; &lt;span class="pl-s1"&gt;sys&lt;/span&gt;.&lt;span class="pl-c1"&gt;argv&lt;/span&gt;[&lt;span class="pl-c1"&gt;1&lt;/span&gt;:])&lt;/pre&gt;
&lt;p&gt;That &lt;code&gt;main()&lt;/code&gt; method - also called from &lt;code&gt;sqlite_scanner/__main__.py&lt;/code&gt; - locates the binary and executes it when the Python package itself is executed, using the &lt;code&gt;sqlite-scanner = sqlite_scanner:main&lt;/code&gt; entry point defined in the wheel.&lt;/p&gt;
&lt;h4 id="which-means-we-can-use-it-as-a-dependency"&gt;Which means we can use it as a dependency&lt;/h4&gt;
&lt;p&gt;Using PyPI as a distribution platform for Go binaries feels a tiny bit abusive, albeit &lt;a href="https://simonwillison.net/2022/May/23/bundling-binary-tools-in-python-wheels/"&gt;there is plenty of precedent&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I’ll justify it by pointing out that this means &lt;strong&gt;we can use Go binaries as dependencies&lt;/strong&gt; for other Python packages now.&lt;/p&gt;
&lt;p&gt;That's genuinely useful! It means that any functionality which is available in a cross-platform Go binary can now be subsumed into a Python package. Python is really good at running subprocesses so this opens up a whole world of useful tricks that we can bake into our Python tools.&lt;/p&gt;
&lt;p&gt;To demonstrate this, I built &lt;a href="https://github.com/simonw/datasette-scan"&gt;datasette-scan&lt;/a&gt; - a new Datasette plugin which depends on &lt;code&gt;sqlite-scanner&lt;/code&gt; and then uses that Go binary to scan a folder for SQLite databases and attach them to a Datasette instance.&lt;/p&gt;
&lt;p&gt;Here's how to use that (without even installing anything first, thanks &lt;code&gt;uv&lt;/code&gt;) to explore any SQLite databases in your Downloads folder:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;uv run --with datasette-scan datasette scan &lt;span class="pl-k"&gt;~&lt;/span&gt;/Downloads&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;If you peek at the code you'll see it &lt;a href="https://github.com/simonw/datasette-scan/blob/1a2b6d1e6b04c8cd05f5676ff7daa877efd99f08/pyproject.toml#L14"&gt;depends on sqlite-scanner&lt;/a&gt; in &lt;code&gt;pyproject.toml&lt;/code&gt; and calls it using &lt;code&gt;subprocess.run()&lt;/code&gt; against &lt;code&gt;sqlite_scanner.get_binary_path()&lt;/code&gt; in its own &lt;a href="https://github.com/simonw/datasette-scan/blob/1a2b6d1e6b04c8cd05f5676ff7daa877efd99f08/datasette_scan/__init__.py#L38-L58"&gt;scan_directories() function&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I've been exploring this pattern for other, non-Go binaries recently - here's &lt;a href="https://github.com/simonw/tools/blob/main/python/livestream-gif.py"&gt;a recent script&lt;/a&gt; that depends on &lt;a href="https://pypi.org/project/static-ffmpeg/"&gt;static-ffmpeg&lt;/a&gt; to ensure that &lt;code&gt;ffmpeg&lt;/code&gt; is available for the script to use.&lt;/p&gt;
&lt;h4 id="building-python-wheels-from-go-packages-with-go-to-wheel"&gt;Building Python wheels from Go packages with go-to-wheel&lt;/h4&gt;
&lt;p&gt;After trying this pattern myself a couple of times I realized it would be useful to have a tool to automate the process.&lt;/p&gt;
&lt;p&gt;I first &lt;a href="https://claude.ai/share/2d9ced56-b3e8-4651-83cc-860b9b419187"&gt;brainstormed with Claude&lt;/a&gt; to check that there was no existing tool to do this. It pointed me to &lt;a href="https://www.maturin.rs/bindings.html#bin"&gt;maturin bin&lt;/a&gt; which helps distribute Rust projects using Python wheels, and &lt;a href="https://github.com/Bing-su/pip-binary-factory"&gt;pip-binary-factory&lt;/a&gt; which bundles all sorts of other projects, but did not identify anything that addressed the exact problem I was looking to solve.&lt;/p&gt;
&lt;p&gt;So I &lt;a href="https://gisthost.github.io/?41f04e4eb823b1ceb888d9a28c2280dd/index.html"&gt;had Claude Code for web build the first version&lt;/a&gt;, then refined the code locally on my laptop with the help of more Claude Code and a little bit of OpenAI Codex too, just to mix things up.&lt;/p&gt;
&lt;p&gt;The full documentation is in the &lt;a href="https://github.com/simonw/go-to-wheel"&gt;simonw/go-to-wheel&lt;/a&gt; repository. I've published that tool to PyPI so now you can run it using:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;uvx go-to-wheel --help&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The &lt;code&gt;sqlite-scanner&lt;/code&gt; package you can &lt;a href="https://pypi.org/project/sqlite-scanner/"&gt;see on PyPI&lt;/a&gt; was built using &lt;code&gt;go-to-wheel&lt;/code&gt; like this:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;uvx go-to-wheel &lt;span class="pl-k"&gt;~&lt;/span&gt;/dev/sqlite-scanner \
  --set-version-var main.version \
  --version 0.1.1 \
  --readme README.md \
  --author &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;Simon Willison&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt; \
  --url https://github.com/simonw/sqlite-scanner \
  --description &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;Scan directories for SQLite databases&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This created a set of wheels in the &lt;code&gt;dist/&lt;/code&gt; folder. I tested one of them like this:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;uv run --with dist/sqlite_scanner-0.1.1-py3-none-macosx_11_0_arm64.whl \
  sqlite-scanner --version&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;When that spat out the correct version number I was confident everything had worked as planned, so I pushed the whole set of wheels to PyPI using &lt;code&gt;twine upload&lt;/code&gt; like this:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;uvx twine upload dist/&lt;span class="pl-k"&gt;*&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;I had to paste in a PyPI API token I had saved previously.&lt;/p&gt;
&lt;h4 id="i-expect-to-use-this-pattern-a-lot"&gt;I expect to use this pattern a lot&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;sqlite-scanner&lt;/code&gt; is very clearly meant as a proof-of-concept for this wider pattern - Python is very much capable of recursively crawling a directory structure looking for files that start with a specific byte prefix on its own!&lt;/p&gt;
&lt;p&gt;That said, I think there's a &lt;em&gt;lot&lt;/em&gt; to be said for this pattern. Go is a great complement to Python - it's fast, compiles to small self-contained binaries, has excellent concurrency support and a rich ecosystem of libraries.&lt;/p&gt;
&lt;p&gt;Go is similar to Python in that it has a strong standard library. Go is particularly good for HTTP tooling - I've built several HTTP proxies in the past using Go's excellent &lt;code&gt;net/http/httputil.ReverseProxy&lt;/code&gt; handler.&lt;/p&gt;
&lt;p&gt;I've also been experimenting with &lt;a href="https://github.com/wazero/wazero"&gt;wazero&lt;/a&gt;, Go's robust and mature zero dependency WebAssembly runtime as part of my ongoing quest for the ideal sandbox for running untrusted code. &lt;a href="https://github.com/simonw/research/tree/main/wasm-repl-cli"&gt;Here's my latest experiment&lt;/a&gt; with that library.&lt;/p&gt;
&lt;p&gt;Being able to seamlessly integrate Go binaries into Python projects without the end user having to think about Go at all - they &lt;code&gt;pip install&lt;/code&gt; and everything Just Works - feels like a valuable addition to my toolbox.&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/uv"&gt;uv&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/go"&gt;go&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/pypi"&gt;pypi&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/packaging"&gt;packaging&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai-assisted-programming"&gt;ai-assisted-programming&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sqlite"&gt;sqlite&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="uv"/><category term="go"/><category term="pypi"/><category term="packaging"/><category term="ai-assisted-programming"/><category term="python"/><category term="datasette"/><category term="projects"/><category term="sqlite"/></entry><entry><title>Introducing Deno Sandbox</title><link href="https://simonwillison.net/2026/Feb/3/introducing-deno-sandbox/#atom-tag" rel="alternate"/><published>2026-02-03T22:44:50+00:00</published><updated>2026-02-03T22:44:50+00:00</updated><id>https://simonwillison.net/2026/Feb/3/introducing-deno-sandbox/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://deno.com/blog/introducing-deno-sandbox"&gt;Introducing Deno Sandbox&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Here's a new hosted sandbox product from the Deno team. It's actually unrelated to Deno itself - this is part of their Deno Deploy SaaS platform. As such, you don't even need to use JavaScript to access it - you can create and execute code in a hosted sandbox using their &lt;a href="https://pypi.org/project/deno-sandbox/"&gt;deno-sandbox&lt;/a&gt; Python library like this:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;&lt;span class="pl-k"&gt;export&lt;/span&gt; DENO_DEPLOY_TOKEN=&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;... API token ...&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
uv run --with deno-sandbox python&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Then:&lt;/p&gt;
&lt;pre&gt;&lt;span class="pl-k"&gt;from&lt;/span&gt; &lt;span class="pl-s1"&gt;deno_sandbox&lt;/span&gt; &lt;span class="pl-k"&gt;import&lt;/span&gt; &lt;span class="pl-v"&gt;DenoDeploy&lt;/span&gt;

&lt;span class="pl-s1"&gt;sdk&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-en"&gt;DenoDeploy&lt;/span&gt;()

&lt;span class="pl-k"&gt;with&lt;/span&gt; &lt;span class="pl-s1"&gt;sdk&lt;/span&gt;.&lt;span class="pl-c1"&gt;sandbox&lt;/span&gt;.&lt;span class="pl-c1"&gt;create&lt;/span&gt;() &lt;span class="pl-k"&gt;as&lt;/span&gt; &lt;span class="pl-s1"&gt;sb&lt;/span&gt;:
    &lt;span class="pl-c"&gt;# Run a shell command&lt;/span&gt;
    &lt;span class="pl-s1"&gt;process&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;sb&lt;/span&gt;.&lt;span class="pl-c1"&gt;spawn&lt;/span&gt;(
        &lt;span class="pl-s"&gt;"echo"&lt;/span&gt;, &lt;span class="pl-s1"&gt;args&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;[&lt;span class="pl-s"&gt;"Hello from the sandbox!"&lt;/span&gt;]
    )
    &lt;span class="pl-s1"&gt;process&lt;/span&gt;.&lt;span class="pl-c1"&gt;wait&lt;/span&gt;()
    &lt;span class="pl-c"&gt;# Write and read files&lt;/span&gt;
    &lt;span class="pl-s1"&gt;sb&lt;/span&gt;.&lt;span class="pl-c1"&gt;fs&lt;/span&gt;.&lt;span class="pl-c1"&gt;write_text_file&lt;/span&gt;(
        &lt;span class="pl-s"&gt;"/tmp/example.txt"&lt;/span&gt;, &lt;span class="pl-s"&gt;"Hello, World!"&lt;/span&gt;
    )
    &lt;span class="pl-en"&gt;print&lt;/span&gt;(&lt;span class="pl-s1"&gt;sb&lt;/span&gt;.&lt;span class="pl-c1"&gt;fs&lt;/span&gt;.&lt;span class="pl-c1"&gt;read_text_file&lt;/span&gt;(
        &lt;span class="pl-s"&gt;"/tmp/example.txt"&lt;/span&gt;
    ))&lt;/pre&gt;
&lt;p&gt;There’s a JavaScript client library as well. The underlying API isn’t documented yet but appears &lt;a href="https://tools.simonwillison.net/zip-wheel-explorer?package=deno-sandbox#deno_sandbox/sandbox.py--L187"&gt;to use WebSockets&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;There’s a lot to like about this system. Sandboxe instances can have up to 4GB of RAM, get 2 vCPUs, 10GB of ephemeral storage, can mount persistent volumes and can use snapshots to boot pre-configured custom images quickly. Sessions can last up to 30 minutes and are billed by CPU time, GB-h of memory and volume storage usage.&lt;/p&gt;
&lt;p&gt;When you create a sandbox you can configure network domains it’s allowed to access.&lt;/p&gt;
&lt;p&gt;My favorite feature is the way it handles API secrets.&lt;/p&gt;
&lt;pre&gt;&lt;span class="pl-k"&gt;with&lt;/span&gt; &lt;span class="pl-s1"&gt;sdk&lt;/span&gt;.&lt;span class="pl-c1"&gt;sandboxes&lt;/span&gt;.&lt;span class="pl-c1"&gt;create&lt;/span&gt;(
    &lt;span class="pl-s1"&gt;allowNet&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;[&lt;span class="pl-s"&gt;"api.openai.com"&lt;/span&gt;],
    &lt;span class="pl-s1"&gt;secrets&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;{
        &lt;span class="pl-s"&gt;"OPENAI_API_KEY"&lt;/span&gt;: {
            &lt;span class="pl-s"&gt;"hosts"&lt;/span&gt;: [&lt;span class="pl-s"&gt;"api.openai.com"&lt;/span&gt;],
            &lt;span class="pl-s"&gt;"value"&lt;/span&gt;: &lt;span class="pl-s1"&gt;os&lt;/span&gt;.&lt;span class="pl-c1"&gt;environ&lt;/span&gt;.&lt;span class="pl-c1"&gt;get&lt;/span&gt;(&lt;span class="pl-s"&gt;"OPENAI_API_KEY"&lt;/span&gt;),
        }
    },
) &lt;span class="pl-k"&gt;as&lt;/span&gt; &lt;span class="pl-s1"&gt;sandbox&lt;/span&gt;:
    &lt;span class="pl-c"&gt;# ... $OPENAI_API_KEY is available&lt;/span&gt;&lt;/pre&gt;
&lt;p&gt;Within the container that &lt;code&gt;$OPENAI_API_KEY&lt;/code&gt; value is set to something like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DENO_SECRET_PLACEHOLDER_b14043a2f578cba...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Outbound API calls to &lt;code&gt;api.openai.com&lt;/code&gt; run through a proxy which is aware of those placeholders and replaces them with the original secret.&lt;/p&gt;
&lt;p&gt;In this way the secret itself is not available to code within the sandbox, which limits the ability for malicious code (e.g. from a prompt injection) to exfiltrate those secrets.&lt;/p&gt;
&lt;p&gt;From &lt;a href="https://news.ycombinator.com/item?id=46874097#46874959"&gt;a comment on Hacker News&lt;/a&gt; I learned that Fly have a project called &lt;a href="https://github.com/superfly/tokenizer"&gt;tokenizer&lt;/a&gt; that implements the same pattern. Adding this to my list of tricks to use with sandoxed environments!

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://news.ycombinator.com/item?id=46874097"&gt;Hacker News&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/fly"&gt;fly&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/deno"&gt;deno&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/security"&gt;security&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sandboxing"&gt;sandboxing&lt;/a&gt;&lt;/p&gt;



</summary><category term="fly"/><category term="deno"/><category term="security"/><category term="python"/><category term="sandboxing"/></entry><entry><title>sqlite-ast 0.1a0</title><link href="https://simonwillison.net/2026/Jan/30/sqlite-ast-2/#atom-tag" rel="alternate"/><published>2026-01-30T06:12:45+00:00</published><updated>2026-01-30T06:12:45+00:00</updated><id>https://simonwillison.net/2026/Jan/30/sqlite-ast-2/#atom-tag</id><summary type="html">
    &lt;p&gt;&lt;strong&gt;Release:&lt;/strong&gt; &lt;a href="https://github.com/simonw/sqlite-ast/releases/tag/0.1a0"&gt;sqlite-ast 0.1a0&lt;/a&gt;&lt;/p&gt;
    &lt;p&gt;I wanted a Python library that could parse SQLite SELECT statements, so I vibe coded this one up based on a specification I reverse-engineered from SQLite's own parser behavior.&lt;/p&gt;
&lt;p&gt;There's an &lt;a href="https://tools.simonwillison.net/sqlite-ast"&gt;interactive playground here&lt;/a&gt; for trying it out in the browser (via Pyodide).&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/sqlite"&gt;sqlite&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/vibe-coding"&gt;vibe-coding&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="sqlite"/><category term="vibe-coding"/><category term="python"/></entry><entry><title>Datasette 1.0a24</title><link href="https://simonwillison.net/2026/Jan/29/datasette-10a24/#atom-tag" rel="alternate"/><published>2026-01-29T17:21:51+00:00</published><updated>2026-01-29T17:21:51+00:00</updated><id>https://simonwillison.net/2026/Jan/29/datasette-10a24/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://docs.datasette.io/en/latest/changelog.html#a24-2026-01-29"&gt;Datasette 1.0a24&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
New Datasette alpha this morning. Key new features:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Datasette's &lt;code&gt;Request&lt;/code&gt; object can now handle &lt;code&gt;multipart/form-data&lt;/code&gt; file uploads via the new &lt;a href="https://docs.datasette.io/en/latest/internals.html#internals-formdata"&gt;await request.form(files=True)&lt;/a&gt;  method. I plan to use this for a &lt;code&gt;datasette-files&lt;/code&gt; plugin to support attaching files to rows of data.&lt;/li&gt;
&lt;li&gt;The &lt;a href="https://docs.datasette.io/en/latest/contributing.html#setting-up-a-development-environment"&gt;recommended development environment&lt;/a&gt; for hacking on Datasette itself now uses &lt;a href="https://github.com/astral-sh/uv"&gt;uv&lt;/a&gt;. Crucially, you can clone Datasette and run &lt;code&gt;uv run pytest&lt;/code&gt; to run the tests without needing to manually create a virtual environment or install dependencies first, thanks to the &lt;a href="https://til.simonwillison.net/uv/dependency-groups"&gt;dev dependency group pattern&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;A new &lt;code&gt;?_extra=render_cell&lt;/code&gt; parameter for both table and row JSON pages to return the results of executing the &lt;a href="https://docs.datasette.io/en/latest/plugin_hooks.html#render-cell-row-value-column-table-database-datasette-request"&gt;render_cell() plugin hook&lt;/a&gt;. This should unlock new JavaScript UI features in the future.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;More details &lt;a href="https://docs.datasette.io/en/latest/changelog.html#a24-2026-01-29"&gt;in the release notes&lt;/a&gt;. I also invested a bunch of work in eliminating flaky tests that were intermittently failing in CI - I &lt;em&gt;think&lt;/em&gt; those are all handled now.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/uv"&gt;uv&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/annotated-release-notes"&gt;annotated-release-notes&lt;/a&gt;&lt;/p&gt;



</summary><category term="projects"/><category term="datasette"/><category term="python"/><category term="uv"/><category term="annotated-release-notes"/></entry><entry><title>Tips for getting coding agents to write good Python tests</title><link href="https://simonwillison.net/2026/Jan/26/tests/#atom-tag" rel="alternate"/><published>2026-01-26T23:55:29+00:00</published><updated>2026-01-26T23:55:29+00:00</updated><id>https://simonwillison.net/2026/Jan/26/tests/#atom-tag</id><summary type="html">
    &lt;p&gt;Someone &lt;a href="https://news.ycombinator.com/item?id=46765460#46765823"&gt;asked&lt;/a&gt; on Hacker News if I had any tips for getting coding agents to write decent quality tests. Here's what I said:&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;I work in Python which helps a lot because there are a TON of good examples of pytest tests floating around in the training data, including things like usage of fixture libraries for mocking external HTTP APIs and snapshot testing and other neat patterns.&lt;/p&gt;
&lt;p&gt;Or I can say "use pytest-httpx to mock the endpoints" and Claude knows what I mean.&lt;/p&gt;
&lt;p&gt;Keeping an eye on the tests is important. The most common anti-pattern I see is large amounts of duplicated test setup code - which isn't a huge deal, I'm much more more tolerant of duplicated logic in tests than I am in implementation, but it's still worth pushing back on.&lt;/p&gt;
&lt;p&gt;"Refactor those tests to use pytest.mark.parametrize" and "extract the common setup into a pytest fixture" work really well there.&lt;/p&gt;
&lt;p&gt;Generally though the best way to get good tests out of a coding agent is to make sure it's working in a project with an existing test suite that uses good patterns. Coding agents pick the existing patterns up without needing any extra prompting at all.&lt;/p&gt;
&lt;p&gt;I find that once a project has clean basic tests the new tests added by the agents tend to match them in quality. It's similar to how working on large projects with a team of other developers work - keeping the code clean means when people look for examples of how to write a test they'll be pointed in the right direction.&lt;/p&gt;
&lt;p&gt;One last tip I use a lot is this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Clone datasette/datasette-enrichments
from GitHub to /tmp and imitate the
testing patterns it uses
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I do this all the time with different existing projects I've written - the quickest way to show an agent how you like something to be done is to have it look at an example.&lt;/p&gt;

    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/testing"&gt;testing&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/coding-agents"&gt;coding-agents&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/generative-ai"&gt;generative-ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llms"&gt;llms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/hacker-news"&gt;hacker-news&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/pytest"&gt;pytest&lt;/a&gt;&lt;/p&gt;



</summary><category term="testing"/><category term="coding-agents"/><category term="python"/><category term="generative-ai"/><category term="ai"/><category term="llms"/><category term="hacker-news"/><category term="pytest"/></entry><entry><title>Anthropic invests $1.5 million in the Python Software Foundation and open source security</title><link href="https://simonwillison.net/2026/Jan/13/anthropic-invests-15-million-in-the-python-software-foundation-a/#atom-tag" rel="alternate"/><published>2026-01-13T23:58:17+00:00</published><updated>2026-01-13T23:58:17+00:00</updated><id>https://simonwillison.net/2026/Jan/13/anthropic-invests-15-million-in-the-python-software-foundation-a/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://pyfound.blogspot.com/2025/12/anthropic-invests-in-python.html?m=1"&gt;Anthropic invests $1.5 million in the Python Software Foundation and open source security&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
This is outstanding news, especially given our decision to withdraw from that NSF grant application &lt;a href="https://simonwillison.net/2025/Oct/27/psf-withdrawn-proposal/"&gt;back in October&lt;/a&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;We are thrilled to announce that Anthropic has entered into a two-year partnership with the Python Software Foundation (PSF) to contribute a landmark total of $1.5 million to support the foundation’s work, with an emphasis on Python ecosystem security. This investment will enable the PSF to make crucial security advances to CPython and the Python Package Index (PyPI) benefiting all users, and it will also sustain the foundation’s core work supporting the Python language, ecosystem, and global community.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Note that while security is a focus these funds will also support other aspects of the PSF's work:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Anthropic’s support will also go towards the PSF’s core work, including the Developer in Residence program driving contributions to CPython, community support through grants and other programs, running core infrastructure such as PyPI, and more.&lt;/p&gt;
&lt;/blockquote&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/open-source"&gt;open-source&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/anthropic"&gt;anthropic&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/psf"&gt;psf&lt;/a&gt;&lt;/p&gt;



</summary><category term="open-source"/><category term="anthropic"/><category term="python"/><category term="ai"/><category term="psf"/></entry><entry><title>Quoting Linus Torvalds</title><link href="https://simonwillison.net/2026/Jan/11/linus-torvalds/#atom-tag" rel="alternate"/><published>2026-01-11T02:29:58+00:00</published><updated>2026-01-11T02:29:58+00:00</updated><id>https://simonwillison.net/2026/Jan/11/linus-torvalds/#atom-tag</id><summary type="html">
    &lt;blockquote cite="https://github.com/torvalds/AudioNoise/blob/71b256a7fcb0aa1250625f79838ab71b2b77b9ff/README.md"&gt;&lt;p&gt;Also note that the python visualizer tool has been basically written by vibe-coding. I know more about analog filters -- and that's not saying much -- than I do about python. It started out as my typical "google and do the monkey-see-monkey-do" kind of programming, but then I cut out the middle-man -- me -- and just used Google Antigravity to do the audio sample visualizer.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p class="cite"&gt;&amp;mdash; &lt;a href="https://github.com/torvalds/AudioNoise/blob/71b256a7fcb0aa1250625f79838ab71b2b77b9ff/README.md"&gt;Linus Torvalds&lt;/a&gt;, Another silly guitar-pedal-related repo&lt;/p&gt;

    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/vibe-coding"&gt;vibe-coding&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/linus-torvalds"&gt;linus-torvalds&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llms"&gt;llms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/generative-ai"&gt;generative-ai&lt;/a&gt;&lt;/p&gt;



</summary><category term="ai"/><category term="vibe-coding"/><category term="linus-torvalds"/><category term="python"/><category term="llms"/><category term="generative-ai"/></entry><entry><title>How uv got so fast</title><link href="https://simonwillison.net/2025/Dec/26/how-uv-got-so-fast/#atom-tag" rel="alternate"/><published>2025-12-26T23:43:15+00:00</published><updated>2025-12-26T23:43:15+00:00</updated><id>https://simonwillison.net/2025/Dec/26/how-uv-got-so-fast/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://nesbitt.io/2025/12/26/how-uv-got-so-fast.html"&gt;How uv got so fast&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Andrew Nesbitt provides an insightful teardown of why &lt;a href="https://github.com/astral-sh/uv"&gt;uv&lt;/a&gt; is so much faster than &lt;code&gt;pip&lt;/code&gt;. It's not nearly as simple as just "they rewrote it in Rust" - &lt;code&gt;uv&lt;/code&gt; gets to skip a huge amount of Python packaging history (which &lt;code&gt;pip&lt;/code&gt; needs to implement for backwards compatibility) and benefits enormously from work over recent years that makes it possible to resolve dependencies across most packages without having to execute the code in &lt;code&gt;setup.py&lt;/code&gt; using a Python interpreter.&lt;/p&gt;
&lt;p&gt;Two notes that caught my eye that I hadn't understood before:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;HTTP range requests for metadata.&lt;/strong&gt; &lt;a href="https://packaging.python.org/en/latest/specifications/binary-distribution-format/"&gt;Wheel files&lt;/a&gt; are zip archives, and zip archives put their file listing at the end. uv tries PEP 658 metadata first, falls back to HTTP range requests for the zip central directory, then full wheel download, then building from source. Each step is slower and riskier. The design makes the fast path cover 99% of cases. None of this requires Rust.&lt;/p&gt;
&lt;p&gt;[...]&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Compact version representation&lt;/strong&gt;. uv packs versions into u64 integers where possible, making comparison and hashing fast. Over 90% of versions fit in one u64. This is micro-optimization that compounds across millions of comparisons.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I wanted to learn more about these tricks, so I fired up &lt;a href="https://simonwillison.net/2025/Nov/6/async-code-research/"&gt;an asynchronous research task&lt;/a&gt; and told it to checkout the &lt;code&gt;astral-sh/uv&lt;/code&gt; repo, find the Rust code for both of those features and try porting it to Python to help me understand how it works.&lt;/p&gt;
&lt;p&gt;Here's &lt;a href="https://github.com/simonw/research/tree/main/http-range-wheel-metadata"&gt;the report that it wrote for me&lt;/a&gt;, the &lt;a href="https://github.com/simonw/research/pull/57"&gt;prompts I used&lt;/a&gt; and the &lt;a href="https://gistpreview.github.io/?0f04e4d1a240bfc3065df5082b629884/index.html"&gt;Claude Code transcript&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;You can try &lt;a href="https://github.com/simonw/research/blob/main/http-range-wheel-metadata/wheel_metadata.py"&gt;the script&lt;/a&gt; it wrote for extracting metadata from a wheel using HTTP range requests like this:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;uv run --with httpx https://raw.githubusercontent.com/simonw/research/refs/heads/main/http-range-wheel-metadata/wheel_metadata.py https://files.pythonhosted.org/packages/8b/04/ef95b67e1ff59c080b2effd1a9a96984d6953f667c91dfe9d77c838fc956/playwright-1.57.0-py3-none-macosx_11_0_arm64.whl -v&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;The Playwright wheel there is ~40MB. Adding &lt;code&gt;-v&lt;/code&gt; at the end causes the script to spit out verbose details of how it fetched the data - &lt;a href="https://gist.github.com/simonw/a5ef83b6e4605d2577febb43fa9ad018"&gt;which looks like this&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Key extract from that output:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[1] HEAD request to get file size...
    File size: 40,775,575 bytes
[2] Fetching last 16,384 bytes (EOCD + central directory)...
    Received 16,384 bytes
[3] Parsed EOCD:
    Central directory offset: 40,731,572
    Central directory size: 43,981
    Total entries: 453
[4] Fetching complete central directory...
    ...
[6] Found METADATA: playwright-1.57.0.dist-info/METADATA
    Offset: 40,706,744
    Compressed size: 1,286
    Compression method: 8
[7] Fetching METADATA content (2,376 bytes)...
[8] Decompressed METADATA: 3,453 bytes

Total bytes fetched: 18,760 / 40,775,575 (100.0% savings)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The section of the report &lt;a href="https://github.com/simonw/research/tree/main/http-range-wheel-metadata#bonus-compact-version-representation"&gt;on compact version representation&lt;/a&gt; is interesting too. Here's how it illustrates sorting version numbers correctly based on their custom u64 representation:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Sorted order (by integer comparison of packed u64):
  1.0.0a1 (repr=0x0001000000200001)
  1.0.0b1 (repr=0x0001000000300001)
  1.0.0rc1 (repr=0x0001000000400001)
  1.0.0 (repr=0x0001000000500000)
  1.0.0.post1 (repr=0x0001000000700001)
  1.0.1 (repr=0x0001000100500000)
  2.0.0.dev1 (repr=0x0002000000100001)
  2.0.0 (repr=0x0002000000500000)
&lt;/code&gt;&lt;/pre&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/http-range-requests"&gt;http-range-requests&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sorting"&gt;sorting&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/uv"&gt;uv&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/performance"&gt;performance&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/rust"&gt;rust&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/vibe-porting"&gt;vibe-porting&lt;/a&gt;&lt;/p&gt;



</summary><category term="http-range-requests"/><category term="sorting"/><category term="uv"/><category term="performance"/><category term="rust"/><category term="python"/><category term="vibe-porting"/></entry><entry><title>uv-init-demos</title><link href="https://simonwillison.net/2025/Dec/24/uv-init-demos/#atom-tag" rel="alternate"/><published>2025-12-24T22:05:23+00:00</published><updated>2025-12-24T22:05:23+00:00</updated><id>https://simonwillison.net/2025/Dec/24/uv-init-demos/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/simonw/uv-init-demos"&gt;uv-init-demos&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;code&gt;uv&lt;/code&gt; has a useful &lt;code&gt;uv init&lt;/code&gt; command for setting up new Python projects, but it comes with a bunch of different options like &lt;code&gt;--app&lt;/code&gt; and &lt;code&gt;--package&lt;/code&gt; and &lt;code&gt;--lib&lt;/code&gt; and I wasn't sure how they differed.&lt;/p&gt;
&lt;p&gt;So I created this GitHub repository which demonstrates all of those options, generated using this &lt;a href="https://github.com/simonw/uv-init-demos/blob/main/update-projects.sh"&gt;update-projects.sh&lt;/a&gt; script (&lt;a href="https://gistpreview.github.io/?9cff2d3b24ba3d5f423b34abc57aec13"&gt;thanks, Claude&lt;/a&gt;) which will run on a schedule via GitHub Actions to capture any changes made by future releases of &lt;code&gt;uv&lt;/code&gt;.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/uv"&gt;uv&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/git-scraping"&gt;git-scraping&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/github-actions"&gt;github-actions&lt;/a&gt;&lt;/p&gt;



</summary><category term="uv"/><category term="git-scraping"/><category term="python"/><category term="projects"/><category term="github-actions"/></entry><entry><title>MicroQuickJS</title><link href="https://simonwillison.net/2025/Dec/23/microquickjs/#atom-tag" rel="alternate"/><published>2025-12-23T20:53:40+00:00</published><updated>2025-12-23T20:53:40+00:00</updated><id>https://simonwillison.net/2025/Dec/23/microquickjs/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/bellard/mquickjs"&gt;MicroQuickJS&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
New project from programming legend Fabrice Bellard, of ffmpeg and QEMU and QuickJS and &lt;a href="https://bellard.org"&gt;so much more&lt;/a&gt; fame:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;MicroQuickJS (aka. MQuickJS) is a Javascript engine targetted at embedded systems. It compiles and runs Javascript programs with as low as 10 kB of RAM. The whole engine requires about 100 kB of ROM (ARM Thumb-2 code) including the C library. The speed is comparable to QuickJS.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;It supports &lt;a href="https://github.com/bellard/mquickjs/blob/17ce6fe54c1ea4f500f26636bd22058fce2ce61a/README.md#javascript-subset-reference"&gt;a subset of full JavaScript&lt;/a&gt;, though it looks like a rich and full-featured subset to me.&lt;/p&gt;
&lt;p&gt;One of my ongoing interests is sandboxing: mechanisms for executing untrusted code - from end users or generated by LLMs - in an environment that restricts memory usage and applies a strict time limit and restricts file or network access. Could MicroQuickJS be useful in that context?&lt;/p&gt;
&lt;p&gt;I fired up Claude Code for web (on my iPhone) and kicked off &lt;a href="https://simonwillison.net/2025/Nov/6/async-code-research/"&gt;an asynchronous research project&lt;/a&gt; to see explore that question:&lt;/p&gt;
&lt;p&gt;My full prompt &lt;a href="https://github.com/simonw/research/pull/50#issue-3757781692"&gt;is here&lt;/a&gt;. It started like this:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;Clone https://github.com/bellard/mquickjs to /tmp&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Investigate this code as the basis for a safe sandboxing environment for running untrusted code such that it cannot exhaust memory or CPU or access files or the network&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;First try building python bindings for this using FFI - write a script that builds these by checking out the code to /tmp and building against that, to avoid copying the C code in this repo permanently. Write and execute tests with pytest to exercise it as a sandbox&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Then build a "real" Python extension not using FFI and experiment with that&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Then try compiling the C to WebAssembly and exercising it via both node.js and Deno, with a similar suite of tests [...]&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I later added to the interactive session:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;Does it have a regex engine that might allow a resource exhaustion attack from an expensive regex?&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;(The answer was no - the regex engine calls the interrupt handler even during pathological expression backtracking, meaning that any configured time limit should still hold.)&lt;/p&gt;
&lt;p&gt;Here's &lt;a href="https://gistpreview.github.io/?6e07c54db7bb8ed8aa0eccfe4a384679"&gt;the full transcript&lt;/a&gt; and the &lt;a href="https://github.com/simonw/research/blob/main/mquickjs-sandbox/README.md"&gt;final report&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Some key observations:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;MicroQuickJS is &lt;em&gt;very&lt;/em&gt; well suited to the sandbox problem. It has robust near and time limits baked in, it doesn't expose any dangerous primitive like filesystem of network access and even has a regular expression engine that protects against exhaustion attacks (provided you configure a time limit).&lt;/li&gt;
&lt;li&gt;Claude span up and tested a Python library that calls a MicroQuickJS shared library (involving a little bit of extra C), a compiled a Python binding and a library that uses the original MicroQuickJS CLI tool. All of those approaches work well.&lt;/li&gt;
&lt;li&gt;Compiling to WebAssembly was a little harder. It got a version working in Node.js and Deno and Pyodide, but the Python libraries wasmer and wasmtime proved harder, apparently because "mquickjs uses setjmp/longjmp for error handling". It managed to get to a working wasmtime version with &lt;a href="https://github.com/simonw/research/blob/main/mquickjs-sandbox/README.md#working-solution"&gt;a gross hack&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I'm really excited about this. MicroQuickJS is tiny, full featured, looks robust and comes from excellent pedigree. I think this makes for a very solid new entrant in the quest for a robust sandbox.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Update&lt;/strong&gt;: I had Claude Code build &lt;a href="https://tools.simonwillison.net/microquickjs"&gt;tools.simonwillison.net/microquickjs&lt;/a&gt;, an interactive web playground for trying out the WebAssembly build of MicroQuickJS, adapted from my previous &lt;a href="https://tools.simonwillison.net/quickjs"&gt;QuickJS plaground&lt;/a&gt;. My QuickJS page loads 2.28 MB (675 KB transferred). The MicroQuickJS one loads 303 KB (120 KB transferred).&lt;/p&gt;
&lt;p&gt;Here are &lt;a href="https://github.com/simonw/tools/pull/180#issue-3758595291"&gt;the prompts I used&lt;/a&gt; for that.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/pyodide"&gt;pyodide&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/deno"&gt;deno&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sandboxing"&gt;sandboxing&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llms"&gt;llms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/nodejs"&gt;nodejs&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/javascript"&gt;javascript&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/generative-ai"&gt;generative-ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/c"&gt;c&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/claude-code"&gt;claude-code&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/webassembly"&gt;webassembly&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/fabrice-bellard"&gt;fabrice-bellard&lt;/a&gt;&lt;/p&gt;



</summary><category term="pyodide"/><category term="deno"/><category term="sandboxing"/><category term="ai"/><category term="llms"/><category term="nodejs"/><category term="python"/><category term="javascript"/><category term="generative-ai"/><category term="c"/><category term="claude-code"/><category term="webassembly"/><category term="fabrice-bellard"/></entry><entry><title>ty: An extremely fast Python type checker and LSP</title><link href="https://simonwillison.net/2025/Dec/16/ty/#atom-tag" rel="alternate"/><published>2025-12-16T23:35:33+00:00</published><updated>2025-12-16T23:35:33+00:00</updated><id>https://simonwillison.net/2025/Dec/16/ty/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://astral.sh/blog/ty"&gt;ty: An extremely fast Python type checker and LSP&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
The team at Astral have been working on this for quite a long time, and are finally releasing the first beta.  They have some big performance claims:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Without caching, ty is consistently between 10x and 60x faster than mypy and Pyright. When run in an editor, the gap is even more dramatic. As an example, after editing a load-bearing file in the PyTorch repository, ty recomputes diagnostics in 4.7ms: 80x faster than Pyright (386ms) and 500x faster than Pyrefly (2.38 seconds). ty is very fast!&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The easiest way to try it out is via &lt;code&gt;uvx&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd my-python-project/
uvx ty check
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I &lt;a href="https://gistpreview.github.io/?a3aff6768e85168d89d4515e3dbcb7d2"&gt;tried it&lt;/a&gt; against &lt;a href="https://sqlite-utils.datasette.io/"&gt;sqlite-utils&lt;/a&gt; and it turns out I have quite a lot of work to do!&lt;/p&gt;
&lt;p&gt;Astral also released a new &lt;a href="https://marketplace.visualstudio.com/items?itemName=astral-sh.ty"&gt;VS Code extension&lt;/a&gt; adding ty-powered language server features like go to definition. I'm still getting my head around how this works and what it can do.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://news.ycombinator.com/item?id=46294289"&gt;Hacker News&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/astral"&gt;astral&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/vs-code"&gt;vs-code&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ty"&gt;ty&lt;/a&gt;&lt;/p&gt;



</summary><category term="astral"/><category term="python"/><category term="vs-code"/><category term="ty"/></entry><entry><title>Poe the Poet</title><link href="https://simonwillison.net/2025/Dec/16/poe-the-poet/#atom-tag" rel="alternate"/><published>2025-12-16T22:57:02+00:00</published><updated>2025-12-16T22:57:02+00:00</updated><id>https://simonwillison.net/2025/Dec/16/poe-the-poet/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://poethepoet.natn.io/"&gt;Poe the Poet&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
I was looking for a way to specify additional commands in my &lt;code&gt;pyproject.toml&lt;/code&gt; file to execute using &lt;code&gt;uv&lt;/code&gt;. There's an &lt;a href="https://github.com/astral-sh/uv/issues/5903"&gt;enormous issue thread&lt;/a&gt; on this in the &lt;code&gt;uv&lt;/code&gt; issue tracker (300+ comments dating back to August 2024) and from there I learned of several options including this one, Poe the Poet.&lt;/p&gt;
&lt;p&gt;It's neat. I added it to my &lt;a href="https://github.com/simonw/s3-credentials"&gt;s3-credentials&lt;/a&gt; project just now and the following now works for running the live preview server for the documentation:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uv run poe livehtml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here's the snippet of TOML I added to my &lt;code&gt;pyproject.toml&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;[&lt;span class="pl-en"&gt;dependency-groups&lt;/span&gt;]
&lt;span class="pl-smi"&gt;test&lt;/span&gt; = [
    &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;pytest&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;,
    &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;pytest-mock&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;,
    &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;cogapp&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;,
    &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;moto&amp;gt;=5.0.4&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;,
]
&lt;span class="pl-smi"&gt;docs&lt;/span&gt; = [
    &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;furo&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;,
    &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;sphinx-autobuild&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;,
    &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;myst-parser&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;,
    &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;cogapp&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;,
]
&lt;span class="pl-smi"&gt;dev&lt;/span&gt; = [
    {&lt;span class="pl-smi"&gt;include-group&lt;/span&gt; = &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;test&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;},
    {&lt;span class="pl-smi"&gt;include-group&lt;/span&gt; = &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;docs&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;},
    &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;poethepoet&amp;gt;=0.38.0&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;,
]

[&lt;span class="pl-en"&gt;tool&lt;/span&gt;.&lt;span class="pl-en"&gt;poe&lt;/span&gt;.&lt;span class="pl-en"&gt;tasks&lt;/span&gt;]
&lt;span class="pl-smi"&gt;docs&lt;/span&gt; = &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;sphinx-build -M html docs docs/_build&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
&lt;span class="pl-smi"&gt;livehtml&lt;/span&gt; = &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;sphinx-autobuild -b html docs docs/_build&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
&lt;span class="pl-smi"&gt;cog&lt;/span&gt; = &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;cog -r docs/*.md&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;

&lt;p&gt;Since &lt;code&gt;poethepoet&lt;/code&gt; is in the &lt;code&gt;dev=&lt;/code&gt; dependency group any time I run &lt;code&gt;uv run ...&lt;/code&gt; it will be available in the environment.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/uv"&gt;uv&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/packaging"&gt;packaging&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/s3-credentials"&gt;s3-credentials&lt;/a&gt;&lt;/p&gt;



</summary><category term="uv"/><category term="packaging"/><category term="python"/><category term="s3-credentials"/></entry><entry><title>I ported JustHTML from Python to JavaScript with Codex CLI and GPT-5.2 in 4.5 hours</title><link href="https://simonwillison.net/2025/Dec/15/porting-justhtml/#atom-tag" rel="alternate"/><published>2025-12-15T23:58:38+00:00</published><updated>2025-12-15T23:58:38+00:00</updated><id>https://simonwillison.net/2025/Dec/15/porting-justhtml/#atom-tag</id><summary type="html">
    &lt;p&gt;I &lt;a href="https://simonwillison.net/2025/Dec/14/justhtml/"&gt;wrote about JustHTML yesterday&lt;/a&gt; - Emil Stenström's project to build a new standards compliant HTML5 parser in pure Python code using coding agents running against the comprehensive html5lib-tests testing library. Last night, purely out of curiosity, I decided to try &lt;strong&gt;porting JustHTML from Python to JavaScript&lt;/strong&gt; with the least amount of effort possible, using Codex CLI and GPT-5.2. It worked beyond my expectations.&lt;/p&gt;
&lt;h4 id="tl-dr"&gt;TL;DR&lt;/h4&gt;
&lt;p&gt;I built &lt;a href="https://github.com/simonw/justjshtml"&gt;simonw/justjshtml&lt;/a&gt;, a dependency-free HTML5 parsing library in JavaScript which passes 9,200 tests from the html5lib-tests suite and imitates the API design of Emil's JustHTML library.&lt;/p&gt;
&lt;p&gt;It took two initial prompts and a few tiny follow-ups. &lt;a href="https://simonwillison.net/2025/Dec/11/gpt-52/"&gt;GPT-5.2&lt;/a&gt; running in &lt;a href="https://github.com/openai/codex"&gt;Codex CLI&lt;/a&gt; ran uninterrupted for several hours, burned through 1,464,295 input tokens, 97,122,176 cached input tokens and 625,563 output tokens and ended up producing 9,000 lines of fully tested JavaScript across 43 commits.&lt;/p&gt;
&lt;p&gt;Time elapsed from project idea to finished library: about 4 hours, during which I also bought and decorated a Christmas tree with family and watched the latest Knives Out movie.&lt;/p&gt;
&lt;h4 id="some-background"&gt;Some background&lt;/h4&gt;
&lt;p&gt;One of the most important contributions of the HTML5 specification ten years ago was the way it precisely specified how &lt;em&gt;invalid&lt;/em&gt; HTML should be parsed. The world is full of invalid documents and having a specification that covers those means browsers can treat them in the same way - there's no more "undefined behavior" to worry about when building parsing software.&lt;/p&gt;
&lt;p&gt;Unsurprisingly, those invalid parsing rules are pretty complex! The free online book &lt;a href="https://htmlparser.info/"&gt;Idiosyncrasies of the HTML parser&lt;/a&gt; by Simon Pieters is an excellent deep dive into this topic, in particular &lt;a href="https://htmlparser.info/parser/"&gt;Chapter 3. The HTML parser&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The Python &lt;a href="https://github.com/html5lib/html5lib-python"&gt;html5lib&lt;/a&gt; project started the &lt;a href="https://github.com/html5lib/html5lib-tests"&gt;html5lib-tests&lt;/a&gt; repository with a set of implementation-independent tests. These have since become the gold standard for interoperability testing of HTML5 parsers, and are used by projects such as &lt;a href="https://github.com/servo/servo"&gt;Servo&lt;/a&gt; which used them to help build &lt;a href="https://github.com/servo/html5ever"&gt;html5ever&lt;/a&gt;, a "high-performance browser-grade HTML5 parser" written in Rust.&lt;/p&gt;
&lt;p&gt;Emil Stenström's &lt;a href="https://github.com/EmilStenstrom/justhtml"&gt;JustHTML&lt;/a&gt; project is a pure-Python implementation of an HTML5 parser that passes the full html5lib-tests suite. Emil &lt;a href="https://friendlybit.com/python/writing-justhtml-with-coding-agents/"&gt;spent a couple of months&lt;/a&gt; working on this as a side project, deliberately picking a problem with a comprehensive existing test suite to see how far he could get with coding agents.&lt;/p&gt;
&lt;p&gt;At one point he had the agents rewrite it based on a close inspection of the Rust html5ever library. I don't know how much of this was direct translation versus inspiration (here's Emil's &lt;a href="https://news.ycombinator.com/item?id=46264195#46267059"&gt;commentary on that&lt;/a&gt;) - his project has 1,215 commits total so it appears to have included a huge amount of iteration, not just a straight port.&lt;/p&gt;
&lt;p&gt;My project &lt;strong&gt;is&lt;/strong&gt; a straight port. I instructed Codex CLI to build a JavaScript version of Emil's Python code.&lt;/p&gt;
&lt;h4 id="the-process-in-detail"&gt;The process in detail&lt;/h4&gt;
&lt;p&gt;I started with a bit of mise en place. I checked out two repos and created an empty third directory for the new project:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;&lt;span class="pl-c1"&gt;cd&lt;/span&gt; &lt;span class="pl-k"&gt;~&lt;/span&gt;/dev
git clone https://github.com/EmilStenstrom/justhtml
git clone https://github.com/html5lib/html5lib-tests
mkdir justjshtml
&lt;span class="pl-c1"&gt;cd&lt;/span&gt; justjshtml&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Then I started Codex CLI for GPT-5.2 like this:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;codex --yolo -m gpt-5.2&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;That &lt;code&gt;--yolo&lt;/code&gt; flag is a shortcut for &lt;code&gt;--dangerously-bypass-approvals-and-sandbox&lt;/code&gt;, which is every bit as dangerous as it sounds.&lt;/p&gt;
&lt;p&gt;My first prompt told Codex to inspect the existing code and use it to build a specification for the new JavaScript library:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;We are going to create a JavaScript port of ~/dev/justhtml - an HTML parsing library that passes the full ~/dev/html5lib-tests test suite. It is going to have a similar API to the Python library but in JavaScript. It will have no dependencies other than raw JavaScript, hence it will work great in the browser and node.js and other environments. Start by reading ~/dev/justhtml and designing the user-facing API for the new library - create a spec.md containing your plan.&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I reviewed the spec, which included a set of proposed milestones, and told it to add another:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;Add an early step to the roadmap that involves an initial version that parses a simple example document that is valid and returns the right results. Then add and commit the spec.md file.&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Here's &lt;a href="https://github.com/simonw/justjshtml/blob/19b8eb1f2ca80f428a3c40862d5ec05d36e5166b/spec.md"&gt;the resulting spec.md file&lt;/a&gt;. My request for that initial version became "Milestone 0.5" which looked like this:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Milestone 0.5 — End-to-end smoke parse (single valid document)&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Implement the smallest end-to-end slice so the public API is real early:
&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;new JustHTML("&amp;lt;html&amp;gt;&amp;lt;head&amp;gt;&amp;lt;/head&amp;gt;&amp;lt;body&amp;gt;&amp;lt;p&amp;gt;Hello&amp;lt;/p&amp;gt;&amp;lt;/body&amp;gt;&amp;lt;/html&amp;gt;")&lt;/code&gt; returns a tree with the expected tag structure and text nodes.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;doc.toText()&lt;/code&gt; returns &lt;code&gt;"Hello"&lt;/code&gt; and &lt;code&gt;doc.errors&lt;/code&gt; is empty for this valid input.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Add &lt;code&gt;scripts/smoke.js&lt;/code&gt; (no deps) that runs the example and asserts the expected structure/output.&lt;/li&gt;
&lt;li&gt;Gate: &lt;code&gt;node scripts/smoke.js&lt;/code&gt; passes.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;Then I told it:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;Implement Milestone 0.5&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;And off it went. The resulting code appeared to work so I said:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;Configure GitHub Actions test.yml to run that on every commit, then commit and push&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I ran &lt;code&gt;git init&lt;/code&gt; and created a private GitHub repository for this project at this point, and set up the local directory to push to that remote.&lt;/p&gt;
&lt;p&gt;Here's &lt;a href="https://github.com/simonw/justjshtml/commit/48497cbd05584c88af757a0736a2ee447468b6b3"&gt;that initial push&lt;/a&gt;. Then I told it:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;OK do the rest, commit and push often&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;And that was almost it!&lt;/p&gt;
&lt;p&gt;I set my laptop to not fall asleep and left it to its devices while we went off to buy a Christmas tree. The "commit and push often" meant I could monitor its progress on my phone by refreshing &lt;a href="https://github.com/simonw/justjshtml/commits/main"&gt;the commit log on GitHub&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I was running this against my $20/month ChatGPT Plus account, which has a five hour token allowance window for Codex CLI. That ran out at 6:35pm and Codex paused, so I waited until the reset point at 7:14pm and typed:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;Keep going&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;At 9:30pm it declared itself done with the following summary message:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;• What’s Done

  - html5lib-tests now run in CI and pass: tokenizer 6810/6810, tree 1770/1782
    (12 skipped: #script-on), serializer 230/230, encoding 82/83 (1 skipped:
    scripted) (.github/workflows/test.yml:1).
  - Public API expanded: stream() (src/stream.js:1), CSS selectors query()/
    matches() (src/selector.js:1), and toMarkdown() (src/markdown.js:1), all
    exported via src/index.js:1.
  - Added no-deps unit scripts for selectors/stream/markdown (scripts/run-
    selector-tests.js:1, scripts/run-stream-tests.js:1, scripts/run-markdown-
    tests.js:1) and wired them into CI.
  - Fixed TreeBuilder so &amp;lt;br&amp;gt; no longer closes &amp;lt;p&amp;gt; (src/treebuilder.js:647).
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As a finishing touch, I had it add a playground interface so I could try out the new library in my browser. I prompted:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;Add a playground.html in the top level folder which loads the necessary ES modules from ./src/... and implements the exact same functionality as seen on https://tools.simonwillison.net/justhtml but using the JavaScript library instead of Pyodide&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;It fetched my &lt;a href="https://tools.simonwillison.net/justhtml"&gt;existing JustHTML playground page&lt;/a&gt; (&lt;a href="https://simonwillison.net/2025/Dec/14/justhtml/#first-impressions-of-justhtml"&gt;described here&lt;/a&gt;) using &lt;code&gt;curl&lt;/code&gt; and built a new &lt;code&gt;playground.html&lt;/code&gt; file that loaded the new JavaScript code instead. This worked &lt;em&gt;perfectly&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;I enabled GitHub Pages for my still-private repo which meant I could access the new playground at this URL:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://simonw.github.io/justjshtml/playground.html"&gt;https://simonw.github.io/justjshtml/playground.html&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2025/justjshtml-playground.jpg" alt="Screenshot of JustJSHTML Playground web application. Header reads &amp;quot;JustJSHTML Playground&amp;quot; with subtitle &amp;quot;A dependency-free JavaScript HTML5 parser - GitHub&amp;quot;. Below is a status bar showing &amp;quot;JavaScript Environment&amp;quot; with a green &amp;quot;Ready&amp;quot; badge. The main input area has &amp;quot;Paste HTML&amp;quot; and &amp;quot;Fetch from URL&amp;quot; buttons, with a text area containing HTML code: &amp;quot;&amp;lt;!DOCTYPE html&amp;gt; &amp;lt;html&amp;gt; &amp;lt;head&amp;gt; &amp;lt;title&amp;gt;Example Page&amp;lt;/title&amp;gt; &amp;lt;/head&amp;gt; &amp;lt;body&amp;gt; &amp;lt;header&amp;gt; &amp;lt;nav&amp;gt; &amp;lt;ul&amp;gt;&amp;quot;. A &amp;quot;Playground Mode&amp;quot; section shows buttons for &amp;quot;CSS Selector Query&amp;quot;, &amp;quot;Pretty Print HTML&amp;quot;, &amp;quot;Tree Structure&amp;quot;, &amp;quot;Stream Events&amp;quot;, &amp;quot;Extract Text&amp;quot;, and &amp;quot;To Markdown&amp;quot; (highlighted in purple). Below is a text field labeled &amp;quot;CSS Selector (optional - leave empty for whole document):&amp;quot; with placeholder &amp;quot;e.g., article, main, .content (or leave empty)&amp;quot; and a green &amp;quot;Convert to Markdown&amp;quot; button. The Output section has a teal header with &amp;quot;Whole document&amp;quot; badge and displays converted markdown: &amp;quot;Example Page&amp;quot; followed by &amp;quot;- [Home](/)&amp;quot; &amp;quot;- [About](/about)&amp;quot; &amp;quot;- [Contact](/contact)&amp;quot;." style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;All it needed now was some documentation:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;Add a comprehensive README with full usage instructions including attribution plus how this was built plus how to use in in HTML plus how to use it in Node.js&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;You can &lt;a href="https://github.com/simonw/justjshtml/blob/f3a33fdb29bf97846fd017185edc8cf82783032e/README.md"&gt;read the result here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;We are now at eight prompts total, running for just over four hours and I've decorated for Christmas and watched &lt;a href="https://en.wikipedia.org/wiki/Wake_Up_Dead_Man"&gt;Wake Up Dead Man&lt;/a&gt; on Netflix.&lt;/p&gt;
&lt;p&gt;According to Codex CLI:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;Token usage: total=2,089,858 input=1,464,295 (+ 97,122,176 cached) output=625,563 (reasoning 437,010)&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;My &lt;a href="https://www.llm-prices.com/#it=2089858&amp;amp;cit=97122176&amp;amp;ot=625563&amp;amp;sel=gpt-5.2"&gt;llm-prices.com calculator&lt;/a&gt; estimates that at $29.41 if I was paying for those tokens at API prices, but they were included in my $20/month ChatGPT Plus subscription so the actual extra cost to me was zero.&lt;/p&gt;
&lt;h4 id="what-can-we-learn-from-this-"&gt;What can we learn from this?&lt;/h4&gt;
&lt;p&gt;I'm sharing this project because I think it demonstrates a bunch of interesting things about the state of LLMs in December 2025.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Frontier LLMs really can perform complex, multi-hour tasks with hundreds of tool calls and minimal supervision. I used GPT-5.2 for this but I have no reason to believe that Claude Opus 4.5 or Gemini 3 Pro would not be able to achieve the same thing - the only reason I haven't tried is that I don't want to burn another 4 hours of time and several million tokens on more runs.&lt;/li&gt;
&lt;li&gt;If you can reduce a problem to a robust test suite you can set a coding agent loop loose on it with a high degree of confidence that it will eventually succeed. I called this &lt;a href="https://simonwillison.net/2025/Sep/30/designing-agentic-loops/"&gt;designing the agentic loop&lt;/a&gt; a few months ago. I think it's the key skill to unlocking the potential of LLMs for complex tasks.&lt;/li&gt;
&lt;li&gt;Porting entire open source libraries from one language to another via a coding agent works extremely well.&lt;/li&gt;
&lt;li&gt;Code is so cheap it's practically free. Code that &lt;em&gt;works&lt;/em&gt; continues to carry a cost, but that cost has plummeted now that coding agents can check their work as they go.&lt;/li&gt;
&lt;li&gt;We haven't even &lt;em&gt;begun&lt;/em&gt; to unpack the etiquette and ethics around this style of development. Is it responsible and appropriate to churn out a direct port of a library like this in a few hours while watching a movie? What would it take for code built like this to be trusted in production?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I'll end with some open questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Does this library represent a legal violation of copyright of either the Rust library or the Python one?&lt;/li&gt;
&lt;li&gt;Even if this is legal, is it ethical to build a library in this way?&lt;/li&gt;
&lt;li&gt;Does this format of development hurt the open source ecosystem?&lt;/li&gt;
&lt;li&gt;Can I even assert copyright over this, given how much of the work was produced by the LLM?&lt;/li&gt;
&lt;li&gt;Is it responsible to publish software libraries built in this way?&lt;/li&gt;
&lt;li&gt;How much better would this library be if an expert team hand crafted it over the course of several months?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Update 11th January 2026&lt;/strong&gt;: I originally ended this post with just these open questions, but I've now provided &lt;a href="https://simonwillison.net/2026/Jan/11/answers/"&gt;my own answers to the questions&lt;/a&gt; in a new post.&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/html"&gt;html&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llms"&gt;llms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/gpt-5"&gt;gpt-5&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/codex-cli"&gt;codex-cli&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/javascript"&gt;javascript&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/generative-ai"&gt;generative-ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai-assisted-programming"&gt;ai-assisted-programming&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/november-2025-inflection"&gt;november-2025-inflection&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/vibe-porting"&gt;vibe-porting&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="ai"/><category term="html"/><category term="llms"/><category term="gpt-5"/><category term="codex-cli"/><category term="javascript"/><category term="python"/><category term="generative-ai"/><category term="ai-assisted-programming"/><category term="november-2025-inflection"/><category term="vibe-porting"/></entry></feed>