<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"><title>Byte My Python</title><link href="https://bytemypython.com/" rel="alternate"/><link href="https://bytemypython.com/feeds/all.atom.xml" rel="self"/><id>https://bytemypython.com/</id><updated>2026-07-13T00:00:00+02:00</updated><subtitle>Python, data, and more</subtitle><entry><title>Neovim TypeScript LSP Setup</title><link href="https://bytemypython.com/neovim-typescript-lsp-setup.html" rel="alternate"/><published>2026-07-13T00:00:00+02:00</published><updated>2026-07-13T00:00:00+02:00</updated><author><name>Nicolas Galler</name></author><id>tag:bytemypython.com,2026-07-13:/neovim-typescript-lsp-setup.html</id><summary type="html">&lt;p&gt;LSP is the technology that allows Neovim (and many other editor) to provide code completion, navigation, and contextual
help.  How it works is a server process that knows all the specifics of the language and is able to examine the files, 
and a protocol (LSP) which lets the editor communicate …&lt;/p&gt;</summary><content type="html">&lt;p&gt;LSP is the technology that allows Neovim (and many other editor) to provide code completion, navigation, and contextual
help.  How it works is a server process that knows all the specifics of the language and is able to examine the files, 
and a protocol (LSP) which lets the editor communicate with that server the same way for all languages.
LSP support is built in to Neovim, but you still have to enable and configure individual languages.
The &lt;code&gt;nvim-lspconfig&lt;/code&gt; package bridges the gap and includes default configuration for many languages.&lt;/p&gt;
&lt;h2&gt;What is special about TypeScript&lt;/h2&gt;
&lt;p&gt;TypeScript is a little different from other languages in that the standard &lt;code&gt;tsserver&lt;/code&gt; language server does not implement
the standard LSP (Language Server Protocol), but a variation of it (that actually predates LSP).  So it needs a thin
adapter to let the editor communicates with it as if it were a regular LSP server.
There are two main options:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;typescript-tools&lt;/code&gt; seemed really promising, but it does not seem to be actively maintained—the last commit was in
  2025&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ts_ls&lt;/code&gt; wraps &lt;code&gt;typescript-language-server&lt;/code&gt;. It is more standard, but slower. For reasonably sized projects, it remains OK.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I was using &lt;code&gt;typescript-tools&lt;/code&gt; until recently, but started running into some issues. When I noticed it was not maintained anymore, I decided to give the standard &lt;code&gt;ts_ls&lt;/code&gt; configuration a try.&lt;/p&gt;
&lt;h2&gt;Enable &lt;code&gt;ts_ls&lt;/code&gt; in Neovim&lt;/h2&gt;
&lt;p&gt;I use &lt;a href="https://github.com/folke/lazy.nvim"&gt;Lazy.nvim&lt;/a&gt;, so the config looks like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kr"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;-- pre-packaged LSP configurations&lt;/span&gt;
        &lt;span class="c1"&gt;-- see https://github.com/neovim/nvim-lspconfig/tree/master/lsp&lt;/span&gt;
        &lt;span class="s2"&gt;&amp;quot;neovim/nvim-lspconfig&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kr"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="c1"&gt;-- for typescript-language-server&lt;/span&gt;
            &lt;span class="n"&gt;vim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lsp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;enable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;ts_ls&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="kr"&gt;end&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This relies on the executable &lt;code&gt;typescript-language-server&lt;/code&gt; being present in &lt;code&gt;PATH&lt;/code&gt; &lt;strong&gt;and&lt;/strong&gt; on the project having a TypeScript installation.&lt;/p&gt;
&lt;p&gt;If you have a warning about &lt;code&gt;typescript-language-server&lt;/code&gt; not found, you typically need to do:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;npm&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;-g&lt;span class="w"&gt; &lt;/span&gt;typescript-language-server
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Note: if you use a Node version manager, this is going to be specific to the version activated for the current project. So you may have to install a copy for each Node version.&lt;/p&gt;
&lt;h2&gt;Fixing a monorepo error&lt;/h2&gt;
&lt;p&gt;Beyond that, one common error I got with monorepos:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;vim.schedule callback: .../neovim/0.12.4/share/nvim/runtime/lua/vim/lsp/client.lua:582: RPC[Error] code_name = InternalError, message = &amp;quot;Request initialize failed with message: Could not find a valid TypeScript installation. Please ensure that the \&amp;quot;typescript\&amp;quot; dependency is installed in the workspace or that a valid `tsserver.path` is specified. Exiting.&amp;quot;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This happens because the language server looks for the TypeScript installed &lt;strong&gt;for this project&lt;/strong&gt;. But in a monorepo, it might not find the right root. For example, GradeBee has this structure:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;root
|
|-- pnpm-lock.yaml -&amp;gt; lock file for the workspace
|-- package.json -&amp;gt; workspace-level config, without TypeScript
+-- frontend
    |
    |-- package.json -&amp;gt; frontend dependencies, including TypeScript
    +-- tsconfig.json
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The default &lt;code&gt;ts_ls&lt;/code&gt; setup will detect the project root by finding the lock file—in this case, the root of the workspace—but TypeScript is not installed at that location.&lt;/p&gt;
&lt;p&gt;There are two possible fixes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Change the repo root detection. But then it still will not work to edit TypeScript files other than the ones under &lt;code&gt;frontend&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Install TypeScript at the workspace level. It is a bit redundant, but it does make it clear that there is a tooling dependency on TypeScript.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I chose the latter option. Since I use pnpm, I can add it to the &lt;code&gt;catalog&lt;/code&gt; key in &lt;code&gt;pnpm-workspace.yaml&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nt"&gt;catalog&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;typescript&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;~6.0.2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Then, in both the root and &lt;code&gt;frontend&lt;/code&gt; &lt;code&gt;package.json&lt;/code&gt;, under &lt;code&gt;devDependencies&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;quot;typescript&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;catalog:&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2&gt;Links and final words&lt;/h2&gt;
&lt;p&gt;This is the current state, but the tools continue to evolve. Typescript 7.0 just came out and the compiler was
rewritten in Go.  Likely,  Microsoft will follow this with an implementation of the LSP server in Go to
address both the speed, and the compatibility concerns, making packages like &lt;code&gt;typescript-language-server&lt;/code&gt; redundant.&lt;/p&gt;
&lt;p&gt;Some links:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/f1code/vimrc"&gt;My neovim config&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/typescript-language-server/typescript-language-server"&gt;typescript-language-server&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/neovim/nvim-lspconfig/blob/master/lua/lspconfig/configs/ts_ls.lua"&gt;nvim-lspconfig&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://neovim.io/doc/user/lsp/"&gt;Neovim LSP Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/pmizio/typescript-tools.nvim"&gt;typescript-tools&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content><category term="Tools"/><category term="neovim"/><category term="typescript"/><category term="lsp"/><category term="developer-tools"/></entry><entry><title>Organizing Agent Skills</title><link href="https://bytemypython.com/organizing-agent-skills.html" rel="alternate"/><published>2026-07-12T00:00:00+02:00</published><updated>2026-07-12T00:00:00+02:00</updated><author><name>Nicolas Galler</name></author><id>tag:bytemypython.com,2026-07-12:/organizing-agent-skills.html</id><summary type="html">&lt;p&gt;Most agents (Codex, Pi, OpenCode, and so on) will by default look for global skills under &lt;code&gt;~/.agents/skills&lt;/code&gt;, but it’s not an established standard. Claude Code and Cursor, for example, only use their own skill folder. The same applies within a project. Regardless of where skills are stored, the …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Most agents (Codex, Pi, OpenCode, and so on) will by default look for global skills under &lt;code&gt;~/.agents/skills&lt;/code&gt;, but it’s not an established standard. Claude Code and Cursor, for example, only use their own skill folder. The same applies within a project. Regardless of where skills are stored, the structure inside the skills folder is similar: individual folders that each contain at least a &lt;code&gt;SKILL.md&lt;/code&gt; file, plus some reference documents or scripts. That part is getting standardized—see &lt;a href="https://agentskills.io/specification"&gt;agentskills.io&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I started my &lt;a href="https://github.com/f1code/skills"&gt;skills&lt;/a&gt; repo a few weeks ago, in an attempt to organize a growing collection of both custom and third-party skills.&lt;/p&gt;
&lt;h2&gt;The challenges&lt;/h2&gt;
&lt;p&gt;My challenges are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;I periodically want to try out new third-party skills, to test a new workflow or just play with something new.&lt;/li&gt;
&lt;li&gt;When I have a third-party skill, I want to be able to keep it updated...&lt;/li&gt;
&lt;li&gt;...but I might also want to customize that third-party skill.&lt;/li&gt;
&lt;li&gt;And then, in addition to that, I have my own custom skills to edit and maintain.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Managing installed skills&lt;/h2&gt;
&lt;p&gt;A new-ish tool from Vercel makes that possible, aptly named "&lt;a href="https://github.com/vercel-labs/skills"&gt;skills&lt;/a&gt;."&lt;/p&gt;
&lt;p&gt;It lets you:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;install a new skill to the global or project folder, given a Git repo or a path within a repo;&lt;/li&gt;
&lt;li&gt;track installed third-party skills in a &lt;code&gt;.skill-lock.json&lt;/code&gt; file;&lt;/li&gt;
&lt;li&gt;update those skills using &lt;code&gt;npx skills@latest update&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;list installed third party skills, based on the skill lock file: &lt;code&gt;npx skills@latest ls -g&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;remove individual installed skills using &lt;code&gt;remove&lt;/code&gt; (though only individual skills right now)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The &lt;code&gt;.skill-lock.json&lt;/code&gt; has a &lt;code&gt;package-lock.json&lt;/code&gt; feel but is more focused on updates. It’s useful because it lets me install and uninstall skills without messing with my custom skills. By keeping everything under Git, I can also make tweaks to third-party skills if I want to try small changes—being careful when updating them, since &lt;code&gt;npx skills update&lt;/code&gt; will silently overwrite changes.&lt;/p&gt;
&lt;h2&gt;My layout&lt;/h2&gt;
&lt;p&gt;I organize my repo like this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;.agents/.skill-lock.json&lt;/code&gt; — the lock file from &lt;code&gt;skills&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.agents/skills/*&lt;/code&gt; — all third-party skills&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.agents/skills/custom/*&lt;/code&gt; — all my custom skills&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Agents will recursively discover skills, so you can use any organization you like within the repo. I haven’t done that yet, but I have seen developers divide skills between engineering-focused and research-focused work. There is not yet a way to activate a specific portion of the skills for a given agent session, though, so the organization is just to keep things tidy, not to increase agent performance.&lt;/p&gt;
&lt;h2&gt;Take away&lt;/h2&gt;
&lt;p&gt;This is a growing area of exploration and one where we as senior developers can really leverage our software engineering experience to boost the whole team.
The need for composable, modular skills is crucial and good organization is paramount - I feel this is going to be a
crucial need and one worth mastering.&lt;br&gt;
I have been doing quite a bit of experimentation - as always, I mostly learn by doing - and will be sharing that in the
future on this blog as well, so stay tuned!&lt;/p&gt;</content><category term="Tools"/><category term="agent-skills"/><category term="developer-tools"/><category term="skills"/></entry><entry><title>GitHub PR Review "View Deployment" button, with Dokku</title><link href="https://bytemypython.com/github-pr-review-view-deployment-button-with-dokku.html" rel="alternate"/><published>2026-07-01T00:00:00+02:00</published><updated>2026-07-01T00:00:00+02:00</updated><author><name>Nicolas Galler</name></author><id>tag:bytemypython.com,2026-07-01:/github-pr-review-view-deployment-button-with-dokku.html</id><summary type="html">&lt;p&gt;I have been playing with review apps linked directly from GitHub PRs. The goal is simple: when someone opens a pull request, GitHub should show a link to a running preview of that branch.&lt;/p&gt;
&lt;p&gt;I use Dokku for deployment of the app on the VPS and it works fine for …&lt;/p&gt;</summary><content type="html">&lt;p&gt;I have been playing with review apps linked directly from GitHub PRs. The goal is simple: when someone opens a pull request, GitHub should show a link to a running preview of that branch.&lt;/p&gt;
&lt;p&gt;I use Dokku for deployment of the app on the VPS and it works fine for the purpose of creating the one-shot, PR review
application.  It's all driven through CLI so very scriptable to fit into a GitHub Action workflow.
I ran into a few snags I should document.&lt;/p&gt;
&lt;h2&gt;Give each PR a URL&lt;/h2&gt;
&lt;p&gt;The first thing to set up is wildcard DNS. I want each PR review app to get a predictable hostname, something like:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;pr-123.example.com
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Once the wildcard points at the Dokku host, the workflow can set the app’s domain during deployment:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;dokku&lt;span class="w"&gt; &lt;/span&gt;domains:set&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;APP_NAME&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;APP_HOSTNAME&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Then Let’s Encrypt can issue the certificate for that review app:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;dokku&lt;span class="w"&gt; &lt;/span&gt;letsencrypt:set&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;APP_NAME&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;email&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;LETSENCRYPT_EMAIL&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
dokku&lt;span class="w"&gt; &lt;/span&gt;letsencrypt:enable&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;APP_NAME&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Nothing especially magical here, but it is nice that this is just Dokku commands. The PR gets a real HTTPS URL without provisioning a separate load balancer, ingress controller, or whatever other bit of cloud ceremony usually comes with “temporary app with HTTPS”.&lt;/p&gt;
&lt;h2&gt;Storage&lt;/h2&gt;
&lt;p&gt;If the app needs persistent files, create named storage for that PR’s review app and mount it into the container:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;dokku&lt;span class="w"&gt; &lt;/span&gt;storage:create&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;data-&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;APP_NAME&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
dokku&lt;span class="w"&gt; &lt;/span&gt;storage:mount&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;APP_NAME&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;data-&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;APP_NAME&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;--container-dir&lt;span class="w"&gt; &lt;/span&gt;/data
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;That gives the app a stable &lt;code&gt;/data&lt;/code&gt; directory for the lifetime of the review app. It is still disposable, just not “lose every uploaded file on the first restart” disposable.&lt;/p&gt;
&lt;p&gt;I also used another trick to initialize the storage from production.  This gives me 2 things:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;make sure the database migrations are going to work correctly on production data&lt;/li&gt;
&lt;li&gt;make the production data available during review&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In my case it was simple... in some cases there are references to external systems in the production data and
things are not quite that easy.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nv"&gt;TARGET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;dokku&lt;span class="w"&gt; &lt;/span&gt;storage:info&lt;span class="w"&gt; &lt;/span&gt;data-&lt;span class="nv"&gt;$APP_NAME&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;--format&lt;span class="w"&gt; &lt;/span&gt;json&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;jq&lt;span class="w"&gt; &lt;/span&gt;-r&lt;span class="w"&gt; &lt;/span&gt;.host_path&lt;span class="sb"&gt;`&lt;/span&gt;
sqlite3&lt;span class="w"&gt; &lt;/span&gt;/data/production/production.db&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;.backup &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TARGET&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;APP_NAME&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.db&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;(just before the &lt;code&gt;dokku storage:mount&lt;/code&gt; command)&lt;/p&gt;
&lt;p&gt;You might also use &lt;code&gt;pg_dump&lt;/code&gt; to take a postgres backup, or &lt;code&gt;rsync&lt;/code&gt; to just sync directory content.&lt;/p&gt;
&lt;h2&gt;Linking from the PR&lt;/h2&gt;
&lt;p&gt;The important part is on the GitHub side: expose the review app as an environment URL. That way GitHub puts the link right on the PR instead of making everyone dig through CI logs.&lt;/p&gt;
&lt;p&gt;The workflow shape is roughly:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;deploy-review-app&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;Build, push, and deploy review app&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;runs-on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;ubuntu-latest&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;pr-${{ github.event.number }}&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;${{ steps.review_app_url.outputs.review_app_url }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This will create an environment using the PR number, and associate an URL based on a step output
(&lt;code&gt;review_app_url&lt;/code&gt;).  I created a step just for the output:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;Generate review app URL&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;review_app_url&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;|&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="no"&gt;# Pass the URL to the environment URL via step outputs&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="no"&gt;APP_NAME=&amp;quot;gradebee-pr-${{ github.event.number }}&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="no"&gt;echo &amp;quot;review_app_url=https://$APP_NAME.test.gradebee.app&amp;quot; &amp;gt;&amp;gt; $GITHUB_OUTPUT&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;One gotcha: GitHub secret masking can prevent the URL from showing up.&lt;/p&gt;
&lt;p&gt;If the environment URL matches a secret value, GitHub may redact it and refuse to display it. So if the domain is stored as a secret, and the final URL contains that exact domain, the PR link can mysteriously disappear. In this case, the domain name itself is not really a secret, so it is better as a variable or plain configuration value. Keep the actual credentials in secrets; do not make the public hostname one.&lt;/p&gt;
&lt;p&gt;One other thing: in my case I was fine with re-using the production secret for the review app (it has to do with user
authentication, and I wanted to have the same users on both sides).  But if you need to differentiate, you'll have to do
a layered approach:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;use a &lt;code&gt;live&lt;/code&gt; environment with production secret&lt;/li&gt;
&lt;li&gt;put the test secret at the repository level&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This way when the production deployment runs it uses the &lt;code&gt;live&lt;/code&gt; secrets, and the test deployments (which have a blank,
just-created environment) will fall back to the repo secrets.&lt;/p&gt;
&lt;h2&gt;Cleanup&lt;/h2&gt;
&lt;p&gt;The other half of PR review apps is making sure they actually go away.&lt;/p&gt;
&lt;p&gt;On the GitHub side, delete the temporary environment when the PR closes:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;Clean up Github environment&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;GH_TOKEN&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;${{ secrets.ENV_ADMIN_TOKEN }}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;|&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="no"&gt;gh api \&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;--method DELETE \&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;-H &amp;quot;Accept: application/vnd.github+json&amp;quot; \&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;/repos/${{ github.repository }}/environments/pr-${{ github.event.number }} || true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;For this to work you need to generate, and store in a secret &lt;code&gt;ENV_ADMIN_TOKEN&lt;/code&gt;, a fine-grained "Personal Access Token"
with read-write access to administration of the repository (&lt;a href="https://docs.github.com/en/rest/deployments/environments?apiVersion=2026-03-10#delete-an-environment"&gt;doc link&lt;/a&gt;).  The regular &lt;code&gt;GITHUB_TOKEN&lt;/code&gt; is not enough.&lt;/p&gt;
&lt;p&gt;On the Dokku side, destroy the app and its storage:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;dokku&lt;span class="w"&gt; &lt;/span&gt;apps:destroy&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;APP_NAME&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
dokku&lt;span class="w"&gt; &lt;/span&gt;storage:destroy&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;data-&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;APP_NAME&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The storage cleanup is easy to forget, especially if the app itself disappears cleanly. But if every PR creates a named volume, those leftovers add up.&lt;/p&gt;
&lt;h2&gt;Takeaway&lt;/h2&gt;
&lt;p&gt;This is the kind of setup I like: a few boring shell commands glued into CI, rather than a bespoke review-app platform. GitHub already has a place to show the PR link, and Dokku has enough primitives to stand up the preview behind it.&lt;/p&gt;
&lt;p&gt;The main thing is to be deliberate about what is disposable: app, domain, GitHub environment, storage. If you create all four on PR open/update, clean up all four on close. Otherwise your “temporary” review apps become a very quiet little infrastructure archaeology project.&lt;/p&gt;
&lt;p&gt;Existing, public github projects can be a good source of inspiration, and the whole workflow as described above can be
viewed on the &lt;a href="https://github.com/f1code/GradeBee"&gt;GradeBee repository&lt;/a&gt;.&lt;/p&gt;</content><category term="Tools"/><category term="dokku"/><category term="review-apps"/><category term="github-actions"/><category term="deployment"/></entry><entry><title>Loop Engineering: A Balance of Human and Agent</title><link href="https://bytemypython.com/loop-engineering-a-balance-of-human-and-agent.html" rel="alternate"/><published>2026-06-25T00:00:00+02:00</published><updated>2026-06-25T00:00:00+02:00</updated><author><name>Nicolas Galler</name></author><id>tag:bytemypython.com,2026-06-25:/loop-engineering-a-balance-of-human-and-agent.html</id><summary type="html">&lt;p&gt;Like many engineers these days, I spend a lot of time on so-called "loop engineering". The core question isn't really which model or tool is fastest—it's about the balance between human and agent. How do you get the agents to do the heavy lifting, while making sure you intervene …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Like many engineers these days, I spend a lot of time on so-called "loop engineering". The core question isn't really which model or tool is fastest—it's about the balance between human and agent. How do you get the agents to do the heavy lifting, while making sure you intervene at exactly the times where you, the human, add the most value?&lt;/p&gt;
&lt;p&gt;I have been working on a skill based on the &lt;a href="https://github.com/antopolskiy/kanban-md"&gt;kanban-md&lt;/a&gt; tool. It already ships with a skill, but I wanted to tailor it to my workflow—run more controlled bites and know when it needed to involve me. And then I have some other skills I use once in a while or experiment with, most notably the superpowers skill, which just came up with some enhancements.&lt;/p&gt;
&lt;p&gt;I ran the same fleshed-out plan two different ways.&lt;/p&gt;
&lt;h2&gt;First run&lt;/h2&gt;
&lt;p&gt;I did a test with a plan broken down into tasks, using the superpowers skill and the subagent-driven workflow.
This is a lot of agent control and it decides exactly where to spawn sub-agents (which it does a lot).&lt;/p&gt;
&lt;p&gt;It took a long time, almost 1h30 (it actually took a lot longer because I had to keep granting permissions, but that was my setup). TBH I'm not sure I would not have been faster coding it by hand.  The permission friction is really my problem there (part of loop engineering).&lt;/p&gt;
&lt;p&gt;For the record, the cost:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;  Total cost:            $37.26
  Total duration (API):  1h 18m 28s
  Total duration (wall): 15h 42m 18s
  Total code changes:    1661 lines added, 170 lines removed
  Usage by model:
       claude-opus-4-8:  35.0k input, 116.0k output, 15.9m cache read, 3.3m cache write ($31.79)
     claude-sonnet-4-6:  197 input, 56.8k output, 10.0m cache read, 364.7k cache write ($5.22)
      claude-haiku-4-5:  425 input, 9.7k output, 1.4m cache read, 49.9k cache write ($0.2511)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2&gt;Second run&lt;/h2&gt;
&lt;p&gt;Next I tried the same plan with the kanban-loop skill I devised, using &lt;a href="https://github.com/antopolskiy/kanban-md"&gt;kanban-md&lt;/a&gt; (it's in my &lt;a href="https://github.com/f1code/skills"&gt;skills&lt;/a&gt; repo). I first asked it to split the plan into sequential tasks (it created 3), then set it to work.&lt;/p&gt;
&lt;p&gt;This took 1h4. It is really nice to be able to check the progress on the kanban board—that's a much healthier place for the human to sit in the loop: glancing at status rather than approving every step. Though I still need to get the agent to be a bit more communicative on its progress when updating the tasks. It was cheaper at $13.92 (I used &lt;a href="https://github.com/f1code/pi-crust"&gt;pi-crust&lt;/a&gt; to calculate that, which is based on estimates—it might not be as accurate).  To take with a huge grain of salt - this was absolutely not a formal benchmark.  But I am not convinced Claude's strategy of aggressively spawning sub-agents always pays off.&lt;/p&gt;
&lt;h2&gt;Where the human actually added value&lt;/h2&gt;
&lt;p&gt;I asked an agent to run a comparison between the two branches. The first implementation let a significant bug through! But it had a more thorough implementation for one of the tasks. Apparently they were both solid (mind you, I hadn't looked at the code yet at this point).&lt;/p&gt;
&lt;p&gt;Once I looked at the code myself I figured out very fast that both implementations were actually not great—because the plan sucked and I didn't review it carefully. So it goes to show: crap in, crap out.&lt;/p&gt;
&lt;p&gt;But here's the loop-engineering lesson. Seeing the code made the problem obvious in a way that staring at the plan never did. The most valuable human intervention wasn't babysitting permissions or even reviewing the plan up front—it was reading the output once the agent had taken a first pass. For $13, that wasn't too bad. Sometimes there is a lot of value in letting the agent run ahead so you can find the rough edges in your own thinking more quickly than you would by reviewing a plan.  There is another version of the skill that takes a hard break after each sub-task for the human to review, and I think, even though we would like for this to be fully automated and hands-off, that's a better proposition.  Then it really comes down to where you want to place the seams, to have something that is reasonably to review, but large enough that you still have a good gain of time from it.&lt;/p&gt;</content><category term="Tools"/><category term="ai-agents"/><category term="loop-engineering"/><category term="kanban"/><category term="workflow"/><category term="code-review"/></entry><entry><title>Switching to the Kitty Terminal Emulator</title><link href="https://bytemypython.com/switching-to-the-kitty-terminal-emulator.html" rel="alternate"/><published>2026-06-23T00:00:00+02:00</published><updated>2026-06-23T00:00:00+02:00</updated><author><name>Nicolas Galler</name></author><id>tag:bytemypython.com,2026-06-23:/switching-to-the-kitty-terminal-emulator.html</id><summary type="html">&lt;p&gt;This weekend I moved from Ghostty + Zellij to &lt;a href="https://sw.kovidgoyal.net/kitty"&gt;Kitty&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The main reason I wanted to drop Zellij was the overhead—especially in terms of memory (Ghostty regularly got up to 8GB of RAM, thanks to it). But there were still a few things missing when using Ghostty without Zellij... Customizing …&lt;/p&gt;</summary><content type="html">&lt;p&gt;This weekend I moved from Ghostty + Zellij to &lt;a href="https://sw.kovidgoyal.net/kitty"&gt;Kitty&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The main reason I wanted to drop Zellij was the overhead—especially in terms of memory (Ghostty regularly got up to 8GB of RAM, thanks to it). But there were still a few things missing when using Ghostty without Zellij... Customizing the click action for example was very difficult (in Zellij you can make a plugin to customize that, and I did one to be able to match the links in a way that fit my workflow better—&lt;a href="https://github.com/f1code/miserly-link"&gt;miserly-link&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;I tried another terminal, Kitty. You could say Ghostty is actually greatly inspired by Kitty which introduced extensions to the terminal such as the &lt;a href="https://sw.kovidgoyal.net/kitty/graphics-protocol/"&gt;Kitty Image Protocol&lt;/a&gt;. Kitty is (like Ghostty) very fast, but is a bit more mature so it has many more features. A lot of the features are very keyboard-centric which I love.&lt;/p&gt;
&lt;h2&gt;Quick start and config&lt;/h2&gt;
&lt;p&gt;Unlike Ghostty which starts with a barebone config, Kitty ships with a very long config file with most of the defaults commented out.&lt;/p&gt;
&lt;p&gt;A lot of the shortcuts work with a shared modifier called "kitty_mod". The default is ctrl+shift but I felt that was a little hard to reach and remapped it to alt+shift. &lt;/p&gt;
&lt;p&gt;Some useful configs:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;inactive_text_alpha -0.6
# fade text for inactive windows (focus indicator)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3&gt;Cheat sheet - some of the most useful shortcuts&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;kitty_mod+Z&lt;/code&gt; = Zoom&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Cmd+Enter&lt;/code&gt; or &lt;code&gt;kitty_mod+Enter&lt;/code&gt;: new kitty window (= new pane within the current tab). This will place automatically in function of the selected &lt;a href="https://sw.kovidgoyal.net/kitty/overview/#layouts"&gt;layout&lt;/a&gt;.
Remap the command to &lt;code&gt;new_window_with_cwd&lt;/code&gt; to have it open at the current working dir.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;kitty_mod+[ or ]&lt;/code&gt;: previous / next window&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Cmd+t or kitty_mod+T&lt;/code&gt;: new tab&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Shift+Cmd+[ or ]&lt;/code&gt;: previous / next tab&lt;/li&gt;
&lt;li&gt;&lt;code&gt;kitty_mod+H&lt;/code&gt; = edit scrollback in nvim (there is a sample configuration line to uncomment for that). The &lt;code&gt;q&lt;/code&gt; key is remapped to exit.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;kitty_mod+L&lt;/code&gt; = switch layout... there are 7 of them. You can configure which ones are enabled—more on that below.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;kitty_mod+P,F&lt;/code&gt; = pick a file, insert in terminal (this is one of several uses of the “hint kitten”, more on that below)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;kitty_mod+F3&lt;/code&gt; = command palette&lt;/li&gt;
&lt;li&gt;&lt;code&gt;kitty_mod+F8&lt;/code&gt; = swap 2 windows, that's pretty handy&lt;/li&gt;
&lt;li&gt;&lt;code&gt;kitty_mod+1,2,3,4&lt;/code&gt; = go to window&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Layouts&lt;/h2&gt;
&lt;p&gt;Something I used a lot in I3. Instead of deciding whether you want to split a window to the right or the bottom, the layout decides that for you. There are 7 layout total. I just kept 4:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Fat (large window at top, split vertically at bottom)&lt;/li&gt;
&lt;li&gt;Tall (large window on left, split horizontally on right)&lt;/li&gt;
&lt;li&gt;Grid&lt;/li&gt;
&lt;li&gt;Stack&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The stack layout is very handy for zooming a single window, but I mapped that one separately to &lt;code&gt;Mod+Z&lt;/code&gt;.
There is a split layout which lets you do windows however you want, with vertical / horizontal splits, but I'm going to
try and live without that one.&lt;/p&gt;
&lt;h2&gt;Kittens&lt;/h2&gt;
&lt;p&gt;Extensibility is the strong point of Kitty, I feel, when comparing with Ghostty. Ghostty has a very nice out of the box experience but still a little immature in terms of extensibility. This is done through &lt;a href="https://sw.kovidgoyal.net/kitty/kittens/"&gt;"kittens"&lt;/a&gt;. There are a number of super useful ones, out of the box.&lt;/p&gt;
&lt;h3&gt;Choosing a font&lt;/h3&gt;
&lt;p&gt;Use &lt;code&gt;kitten choose-fonts&lt;/code&gt; to pick a font. I tried a few, settled on &lt;a href="https://rubjo.github.io/victor-mono/"&gt;Victor Mono&lt;/a&gt;, downloaded with Homebrew:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;brew&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;font-victor-mono
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;In some (most?) terminals you are advised to install a “nerd font”, which has some extra icons. But kitty will actually transparently detect these icons and render them so this is not necessary.&lt;/p&gt;
&lt;h3&gt;Open anything&lt;/h3&gt;
&lt;p&gt;Kitten hints, this shows a letter or number by everything that looks like a file, so you can open it in your favorite editor, little like Vimium. I added a shortcut to open in nvim (mapping q to quit, like in the scrollback pager edit action):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;map --allow-fallback=shifted,ascii kitty_mod+p&amp;gt;e kitten hints --type path --program=&amp;quot;launch --type=overlay nvim --cmd &amp;#39;nnoremap q ZQ&amp;#39;&amp;quot;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This is a really awesome feature for the mouse-adverse crowd.&lt;/p&gt;
&lt;h3&gt;Notifications&lt;/h3&gt;
&lt;p&gt;Another seemingly minor but very useful feature, "kitten notify" replaces &lt;code&gt;terminal-notifier&lt;/code&gt; but is smarter about
knowing which kitty window triggered the notification.  I use this as completion hook for my coding agent.&lt;/p&gt;
&lt;h2&gt;Final (first) impression&lt;/h2&gt;
&lt;p&gt;After a weekend of use, performance is good, I'm getting more and more used to the different shortcuts and way of
working things, and keep finding little gems.  I've only scratched the surface - I'll keep updating this as I find more
of them!&lt;/p&gt;</content><category term="Tools"/><category term="kitty"/><category term="terminal"/><category term="ghostty"/><category term="zellij"/><category term="productivity"/></entry><entry><title>Why Git's Linear Log Can Lie to You</title><link href="https://bytemypython.com/why-gits-linear-log-can-lie-to-you.html" rel="alternate"/><published>2026-01-30T00:00:00+01:00</published><updated>2026-01-30T00:00:00+01:00</updated><author><name>Nicolas Galler</name></author><id>tag:bytemypython.com,2026-01-30:/why-gits-linear-log-can-lie-to-you.html</id><summary type="html">&lt;p&gt;When your team uses merge commits instead of rebasing, &lt;code&gt;git log&lt;/code&gt; can deceive you. That flat, linear list of commits doesn't tell the whole story — and if you're trying to figure out what's actually in a release, you might draw the wrong conclusions.&lt;/p&gt;
&lt;p&gt;Here's a scenario I ran into recently …&lt;/p&gt;</summary><content type="html">&lt;p&gt;When your team uses merge commits instead of rebasing, &lt;code&gt;git log&lt;/code&gt; can deceive you. That flat, linear list of commits doesn't tell the whole story — and if you're trying to figure out what's actually in a release, you might draw the wrong conclusions.&lt;/p&gt;
&lt;p&gt;Here's a scenario I ran into recently.&lt;/p&gt;
&lt;h2&gt;The Setup&lt;/h2&gt;
&lt;p&gt;I opened a PR yesterday for a bug fix on our release branch. The PR was approved but only merged today. Meanwhile, yesterday the CI pipeline built and tagged a release image from the same branch.&lt;/p&gt;
&lt;p&gt;Looking at &lt;code&gt;git log&lt;/code&gt;, you might think my fix made it into that image:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;9c2600b (HEAD -&amp;gt; release/2.9.0, origin/release/2.9.0) Merged in fix/user-session-timeout (pull request #1532)
faf20e5 Merged in fix/dashboard-label-update (pull request #1528)
31b3a38 (tag: release/2.9.0-build-1678) Merged in fix/array-null-check (pull request #1529)
a992bf9 DASH-456 update E2E with fixed label update
72da568 (origin/fix/user-session-timeout) SESSION-789: set up connection pooling
7ca61c1 SESSION-789: fix for mock use in unit tests
a598fd9 SESSION-789: work around timeout in tests
346236a SESSION-789: ensure session is not eagerly bound
3d2d424 Merged in fix/picklist-value-type (pull request #1530)
095b0c1 fix PICK-234 incorrect value for picklist
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;See the problem? The commits from my branch (&lt;code&gt;72da568&lt;/code&gt;, &lt;code&gt;7ca61c1&lt;/code&gt;, etc.) appear &lt;em&gt;before&lt;/em&gt; the release tag at &lt;code&gt;31b3a38&lt;/code&gt;. A quick glance suggests they're included in the build.&lt;/p&gt;
&lt;p&gt;But they're not.&lt;/p&gt;
&lt;h2&gt;The Reality&lt;/h2&gt;
&lt;p&gt;Run &lt;code&gt;git log --graph --oneline --decorate&lt;/code&gt; instead, and the true picture emerges:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;*   9c2600b (HEAD -&amp;gt; release/2.9.0, origin/release/2.9.0) Merged in fix/user-session-timeout (pull request #1532)
|\  
| * 72da568 (origin/fix/user-session-timeout) SESSION-789: set up connection pooling
| * 7ca61c1 SESSION-789: fix for mock use in unit tests
| * a598fd9 SESSION-789: work around timeout in tests
| * 346236a SESSION-789: ensure session is not eagerly bound
* |   faf20e5 Merged in fix/dashboard-label-update (pull request #1528)
|\ \  
| * | a992bf9 DASH-456 update E2E with fixed label update
| * | 7b14e0c (origin/fix/dashboard-label-update) DASH-456 fix label update issue
* | |   31b3a38 (tag: release/2.9.0-build-1678) Merged in fix/array-null-check (pull request #1529)
|\ \ \  
| |_|/  
|/| |   
| * | f85fe64 (origin/fix/array-null-check) ARRAY-567: handle undefined array
| |/  
* |   3d2d424 Merged in fix/picklist-value-type (pull request #1530)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Now you can see that my fix branch runs &lt;em&gt;parallel&lt;/em&gt; to the main line. The merge commit that actually incorporates those changes (&lt;code&gt;9c2600b&lt;/code&gt;) comes &lt;em&gt;after&lt;/em&gt; the release tag. My fix wasn't in the build.&lt;/p&gt;
&lt;h2&gt;Why This Happens&lt;/h2&gt;
&lt;p&gt;Without rebasing, feature branches keep their original commit timestamps. When merged, Git interleaves these commits chronologically in the flat log view — even though they weren't actually part of the main branch until the merge commit.&lt;/p&gt;
&lt;p&gt;The flat &lt;code&gt;git log&lt;/code&gt; essentially overlays the log from the feature branch onto the trunk. You end up reading multiple parallel histories stacked on top of each other based on date, which can be deeply misleading.&lt;/p&gt;
&lt;h2&gt;Reading the Log Correctly&lt;/h2&gt;
&lt;p&gt;Two approaches to get the truth:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. Use &lt;code&gt;--graph&lt;/code&gt;&lt;/strong&gt; to see the actual branch structure:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;git&lt;span class="w"&gt; &lt;/span&gt;log&lt;span class="w"&gt; &lt;/span&gt;--graph&lt;span class="w"&gt; &lt;/span&gt;--oneline&lt;span class="w"&gt; &lt;/span&gt;--decorate
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;2. Use &lt;code&gt;--first-parent&lt;/code&gt;&lt;/strong&gt; to see only the merge commits on the main line:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;git&lt;span class="w"&gt; &lt;/span&gt;log&lt;span class="w"&gt; &lt;/span&gt;--first-parent&lt;span class="w"&gt; &lt;/span&gt;--oneline
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This shows you exactly what was merged and when, ignoring the individual commits within each branch. The order of these merge commits can be trusted.&lt;/p&gt;
&lt;p&gt;Most Git hosting platforms (GitHub, GitLab, Bitbucket) also show this graphically in their commit views — branches in flight run parallel until the merge commit actually brings them in.&lt;/p&gt;
&lt;h2&gt;A Case for Rebasing&lt;/h2&gt;
&lt;p&gt;This experience reinforced why rebasing is worth the effort, especially for longer-lived branches (more than a week or so):&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;When syncing with the main branch&lt;/strong&gt;: Rebase rather than merge. Don't use the "Sync" button in Bitbucket or similar — it creates a merge commit that interleaves histories. Instead:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;git&lt;span class="w"&gt; &lt;/span&gt;fetch&lt;span class="w"&gt; &lt;/span&gt;origin
git&lt;span class="w"&gt; &lt;/span&gt;rebase&lt;span class="w"&gt; &lt;/span&gt;origin/main
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Before merging your PR&lt;/strong&gt;: Squash trivial commits (typo fixes, "oops" commits, etc.) into logical units. This keeps the history readable and makes each commit meaningful.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why it matters&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;git log&lt;/code&gt; reads linearly and accurately represents the state of the repo at each point in time&lt;/li&gt;
&lt;li&gt;&lt;code&gt;git bisect&lt;/code&gt; works correctly — you can binary search through commits to find regressions without getting lost in parallel histories&lt;/li&gt;
&lt;li&gt;Rolling back to a pre-merge state is straightforward&lt;/li&gt;
&lt;li&gt;Code review is easier when commits tell a coherent story&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When you merge without rebasing, the histories become intermingled. Going back to the pre-merge version of a branch or using &lt;code&gt;git bisect&lt;/code&gt; to analyze regressions becomes challenging because you're navigating through multiple parallel timelines that have been flattened into one.&lt;/p&gt;
&lt;h2&gt;Takeaway&lt;/h2&gt;
&lt;p&gt;If your team uses merge commits, remember that &lt;code&gt;git log&lt;/code&gt; without flags can mislead you about what's actually in a given commit or tag. When debugging "is this fix in production?", reach for &lt;code&gt;--graph&lt;/code&gt; or &lt;code&gt;--first-parent&lt;/code&gt; to get the true story.&lt;/p&gt;
&lt;p&gt;And if you want to avoid these headaches altogether, consider rebasing your feature branches before merging — especially the long-lived ones. Your future self (and your teammates) will thank you when bisecting a regression at 2 AM.&lt;/p&gt;</content><category term="Tools"/><category term="git"/><category term="version-control"/><category term="debugging"/></entry><entry><title>The QuerySet Database Binding Gotcha</title><link href="https://bytemypython.com/the-queryset-database-binding-gotcha.html" rel="alternate"/><published>2026-01-27T00:00:00+01:00</published><updated>2026-01-27T00:00:00+01:00</updated><author><name>Nicolas Galler</name></author><id>tag:bytemypython.com,2026-01-27:/the-queryset-database-binding-gotcha.html</id><summary type="html">&lt;p&gt;When working with Django's multi-database support, there's a subtle issue that can trip you up: QuerySets remember which database they're bound to, and this can lead to unexpected conflicts.&lt;/p&gt;
&lt;h2&gt;The Setup&lt;/h2&gt;
&lt;p&gt;In our codebase (Prancer), we have custom managers that use &lt;code&gt;using(self.db)&lt;/code&gt; to bind a QuerySet to a …&lt;/p&gt;</summary><content type="html">&lt;p&gt;When working with Django's multi-database support, there's a subtle issue that can trip you up: QuerySets remember which database they're bound to, and this can lead to unexpected conflicts.&lt;/p&gt;
&lt;h2&gt;The Setup&lt;/h2&gt;
&lt;p&gt;In our codebase (Prancer), we have custom managers that use &lt;code&gt;using(self.db)&lt;/code&gt; to bind a QuerySet to a specific database. This is a common pattern when you want to route queries to a replica for read-heavy operations.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SomeCustomManager&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Manager&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_queryset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;super&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_queryset&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;using&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The key thing to understand is that once you call &lt;code&gt;.using()&lt;/code&gt;, that QuerySet is bound to that database &lt;strong&gt;from that point forward&lt;/strong&gt;. Any further filtering or chaining will still use that database.&lt;/p&gt;
&lt;h2&gt;The Problem&lt;/h2&gt;
&lt;p&gt;We ran into trouble with &lt;code&gt;PatientDataSource&lt;/code&gt; (and generally when using &lt;code&gt;HistoryTableRule&lt;/code&gt;). These components accept a QuerySet that is constructed at initialization time — essentially acting as a configuration object.&lt;/p&gt;
&lt;p&gt;The issue: this QuerySet is effectively &lt;strong&gt;global state&lt;/strong&gt;. It gets created once, bound to the &lt;code&gt;'default'&lt;/code&gt; database, and then reused across requests.&lt;/p&gt;
&lt;p&gt;Later, when a query actually executes:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Filters are added to this pre-constructed QuerySet&lt;/li&gt;
&lt;li&gt;The result is used to filter the main Data Source QuerySet&lt;/li&gt;
&lt;li&gt;But the main QuerySet is bound to &lt;code&gt;replica0&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;And now you have a conflict: you're trying to combine QuerySets that point to different databases. Django does not handle this gracefully.&lt;/p&gt;
&lt;h2&gt;Why This Happens&lt;/h2&gt;
&lt;p&gt;The root cause is the disconnect between:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Initialization time&lt;/strong&gt; — when the QuerySet is constructed and bound to &lt;code&gt;default&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Execution time&lt;/strong&gt; — when the actual query runs and should use &lt;code&gt;replica0&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you're building reusable components that accept QuerySets as configuration, you need to be aware that the database binding travels with the QuerySet.&lt;/p&gt;
&lt;h2&gt;The Fix&lt;/h2&gt;
&lt;p&gt;The solution depends on your specific case, but generally:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Defer QuerySet construction until execution time, rather than initialization time&lt;/li&gt;
&lt;li&gt;Pass a callable (like a lambda or method reference) instead of a pre-built QuerySet&lt;/li&gt;
&lt;li&gt;Explicitly rebind the QuerySet with &lt;code&gt;.using()&lt;/code&gt; right before combining it with other QuerySets - this lets Django
re-evaluate the database context&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The last option is often the quickest fix, but the first two are cleaner architecturally — they avoid the "global state" problem entirely.&lt;/p&gt;
&lt;h2&gt;Takeaway&lt;/h2&gt;
&lt;p&gt;When you see &lt;code&gt;.using()&lt;/code&gt; in your codebase, think carefully about when that QuerySet is created versus when it's used. If there's a gap, you might be setting yourself up for a database routing conflict.&lt;/p&gt;</content><category term="Django"/><category term="django"/><category term="database"/><category term="queryset"/><category term="replica"/></entry><entry><title>Testing Lambda Functions with LocalStack and pytest</title><link href="https://bytemypython.com/testing-lambda-functions-with-localstack-and-pytest.html" rel="alternate"/><published>2026-01-25T00:00:00+01:00</published><updated>2026-01-25T00:00:00+01:00</updated><author><name>Nicolas Galler</name></author><id>tag:bytemypython.com,2026-01-25:/testing-lambda-functions-with-localstack-and-pytest.html</id><summary type="html">&lt;p&gt;When integration-testing code that calls AWS Lambda, you don't always want to hit the real thing. LocalStack gives you a local, containerized version of AWS services — including Lambda — that you can spin up, use, and tear down as part of your test suite.&lt;/p&gt;
&lt;p&gt;Here's a pattern I've been using: create …&lt;/p&gt;</summary><content type="html">&lt;p&gt;When integration-testing code that calls AWS Lambda, you don't always want to hit the real thing. LocalStack gives you a local, containerized version of AWS services — including Lambda — that you can spin up, use, and tear down as part of your test suite.&lt;/p&gt;
&lt;p&gt;Here's a pattern I've been using: create a Lambda function on the fly inside a pytest fixture, wait for it to become active, then hand off a boto3 client to the tests. Sounds straightforward, but there are a few gotchas.&lt;/p&gt;
&lt;h2&gt;The fixture&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nd"&gt;@pytest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fixture&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;module&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;update_schema_lambda&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;testdata_dir&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Generator&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LambdaClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;LocalStackContainer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;with_services&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;lambda&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;with_volume_mapping&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;testdata_dir&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;/testdata&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;with_volume_mapping&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;/var/run/docker.sock&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;/var/run/docker.sock&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;container&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;endpoint_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;container&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_url&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;container&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;sh -c &amp;#39;cd /testdata &amp;amp;&amp;amp; zip /tmp/lambda.zip mock_update_schema.py&amp;#39;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exit_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;container&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;span class="s2"&gt;        awslocal lambda create-function &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="s2"&gt;    --function-name &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;update_metadata_function&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="s2"&gt;    --region us-east-1 &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="s2"&gt;    --runtime python3.9 &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="s2"&gt;    --handler mock_update_schema.handler &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="s2"&gt;    --memory-size 128 &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="s2"&gt;    --zip-file fileb:///tmp/lambda.zip &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="s2"&gt;    --role arn:aws:iam::000000000000:role/lambda-role&lt;/span&gt;
&lt;span class="s2"&gt;                       &amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exit_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;container&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;span class="s2"&gt;        awslocal lambda wait function-active-v2 &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="s2"&gt;    --region us-east-1 &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="s2"&gt;    --function-name &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;update_metadata_function&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="s2"&gt;    &amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exit_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
        &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;lambda&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;endpoint_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;endpoint_url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Let's break it down.&lt;/p&gt;
&lt;h2&gt;Starting the container&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;LocalStackContainer&lt;/code&gt; (from testcontainers) spins up LocalStack with only the services you need. Here we're mounting two volumes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/testdata&lt;/code&gt; — where the Lambda handler code lives&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/var/run/docker.sock&lt;/code&gt; — LocalStack needs this to run Lambdas in their own containers (Lambda-in-Docker)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Creating the Lambda&lt;/h2&gt;
&lt;p&gt;Inside the container, we use &lt;code&gt;awslocal&lt;/code&gt; (LocalStack's wrapper around the AWS CLI) to zip up the handler and create the function. Nothing too surprising here, except for one thing: &lt;strong&gt;always pass &lt;code&gt;--region&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;LocalStack can be picky about regions. If you omit &lt;code&gt;--region&lt;/code&gt;, it might default to something unexpected, and then your boto3 client (which defaults to &lt;code&gt;us-east-1&lt;/code&gt;) won't find your function. Save yourself the debugging headache and be explicit.&lt;/p&gt;
&lt;h2&gt;Waiting for the Lambda to be ready&lt;/h2&gt;
&lt;p&gt;This is the part that bit me:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;awslocal&lt;span class="w"&gt; &lt;/span&gt;lambda&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;wait&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;function&lt;/span&gt;-active-v2&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--region&lt;span class="w"&gt; &lt;/span&gt;us-east-1&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--function-name&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;config.update_metadata_function&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Lambda functions aren't invocable immediately after creation. The &lt;code&gt;wait function-active-v2&lt;/code&gt; command blocks until the function transitions to the &lt;code&gt;Active&lt;/code&gt; state. Skip this, and your first invoke will likely fail with a cryptic error about the function not being ready.&lt;/p&gt;
&lt;h2&gt;Getting the endpoint URL&lt;/h2&gt;
&lt;p&gt;Once the container is up, &lt;code&gt;container.get_url()&lt;/code&gt; gives you the endpoint URL to pass to boto3:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;lambda&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;endpoint_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;endpoint_url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This tells boto3 to talk to LocalStack instead of real AWS. Without it, your tests would try to hit production (and probably fail with auth errors, or worse, succeed and cost you money).&lt;/p&gt;
&lt;h2&gt;Putting it together&lt;/h2&gt;
&lt;p&gt;With this fixture, any test that depends on &lt;code&gt;update_schema_lambda&lt;/code&gt; gets a fully-configured boto3 Lambda client pointing at a fresh, ephemeral LocalStack instance with the mock function already deployed and ready to invoke.&lt;/p&gt;
&lt;p&gt;The container tears down automatically when the module's tests finish, thanks to the context manager.&lt;/p&gt;
&lt;p&gt;One last tip: if your tests are slow, consider &lt;code&gt;scope="session"&lt;/code&gt; instead of &lt;code&gt;scope="module"&lt;/code&gt; — but be aware that tests will then share state, so make sure your Lambda is idempotent or reset between tests.&lt;/p&gt;</content><category term="Tools"/><category term="python"/><category term="testing"/><category term="pytest"/><category term="aws"/><category term="lambda"/><category term="localstack"/></entry><entry><title>Getting Started with MicroPython (Raspberry Pi Pico)</title><link href="https://bytemypython.com/getting-started-with-micropython-raspberry-pi-pico.html" rel="alternate"/><published>2026-01-23T00:00:00+01:00</published><updated>2026-01-23T00:00:00+01:00</updated><author><name>Nicolas Galler</name></author><id>tag:bytemypython.com,2026-01-23:/getting-started-with-micropython-raspberry-pi-pico.html</id><summary type="html">&lt;p&gt;If you’ve just unboxed a Pico (or any similar MicroPython-capable board), the first hour can be surprisingly fiddly: flashing the right firmware, getting a REPL, and figuring out how to copy a script over without reinventing the wheel. I’m just getting started with MicroPython myself, so this post …&lt;/p&gt;</summary><content type="html">&lt;p&gt;If you’ve just unboxed a Pico (or any similar MicroPython-capable board), the first hour can be surprisingly fiddly: flashing the right firmware, getting a REPL, and figuring out how to copy a script over without reinventing the wheel. I’m just getting started with MicroPython myself, so this post is basically my “here’s what worked” notes from that first setup.&lt;/p&gt;
&lt;p&gt;One thing that helped early on was doing a quick “sanity check” with the Pico C/C++ SDK: build and flash a tiny example once. It doesn’t teach you Python, but it &lt;em&gt;does&lt;/em&gt; confirm your USB connection and board setup are solid.&lt;/p&gt;
&lt;p&gt;After that, here’s the shortest path I found to a working MicroPython REPL.&lt;/p&gt;
&lt;h2&gt;Flash MicroPython (UF2)&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Download the MicroPython firmware (a &lt;code&gt;.uf2&lt;/code&gt; file) from the official docs: &lt;a href="https://www.raspberrypi.com/documentation/microcontrollers/micropython.html"&gt;Raspberry Pi MicroPython documentation&lt;/a&gt;. Make sure you pick the firmware for &lt;strong&gt;your exact board&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Unplug the board (if it’s connected).&lt;/li&gt;
&lt;li&gt;Hold &lt;strong&gt;BOOTSEL&lt;/strong&gt; (the small white button on top), then plug the board back in. It should mount as a USB drive.&lt;/li&gt;
&lt;li&gt;Copy the &lt;code&gt;.uf2&lt;/code&gt; onto that drive. The board will reboot automatically once the copy completes.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Connect to the REPL&lt;/h2&gt;
&lt;p&gt;After flashing, connect to the serial REPL - the interactive Python command line:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;minicom&lt;span class="w"&gt; &lt;/span&gt;-o&lt;span class="w"&gt; &lt;/span&gt;-D&lt;span class="w"&gt; &lt;/span&gt;/dev/ttyACM0
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Then you can run Python command (if you don't see a prompt at first, try just pressing enter to activate the REPL).
Good to know: to exit Minicom, press Ctrl+A then X.&lt;/p&gt;
&lt;p&gt;If you hit permission errors accessing &lt;code&gt;/dev/ttyACM0&lt;/code&gt;, add your user to the &lt;code&gt;dialout&lt;/code&gt; group and re-login:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;usermod&lt;span class="w"&gt; &lt;/span&gt;-aG&lt;span class="w"&gt; &lt;/span&gt;dialout&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$USER&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2&gt;First test: turn on the built-in LED&lt;/h2&gt;
&lt;p&gt;At the REPL, try turning on the built-in LED:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;machine&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Pin&lt;/span&gt;

&lt;span class="n"&gt;led&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Pin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;LED&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Pin&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OUT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;led&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2&gt;Copy scripts to the board with &lt;code&gt;rshell&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;To upload a script (and have it run automatically on boot), copy it to &lt;code&gt;main.py&lt;/code&gt; using &lt;code&gt;rshell&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# first, install rshell&lt;/span&gt;
uv&lt;span class="w"&gt; &lt;/span&gt;tool&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;rshell

&lt;span class="c1"&gt;# copy a file to run automatically&lt;/span&gt;
rshell&lt;span class="w"&gt; &lt;/span&gt;cp&lt;span class="w"&gt; &lt;/span&gt;my_file.py&lt;span class="w"&gt; &lt;/span&gt;/pyboard/main.py

&lt;span class="c1"&gt;# inspect files on the device&lt;/span&gt;
rshell&lt;span class="w"&gt; &lt;/span&gt;ls&lt;span class="w"&gt; &lt;/span&gt;/pyboard
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Restart the device, and it will run the program.  To restart, the most straightforward is unplug / replug, but you can also connect to the REPL using minicom and press Ctrl+D, or use machine.reset():&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;machine&lt;/span&gt;
&lt;span class="n"&gt;machine&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reset&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;That’s it for the initial setup. Next up I want to explore the ecosystem a bit more (sensors, simple displays, maybe a small servo project). So far, MicroPython looks like a great way to make programming feel tangible quickly—and a really promising tool for introducing kids to coding and robotics without getting stuck in tooling.&lt;/p&gt;</content><category term="MicroPython"/><category term="micropython"/><category term="raspberry-pi"/><category term="pico"/><category term="embedded"/></entry><entry><title>Managing Python Monorepos with uv Workspaces and AWS Lambda</title><link href="https://bytemypython.com/managing-python-monorepos-with-uv-workspaces-and-aws-lambda.html" rel="alternate"/><published>2026-01-19T00:00:00+01:00</published><updated>2026-01-19T00:00:00+01:00</updated><author><name>Nicolas Galler</name></author><id>tag:bytemypython.com,2026-01-19:/managing-python-monorepos-with-uv-workspaces-and-aws-lambda.html</id><summary type="html">&lt;p&gt;uv workspaces are a super tool when developing interconnected Python packages, especially in mono-repo setups. uv will set it up pretty automatically if you have a &lt;code&gt;pyproject.toml&lt;/code&gt; at the root of the repo and you run &lt;code&gt;uv init&lt;/code&gt; inside a subfolder.&lt;/p&gt;
&lt;p&gt;What this means is:
 - only &lt;strong&gt;one&lt;/strong&gt; venv will …&lt;/p&gt;</summary><content type="html">&lt;p&gt;uv workspaces are a super tool when developing interconnected Python packages, especially in mono-repo setups. uv will set it up pretty automatically if you have a &lt;code&gt;pyproject.toml&lt;/code&gt; at the root of the repo and you run &lt;code&gt;uv init&lt;/code&gt; inside a subfolder.&lt;/p&gt;
&lt;p&gt;What this means is:
 - only &lt;strong&gt;one&lt;/strong&gt; venv will be set up, with all dependencies for all projects (this is pretty nice for the IDE because you don't need to keep switching venvs, but note the important caveat below)
 - you can reference a local project by adding it in &lt;code&gt;[tool.uv.sources]&lt;/code&gt;, and uv will automatically install it as editable (so you won't need to build each time you make a change and keep reinstalling your project):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;[tool.uv.sources]&lt;/span&gt;
&lt;span class="n"&gt;common_logging&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;workspace&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;one caveat: if your projects have conflicting dependencies, uv will fail the install! For microservices, it is probably best to keep dependencies on compatible versions anyway&lt;/li&gt;
&lt;li&gt;another caveat: since there is only one venv, it's possible the IDE will detect a package that is not really in the project dependencies, and you accidentally import it! This can easily go unnoticed during local development.&lt;/li&gt;
&lt;li&gt;for that, you might be better off forgoing workspaces and using a path dependency:&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;[tool.uv.sources]&lt;/span&gt;
&lt;span class="n"&gt;common_logging&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;../common/logging&amp;quot;&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;editable&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;I have decided to keep using uv workspaces, but we have a CI check to catch that type of problem before it reaches production.&lt;/p&gt;
&lt;h2&gt;Running uv in a workspace&lt;/h2&gt;
&lt;p&gt;From the root of a workspace, &lt;code&gt;uv sync&lt;/code&gt; will install &lt;strong&gt;only&lt;/strong&gt; the workspace-level packages.  Likewise &lt;code&gt;uv run&lt;/code&gt; will not install all the packages.  This is &lt;strong&gt;not&lt;/strong&gt; a way to specify common dependencies, because they will only be installed if the workspace root is installed (so for example &lt;code&gt;uv export&lt;/code&gt; inside of the package will not include those "shared" deps).&lt;/p&gt;
&lt;p&gt;Now, as you go into a package and run &lt;code&gt;uv sync&lt;/code&gt;, uv will add the dependencies from that package to the venv... so eventually you will have everything installed.  But you can't be sure that is always the case.  This is also nice if you want to install deps for only one package, before running tests in CI, to make sure it does not accidentally depend on the other packages' dependencies.&lt;/p&gt;
&lt;p&gt;To run and include all package dependencies (needed for example for a type checker!), use&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nx"&gt;uv&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;run&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="nx"&gt;all&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;packages&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2&gt;Use in Docker build&lt;/h2&gt;
&lt;p&gt;The following examples assume &lt;strong&gt;AWS Lambda container images&lt;/strong&gt;, which explains some of the paths (&lt;code&gt;/var/task&lt;/code&gt;, &lt;code&gt;/var/lang/lib&lt;/code&gt;) and the base images used. The general approach still applies to non-Lambda containers, but paths and base images would need to be adjusted.&lt;/p&gt;
&lt;p&gt;There are two catches:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;you don't want to install all the workspace dependencies necessarily — some are not shared among all your microservices&lt;/li&gt;
&lt;li&gt;you don't want to install the local dependencies in the same layer as the core dependencies, because the core dependencies are less likely to change (and, more importantly, are much bigger)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is especially important for Lambda images, where image size and layer caching directly impact cold start performance.&lt;/p&gt;
&lt;p&gt;I struggled a bit with the latter, but in fact uv makes it rather easy. It's not super well documented though — there are many different ways to get there that will sort of work but leave you with some cruft in the final image.&lt;/p&gt;
&lt;h3&gt;First attempt using uv sync&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;you do need to copy all the &lt;code&gt;pyproject.toml&lt;/code&gt; files and the single &lt;code&gt;uv.lock&lt;/code&gt; file that you are going to need. This includes all your local dependencies, and that makes sense — if you changed a requirement in a local dependency, it's now part of your core deps, and you can include it in the base layer&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;pyproject.toml&lt;span class="w"&gt; &lt;/span&gt;uv.lock&lt;span class="w"&gt; &lt;/span&gt;/build
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;services/common/logging/pyproject.toml&lt;span class="w"&gt; &lt;/span&gt;/build/services/common/logging/
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;services/&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SERVICE_NAME&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;/pyproject.toml&lt;span class="w"&gt; &lt;/span&gt;./
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;then, install the core deps, making sure you specify the &lt;code&gt;--package&lt;/code&gt; argument to install only for that part of the workspace, and the &lt;code&gt;--no-install-local&lt;/code&gt; argument to prevent including other workspace members:&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;RUN&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;uv&lt;span class="w"&gt; &lt;/span&gt;sync&lt;span class="w"&gt; &lt;/span&gt;--frozen&lt;span class="w"&gt; &lt;/span&gt;--no-install-local&lt;span class="w"&gt; &lt;/span&gt;--no-dev&lt;span class="w"&gt; &lt;/span&gt;--package&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SERVICE_NAME&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;next, the local dependencies… uv will figure out which ones of those are needed for the project based on the workspace graph:&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;services/common&lt;span class="w"&gt; &lt;/span&gt;/build/services/common
&lt;span class="k"&gt;RUN&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;uv&lt;span class="w"&gt; &lt;/span&gt;sync&lt;span class="w"&gt; &lt;/span&gt;--frozen&lt;span class="w"&gt; &lt;/span&gt;--no-editable&lt;span class="w"&gt; &lt;/span&gt;--no-install-project&lt;span class="w"&gt; &lt;/span&gt;--no-dev&lt;span class="w"&gt; &lt;/span&gt;--package&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SERVICE_NAME&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;and finally, the code for this service:&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;services/&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SERVICE_NAME&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;./
&lt;span class="k"&gt;RUN&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;uv&lt;span class="w"&gt; &lt;/span&gt;sync&lt;span class="w"&gt; &lt;/span&gt;--frozen&lt;span class="w"&gt; &lt;/span&gt;--no-editable&lt;span class="w"&gt; &lt;/span&gt;--no-dev&lt;span class="w"&gt; &lt;/span&gt;--package&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SERVICE_NAME&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;I refined this a little bit, because the above will leave two copies of the source code (one under &lt;code&gt;/build/services&lt;/code&gt; and one under the venv), which is undesirable in a Lambda image where size matters:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c"&gt;# Copy dependency files: workspace, and service project&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;pyproject.toml&lt;span class="w"&gt; &lt;/span&gt;uv.lock&lt;span class="w"&gt; &lt;/span&gt;/build
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;services/&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SERVICE_NAME&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;/pyproject.toml&lt;span class="w"&gt; &lt;/span&gt;./

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

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

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

&lt;p&gt;Now, the downside is that you are left with a few unneeded things in this final image: there are some leftovers from the venv and the uv command itself. I was also worried that running the uv command directly in the final image could leave me with some surprises, especially in a production Lambda environment.&lt;/p&gt;
&lt;h3&gt;Second attempt using pip&lt;/h3&gt;
&lt;p&gt;To work around the above problem, I install the dependencies in two steps into separate folders. Then, I do another stage in the Docker build to collect all these dependencies into a clean final image.&lt;/p&gt;
&lt;p&gt;Despite the title, this still relies on uv for dependency resolution — pip is only used for the final installation step, which keeps the runtime image simpler.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c"&gt;# --- Build Stage ---&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;public.ecr.aws/lambda/python:3.14-arm64&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;builder&lt;/span&gt;

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

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

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

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

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

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

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

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

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

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

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

&lt;span class="c"&gt;# Set the CMD to your handler&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;ingestion_worker.main.handler&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This results in a smaller, cleaner final image that contains only what Lambda needs at runtime.&lt;/p&gt;</content><category term="Tools"/><category term="python"/><category term="uv"/><category term="tooling"/><category term="aws"/><category term="lambda"/></entry><entry><title>Exploring Modern Python Type Checkers</title><link href="https://bytemypython.com/exploring-modern-python-type-checkers.html" rel="alternate"/><published>2026-01-12T00:00:00+01:00</published><updated>2026-01-12T00:00:00+01:00</updated><author><name>Nicolas Galler</name></author><id>tag:bytemypython.com,2026-01-12:/exploring-modern-python-type-checkers.html</id><summary type="html">&lt;p&gt;I’ve been playing around with alternative Python type checkers and language servers, looking beyond the more legacy options to find a setup that’s powerful without getting in the way. Along the way, I compared &lt;a href="https://docs.astral.sh/ty/"&gt;ty&lt;/a&gt; and &lt;a href="https://pyrefly.org/"&gt;Pyrefly&lt;/a&gt;, tweaked extensions and inlay hints, and ran into a few interesting …&lt;/p&gt;</summary><content type="html">&lt;p&gt;I’ve been playing around with alternative Python type checkers and language servers, looking beyond the more legacy options to find a setup that’s powerful without getting in the way. Along the way, I compared &lt;a href="https://docs.astral.sh/ty/"&gt;ty&lt;/a&gt; and &lt;a href="https://pyrefly.org/"&gt;Pyrefly&lt;/a&gt;, tweaked extensions and inlay hints, and ran into a few interesting differences in behavior, ergonomics, and typing semantics. The result isn’t a definitive winner yet, but it did surface some useful insights—and a couple of gotchas about Python typing that are easy to miss.&lt;/p&gt;
&lt;p&gt;Both extensions were very easy to install from the Extension Marketplace in Cursor.&lt;/p&gt;
&lt;p&gt;I disabled the &lt;strong&gt;“Python” extension from Anysphere&lt;/strong&gt; (keeping the &lt;code&gt;ms-python&lt;/code&gt; one). Anysphere’s extension is essentially &lt;strong&gt;pyright&lt;/strong&gt;, which was doing double duty with &lt;strong&gt;ty&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;For &lt;strong&gt;ty&lt;/strong&gt;, I also disabled the language server, as suggested in the documentation:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;python.languageServer&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;None&amp;quot;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;After that, there were still some logs being output:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ty&lt;/li&gt;
&lt;li&gt;ty language server&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;There were also &lt;strong&gt;inlay hints&lt;/strong&gt; showing inferred types. They’re somewhat useful, but overall a bit too much visual clutter, so I disabled them. I can still see variable types via tooltips when needed.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;I also tried &lt;strong&gt;Pyrefly&lt;/strong&gt;. The setup was similar, but I didn’t need to manually disable the language server—Pyrefly automatically disables &lt;strong&gt;Pylance&lt;/strong&gt;. It offers a few more customization options. One thing to note: by default, it will &lt;strong&gt;not display type errors unless a configuration file is found in the project&lt;/strong&gt;. This can be forced via the &lt;em&gt;“Display Type Errors”&lt;/em&gt; setting.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Where &lt;strong&gt;Pyrefly’s language server&lt;/strong&gt; was better:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;In one instance, it was able to automatically locate missing imports when I used &lt;code&gt;Cmd + .&lt;/code&gt;. This involved a type alias, where &lt;strong&gt;ty&lt;/strong&gt; took a few more shortcuts.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Inlay hints comparison&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Ty was a bit more obnoxious with inlay hints out of the box, while Pyrefly tended to include them only when they were genuinely useful. For example:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;merge_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dest&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;source&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;check_sql_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_validate_source_schema&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;stmt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;build_update_statement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;con&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;namespace&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Both tools correctly inferred the type of &lt;code&gt;source&lt;/code&gt; as &lt;code&gt;SqlIdString&lt;/code&gt; (rather than the original &lt;code&gt;str&lt;/code&gt;), but:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;ty&lt;/strong&gt; added an inlay hint&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;pyrefly&lt;/strong&gt; did not, unless I was also renaming the variable&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Ty also tended to union types with &lt;code&gt;| Unknown&lt;/code&gt; when using list comprehensions or list literals, which was pretty weird and annoying. Thankfully, there’s already an open issue for this, so I’ll keep tracking it.&lt;/p&gt;
&lt;p&gt;Pyrefly had one case where it refused to redefine a variable’s type via annotation. That’s valid Python, though arguably questionable style.&lt;/p&gt;
&lt;p&gt;Finally, I use Protocol for my interfaces on the project, and Pyrefly had generally more helpful messages when an interface was not correctly implemented.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Both of those are easy to integrate in the CI workflow, and they play nicer than pyright with Python's package manager.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Overall, I’m more aligned with &lt;strong&gt;Pyrefly&lt;/strong&gt;, but I’m sticking with &lt;strong&gt;ty&lt;/strong&gt; for now—mainly because I resonate more with Astral’s values than Meta’s. I’ll revisit that choice if ty starts to cause friction.&lt;/p&gt;
&lt;p&gt;One final typing takeaway: &lt;strong&gt;a type alias is not the same as &lt;code&gt;NewType&lt;/code&gt;&lt;/strong&gt;. A type alias doesn’t provide real type safety—it’s just a nickname for an existing type.&lt;/p&gt;</content><category term="Language"/><category term="vscode"/><category term="type-checking"/><category term="ty"/><category term="pyrefly"/></entry><entry><title>Stop Fighting Circular Imports</title><link href="https://bytemypython.com/stop-fighting-circular-imports.html" rel="alternate"/><published>2025-10-05T00:00:00+02:00</published><updated>2025-10-05T00:00:00+02:00</updated><author><name>Nicolas Galler</name></author><id>tag:bytemypython.com,2025-10-05:/stop-fighting-circular-imports.html</id><summary type="html">&lt;blockquote&gt;
&lt;p&gt;The code is good, but you are missing the type hints!&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Oh, I know, but as soon as I add them, I get these circular import errors!&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Mmm, that's often a smell, let's dig a little&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Sounds familiar?  Yet sometimes even when digging in, the design is fine, and the …&lt;/p&gt;</summary><content type="html">&lt;blockquote&gt;
&lt;p&gt;The code is good, but you are missing the type hints!&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Oh, I know, but as soon as I add them, I get these circular import errors!&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Mmm, that's often a smell, let's dig a little&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Sounds familiar?  Yet sometimes even when digging in, the design is fine, and the modules really need to import each other, especially if you want to keep things simple and not introduce a Java-style interface hierarchy.&lt;/p&gt;
&lt;p&gt;Turns out, modern Python has a solution for this, in fact, several of them.  Let's check it out :) &lt;/p&gt;
&lt;p&gt;Here a simple example:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# post.py&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;models.user&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;User&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;author&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;author&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;author&lt;/span&gt;

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

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;posts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;posts&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This gives:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;ImportError: cannot import name 'Post' from partially initialized module 'models.post' (most likely due to a circular import) (/home/nico/scratch/models/post.py)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;So, first solution, from PEP-563, was to be able to do the annotations as strings.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;author&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;User&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;author&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;author&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;But that's not enough for most tools, you also need to tell it where to find &lt;code&gt;User&lt;/code&gt;... yet you can't just import it as it would be back to a circular import, so you use a special "TYPE_CHECKING" guard:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;TYPE_CHECKING&lt;/span&gt;

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

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;author&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;User&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;author&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;author&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;And finally you can turn on these string annotations for the whole module using a &lt;strong&gt;future&lt;/strong&gt; import:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;__future__&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;annotations&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;TYPE_CHECKING&lt;/span&gt;

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

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;author&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;author&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;author&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Now, as often in Python, there are a few layers added over time.  For the full details, you'll want to read the PEPs: &lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;start with &lt;a href="https://peps.python.org/pep-0563/"&gt;PEP-563&lt;/a&gt;, which is rather simple, and is available &lt;strong&gt;today&lt;/strong&gt; (and as early as Python 3.7)&lt;/li&gt;
&lt;li&gt;then move on &lt;a href="https://peps.python.org/pep-0649/"&gt;PEP-649&lt;/a&gt;
and its little sister &lt;a href="https://peps.python.org/pep-0749/"&gt;PEP-749&lt;/a&gt;.  They change the way this is done, but this won't be available until Python 3.14.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Then, under the hood things will be quite different and it won't use the "string" annotations anymore.  For most usage (when annotating code for static type hints), you will just be able to drop the &lt;code&gt;__future__&lt;/code&gt; import.  &lt;/p&gt;</content><category term="Language"/><category term="type-checking"/><category term="import"/></entry><entry><title>My 5 Best Tricks using Pytest with Django</title><link href="https://bytemypython.com/my-5-best-tricks-using-pytest-with-django.html" rel="alternate"/><published>2024-04-25T00:00:00+02:00</published><updated>2024-04-25T00:00:00+02:00</updated><author><name>Nicolas Galler</name></author><id>tag:bytemypython.com,2024-04-25:/my-5-best-tricks-using-pytest-with-django.html</id><summary type="html"/><content type="html">&lt;p&gt;Test driven development is THE way I code, without compromise.  &lt;strong&gt;Maybe&lt;/strong&gt; if I am doing a quick spike of throw away code...
or some strictly UI code that I know is going to evolve a lot... but anything on the backend, I always start with setting up tests.
Often I work with legacy or semi-legacy code bases that don't have good tests and a big part of the work is setting those up - after
that the code practically writes itself :)&lt;/p&gt;
&lt;p&gt;Anyway, back to pytest.  Pytest is that awesome library and test runner for Python that makes it easy (well, easier) to write small and readable tests,
decoupling the part that is going to create all the bits of data that the test needs, from the test code itself.  It does this using 2 neat
features: fixtures, and test parametrization.  It also has a quite large ecosystem for integrating with application frameworks, like of course Django.&lt;/p&gt;
&lt;p&gt;Of course, all the examples in the Django documentation, refer to the TestCase classes they provide, and it's not always straightforward to adapt
to pytest cases.  The big problem I ran into with TestCase is that they tend to lead to large, self-contained classes that become very hard to understand.
You can get away from that by crafting utility classes... or mixins for your test cases, etc... but if you have complex relationships between your models
this can devolve into a spiderweb since your utility methods have to account for the potential intricacies of the tests.  Fixtures are one way to
address that, and the big advantage they have is that they can be locally overridden as the test needs it - I will get to that.&lt;/p&gt;
&lt;p&gt;I had used pytest before, quite a bit, but I ran into some unique difficulties with Django that I wanted to document.  I am not going to go into
the details of pytest itself because there is already quite a lot about it, except in specific cases where the interaction with Django is important.&lt;/p&gt;
&lt;div class="section" id="install-the-pytest-django-package"&gt;
&lt;h2&gt;Install the pytest-django package&lt;/h2&gt;
&lt;p&gt;Probably the first hit when you googled &amp;quot;pytest&amp;quot; and &amp;quot;django&amp;quot;.  Install that.  It will give you a bunch of fixtures that you can use to
test the Django application.  The full documentation is at &lt;a class="reference external" href="https://pytest-django.readthedocs.io/en/latest/"&gt;https://pytest-django.readthedocs.io/en/latest/&lt;/a&gt; and worth a read.  The fixtures
I use the most are the ones related to the database (db, transactional_db), there are also some wrappers for the custom assertions
normally available through Django TestCase subclassing (like assertRedirects, assertTemplateUsed, etc) and some Django helpers
that are made available as fixtures instead (like rf for RequestFactory, client, or django_assert_num_queries)&lt;/p&gt;
&lt;p&gt;One thing you have to do is to point pytest to your Django settings module.  You can do that by setting DJANGO_SETTINGS_MODULE in your pytest.ini
file (if it does not exist, create it in the root of your project):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="k"&gt;[pytest]&lt;/span&gt;
&lt;span class="na"&gt;DJANGO_SETTINGS_MODULE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;myproject.settings&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="section" id="using-reuse-db"&gt;
&lt;h2&gt;Using --reuse-db&lt;/h2&gt;
&lt;p&gt;For us it was a big deal to be able to reuse the database between tests - initializing the schema from scratch takes about 15 minutes on a
good day.  The --reuse-db flag is a lifesaver.  It will keep the database around between test runs, and only reinitialize the schema if
you add migrations.  Keep in mind that if you modify migrations, it will not pick that up - you have to either manually reset the test database,
or use the --create-db flag to force a new database to be created.&lt;/p&gt;
&lt;p&gt;You can add that flag in your pytest.ini file:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="k"&gt;[pytest]&lt;/span&gt;
&lt;span class="na"&gt;addopts&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;--reuse-db&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="section" id="understand-transaction-scope-and-database-fixtures"&gt;
&lt;h2&gt;Understand transaction scope and database fixtures&lt;/h2&gt;
&lt;p&gt;This is a really important one to understand because the tests are less &amp;quot;self-contained&amp;quot; than with TestCase.
By default, pytest will prevent any database access.  This is a good thing, because it makes the tests run faster, and it makes it
easier to reason about the tests.  But sometimes you need to access the database, and that's where the fixtures come in.  &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;pytest-django&lt;/span&gt;&lt;/tt&gt;
provides 2 different fixtures for this: &lt;tt class="docutils literal"&gt;db&lt;/tt&gt; and &lt;tt class="docutils literal"&gt;transactional_db&lt;/tt&gt;.  Internally this will map to Django &lt;tt class="docutils literal"&gt;TestCase&lt;/tt&gt; and &lt;tt class="docutils literal"&gt;TransactionTestCase&lt;/tt&gt;
classes.  The difference is that &lt;tt class="docutils literal"&gt;db&lt;/tt&gt; will run the whole test within a transaction and roll it back at the end of the test.
&lt;tt class="docutils literal"&gt;transactional_db&lt;/tt&gt; will start by serializing the state of the whole database, then run the test without a transaction, and at the end
it will reset the database using the saved state.  This is useful if you need to test code that will manipulate transactions explicitly
or use multiple database connections.  With a large database it's also very slow.  Because pytest encourages small, focused tests, and
can use test parametrization to quickly generate a large number of tests, &lt;tt class="docutils literal"&gt;transactional_db&lt;/tt&gt; can be &lt;strong&gt;very&lt;/strong&gt; slow.  Another problem of
&lt;tt class="docutils literal"&gt;transactional_db&lt;/tt&gt; is that if the test crashes in an abnormal way (say, your computer crashes, or you run in a debugger and stop the session),
the cleanup will not run, and the database will be left in the dirty state.  Again these problems are not specific to pytest, they also
exists with &lt;tt class="docutils literal"&gt;TestCase&lt;/tt&gt; and &lt;tt class="docutils literal"&gt;TransactionTestCase&lt;/tt&gt;, but as pytest lets you write more tests more easily you are more likely to run into them.&lt;/p&gt;
&lt;p&gt;Generally, use no database if you can afford it (but often Django apps are designed to use the ORM extensively so this is not always possible),
or use the &lt;tt class="docutils literal"&gt;db&lt;/tt&gt; fixture.  Use the &lt;tt class="docutils literal"&gt;transactional_db&lt;/tt&gt; fixture only if you absolutely need to.&lt;/p&gt;
&lt;p&gt;If you have fixtures that need to access the database, you have 2 choices:&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;you can have &lt;tt class="docutils literal"&gt;db&lt;/tt&gt; or &lt;tt class="docutils literal"&gt;transactional_db&lt;/tt&gt; as a dependency to the fixture.  But then this means the fixture, not the test, will decide how the
db is managed (if multiple fixtures request different database fixtures, pytest will use &lt;tt class="docutils literal"&gt;transactional_db&lt;/tt&gt;).&lt;/li&gt;
&lt;li&gt;you can request the fixture from the test itself.  I prefer this method because it makes it clear that the test is using the database.  You can
put a mark at the module level, if all tests within the file are going to need the database.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;div class="section" id="use-django-db-blocker-for-custom-database-setup"&gt;
&lt;h2&gt;Use django_db_blocker for custom database setup&lt;/h2&gt;
&lt;p&gt;This is where you absolutely need to master the concept of fixture scope.  By default, fixtures are function-scoped, which means that
they are created and destroyed for each test function.  This is also the case for the &lt;tt class="docutils literal"&gt;db&lt;/tt&gt; and &lt;tt class="docutils literal"&gt;transactional_db&lt;/tt&gt; fixtures, which means you
&lt;strong&gt;cannot&lt;/strong&gt; have a fixture with a wider scope that uses the database.  This is a problem if you want to share fixtures between tests.
You can then use &lt;tt class="docutils literal"&gt;django_db_blocker&lt;/tt&gt; to manually control the database access.  This is a context manager that will allow you to run code
that access the database.  You also need to request the &lt;tt class="docutils literal"&gt;django_db_setup&lt;/tt&gt; fixture in that case, which runs once per session and ensures
the migrations are run on the test database.  Here is an example:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;pytest&lt;/span&gt;

&lt;span class="nd"&gt;@pytest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fixture&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;module&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;my_model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;django_db_setup&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;django_db_blocker&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;django_db_blocker&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;unblock&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="c1"&gt;# this runs once per module, but will be seen by all the tests&lt;/span&gt;
        &lt;span class="n"&gt;some_complicated_model_setup_that_takes_a_long_time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;yield&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;django_db_blocker&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;unblock&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="c1"&gt;# I need to do this since nobody is going to do it for me!&lt;/span&gt;
        &lt;span class="n"&gt;some_complicated_model_teardown&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_my_model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;my_model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# I run within a transaction&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;my_model&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="c1"&gt;# and at the end of the test, the transaction is rolled back&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_my_model_more&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;my_model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# I also run within a (different) transaction&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;my_model&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Normally, with a function-scoped fixture, the &lt;tt class="docutils literal"&gt;my_model&lt;/tt&gt; fixture would be created and destroyed for each test.  But with the &lt;tt class="docutils literal"&gt;module&lt;/tt&gt; scope,
it only runs once.  Note how I still have to request the &lt;tt class="docutils literal"&gt;db&lt;/tt&gt; fixture in the test, because I exited the context manager before the test,
so that the tests are still isolated from each other.&lt;/p&gt;
&lt;p&gt;I don't really like this code because it is not very readable and it makes the tests less isolated.
But if you absolutely have to do a long-running setup you might need this.
There is no equivalent to the class level &lt;tt class="docutils literal"&gt;setUpTestData&lt;/tt&gt; of Django, because this conflicts with the concept of fixture scope in
pytest - you have to achieve the same result by using a module-scoped fixture, but this also requires you to do a little bit of work
to ensure the database is properly set up and torn down.  The next tip will show a safer way to do this.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="use-a-larger-scope-transaction-for-complex-initialization"&gt;
&lt;h2&gt;Use a larger-scope transaction for complex initialization&lt;/h2&gt;
&lt;p&gt;By combining the above 2 tips, it is possible to offer a fixture that runs at a higher than function scope and provides a transactional
context.  The tests will then operate within their own, nested transaction, and will still be isolated from each other.
This is only possible if the database backend supports nested transactions (or savepoints, as Django will automatically use that within
a transaction).  Which is the case for most popular databases supported by Django.&lt;/p&gt;
&lt;p&gt;You use &lt;tt class="docutils literal"&gt;django_db_blocker&lt;/tt&gt; to open the transaction at a higher scope, &lt;tt class="docutils literal"&gt;yield&lt;/tt&gt; within it so that your test function can run and
create a nested transaction within that scope, then close the transaction at the end of the test (simply by exiting the context manager).&lt;/p&gt;
&lt;p&gt;Here is an example:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;pytest&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;django.db&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;transaction&lt;/span&gt;

&lt;span class="nd"&gt;@pytest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fixture&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;module&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;my_model_transaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;django_db_setup&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;django_db_blocker&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;django_db_blocker&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;unblock&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;transaction&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;atomic&lt;/span&gt;&lt;span class="p"&gt;()):&lt;/span&gt;
        &lt;span class="c1"&gt;# this runs once per module, but will be seen by all the tests&lt;/span&gt;
        &lt;span class="n"&gt;some_complicated_model_setup_that_takes_a_long_time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="c1"&gt;# yield within the transaction context&lt;/span&gt;
        &lt;span class="k"&gt;yield&lt;/span&gt;
        &lt;span class="c1"&gt;# no need for teardown, since we use a transaction&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_my_model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;my_model_transaction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# I run within a nested transaction&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;my_model_transaction&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="c1"&gt;# and at the end of the test, the transaction is rolled back&lt;/span&gt;
    &lt;span class="c1"&gt;# (but not the outer transaction which has the model setup)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_my_model_more&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;my_model_transaction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# I also run within a (different) transaction&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;my_model_transaction&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;One big downside is that this cannot be combined with &lt;tt class="docutils literal"&gt;transactional_db&lt;/tt&gt; since there is always a transaction running.  So if you have
even one test that needs to run without a transaction, this will not work.  In my case I had a pretty neat setup using this but realized
toward the end that some tests needed to run outside of the transaction, so I had to refactor the whole thing.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="sharing-fixtures-between-django-apps"&gt;
&lt;h2&gt;Sharing fixtures between Django apps&lt;/h2&gt;
&lt;p&gt;Something about scope and pytest_plugins vs import&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="django-testcase-still-work"&gt;
&lt;h2&gt;Django TestCase still work!&lt;/h2&gt;
&lt;p&gt;Pytest will still pick up and run your Django TestCase classes.  This is useful if you have a lot of tests already written in that style, and you
don't want to convert them all at once.  You can run them in parallel with your pytest tests, and slowly convert them over time.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="bonus-some-useful-links"&gt;
&lt;h2&gt;Bonus: Some useful links&lt;/h2&gt;
&lt;/div&gt;
</content><category term="Django"/></entry><entry><title>Your database is not elastic!</title><link href="https://bytemypython.com/your-database-is-not-elastic.html" rel="alternate"/><published>2024-04-17T00:00:00+02:00</published><updated>2024-04-17T00:00:00+02:00</updated><author><name>Nicolas Galler</name></author><id>tag:bytemypython.com,2024-04-17:/your-database-is-not-elastic.html</id><summary type="html">&lt;p class="first last"&gt;Reality of web development when dealing with elastic compute resources and a relational database&lt;/p&gt;
</summary><content type="html">&lt;p&gt;Recently, I had a tough experience with a web app that didn’t scale as planned. We built the app to easily add more servers, but the database couldn’t keep up. This is a common issue in web development and often gets overlooked until it’s too late. Databases, especially relational ones, aren’t as flexible as other parts of the app. While you can quickly add more web servers or queue workers, scaling a database horizontally (adding more servers) is tricky and costly.&lt;/p&gt;
&lt;p&gt;You can create read-only replicas, but setting them up isn’t easy or cheap. Sharding, another option, involves splitting the database into pieces, but it’s complicated and expensive. Handling transactions across these pieces is also a headache. Scaling up (making one server bigger) isn’t a long-term fix. Eventually, you’ll hit the limit of the biggest server you can afford.&lt;/p&gt;
&lt;p&gt;It’s a strange reversal. In the past, when I worked on distributed apps like Windows-based CRM systems, the database was often the only part that could scale, while client machines couldn’t. Now, it’s the opposite.&lt;/p&gt;
&lt;img alt="Desktop vs Web App Scaling" class="align-center" src="images/desktop_vs_web_app_scaling.png" /&gt;
&lt;p&gt;This reality hit home for me last week: a deployment which had been carefully planned and tested for weeks quickly went south
when it became obvious that too much of a burden was shifted to the database.  The web workers happily scaled out to try and
answer the requests faster, which actually compounded the problem since it added to the load on the one thing that would not,
could not scale: the database.&lt;/p&gt;
&lt;p&gt;Eventually we were able to resolve this with not too much broken bones by scaling up the database, until the application could be corrected.
The cloud managed database service made that easier but it was still not a seamless process (and is also rather costly since it can't automatically scale back down either).
And the feature had to be re-architected to be less database intensive - thankfully the change was rather isolated so we could turn that around quickly.&lt;/p&gt;
&lt;p&gt;This experience taught us an important lesson: databases are a critical part of web development that require careful planning to avoid major problems down the line.&lt;/p&gt;
</content><category term="Django"/></entry><entry><title>A Python-powered blog on AWS Cloudfront</title><link href="https://bytemypython.com/a-python-powered-blog-on-aws-cloudfront.html" rel="alternate"/><published>2024-04-08T06:29:00+02:00</published><updated>2024-04-08T06:29:00+02:00</updated><author><name>Nicolas Galler</name></author><id>tag:bytemypython.com,2024-04-08:/a-python-powered-blog-on-aws-cloudfront.html</id><summary type="html">&lt;p class="first last"&gt;Easy setup of a blog using a popular Python-powered static site generator and AWS tools.&lt;/p&gt;
</summary><content type="html">&lt;p&gt;To start this blog, naturally I wanted to use a Python tool.  I was attracted by the philosophy of
Jekyll for static site generation, and as it seems every language has his static site generator
(SSG) nowadays, I picked a popular Python equivalent: Pelican.  Why Pelican?  I actually started
with &amp;quot;Nikola&amp;quot;, but ran into a few minor annoyances.  The Pelican package seems quite polished: they have a
large plugin repository, they support reStructuredText by default (and I liked that they used docinfo
for the metadata which seemed more appropriate than directives), and a modular architecture.  Their documentation
is pretty good too.  Importantly, having a static site generator means my data remains easily exploitable
by another engine, so it was not too important to spend much time on this decision now (indeed a large part
of the job of a software engineer is knowing which decisions are important to spend time on, which ones
can be deferred, and how to keep these decisions flexible).
I decided to host it on Cloudfront, I have a lot of experience with the more &amp;quot;backend&amp;quot; AWS services so it should be a fun learning
experiment (again, an easy to modify later decision).  Cloudfront used to be rather pricey but now has a generous free tier
that is more than sufficient for a personal blog.&lt;/p&gt;
&lt;p&gt;This short article serves to document my journey as a howto if you are interested in replicating it.&lt;/p&gt;
&lt;div class="section" id="create-a-repo"&gt;
&lt;h2&gt;Create a repo&lt;/h2&gt;
&lt;p&gt;The first step to anything :)
I picked github, by default.  Remember having to pick between RCS and CVS?  Yeah, I'm that old.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="install-pelican"&gt;
&lt;h2&gt;Install Pelican&lt;/h2&gt;
&lt;p&gt;I used &lt;tt class="docutils literal"&gt;pipenv install pelican&lt;/tt&gt;.   By default it does not have Markdown support.  Installing
with pipenv means it will be kept separate from the system packages and installed within its own isolated environment.
It's a good practice in general for Python development.  From that point on you have to prefix all commands with
&lt;tt class="docutils literal"&gt;pipenv run&lt;/tt&gt;, or use &lt;tt class="docutils literal"&gt;pipenv shell&lt;/tt&gt; to drop into a shell with the path to the virtual environment activated.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="generate-blog-skeleton"&gt;
&lt;h2&gt;Generate blog skeleton&lt;/h2&gt;
&lt;p&gt;Use &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;pelican-quickstart&lt;/span&gt;&lt;/tt&gt; (but actually &lt;tt class="docutils literal"&gt;pipenv run &lt;span class="pre"&gt;pelican-quickstart&lt;/span&gt;&lt;/tt&gt;!) to generate a skeleton.  Initially I had already
created the &lt;tt class="docutils literal"&gt;content&lt;/tt&gt; folder and it errored out, but no biggy as it only serves to create an empty directory anyway.
When running the quickstart, enter the name of the S3 bucket you will publish to.  It doesn't have to exist yet.&lt;/p&gt;
&lt;p&gt;I added a post under &lt;tt class="docutils literal"&gt;content&lt;/tt&gt; so it would have something to show.  Pelican will automatically determine the category based
on the post path, so I put mine under &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;content/tools//pelican-aws-cloudfront-blog.rst&lt;/span&gt;&lt;/tt&gt;.&lt;/p&gt;
&lt;p&gt;At that point &lt;tt class="docutils literal"&gt;pelican &lt;span class="pre"&gt;-r&lt;/span&gt; &lt;span class="pre"&gt;-l&lt;/span&gt;&lt;/tt&gt; will build and serve locally.  Pelican generated a Makefile so you can also do &lt;tt class="docutils literal"&gt;make
devserver&lt;/tt&gt;.  This was a nice touch, I like Makefile.  Just stop with Ctrl+C when done.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="push-to-aws"&gt;
&lt;h2&gt;Push to AWS&lt;/h2&gt;
&lt;p&gt;You must have a correctly configured AWS CLI for this step.  Refer to the &lt;a class="reference external" href="https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-configure.html"&gt;Amazon documentation&lt;/a&gt; for this.&lt;/p&gt;
&lt;ol class="arabic"&gt;
&lt;li&gt;&lt;p class="first"&gt;First, create the bucket.  The easiest is to do it in the AWS console.  Make sure you uncheck &amp;quot;Block public access&amp;quot;.
Also check &amp;quot;ACLs Enabled&amp;quot;.  The best practice is to leave ACL disabled, and create a policy, but it's a bit of a hassle and
does not really make a difference in this case since we'll be the only one uploading to the bucket.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;Run &lt;tt class="docutils literal"&gt;pipenv make s3_upload&lt;/tt&gt; to upload the files.  You may have to edit the Makefile to correct the name of the bucket.
I also edited the Makefile at that point to remove&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;Create a CloudFront distribution in the AWS Console.  Specify the bucket as the &amp;quot;origin&amp;quot;.  Leave the WAF (web application
firewall) disabled.  Also change &amp;quot;Origin access&amp;quot; to public.  Again, it would be possible / best practice to specify this
using a policy... but no real purpose here.  Lastly, set the &amp;quot;Default root object&amp;quot; to &amp;quot;index.html&amp;quot;.  We'll come back later to
configure the custom domain.
Without configuring an alternate domain, CloudFront will set up a random
&lt;tt class="docutils literal"&gt;cloudfront.net&lt;/tt&gt; domain name I can use for testing (something like &lt;a class="reference external" href="https://d2dl3dxwttz643.cloudfront.net/"&gt;d2dl3dxwttz643.cloudfront.net&lt;/a&gt; I ran into some &amp;quot;Access Denied&amp;quot; error here... to troubleshoot, first run a curl
command to retrieve the file directly from S3:&lt;/p&gt;
&lt;pre class="code bash literal-block"&gt;
curl&lt;span class="w"&gt; &lt;/span&gt;www.bytemypython.com.s3.us-east-1.amazonaws.com/index.html
&lt;/pre&gt;
&lt;p&gt;That got me &amp;quot;Access Denied&amp;quot; so I knew something was up with the bucket permissions.  I found that I had messed up one of
the previous steps: I had left ACL disabled on the bucket.  I went back and re-enabled them through the console, and redid
the upload.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;Register a domain.  This is pretty easy with AWS Route53 and it will integrate with the rest.  Go to &lt;a class="reference external" href="https://console.aws.amazon.com/route53/domains/home"&gt;AWS Route53 in the
AWS Console&lt;/a&gt;, click Register Domain, and follow the prompt - it
will take a little while to complete.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The way CloudFront works, it will download the files from S3 the first time they are requested, then cache them for up to 24h by default.
Just something to be aware of when you publish new content.  You can modify this behavior under the &amp;quot;Behaviors&amp;quot; tab of the CloudFront
console.  You can also create an immediate invalidation for all the cached files, with a little bit of effort it would be possible
to combine to automate that within the upload command.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="create-or-customize-a-theme"&gt;
&lt;h2&gt;Create or customize a theme&lt;/h2&gt;
&lt;p&gt;I wanted to customize the theme a bit.  I found a nice one called &amp;quot;&lt;a class="reference external" href="https://github.com/iKevinY/pneumatic"&gt;Pneumatic&lt;/a&gt;&amp;quot; that I liked, quite minimalist, very clean.
Since I am a full stack dev, I wanted to fiddle with it.  I copied the templates but restarted the CSS from scratch using the
&lt;a class="reference external" href="https://picocss.com/"&gt;pico&lt;/a&gt; framework.  I have not experimented with it before, and it's a good occasion to try a different
approach to CSS - at work we usually have the safe choices of Bootstrap or Material Design.&lt;/p&gt;
&lt;p&gt;This lead me to discover the plugin feature of Pelican, which is very easy to use with
pip.  The &lt;a class="reference external" href="https://github.com/pelican-plugins/webassets"&gt;webasset&lt;/a&gt; plugin is a good one to use for this purpose.  It can preprocess files
and generate a new file name that can then be included in the HTML template.  I had a bit of fun with CSS and off we went :)&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="update-july-2026"&gt;
&lt;h2&gt;Update (July 2026)&lt;/h2&gt;
&lt;p&gt;The build process to publish to AWS was a bit annoying, and CloudFront added needless friction - I updated the workflow to do the build in Github Actions, and publish to Github Pages.
The configuration is visible in the &lt;a class="reference external" href="https://github.com/f1code/byte_my_python/tree/main/.github/workflows"&gt;repo&lt;/a&gt;.&lt;/p&gt;
&lt;/div&gt;
</content><category term="Tools"/></entry></feed>