<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">

  <title><![CDATA[Trey Hunner]]></title>
  <link href="https://treyhunner.com/atom.xml" rel="self"/>
  <link href="https://treyhunner.com/"/>
  <updated>2026-02-20T14:06:16-08:00</updated>
  <id>https://treyhunner.com/</id>
  <author>
    <name><![CDATA[Trey Hunner]]></name>
    
  </author>
  <generator uri="http://octopress.org/">Octopress</generator>

  
  <entry>
    <title type="html"><![CDATA[On the enviromental impact of using LLMs for writing code]]></title>
    <link href="https://treyhunner.com/2026/02/on-the-enviromental-impact-of-llms-for-coding/"/>
    <updated>2026-02-20T14:30:00-08:00</updated>
    <id>https://treyhunner.com/2026/02/on-the-enviromental-impact-of-llms-for-coding</id>
    <content type="html"><![CDATA[<p>I&rsquo;ve had <em>many</em> conversations over the past year with friends and colleagues about LLMs.
Some conversations have focused on their uses and misuses and some have focused on big picture concerns.</p>

<p>There are many reasons to be concerned about LLMs: job displacement, intellectual property issues, moral hazards. The one I hear the most is the environmental impact.</p>

<p><strong>TL;DR</strong>: I think the environmental impact of LLMs is primarily a climate change problem, that the impact of individual LLM use on this problem is relatively small, and that climate change is best addressed through collective action (ideally taxes, subsidies, and regulations). But since systemic solutions aren&rsquo;t coming soon, understanding the relative impact of specific activities (including LLM use) does matter.</p>

<h2>Energy use of individual LLM prompts</h2>

<p>When thinking about the energy use of LLMs, I tend to agree with the framing that Hannah Ritchie takes: this <em>is</em> something to be concerned about, but many headlines are misleading.</p>

<p>Hannah Ritchie is a data scientist, deputy editor at Our World in Data, and co-host of the podcast Solving for Climate. In Hannah&rsquo;s <a href="https://hannahritchie.substack.com/p/ai-energy-demand">post about the overall energy demand from AI data centers</a>, she notes that data centers in general make up <strong>a small fraction of the world&rsquo;s global electricity demand</strong> and that they are estimated to be a fairly small portion of the growth in global electricity use until 2030.</p>

<p>That doesn&rsquo;t mean that the energy demand from data centers isn&rsquo;t a problem. But, as she notes, due to the very uneven distribution of data centers around the globe, this is <strong>a very localized problem</strong>:</p>

<blockquote><p>What’s crucial here is that the energy demands for AI are very localised. This means there can be severe strain on the grid at a highly localised level, even if the impact on total energy demand is small.</p></blockquote>

<p>Hannah also has <a href="https://hannahritchie.substack.com/p/carbon-footprint-chatgpt">a follow up post on individual LLM use</a> where she suggests that individual users should &ldquo;stop stressing about the energy and carbon footprint&rdquo; of LLMs.</p>

<p>In all of these posts, she attempts to put numbers into perspective:</p>

<blockquote><p>The reason we often think that ChatGPT is an energy guzzler is because of the initial statement: it uses 10 times more energy than a Google search. Even if this is accurate, what’s missing is the context that a Google search uses a really tiny amount of energy. Even 10 times a really tiny number is still tiny.</p></blockquote>

<p>Hannah also published <a href="https://hannahritchie.substack.com/p/ai-footprint-august-2025">a follow up to her follow up post</a> where she seriously questions the 3 Wh figure (10 times higher than a Google search) that&rsquo;s often used when discussing the energy use of LLM queries. She considers whether the median LLM query may use closer to 0.3 Wh of energy.</p>

<p>I lean heavily on Ritchie&rsquo;s analysis throughout this post because I think her data-oriented framing of this issue is helpful. That said, detailed energy data from AI companies is scarce so her estimates are inherently fuzzy.</p>

<h2>Energy use of coding agents</h2>

<p>Hannah&rsquo;s posts about individual LLM usage focuses on the &ldquo;median query&rdquo; in an LLM&hellip; but using a coding agent (Claude Code, Codex, etc.) to navigate through a repository and generate lots of code likely involves quite a bit more usage than a &ldquo;median query&rdquo; does.</p>

<p>Simon P. Couch <a href="https://www.simonpcouch.com/blog/2026-01-20-cc-impact/">recently wrote a post</a> about the energy use he estimates for his Claude Code usage. He estimates that <strong>his Claude Code usage</strong> results in approximately <strong>1,300 Wh</strong> of energy each day. That&rsquo;s quite a bit higher than either 0.3 Wh or 3 Wh for a median LLM query.</p>

<p>If heavy coding agent use results in 1,300 Wh of energy, that represents <strong>3.7%</strong> of the average American&rsquo;s daily electricity use (based on an <a href="https://ourworldindata.org/explorers/energy?Total+or+Breakdown=Total&amp;Energy+or+Electricity=Electricity+only&amp;Metric=Per+capita+generation&amp;country=USA~GBR~CHN~OWID_WRL~IND~BRA~ZAF">average electricity generation per person</a> of 12,712 kWh in the US). Increasing your energy use by 4% is <em>not</em> nothing&hellip; but how bad is it?</p>

<h2>Climate impact of coding agents</h2>

<p>Increased energy usage isn&rsquo;t as concerning to me as the negative impacts that the energy usage might have on the climate.</p>

<p>Going with <a href="https://andymasley.substack.com/p/whats-the-full-hidden-climate-cost">Adam Masley&rsquo;s assumptions</a> of 0.37 grams of carbon dioxide emissions per watt-hour (Wh) in the US and the assumption that data centers use energy that&rsquo;s 48% more carbon intensive than the average US power plant, <strong>1,300 Wh per day of coding agent usage</strong> could result in 0.5 grams of CO₂ per Wh, for about <strong>715 grams of CO₂ emissions per day</strong> from coding agent use.</p>

<p>The annual carbon footprint of the average American is <a href="https://ourworldindata.org/explorers/co2?time=earliest..2022&amp;focus=~USA&amp;Gas+or+Warming=CO%E2%82%82&amp;Accounting=Consumption-based&amp;Fuel+or+Land+Use+Change=All+fossil+emissions&amp;Count=Per+capita&amp;country=CHN~USA~IND~GBR~OWID_WRL">16,300 kg of CO₂</a>, which is about 44.7 kg of CO₂ per day. So 715 grams of CO₂ from a day of coding agent usage may be about <strong>1.6% of the average American&rsquo;s daily carbon footprint</strong> of 44.7 kg of CO₂.</p>

<p>Again, 1.6% is not nothing. It&rsquo;s not <em>enormous</em> but it&rsquo;s certainly measurable.</p>

<p>I think that any discussion about greenhouse gas emissions should be considered in the context of the rest of life and ideally in context of the social cost of those emissions. I haven&rsquo;t tried quantifying the social cost, but I have looked up other activities and their climate impact.</p>

<p>For the sake of comparison, here are some <em>very rough</em> estimates of carbon dioxide equivalent emissions (CO₂e) for various consumables and activities:</p>

<ul>
<li>one hour of Netflix: 50 grams CO₂e</li>
<li>a banana: 80 grams CO₂e</li>
<li>one hour of Zoom: 100 grams CO₂e</li>
<li>half cup tofu: 300 grams CO₂e</li>
<li>half cup (dairy) greek yogurt: 400 grams CO₂e</li>
<li>driving 5 miles in an EV: 500 grams CO₂e</li>
<li>5 minute shower: 700 grams CO₂e</li>
<li><strong>1 day of using a coding agent: 700 grams CO₂e</strong> (based on <a href="https://www.simonpcouch.com/blog/2026-01-20-cc-impact/">one user&rsquo;s estimate</a>)</li>
<li>driving 5 miles in a gas car: 1,500 grams CO₂e</li>
<li>beef burger: 5,000 grams CO₂e</li>
<li>flying SAN to ORD (one-way): 400,000 grams CO₂e</li>
</ul>


<p>Treat those numbers as &ldquo;likely correct within an order of magnitude&rdquo;. Most of those numbers have pretty large error bars, as the industries that have solid data on these figures rarely publish them and even when they do, the actual emissions can vary greatly due to the number of variables involved.</p>

<p>If you&rsquo;re curious about any specific measurement, compute them based on sources you trust. I recommend Our World in Data: <a href="https://ourworldindata.org/grapher/ghg-per-kg-poore">food</a>, <a href="https://greenly.earth/en-us/leaf-media/data-stories/the-carbon-cost-of-streaming#anchor-articles_content$d086800e-3afb-4608-9c27-1f13bc477813">streaming video</a>, <a href="https://ourworldindata.org/travel-carbon-footprint">transportation</a>.</p>

<p>Note that I am not saying &ldquo;using LLMs is not bad because people eat beef&rdquo;, which would be <a href="https://andymasley.substack.com/p/a-cheat-sheet-for-conversations-about?utm_source=publication-search">whataboutism</a>. But I don&rsquo;t think that we should discuss the carbon emissions of any given activity in a vacuum.</p>

<h2>Water use</h2>

<p>I&rsquo;m going to mostly skip over the water use of LLMs because I find it far less concerning than the energy use. Based on Andy Masley&rsquo;s numbers (from his provocatively-titled post <a href="https://andymasley.substack.com/p/the-ai-water-issue-is-fake">The AI water issue is fake</a>), a median LLM query uses around 2ml of water including offsite power generation (0.3mL of water if we don&rsquo;t count power generation). That means that a full day of coding agent use would consume about 6.5 liters of water (0.6 liters for everything but the power generation).</p>

<p>The average American&rsquo;s daily water footprint is 1,600 liters. So 6.5 liters of water (largely from offsite power generation) represents roughly <strong>0.4% of the average American&rsquo;s daily water footprint</strong>. 90% of that water comes from the water required for power generation because generating electricity from natural gas, coal, and nuclear all requires water.</p>

<p>Like energy use, water use is a very localized issue. Unlike energy use, water use doesn&rsquo;t also contribute to climate change. Water use is worth discussing, but I find the climate impact of coding agents much more concerning. I&rsquo;m also not sure how much the local impact of water use should be a concern, since it matters what type of water is used, how much of that water is returned to the source, and what else that water would be used for. Hank Green recently released <a href="https://www.youtube.com/watch?v=H_c6MWk7PQc">an interesting video on water use</a>.</p>

<h2>Climate change requires collective action</h2>

<p>The biggest environmental concern of LLMs is how much they contribute to climate change.</p>

<p>Climate change is a collective action problem. We need to greatly limit greenhouse gas emissions. And in a world of goods with variable market-based prices, a negative externality like greenhouse gas emissions warrants a tax. A carbon tax or a cap and trade program could go <em>very far</em> in accomplishing that, but I&rsquo;m skeptical that we&rsquo;ll see either of those happen on a massive international scale (and especially not in the US) anytime soon.</p>

<p><strong>Aside</strong>: did you know that the US had <a href="https://www.congress.gov/crs-product/IF12916">an Interagency Working Group on the Social Cost of Greenhouse Gases</a> which published official estimates on the cost of carbon emissions? It was established under Obama, disbanded under Trump, reestablished under Biden, and then disbanded again under Trump. I really wish the US would implement a carbon tax so that we can stop focusing on specific carbon-emitting activities and instead properly price carbon emissions in aggregate.</p>

<h2>So why talk about LLMs specifically?</h2>

<p>If climate change is best addressed by big picture solutions, like a tax on all greenhouse gas emissions, then why even look into the climate impact of specific activities, like LLM use?</p>

<p>Because we don&rsquo;t have a carbon tax. And we&rsquo;re probably not getting one anytime soon.</p>

<p>In the absence of systemic solutions, general sentiment about what&rsquo;s a <em>big</em> problem and what&rsquo;s a <em>small</em> problem shapes how we respond&hellip; as individuals, in communities, as consumers and workers within companies, and as citizens pressuring various levels of government.</p>

<p>We can&rsquo;t expect individuals to weigh the climate impact of every action they take (thinking of The Good Place&rsquo;s S03E11: The Book of Dougs). And hoping that corporations will grow a conscience is wishful thinking. But having a shared understanding of the relative impacts of various climate concerns <em>does</em> have <em>some</em> effect on what gets attention, what gets research, and what gets regulated.</p>

<p>So I think it&rsquo;s worth understanding the relative impact of different activities, including LLM use. Putting numbers in context helps us have better conversations and direct our collective energy more effectively.</p>

<h2>Global and local, individual and collective</h2>

<p>There are two angles I see this problem from:</p>

<ol>
<li>The global problem of climate change from greenhouse gas emissions</li>
<li>The local impacts caused by data centers</li>
</ol>


<p>For climate change overall, I think we should:</p>

<ul>
<li>Pressure the companies that make LLMs to publish detailed information about carbon emissions</li>
<li>Pressure our politicians to tax greenhouse gas emissions&hellip; or at least to tax activities that result in greenhouse gas emissions in a way that would mimic the outcome of a carbon tax</li>
<li>Pressure our politicians to subsidize activities that lower greenhouse gas emissions, like increasing solar and battery technology use on the electrical grid</li>
<li>Talk about climate change and encourage our colleagues and loved ones to talk about it</li>
</ul>


<p>When it comes to AI data centers, I think focusing on their impacts to local grids makes more sense than focusing on their global impacts. The uneven distribution of data centers makes their local impacts disproportionately large in comparison to their global impacts. I see this as similar to the problem of pig farms. Eastern North Carolina has waste lagoons from pig farms that cause serious issues for nearby communities. Both data centers and pig farms are very unevenly distributed and both cause big problems for their neighboring communities.</p>

<p>On an individual level, I would rather see people eat less meat and dairy products than worry about the climate impact of using LLM coding agents. But more so than either of those, I would rather that we have hard and honest conversations with colleagues and loved ones and pressure our elected representatives in government to take action on this problem.</p>

<p>Americans have a habit of thinking about systemic problems through the lens of individual action. As I noted in <a href="https://youtu.be/Uuhu-F05A7k?feature=shared&amp;t=3173">my lightning talk at PyCon last year</a>, system-level problems require system-level solutions. If the climate impact of LLMs concerns you, I&rsquo;d encourage you to direct that energy toward the people and institutions that can change these systems.</p>
]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[My favorite reads of 2025]]></title>
    <link href="https://treyhunner.com/2025/12/my-favorite-reads-of-2025/"/>
    <updated>2025-12-31T13:00:00-08:00</updated>
    <id>https://treyhunner.com/2025/12/my-favorite-reads-of-2025</id>
    <content type="html"><![CDATA[<p>I read 41 books this year, all via audiobook. Below are my reviews for <strong>my 13 favorite reads</strong> out of the 41 books I read.</p>

<p>If you enjoy audobooks, I recommend switching from Audible to <a href="https://libro.fm/referral?rf_code=lfm240965">Libro.fm</a> (that&rsquo;s a referral link). Audible has some pretty slimy business practices (<a href="https://treyhunner.com/2024/12/my-favorite-audiobooks-of-2024/">noted in my 2024 audiobooks post</a>).</p>

<h2>Books about World Betterment</h2>

<p>These 7 books are on immigration, economics, sociology, and welfare overall. They&rsquo;re all on the topic of making the world a better place for all of us.</p>

<h3>&ldquo;The Upswing&rdquo; by Robert D. Putnam with Shaylyn Romney Garrett</h3>

<p><strong><a href="https://app.thestorygraph.com/books/75a980aa-b6e5-4251-b654-697628a0a9f7">The Upswing</a></strong> is probably the most important book I read in 2025.
This book is about moving society away from individualism to and back toward communitarianism: moving from &ldquo;me&rdquo; to &ldquo;we&rdquo;.</p>

<p>I watched the <a href="https://joinordiefilm.com">Join Or Die</a> documentary (it&rsquo;s on Netflix) before reading this book.
The two are different and I would recommend both.</p>

<h3>&ldquo;Mind if I Order the Cheeseburger?: And Other Questions People Ask Vegans&rdquo; by Sherry F. Colb</h3>

<p>From the title <strong><a href="https://app.thestorygraph.com/books/82076223-39f1-4c34-8752-8bda2600e775">Mind if I Order the Cheeseburger</a></strong>, I assumed this would be a snarky book written for vegans. It is not snarky and it&rsquo;s not primarily for vegans.</p>

<p>This book is for <strong>non-vegans who have questions about veganism</strong>.
It&rsquo;s fairly plainly written and easy to understand.</p>

<p>I found myself taking issue with some of the arguments in this book but I also found myself admiring some of the arguments and analogies. There were more than a few issues I had never even considered before.</p>

<p>If you&rsquo;re <strong>curious about veganism</strong> but hoping to avoid descriptions of the specifics of animal suffering, this is a pretty good book to start with. The author kindly lays out her reasoning without delving too deep into the specifics of factory farming.</p>

<h3>&ldquo;Streets of Gold: America&rsquo;s Untold Story of Immigrant Success&rdquo; by Ran Abramitzky and Leah Boustan</h3>

<p><strong><a href="https://app.thestorygraph.com/books/e983bd0e-d61b-4355-b527-f7a967d78566">Streets of Gold</a></strong> is a book on immigration written by economists in a way that is fairly easy for non-economists to understand. I&rsquo;m somewhat surprised that such a book exists.</p>

<p>I wish <strong>all immigration skeptics</strong> read this book and considered the data referenced and the arguments made.</p>

<h3>&ldquo;The Case for Open Borders&rdquo; by John Washington</h3>

<p>I assumed <strong><a href="https://app.thestorygraph.com/books/b621ee39-e669-4cbe-b74c-4846f0934084">The Case for Open Borders</a></strong> might be focused on the economic case for open borders. It partly focused on that, but it didn&rsquo;t primarily take an economic angle. I found this interesting and thought-provoking.</p>

<p>If you are <strong>skeptical of the idea of radically increasing US immigration</strong>, this is the first book I would probably recommend reading.</p>

<h3>&ldquo;Under The Influence&rdquo; by Robert H. Frank</h3>

<p><strong><a href="https://app.thestorygraph.com/books/f85fe8f8-a130-4e8d-9617-d25cb9e936a5">Under The Influence</a></strong> feels like an economist <strong>applying economics to sociology</strong>. I found it really interesting.
I really enjoyed Robert H. Frank&rsquo;s <a href="https://treyhunner.com/2017/01/my-favorite-audiobooks-of-2016/">Success and Luck</a>. This book is quite different from that one.</p>

<p>I enjoyed the many comparisons of regulation versus taxes, but I especially enjoyed the discussions about what is socially feasible. An idea in theory doesn&rsquo;t do much good if it won&rsquo;t work in practice.</p>

<h3>&ldquo;In This Economy?: How Money &amp; Markets Really Work&rdquo; by Kyla Scanlon</h3>

<p><strong><a href="https://app.thestorygraph.com/books/47d2ae10-c19d-41ad-b8a6-75109d5351c1">In This Economy?</a></strong> may be my new go-to recommendation for a book <strong>on the importance of economics and economic thinking for the average person</strong> (folks not already into economics).</p>

<p>Unfortunately, <strong>it doesn&rsquo;t drive too deep into economic thinking</strong> but it <em>does</em> discuss the current state of money and markets in the US in a relatable way.</p>

<p>I am still on the lookout for a great &ldquo;here&rsquo;s why economics is so important&rdquo; explainer book.</p>

<h3>&ldquo;On Tyranny&rdquo; by Timothy Snyder</h3>

<p><strong><a href="https://app.thestorygraph.com/books/e36e1a7c-90d5-4fca-92cc-20b225228db9">On Tyranny</a></strong> was quite short.
I really enjoy a good short read.</p>

<p>Here are some quotes I wrote down from the chapter on &ldquo;being kind to our language&rdquo;:</p>

<blockquote><p>Think up your own way of speaking, even if only to convey that thing you think everyone is saying.</p>

<p>Politicians in our times feed their cliches to television, where even those who wish to disagree repeat them. Television purports to challenge political language by conveying images, but the succession from one frame to another can hinder a sense of resolution. Everything happens fast but nothing actually happens. Each story on televised news is breaking, until it is displaced by the next one, so we are hit with wave upon wave, but never see the ocean. The effort to define the shape and significance of events require words and concepts that allude us when we are entranced by visual stimuli. Watching televised news is sometimes little more than looking at someone who is also looking at a picture. We take this collective trance to be normal. We have slowly fallen into it.</p>

<p>Staring at screens is perhaps unavoidable, but the 2 dimensional world makes little sense unless we can draw upon a mental armory that we have developed somewhere else. When we repeat the same words and phrases that appear in the daily media, we accept the absence of a larger framework. To have such a framework requires more concepts and having more concepts requires more reading.</p></blockquote>

<h2>Books about Self Betterment</h2>

<p>The line between world betterment and self betterment isn&rsquo;t always clear but to me, all 3 of these books felt a bit more focused on how the reader could better their own life.</p>

<h3>&ldquo;You&rsquo;re Not Listening&rdquo; by Kate Murphy</h3>

<p><strong><a href="https://app.thestorygraph.com/books/8d99b1d4-4fa0-4c77-9220-292f1168533a">You&rsquo;re Not Listening</a></strong> was more interesting than immediately actionable, but I can&rsquo;t complain. This was a fascinating meandering through the world of communication as studied by researchers and as put into practice by hostage negotiators, salespeople, focus group facilitators, etc.</p>

<p>This book was a fairly quick and easy read and I feel it was well-thought and well-edited.</p>

<p>Two takeaways I wrote down:</p>

<ol>
<li>Carl Rogers (who coined &ldquo;active listening&rdquo;) says &ldquo;listening to opposing viewpoints is the only way to grow&rdquo;</li>
<li>Good listener know understanding is not binary: your understanding can always be improved.</li>
</ol>


<p>I didn&rsquo;t really take notes, but I wish I had.</p>

<h3>&ldquo;Hope for Cynics&rdquo; by Jamil Zaki</h3>

<p><strong><a href="https://app.thestorygraph.com/books/44ecbd04-2060-43f1-93c8-d6bd8c331c23">Hope for Cynics</a></strong> is all about an idea I think is greatly needed <em>today</em>: how to turn cynicism into hope <em>and</em> productive action toward making a better world.</p>

<p>Most people, myself included, at some point despair to the point of complacency with politics, the climate, etc. This book is meant to <strong>inspire action</strong> instead.</p>

<p>I really liked the idea of hopefully skepticism as a level-headed replacement for cynicism.
I was glad that Robert Putnam&rsquo;s The Upswing was also discussed toward the end.</p>

<h3>&ldquo;Vegan for Life&rdquo; by Jack Norris and Virginia Messina</h3>

<p>The nutrition advice in <strong><a href="https://app.thestorygraph.com/books/2637d1c8-7f82-4094-bb05-5815cb3f4317">Vegan for Life</a></strong> is similar to <a href="https://app.thestorygraph.com/reviews/57fbf0bf-509a-4176-a8e1-cd5c53c5de7e?redirect=true">Harvard Medical School&rsquo;s Eat, Drink, and Be Healthy</a> but it&rsquo;s targeted specifically to those eating vegan.
If you&rsquo;re concerned about your health as you switch away from animal-based products, read this.</p>

<h2>Fiction Books</h2>

<p>I didn&rsquo;t read a lot of fiction this year. Of the bit of fiction I did read, Hank Green&rsquo;s An Absolutely Remarkable Thing is the book I&rsquo;d most recommend. I&rsquo;d also recommend the sequel. They both made me think and made me have big feelings.</p>

<h3>&ldquo;An Absolutely Remarkable&rdquo; Thing by Hank Green</h3>

<p><strong><a href="https://app.thestorygraph.com/books/5e5cf4af-9ce8-458d-9a15-ae955cfed7a8">An Absolutely Remarkable Thing</a></strong> was a really fun book. I read the sequel immediately after I finished this.</p>

<p>I think I enjoy Hank Green&rsquo;s fiction more than John Green&rsquo;s.</p>

<p>I don&rsquo;t want to spoil the book, so I&rsquo;ll just say that if you think you&rsquo;d like thoughtful fiction where a big thing happens that changes the world, I&rsquo;d recommend this book and <a href="https://app.thestorygraph.com/reviews/bdfd7e15-d464-453f-8662-95db0f942d75">its sequel</a>.</p>

<h3>&ldquo;The Pioneer&rdquo; by Bridget Tyler</h3>

<p><strong><a href="https://app.thestorygraph.com/books/b72b3c97-5001-4ae7-9e4f-6bac34b1fcdc">The Pioneer</a></strong> is a young adult sci-fi novel which I found to be a pretty easy read.
There is some &ldquo;what is personhood&rdquo;-style grappling (to counter the typical alien xenophobia) that&rsquo;s slightly reminiscent of Octavia Butler&rsquo;s <a href="https://app.thestorygraph.com/reviews/0e92c566-34e5-40ba-9dd0-91e3af3a1708">Dawn</a> (which I <em>love</em>).</p>

<p>I did find myself slightly frustrated that in this book humanity ends up with faster-than-light travel and yet we still harm creatures that feel pain for very little benefit to ourselves.
I don&rsquo;t regret reading this and ended up reading the sequel as well.</p>

<h3>&ldquo;Children of Ruin&rdquo; by Adrian Tchaikovsky</h3>

<p>&ldquo;We&rsquo;re going on an adventure.&rdquo; (you&rsquo;ll get this quote stuck in your head)</p>

<p>There&rsquo;s even more going on in <strong><a href="https://app.thestorygraph.com/books/4f13033f-3b38-44d6-8373-47e14727d28d">Children of Ruin</a></strong> than the first one (<a href="https://app.thestorygraph.com/books/142bc3cb-3aac-49e5-8527-b3cc9675f158">Children of Time</a>). There were definitely moments where I thought &ldquo;wait who is this person, what species are they, and where are they in the universe&rdquo;. I think if it hadn&rsquo;t been a couple years since I read the first book, I would have had an easier time following the plot.
Overall, I really enjoyed this sequel.</p>

<h2>Recommendations From Past Years</h2>

<p>To read my audiobook recommendations from past years see:</p>

<ul>
<li><a href="https://treyhunner.com/2024/12/my-favorite-audiobooks-of-2024/">top 9 of <strong>2024</strong> &amp; 30 of <strong>2017</strong>-<strong>2023</strong></a></li>
<li><a href="https://treyhunner.com/2017/01/my-favorite-audiobooks-of-2016/">top 12 of <strong>2016</strong></a></li>
<li><a href="https://treyhunner.com/2015/12/my-favorite-audiobooks-of-2015/">top 15 of <strong>2015</strong></a></li>
<li><a href="https://treyhunner.com/2014/12/top-6-books-of-2014/">top 6 of <strong>2014</strong></a></li>
</ul>


<p>I&rsquo;ve listened to over 300 audiobooks over the past decade and I&rsquo;m always looking for new books to listen to.
If <strong>you have recommendations for me</strong>, please comment below, message me on social media, or email me.</p>
]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Default Apps of 2025]]></title>
    <link href="https://treyhunner.com/2025/12/default-apps-of-2025/"/>
    <updated>2025-12-22T20:32:27-08:00</updated>
    <id>https://treyhunner.com/2025/12/default-apps-of-2025</id>
    <content type="html"><![CDATA[<p>Here are my default apps of 2025. My <a href="https://treyhunner.com/2025/01/my-default-apps-of-2024/">2024 list is here</a>.</p>

<p>The Libro, YNAB, SavvyCal, and GLM links below are referral links. You can <a href="https://raindrop.io/treyhunner/referral-links-47224019">find more of my referral links here</a>.
I&rsquo;d love a free audiobook if you end up switching from Audible to Libro.fm (you should seriously consider it, as I <a href="https://treyhunner.com/2024/12/my-favorite-audiobooks-of-2024/">noted at the end of this audiobook review post</a>). 💗</p>

<ul>
<li>📝 <strong>Notes</strong>: <a href="https://obsidian.md">Obsidian</a> + NeoVim</li>
<li>✅ <strong>To-Do</strong>: Obsidian</li>
<li>✍ <strong>Writing</strong>: Obsidian + NeoVim</li>
<li>🧑‍💻 <strong>Code Editor</strong>: NeoVim</li>
<li>⏲ <strong>Focus Mode</strong>: <a href="https://play.google.com/store/apps/details?id=com.AT.PomodoroTimer.timer&amp;hl=en_US">Focus Friend</a> &amp; <a href="https://play.google.com/store/apps/details?id=com.AT.PomodoroTimer.timer&amp;hl=en_US">Brain Focus</a></li>
<li>🔍 <strong>Search</strong>: <a href="https://kagi.com">Kagi</a> (and sometimes the free Perplexity)</li>
<li>🤖 <strong>LLM chat in-browser</strong>: Claude mostly, but sometimes Chat GPT, Z.ai&rsquo;s GLM, or Typing Mind with API keys</li>
<li>🤖 <strong>LLM coding agent</strong>: Claude Code mostly, sometimes with a Claude subscription and sometimes with my <a href="https://z.ai/subscribe?ic=PGLZJ2GAZP">Z.ai GLM subscription</a></li>
<li>🌐 <strong>Browser</strong>: <a href="https://kagi.com">Vivaldi</a></li>
<li>🔖 <strong>Bookmarks</strong>: <a href="https://raindrop.io">Raindrop.io</a></li>
<li>📚 <strong>Audiobooks</strong>: Libby (💖), Spotify (meh), <a href="https://libro.fm/referral?rf_code=lfm240965">Libro</a> (💖), Audible (yuck)</li>
<li>🎤 <strong>Podcasts</strong>: <a href="https://www.podcastrepublic.net">Podcast Republic</a> (switched from Pocket Casts this year)</li>
<li>🎵 <strong>Music</strong>: Spotify</li>
<li>📼 <strong>Screencasting</strong>: OBS + Kdenlive + <a href="https://github.com/treyhunner/dotfiles/blob/0ab0a3d2df45940e38aad5729f5cdc1c72932226/bin/caption">Whisper API</a> + <a href="https://github.com/treyhunner/dotfiles/blob/0ab0a3d2df45940e38aad5729f5cdc1c72932226/bin/normalize">Custom Python scripts</a></li>
<li>📁 <strong>Cloud File Storage</strong>: a mix of Git, Dropbox, Google Drive, and Obsidian Sync</li>
<li>📜 <strong>Word Processing</strong>: Google Drive</li>
<li>📈 <strong>Spreadsheets</strong>: Google Drive, Airtable, occasionally LibreOffice</li>
<li>💰 <strong>Budgeting and Personal Finance</strong>: <a href="https://ynab.com/referral/?ref=d3KJBKwg5DKsKgDr">YNAB</a></li>
<li>💬 <strong>Chat</strong>: Signal, Discord, Slack</li>
<li>📆 <strong>Scheduling + Booking</strong>: <a href="https://savvycal.com/?via=trey">SavvyCal</a></li>
<li>📆 <strong>Calendar</strong>: Google Calendar</li>
<li>📹 <strong>Video Calls</strong>: Google Meet + Discord + Zoom</li>
<li>🔐 <strong>Password Management</strong>: <a href="https://bitwarden.com">Bitwarden</a></li>
<li>🔏 <strong>Multi-Factor Auth</strong>: <a href="https://play.google.com/store/apps/details?id=com.beemdevelopment.aegis">Aegis</a> (Android) + <a href="https://flathub.org/apps/com.belmoussaoui.Authenticator">Authenticator</a> (laptop)</li>
<li>🐚 <strong>Terminal</strong>: Gnome Terminal + Tmux + <a href="https://github.com/treyhunner/tmuxstart">Tmuxstart</a> + <a href="https://starship.rs">Starship</a> + <a href="https://direnv.net">Direnv</a></li>
<li>🐍 <strong>Python Installation Manager</strong>: <a href="https://docs.astral.sh/uv/">uv</a></li>
<li>🐍 <strong>Python script runner</strong>: <a href="https://github.com/treyhunner/uvrs">uvrs</a> (hopefully to be replaced by uv eventually)</li>
<li>🗃️ <strong>Version Control</strong>: Git</li>
<li>🖥️ <strong>DNS</strong>: Cloudflare</li>
<li>📮 <strong>Email</strong>: Gmail</li>
<li>🎒 <strong>Backups</strong>: Manual Calendar Event + Git</li>
<li>🏠 <strong>Smart Home Hub</strong>: Google Home (maybe I&rsquo;ll use Home Assistant eventually?)</li>
<li>🏃 <strong>Fitness Tracker</strong>: Fitbit</li>
</ul>


<p>Also see:
- <a href="https://treyhunner.com/2024/10/switching-from-virtualenvwrapper-to-direnv-starship-and-uv/">My direnv, starship, and uv setup for Python</a>
- <a href="https://treyhunner.com/2025/10/handy-python-repl-modifications/">My custom Python shell configuration tool</a>
- <a href="https://github.com/treyhunner/dotfiles">my dotfiles</a> for my customized Python shell configuration, how I manage my project directories, and more.</p>

<h2>Biggest changes from 2024</h2>

<p>I switched from Pocket Casts to Podcast Republic.
I&rsquo;m not entirely satisfied with either.
I wish there was a podcast app that allowed me to easily maintain multiple queues/coming up-style playlists at once but wasn&rsquo;t so complex.</p>

<p>My LLM use changed this year, first with the discovery of Typing Mind (thanks to my friend <a href="https://www.dataschool.io/save-money-on-chatgpt/">Kevin Markham</a>) and later thanks to command-line LLM agents becoming a thing.</p>

<p>I also started using Hank Green&rsquo;s Focus Friend app.
Or maybe playing it, since it&rsquo;s a bit of a game plus pomodoro-style app in one.</p>

<p>I continue to <em>really appreciate</em> Kagi and have been getting more use out of both Raindrop and Obsidian this year (which were both new to me last year).</p>

<p>I&rsquo;ve also been really enjoying using uv a bit more seriously than I was last year.
I didn&rsquo;t mention it above, but I&rsquo;ve finally listened to Jeff&rsquo;s repeated <a href="https://micro.webology.dev/categories/justfiles/">recommendations</a> to use <a href="https://github.com/casey/just">justfiles</a> and I&rsquo;m loving those too.</p>
]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Python Black Friday &amp; Cyber Monday sales (2025)]]></title>
    <link href="https://treyhunner.com/2025/11/python-black-friday-and-cyber-monday-sales-2025/"/>
    <updated>2025-11-24T09:00:00-07:00</updated>
    <id>https://treyhunner.com/2025/11/python-black-friday-and-cyber-monday-sales-2025</id>
    <content type="html"><![CDATA[<p>It&rsquo;s time for some <strong>discounted</strong> Python-related skill-building.
This is my <strong><a href="https://treyhunner.com/blog/categories/sales/">eighth annual compilation</a></strong> of Python learning-related Black Friday &amp; Cyber Monday deals.
If you find a Python-related deal in the next week that isn&rsquo;t on this list, please contact me.</p>

<h2>Python-related sales</h2>

<p>It&rsquo;s not even Black Friday yet, but most of these Python-related Black Friday sales are <strong>already live</strong>:</p>

<ul>
<li><strong><a href="https://www.pythonmorsels.com/lifetime-access-sale">Python Morsels</a></strong>: I&rsquo;m offering <strong>lifetime access</strong> for the second time ever (more details below)</li>
<li><strong><a href="https://courses.dataschool.io/pass">Data School</a></strong>: a new subscription to access all of Kevin&rsquo;s <strong>7 courses plus all upcoming courses</strong></li>
<li><strong><a href="http://talkpython.fm/black-friday">Talk Python</a></strong>: AI Python bundle, the Everything Bundle, and Michael&rsquo;s Talk Python in Production</li>
<li><strong><a href="https://lernerpython.com/pricing/">Reuven Lerner</a></strong>: get 20% off <strong>your first year</strong> of the <strong>LernerPython+data</strong> tier (code <code>BF2025</code>)</li>
<li><strong><a href="https://pythontest.com/2025-black-friday/">Brian Okken</a></strong>: get 50% off his pytest books and courses (code <code>SAVE50</code>)</li>
<li><strong><a href="https://mathspp.gumroad.com/l/all-books-bundle/BF202550">Rodrigo</a></strong>: get 50% off all his books including his all books bundle (code <code>BF202550</code>)</li>
<li><strong><a href="https://www.blog.pythonlibrary.org/2025/11/18/black-friday-python-deals-came-early/">Mike Driscoll</a></strong>: get 50% off all his <strong>Python books and courses</strong> (code <code>BLACKISBACK</code>)</li>
<li><strong><a href="https://thepythoncodingplace.thinkific.com/bundles/the-python-coding-place-membership?coupon=black50">The Python Coding Place</a></strong>: get 50% the <strong>all course bundle</strong> of 12 courses</li>
<li><strong><a href="https://learnbyexample.gumroad.com/l/all-books/FestiveOffer">Sundeep Agarwal</a></strong>: ~55% off Sundeep&rsquo;s <strong>all books bundle</strong> with code <code>FestiveOffer</code></li>
<li><strong><a href="https://nodeledge.ai/learn/paths/essential-python-for-data-science-and-ml">Nodeledge</a></strong>: get 20% off your first payment (code <code>BF2025</code>)</li>
<li><strong><a href="https://www.oreilly.com/online-learning/2025-cyber-sale.html?code=CYBERSAVE25">O'Reilly Media</a></strong>: 40% off <strong>the first year</strong> with code <code>CYBERWEEK25</code> ($299 instead of $499)</li>
<li><strong><a href="https://www.manning.com/catalog/programming-languages-and-styles/python">Manning</a></strong> is offering 50% off from Nov 25 to Dec 1</li>
<li><strong><a href="https://mastodon.social/@b0rk@jvns.ca/115571664177415123">Wizard Zines</a></strong>: 50% all of Julia Evan&rsquo;s <em>great</em> <strong>zines on various tech topics</strong> that I personally find both fun and useful (not Python-related, but one of my favorite annual sales)&hellip; this is a one-day Black Friday exclusive sale</li>
</ul>


<!--
Likely additional sales:

- **[Brian Okken](https://courses.pythontest.com)**
- **[Matt Harrison][]**
- **[Rodrigo][]**
- **[Test Driven][]**
- **[Pragmatic Bookshelf][]**
- **[No Starch][]**
- **[Manning][]**
-->


<p>I will be keeping an idea on other potential sales and updating this post as I find them.
If you&rsquo;ve seen a sale that I haven&rsquo;t, please contact me or comment below.</p>

<h2>Django-related sales</h2>

<p>Adam Johnson has also published a list of <a href="https://adamj.eu/tech/2025/11/20/django-black-friday-deals-2025/">Django-related deals for Black Friday</a> (which he&rsquo;s been doing for a few years now).
I&rsquo;ve included links below to the different sections in Adam&rsquo;s post&hellip;</p>

<p>Courses and books:</p>

<ul>
<li><a href="https://adamj.eu/tech/2025/11/20/django-black-friday-deals-2025/#my-books">Adam&rsquo;s books</a>: his books and bundles on Django, Git, and GitHub DX and Django test performance are all 50% off</li>
<li><a href="https://adamj.eu/tech/2025/11/20/django-black-friday-deals-2025/#learndjango-com-course-bundle">LearnDjango.com course bundle</a>: Will Vincent is offering 50% off his 3 courses</li>
<li><a href="https://adamj.eu/tech/2025/11/20/django-black-friday-deals-2025/#django-5-by-example">Django 5 By Example</a>: 30% off this book by Antonio Melé</li>
<li><a href="https://adamj.eu/tech/2025/11/20/django-black-friday-deals-2025/#django-in-action">Django in Action</a>: 50% off this book by Christopher Trudeau</li>
<li><a href="https://adamj.eu/tech/2025/11/20/django-black-friday-deals-2025/#async-patterns-in-django">Async Patterns in Django</a>: 50% off this book by Paul Bailey</li>
</ul>


<p>Packages related to Django:</p>

<ul>
<li><a href="https://adamj.eu/tech/2025/11/20/django-black-friday-deals-2025/#aidas-bendoraitis-paid-packages">Aidas Bendoraitis</a>: has 3 Django packages available at a 20% discount</li>
<li><a href="https://adamj.eu/tech/2025/11/20/django-black-friday-deals-2025/#appliku">Appliku</a>: 30% of annual plans for this Django deployment tool</li>
<li><a href="https://adamj.eu/tech/2025/11/20/django-black-friday-deals-2025/#saas-pegasus">Saas Pegasus</a>: 50% off the unlimited license for this configurable Django project template</li>
</ul>


<p>Adam also gives a shout out to <a href="https://adamj.eu/tech/2025/11/20/django-black-friday-deals-2025/#bonus-django-itself">sponsoring Django itself</a>.</p>

<h2>More on my sale: Python Morsels Lifetime Access</h2>

<p>Python Morsels is a hands-on, <strong>exercise-driven</strong> Python skill-building platform designed to help developers <strong>write cleaner, more idiomatic Python</strong> through real-world practice.
This growing library of exercises, videos, and courses is aimed primarily at intermediate and professional developers.
If you haven&rsquo;t used Python Morsels, you can read about it <a href="https://treyhunner.com/2025/11/lifetime-access-sale-2025/">in my sale announcement post</a>.</p>

<p>This Black Friday / Cyber Monday, I&rsquo;m offering <a href="https://www.pythonmorsels.com/lifetime-access-sale">lifetime access</a>: all current and future content for in a single payment.
If you&rsquo;d like to build confidence in your everyday Python skills, consider this sale.
It&rsquo;s <strong>about 50% cheaper than paying annually for five years</strong>.</p>

<p><a href="https://pythonmorsels.com/lifetime-access-sale/" class="subscribe-btn form-big">Get lifetime access to Python Morsels</a></p>
]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Python Morsels Lifetime Access Sale]]></title>
    <link href="https://treyhunner.com/2025/11/lifetime-access-sale-2025/"/>
    <updated>2025-11-21T14:42:36-08:00</updated>
    <id>https://treyhunner.com/2025/11/lifetime-access-sale-2025</id>
    <content type="html"><![CDATA[<p>If you code in Python regularly, you&rsquo;re already learning new things everyday.</p>

<p>You hit a wall, or something breaks. Then you search around, spend some hours on Stack Overflow, and eventually, you figure it out.</p>

<p><strong>But this kind of learning is unstructured. It&rsquo;s reactive, instead of intentional.</strong></p>

<p>You fix the problem at hand, but the underlying gaps in your knowledge remain unaddressed.</p>

<h2>A more structured way to improve your Python skills</h2>

<p>Python Morsels gives you <strong>a structured, hands-on way to improve your Python skills</strong> through weekly practice:</p>

<ul>
<li>Notice significant progress with as little as 30 minutes a week</li>
<li>Learn to naturally think more Pythonically</li>
<li>Explore new approaches to problem-solving</li>
<li>Challenge yourself to get outside your comfort zone regularly</li>
</ul>


<p>Python Morsels is a subscription service because <strong>I&rsquo;m adding new learning resources almost every week</strong>.</p>

<p>But through December 1st, you can get <strong><a href="https://pythonmorsels.com/lifetime-access-sale/">lifetime access</a></strong> for a one-time payment.</p>

<h2>How it works</h2>

<p>When you sign up for Python Morsels, you&rsquo;ll choose your current Python skill level, from novice to advanced.</p>

<p>Based on your skill level, <strong>each Monday I&rsquo;ll send you a personalized routine</strong> with:</p>

<ul>
<li>a short screencast to watch (or read)</li>
<li>a multi-part exercise to move you outside your comfort zone</li>
<li>a mini exercise that you can accomplish in just 10 minutes</li>
<li>links to dive deeper into subsequent screencasts and exercises</li>
</ul>


<p>Think of Python Morsels as a gym for your Python skills: you come in for quick training sessions, put in the reps, and make a little more progress each time.</p>

<p>All these resources are accessible to you forever, and with lifetime access you&rsquo;ll never pay another subscription fee.</p>

<h2>What Python Morsels includes</h2>

<p>Python Morsels has grown a lot over the past 8 years. Currently, Python Morsels has:</p>

<ul>
<li>235+ Video Lessons (or screencasts, as I call them)</li>
<li>262+ Hands-On Exercises (with solution walkthroughs)</li>
<li>500+ Optional Bonuses (to challenge every skill level)</li>
<li>303+ Articles Organized Topic-Wise (if you prefer reading)</li>
</ul>


<p>I&rsquo;ll be sending you personalized recommendations every week, but you can use these resources however they fit your routine: as learning guides, hands-on practice sessions, quick cheatsheets, long-term reference material, or quick Python workouts.</p>

<p>In addition to this, Python Morsels also gives you access to:</p>

<ul>
<li>Python Jumpstart - a structured course for beginners</li>
<li>Additional Deep-Dive Courses - structured tracks to master a concept</li>
<li>Exercise Paths - topic-based exercises to strengthen a specific skill</li>
</ul>


<p>Because Python Morsels runs as an active subscription service, I&rsquo;m always adding new screencasts, new exercises, and updated material on a weekly or monthly cycle. I also keep everything up-to-date with each new Python release, incorporating newly added features and retiring end-of-life&rsquo;d Python versions.</p>

<h2>Lock in lifetime access</h2>

<p>Python Morsels usually costs <strong>$240/year</strong> but you can get <strong>lifetime access</strong> through December 1st for a one-time payment. I&rsquo;ve only offered lifetime access <strong>once before</strong> in 8 years.</p>

<p>If you&rsquo;ve been on the fence about subscribing to Python Morsels or want to invest in building a daily learning habit, this is a good time to do it.</p>

<p><a href="https://pythonmorsels.com/lifetime-access-sale/" class="subscribe-btn form-big">Get lifetime access to Python Morsels</a></p>

<p>If you have questions about the sale, please comment below or <a href="mailto:he&amp;#108;p&amp;#64;&amp;#112;%7&amp;#57;th%6Fnmo&amp;#114;s%6&amp;#53;ls&amp;#46;&amp;#99;&amp;#111;m">email me</a>.</p>
]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Handy Python REPL Modifications]]></title>
    <link href="https://treyhunner.com/2025/10/handy-python-repl-modifications/"/>
    <updated>2025-10-08T19:59:20-07:00</updated>
    <id>https://treyhunner.com/2025/10/handy-python-repl-modifications</id>
    <content type="html"><![CDATA[<p>I find myself in the Python REPL <em>a lot</em>.</p>

<p>I open up the REPL to play with an idea, to use Python as a calculator or quick and dirty text parsing tool, to record a screencast, to come up with a code example for an article, and (most importantly for me) to teach Python.
My Python courses and workshops are based largely around writing code together to guess how something works, try it out, and repeat.</p>

<p>As I&rsquo;ve written about before, you can <a href="https://treyhunner.com/2024/10/adding-keyboard-shortcuts-to-the-python-repl/">add custom keyboard shortcuts</a> to the new Python REPL (since 3.13) and <a href="https://treyhunner.com/2025/09/customizing-your-python-repl-color-scheme/">customize the REPL syntax highlighting</a> (since 3.14).
If you spend time in the Python REPL and wish it behaved a little more <strong>like your favorite editor</strong>, these tricks can come in handy.</p>

<p>I have added <strong>custom keyboard shortcuts</strong> to my REPL and other modifications to help me <strong>more quickly write and edit code in my REPL</strong>.
I&rsquo;d like to share some of the modifications that I&rsquo;ve found helpful in my own Python REPL.</p>

<h2>Creating a PYTHONSTARTUP file</h2>

<p>If you want to run Python code every time an interactive prompt (a REPL) starts, you can make a PYTHONSTARTUP file.</p>

<p>When Python launches an interactive prompt, it checks for a <code>PYTHONSTARTUP</code> environment variable.
If it finds one, it treats it as a filename that contains Python code and it <strong>runs all the code in that file</strong>, as if you had copy-pasted the code into the REPL.</p>

<p>So all of the modifications I have made to my Python REPL rely on this <code>PYTHONSTARTUP</code> variable in my <code>~/.zshenv</code> file:</p>

<figure class='code'><figcaption><span></span></figcaption><div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class='line-number'>1</span>
</pre></td><td class='code'><pre><code class='sh'><span class='line'><span class="nb">export </span><span class="nv">PYTHONSTARTUP</span><span class="o">=</span><span class="nv">$HOME</span>/.startup.py
</span></code></pre></td></tr></table></div></figure>


<p>If you use bash, you&rsquo;ll put that in your <code>~/.bashrc</code> file.
If you&rsquo;re on Windows <a href="https://gist.github.com/mitchmindtree/92c8e37fa80c8dddee5b94fc88d1288b">you&rsquo;ll need to set an environment variable the Windows way</a>.</p>

<p>With that variable set, I can now create a <code>~/.startup.py</code> file that has Python code in it.
That code will automatically run every time I launch a new Python REPL, whether within a virtual environment or outside of one.</p>

<h2>My REPL keyboard shortcuts</h2>

<p>The quick summary of my <em>current</em> modifications are:</p>

<ul>
<li>Pressing <strong>Home</strong> moves to the <strong>first character in the code block</strong></li>
<li>Pressing <strong>End</strong> moves to the <strong>last character in the code block</strong></li>
<li>Pressing <strong>Alt+M</strong> moves to the <strong>first character</strong> on the current line</li>
<li>Pressing <strong>Shift+Tab</strong> removes <strong>common indentation</strong> from the code block</li>
<li>Pressing <strong>Alt+Up</strong> swaps the current line with <strong>the line above it</strong></li>
<li>Pressing <strong>Alt+Down</strong> swaps the current line with <strong>the line below it</strong></li>
<li>Pressing <strong>Ctrl+N</strong> inserts <strong>a specific list of numbers</strong></li>
<li>Pressing <strong>Ctrl+F</strong> inserts <strong>a specific list of strings</strong></li>
</ul>


<p>If you&rsquo;ve read <a href="https://www.pythonmorsels.com/repl-features/">my Python REPL shortcuts</a> article, you know that we can use <strong>Ctrl+A</strong> to move to the beginning of the line and <strong>Ctrl+E</strong> to move to the end of the line.
I already use those instead of the Home and End keys, so I decided to rebind Home and End to do something different.</p>

<p>The <strong>Alt+M</strong> key combination is essentially the same as <code>Alt+M</code> in Emacs or <code>^</code> in Vim. I usually prefer to move to the beginning of the non-whitespace in a line rather than to the beginning of the <em>entire</em> line.</p>

<p>The <strong>Shift+Tab</strong> functionality is basically a fancy wrapper around <a href="https://www.pythonmorsels.com/dedent/">using <code>textwrap.dedent</code></a>: it dedents the current code block while keeping the cursor over the same character it was at before.</p>

<p>The <strong>Ctrl+N</strong> and <strong>Ctrl+F</strong> shortcuts make it easier for me to grab an example data structure to work with when teaching.</p>

<p>In addition to the above changes, I also modify my color scheme to work nicely with my Solarized Light color scheme in Vim.</p>

<h2>I created a pyrepl-hacks library for this</h2>

<p>My PYTHONSTARTUP file became so messy that I ended up creating a <a href="https://github.com/treyhunner/pyrepl-hacks">pyrepl-hacks library</a> to help me with these modifications.</p>

<p><a href="https://github.com/treyhunner/dotfiles/commits/main/startup.py">My PYTHONSTARTUP file</a> now looks pretty much like this:</p>

<figure class='code'><figcaption><span></span></figcaption><div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class='line-number'>1</span>
<span class='line-number'>2</span>
<span class='line-number'>3</span>
<span class='line-number'>4</span>
<span class='line-number'>5</span>
<span class='line-number'>6</span>
<span class='line-number'>7</span>
<span class='line-number'>8</span>
<span class='line-number'>9</span>
<span class='line-number'>10</span>
<span class='line-number'>11</span>
<span class='line-number'>12</span>
<span class='line-number'>13</span>
<span class='line-number'>14</span>
<span class='line-number'>15</span>
<span class='line-number'>16</span>
<span class='line-number'>17</span>
<span class='line-number'>18</span>
<span class='line-number'>19</span>
<span class='line-number'>20</span>
<span class='line-number'>21</span>
<span class='line-number'>22</span>
<span class='line-number'>23</span>
<span class='line-number'>24</span>
<span class='line-number'>25</span>
<span class='line-number'>26</span>
<span class='line-number'>27</span>
<span class='line-number'>28</span>
<span class='line-number'>29</span>
<span class='line-number'>30</span>
<span class='line-number'>31</span>
<span class='line-number'>32</span>
<span class='line-number'>33</span>
<span class='line-number'>34</span>
<span class='line-number'>35</span>
<span class='line-number'>36</span>
<span class='line-number'>37</span>
</pre></td><td class='code'><pre><code class='python'><span class='line'><span class="kn">import</span> <span class="nn">pathlib</span> <span class="kn">as</span> <span class="nn">_pathlib</span><span class="o">,</span> <span class="nn">sys</span> <span class="kn">as</span> <span class="nn">_sys</span>
</span><span class='line'><span class="n">_sys</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="nb">str</span><span class="p">(</span><span class="n">_pathlib</span><span class="o">.</span><span class="n">Path</span><span class="o">.</span><span class="n">home</span><span class="p">()</span> <span class="o">/</span> <span class="s">&quot;.pyhacks&quot;</span><span class="p">))</span>
</span><span class='line'>
</span><span class='line'><span class="k">try</span><span class="p">:</span>
</span><span class='line'>    <span class="kn">import</span> <span class="nn">pyrepl_hacks</span> <span class="kn">as</span> <span class="nn">_repl</span>
</span><span class='line'><span class="k">except</span> <span class="ne">ImportError</span><span class="p">:</span>
</span><span class='line'>    <span class="n">_repl</span> <span class="o">=</span> <span class="bp">None</span>  <span class="c"># We&#39;re on Python 3.12 or below</span>
</span><span class='line'><span class="k">else</span><span class="p">:</span>
</span><span class='line'>    <span class="n">_repl</span><span class="o">.</span><span class="n">bind</span><span class="p">(</span><span class="s">&quot;Home&quot;</span><span class="p">,</span> <span class="s">&quot;home&quot;</span><span class="p">)</span>
</span><span class='line'>    <span class="n">_repl</span><span class="o">.</span><span class="n">bind</span><span class="p">(</span><span class="s">&quot;End&quot;</span><span class="p">,</span> <span class="s">&quot;end&quot;</span><span class="p">)</span>
</span><span class='line'>    <span class="n">_repl</span><span class="o">.</span><span class="n">bind</span><span class="p">(</span><span class="s">&quot;Alt+M&quot;</span><span class="p">,</span> <span class="s">&quot;move-to-indentation&quot;</span><span class="p">)</span>
</span><span class='line'>    <span class="n">_repl</span><span class="o">.</span><span class="n">bind</span><span class="p">(</span><span class="s">&quot;Shift+Tab&quot;</span><span class="p">,</span> <span class="s">&quot;dedent&quot;</span><span class="p">)</span>
</span><span class='line'>    <span class="n">_repl</span><span class="o">.</span><span class="n">bind</span><span class="p">(</span><span class="s">&quot;Alt+Down&quot;</span><span class="p">,</span> <span class="s">&quot;move-line-down&quot;</span><span class="p">)</span>
</span><span class='line'>    <span class="n">_repl</span><span class="o">.</span><span class="n">bind</span><span class="p">(</span><span class="s">&quot;Alt+Up&quot;</span><span class="p">,</span> <span class="s">&quot;move-line-up&quot;</span><span class="p">)</span>
</span><span class='line'>    <span class="n">_repl</span><span class="o">.</span><span class="n">bind_to_insert</span><span class="p">(</span><span class="s">&quot;Ctrl+N&quot;</span><span class="p">,</span> <span class="s">&quot;[2, 1, 3, 4, 7, 11, 18, 29]&quot;</span><span class="p">)</span>
</span><span class='line'>    <span class="n">_repl</span><span class="o">.</span><span class="n">bind_to_insert</span><span class="p">(</span>
</span><span class='line'>        <span class="s">&quot;Ctrl+F&quot;</span><span class="p">,</span>
</span><span class='line'>        <span class="s">&#39;[&quot;apples&quot;, &quot;oranges&quot;, &quot;bananas&quot;, &quot;strawberries&quot;, &quot;pears&quot;]&#39;</span><span class="p">,</span>
</span><span class='line'>    <span class="p">)</span>
</span><span class='line'>
</span><span class='line'>    <span class="k">try</span><span class="p">:</span>
</span><span class='line'>        <span class="c"># Solarized Light theme to match vim</span>
</span><span class='line'>        <span class="n">_repl</span><span class="o">.</span><span class="n">update_theme</span><span class="p">(</span>
</span><span class='line'>            <span class="n">keyword</span><span class="o">=</span><span class="s">&quot;green&quot;</span><span class="p">,</span>
</span><span class='line'>            <span class="n">builtin</span><span class="o">=</span><span class="s">&quot;blue&quot;</span><span class="p">,</span>
</span><span class='line'>            <span class="n">comment</span><span class="o">=</span><span class="s">&quot;intense blue&quot;</span><span class="p">,</span>
</span><span class='line'>            <span class="n">string</span><span class="o">=</span><span class="s">&quot;cyan&quot;</span><span class="p">,</span>
</span><span class='line'>            <span class="n">number</span><span class="o">=</span><span class="s">&quot;cyan&quot;</span><span class="p">,</span>
</span><span class='line'>            <span class="n">definition</span><span class="o">=</span><span class="s">&quot;blue&quot;</span><span class="p">,</span>
</span><span class='line'>            <span class="n">soft_keyword</span><span class="o">=</span><span class="s">&quot;bold green&quot;</span><span class="p">,</span>
</span><span class='line'>            <span class="n">op</span><span class="o">=</span><span class="s">&quot;intense green&quot;</span><span class="p">,</span>
</span><span class='line'>            <span class="n">reset</span><span class="o">=</span><span class="s">&quot;reset, intense green&quot;</span><span class="p">,</span>
</span><span class='line'>        <span class="p">)</span>
</span><span class='line'>    <span class="k">except</span> <span class="ne">ImportError</span><span class="p">:</span>
</span><span class='line'>        <span class="k">pass</span>  <span class="c"># We&#39;re on Python 3.13 or below</span>
</span><span class='line'>
</span><span class='line'><span class="k">del</span> <span class="n">_repl</span><span class="p">,</span> <span class="n">_pathlib</span><span class="p">,</span> <span class="n">_sys</span>  <span class="c"># Avoid global REPL namespace pollution</span>
</span></code></pre></td></tr></table></div></figure>


<p>That&rsquo;s pretty short!</p>

<p>But wait&hellip; won&rsquo;t this fail unless pyrepl-hacks is installed in every virtual environment <em>and</em> installed globally for every Python version on my machine?</p>

<p>That&rsquo;s where that <code>sys.path.append</code> trick comes in handy&hellip;</p>

<h2>Wait&hellip; let&rsquo;s acknowledge the dragons 🐲</h2>

<p>At this point I&rsquo;d like to pause to note that all of this relies on using an implementation detail of Python that is deliberately undocumented because it <em>is not designed</em> to be used by end users.</p>

<p>The above code all relies on the <code>_pyrepl</code> module that was added in Python 3.13 (and optionally the <code>_colorize</code> module that was added in Python 3.14).</p>

<p>When I run a new future version of Python (for example Python 3.15) this solution may break.
I&rsquo;m willing to take that risk, as I know that I can always unset my shell&rsquo;s <code>PYTHONSTARTUP</code> variable or clear out my startup file.</p>

<p>So, just be aware&hellip; here be (private undocumented implementation detail) dragons.</p>

<h2>Monkey patching <code>sys.path</code> to allow importing <code>pyrepl_hacks</code></h2>

<p>I didn&rsquo;t install pyrepl-hacks <em>the usual way</em>.
Instead, I installed it in a very specific location.</p>

<p>I created a <code>~/.pyhacks</code> directory and then installed <code>pyrepl-hacks</code> <em>into</em> that directory:</p>

<figure class='code'><figcaption><span></span></figcaption><div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class='line-number'>1</span>
<span class='line-number'>2</span>
</pre></td><td class='code'><pre><code class='bash'><span class='line'><span class="nv">$ </span>mkdir -p ~/.pyhacks
</span><span class='line'><span class="nv">$ </span>python -m pip install pyrepl-hacks --target ~/.pyhacks
</span></code></pre></td></tr></table></div></figure>


<p>In order for the <code>pyrepl_hacks</code> Python package to work, it needs to available within every Python REPL I might launch.
Normally that would mean that it needs to be installed in every virtual environment that Python runs within.
This trick avoids that constraint.</p>

<p>When Python tries to import a module, it iterates through the <code>sys.path</code> directory list.
Any Python packages found <em>within</em> any of the <code>sys.path</code> directories may be imported.</p>

<p>So monkey patching <code>sys.path</code> within my PYTHONSTARTUP file allows <code>pyrepl_hacks</code> to be imported in every Python interpreter I launch:</p>

<figure class='code'><figcaption><span></span></figcaption><div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class='line-number'>1</span>
<span class='line-number'>2</span>
<span class='line-number'>3</span>
</pre></td><td class='code'><pre><code class='python'><span class='line'><span class="kn">from</span> <span class="nn">pathlib</span> <span class="kn">import</span> <span class="n">Path</span>
</span><span class='line'><span class="kn">import</span> <span class="nn">sys</span>
</span><span class='line'><span class="n">sys</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="nb">str</span><span class="p">(</span><span class="n">Path</span><span class="o">.</span><span class="n">home</span><span class="p">()</span> <span class="o">/</span> <span class="s">&quot;.pyhacks&quot;</span><span class="p">))</span>
</span></code></pre></td></tr></table></div></figure>


<p>With those 3 lines (or something like them) placed in my PYTHONSTARTUP file, all interactive Python interpreters I launch will be able to import modules that are located in my <code>~/.pyhacks</code> directory.</p>

<h2>Creating your own custom REPL commands</h2>

<p>That&rsquo;s pretty neat.
But what if you want to invent your own REPL commands?</p>

<p>Well, the <code>bind</code> utility I&rsquo;ve created in the <code>pyrepl_hacks</code> module can be used as a decorator for that.</p>

<p>This will make Ctrl+X followed by Ctrl+R insert <code>import subprocess</code> followed by <code>subprocess.run("", shell=True)</code> with the cursor positioned in between the double quotes after it&rsquo;s all inserted:</p>

<figure class='code'><figcaption><span></span></figcaption><div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class='line-number'>1</span>
<span class='line-number'>2</span>
<span class='line-number'>3</span>
<span class='line-number'>4</span>
<span class='line-number'>5</span>
<span class='line-number'>6</span>
<span class='line-number'>7</span>
<span class='line-number'>8</span>
<span class='line-number'>9</span>
<span class='line-number'>10</span>
</pre></td><td class='code'><pre><code class='python'><span class='line'><span class="kn">import</span> <span class="nn">pyrepl_hacks</span> <span class="kn">as</span> <span class="nn">_repl</span>
</span><span class='line'>
</span><span class='line'><span class="nd">@_repl.bind</span><span class="p">(</span><span class="s">r&quot;Ctrl+X Ctrl+R&quot;</span><span class="p">,</span> <span class="n">with_event</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
</span><span class='line'><span class="k">def</span> <span class="nf">subprocess_run</span><span class="p">(</span><span class="n">reader</span><span class="p">,</span> <span class="n">event_name</span><span class="p">,</span> <span class="n">event</span><span class="p">):</span>
</span><span class='line'>    <span class="sd">&quot;&quot;&quot;Ctrl+X followed by Ctrl+R will insert a subprocess.run command.&quot;&quot;&quot;</span>
</span><span class='line'>    <span class="n">reader</span><span class="o">.</span><span class="n">insert</span><span class="p">(</span><span class="s">&quot;import subprocess</span><span class="se">\n</span><span class="s">&quot;</span><span class="p">)</span>
</span><span class='line'>    <span class="n">code</span> <span class="o">=</span> <span class="s">&#39;subprocess.run(&quot;&quot;, shell=True)&#39;</span>
</span><span class='line'>    <span class="n">reader</span><span class="o">.</span><span class="n">insert</span><span class="p">(</span><span class="n">code</span><span class="p">)</span>
</span><span class='line'>    <span class="k">for</span> <span class="n">_</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">code</span><span class="p">)</span> <span class="o">-</span> <span class="n">code</span><span class="o">.</span><span class="n">index</span><span class="p">(</span><span class="s">&#39;&quot;&quot;&#39;</span><span class="p">)</span> <span class="o">-</span> <span class="mi">1</span><span class="p">):</span>
</span><span class='line'>        <span class="n">_repl</span><span class="o">.</span><span class="n">commands</span><span class="o">.</span><span class="n">left</span><span class="p">(</span><span class="n">reader</span><span class="p">,</span> <span class="n">event_name</span><span class="p">,</span> <span class="n">event</span><span class="p">)</span>
</span></code></pre></td></tr></table></div></figure>


<p>You can read more about the ins and outs of the pyrepl-hacks package <a href="https://github.com/treyhunner/pyrepl-hacks#readme">in the readme file</a>.</p>

<h2>pyrepl-hacks is just a fancy wrapper</h2>

<p>The pyrepl-hacks package is really just a fancy wrapper around Python&rsquo;s <code>_pyrepl</code> and <code>_colorize</code> modules.</p>

<p>Why did I make a whole package and then modify my <code>sys.path</code> to use it, when I could have just used <code>_pyrepl</code> directly?</p>

<p>Three reasons:</p>

<ol>
<li>To make creating new commands <em>a bit</em> easier (functions can be used instead of classes)</li>
<li>To make the key bindings look a bit nicer (I prefer <code>"Ctrl+M"</code> over <code>r"\C-M"</code>)</li>
<li>To hide my hairy hacks behind a shiny API ✨</li>
</ol>


<p>Before I made pyrepl-hacks, I implemented these commands directly within my PYTHONSTARTUP file by reaching into the internals of <code>_pyrepl</code> directly.</p>

<p>My PYTHONSTARTUP file before pyrepl-hacks was <a href="https://pym.dev/p/35q9e/">over 100 lines longer</a>.</p>

<h2>Try pyrepl-hacks and leave feedback</h2>

<p>My hope is that the <a href="https://github.com/treyhunner/pyrepl-hacks">pyrepl-hacks</a> library will be obsolete one day.
Eventually the <code>_pyrepl</code> module might be renamed to <code>pyrepl</code> (or maybe just <code>repl</code>?) and it will have a well-documented friendly-ish public interface.</p>

<p>In the meantime, I plan to maintain pyrepl-hacks.
As Python 3.15 is developed, I&rsquo;ll make sure it continues to work.
And I may add more useful commands if I think of any.</p>

<p>If you hack your own REPL, I&rsquo;d love to hear what modifications you come up with.
And if you have thoughts on how to improve pyrepl-hacks, please open an issue or get in touch.</p>

<p>Also, <strong>if you use Windows</strong>, <a href="https://github.com/treyhunner/pyrepl-hacks/issues/1">please help me</a> confirm <strong>whether certain keys work on Windows</strong>. Thanks!</p>

<p>Contributions and ideas welcome!</p>
]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Customizing your Python REPL's color scheme (Python 3.14+)]]></title>
    <link href="https://treyhunner.com/2025/09/customizing-your-python-repl-color-scheme/"/>
    <updated>2025-09-04T14:00:00-07:00</updated>
    <id>https://treyhunner.com/2025/09/customizing-your-python-repl-color-scheme</id>
    <content type="html"><![CDATA[<p>Did you know that Python 3.14 will include <a href="https://docs.python.org/3.14/whatsnew/3.14.html#whatsnew314-pyrepl-highlighting">syntax highlighting</a> in the REPL?</p>

<p>Python 3.14 is due to be <a href="https://peps.python.org/pep-0745/">officially released</a> in about a month.
I recommended tweaking your Python setup now so you&rsquo;ll have your ideal color scheme on release day.</p>

<p><img src="https://treyhunner.com/images/python3.14-repl-syntax-highlighting.png" title="Python 3.14 REPL with syntax highlighting using custom color scheme" ></p>

<p>But&hellip; what if the default syntax colors don&rsquo;t match the colors that your text editor uses?</p>

<p>Well, fortunately you can customize your color scheme!</p>

<p><strong>Warning</strong>: I am recommending using an undocumented private module (it has an <code>_</code>-prefixed name) which may change in future Python versions.
Do not use this module in production code.</p>

<h2>Installing Python 3.14</h2>

<p>Don&rsquo;t have Python 3.14 installed yet?</p>

<p>If you have <a href="https://docs.astral.sh/uv/">uv</a> installed, you can run this command to launch Python 3.14:</p>

<figure class='code'><figcaption><span></span></figcaption><div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class='line-number'>1</span>
</pre></td><td class='code'><pre><code class='bash'><span class='line'><span class="nv">$ </span>uv run --python 3.14 python
</span></code></pre></td></tr></table></div></figure>


<p>That will automatically install 3.14 (if you don&rsquo;t have it yet) and run it.</p>

<h2>Setting a theme</h2>

<p>I have my terminal colors set to the Solarized Light color palette and I have Vim use a Solarized Light color scheme as well.</p>

<p>The REPL doesn&rsquo;t <em>quite</em> match my text editor by default:</p>

<p><img src="https://treyhunner.com/images/python3.14-repl-default-color-scheme.png" title="Python 3.14 REPL with default syntax highlighting" ></p>

<p>The numbers, comments, strings, and keywords are all different colors than my text editor.</p>

<p>This code makes the Python REPL use <em>nearly</em> the same syntax highlighting as my text editor:</p>

<figure class='code'><figcaption><span></span></figcaption><div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class='line-number'>1</span>
<span class='line-number'>2</span>
<span class='line-number'>3</span>
<span class='line-number'>4</span>
<span class='line-number'>5</span>
<span class='line-number'>6</span>
<span class='line-number'>7</span>
<span class='line-number'>8</span>
<span class='line-number'>9</span>
<span class='line-number'>10</span>
<span class='line-number'>11</span>
<span class='line-number'>12</span>
<span class='line-number'>13</span>
<span class='line-number'>14</span>
<span class='line-number'>15</span>
</pre></td><td class='code'><pre><code class='python'><span class='line'><span class="kn">from</span> <span class="nn">_colorize</span> <span class="kn">import</span> <span class="n">set_theme</span><span class="p">,</span> <span class="n">default_theme</span><span class="p">,</span> <span class="n">Syntax</span><span class="p">,</span> <span class="n">ANSIColors</span>
</span><span class='line'>
</span><span class='line'><span class="n">set_theme</span><span class="p">(</span><span class="n">default_theme</span><span class="o">.</span><span class="n">copy_with</span><span class="p">(</span>
</span><span class='line'>    <span class="n">syntax</span><span class="o">=</span><span class="n">Syntax</span><span class="p">(</span>
</span><span class='line'>        <span class="n">keyword</span><span class="o">=</span><span class="n">ANSIColors</span><span class="o">.</span><span class="n">GREEN</span><span class="p">,</span>
</span><span class='line'>        <span class="n">builtin</span><span class="o">=</span><span class="n">ANSIColors</span><span class="o">.</span><span class="n">BLUE</span><span class="p">,</span>
</span><span class='line'>        <span class="n">comment</span><span class="o">=</span><span class="n">ANSIColors</span><span class="o">.</span><span class="n">INTENSE_BLUE</span><span class="p">,</span>
</span><span class='line'>        <span class="n">string</span><span class="o">=</span><span class="n">ANSIColors</span><span class="o">.</span><span class="n">CYAN</span><span class="p">,</span>
</span><span class='line'>        <span class="n">number</span><span class="o">=</span><span class="n">ANSIColors</span><span class="o">.</span><span class="n">CYAN</span><span class="p">,</span>
</span><span class='line'>        <span class="n">definition</span><span class="o">=</span><span class="n">ANSIColors</span><span class="o">.</span><span class="n">BLUE</span><span class="p">,</span>
</span><span class='line'>        <span class="n">soft_keyword</span><span class="o">=</span><span class="n">ANSIColors</span><span class="o">.</span><span class="n">BOLD_GREEN</span><span class="p">,</span>
</span><span class='line'>        <span class="n">op</span><span class="o">=</span><span class="n">ANSIColors</span><span class="o">.</span><span class="n">INTENSE_GREEN</span><span class="p">,</span>
</span><span class='line'>        <span class="n">reset</span><span class="o">=</span><span class="n">ANSIColors</span><span class="o">.</span><span class="n">RESET</span> <span class="o">+</span> <span class="n">ANSIColors</span><span class="o">.</span><span class="n">INTENSE_GREEN</span><span class="p">,</span>
</span><span class='line'>    <span class="p">),</span>
</span><span class='line'><span class="p">))</span>
</span></code></pre></td></tr></table></div></figure>


<p>Check it out:</p>

<p><img src="https://treyhunner.com/images/python3.14-repl-syntax-highlighting.png" title="Python 3.14 REPL with syntax highlighting using custom color scheme" ></p>

<p>Neat, right?</p>

<p>But&hellip; I want this to be enabled by default!</p>

<h2>Using a <code>PYTHONSTARTUP</code> file</h2>

<p>You can use a <code>PYTHONSTARTUP</code> file to run code every time a new Python process starts.</p>

<p>If Python sees a <code>PYTHONSTARTUP</code> environment variable when it starts up, it will open that file and evaluate the code within it.</p>

<p>I have this in my <code>~/.zshrc</code> file to set the <code>PYTHONSTARTUP</code> environment variable to <code>~/.startup.py</code>:</p>

<figure class='code'><figcaption><span></span></figcaption><div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class='line-number'>1</span>
<span class='line-number'>2</span>
</pre></td><td class='code'><pre><code class='bash'><span class='line'><span class="c"># Setup python-launcher to use startup file</span>
</span><span class='line'><span class="nb">export </span><span class="nv">PYTHONSTARTUP</span><span class="o">=</span><span class="nv">$HOME</span>/.startup.py
</span></code></pre></td></tr></table></div></figure>


<p>In my <code>~/.startup.py</code> file, I have this code:</p>

<figure class='code'><figcaption><span></span></figcaption><div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class='line-number'>1</span>
<span class='line-number'>2</span>
<span class='line-number'>3</span>
<span class='line-number'>4</span>
<span class='line-number'>5</span>
<span class='line-number'>6</span>
<span class='line-number'>7</span>
<span class='line-number'>8</span>
<span class='line-number'>9</span>
<span class='line-number'>10</span>
<span class='line-number'>11</span>
<span class='line-number'>12</span>
<span class='line-number'>13</span>
<span class='line-number'>14</span>
<span class='line-number'>15</span>
<span class='line-number'>16</span>
<span class='line-number'>17</span>
<span class='line-number'>18</span>
<span class='line-number'>19</span>
<span class='line-number'>20</span>
<span class='line-number'>21</span>
<span class='line-number'>22</span>
<span class='line-number'>23</span>
<span class='line-number'>24</span>
<span class='line-number'>25</span>
</pre></td><td class='code'><pre><code class='python'><span class='line'><span class="k">def</span> <span class="nf">_main</span><span class="p">():</span>
</span><span class='line'>    <span class="sd">&quot;&quot;&quot;Everything&#39;s in a function to avoid polluting the global scope.&quot;&quot;&quot;</span>
</span><span class='line'>    <span class="k">try</span><span class="p">:</span>
</span><span class='line'>        <span class="kn">from</span> <span class="nn">_colorize</span> <span class="kn">import</span> <span class="n">set_theme</span><span class="p">,</span> <span class="n">default_theme</span><span class="p">,</span> <span class="n">Syntax</span><span class="p">,</span> <span class="n">ANSIColors</span>
</span><span class='line'>    <span class="k">except</span> <span class="ne">ImportError</span><span class="p">:</span>
</span><span class='line'>        <span class="k">pass</span>  <span class="c"># Python 3.13 and below</span>
</span><span class='line'>    <span class="k">else</span><span class="p">:</span>
</span><span class='line'>        <span class="c"># Define Solarized Light colors</span>
</span><span class='line'>        <span class="n">solarized_light_theme</span> <span class="o">=</span> <span class="n">default_theme</span><span class="o">.</span><span class="n">copy_with</span><span class="p">(</span>
</span><span class='line'>            <span class="n">syntax</span><span class="o">=</span><span class="n">Syntax</span><span class="p">(</span>
</span><span class='line'>                <span class="n">keyword</span><span class="o">=</span><span class="n">ANSIColors</span><span class="o">.</span><span class="n">GREEN</span><span class="p">,</span>
</span><span class='line'>                <span class="n">builtin</span><span class="o">=</span><span class="n">ANSIColors</span><span class="o">.</span><span class="n">BLUE</span><span class="p">,</span>
</span><span class='line'>                <span class="n">comment</span><span class="o">=</span><span class="n">ANSIColors</span><span class="o">.</span><span class="n">INTENSE_BLUE</span><span class="p">,</span>
</span><span class='line'>                <span class="n">string</span><span class="o">=</span><span class="n">ANSIColors</span><span class="o">.</span><span class="n">CYAN</span><span class="p">,</span>
</span><span class='line'>                <span class="n">number</span><span class="o">=</span><span class="n">ANSIColors</span><span class="o">.</span><span class="n">CYAN</span><span class="p">,</span>
</span><span class='line'>                <span class="n">definition</span><span class="o">=</span><span class="n">ANSIColors</span><span class="o">.</span><span class="n">BLUE</span><span class="p">,</span>
</span><span class='line'>                <span class="n">soft_keyword</span><span class="o">=</span><span class="n">ANSIColors</span><span class="o">.</span><span class="n">BOLD_GREEN</span><span class="p">,</span>
</span><span class='line'>                <span class="n">op</span><span class="o">=</span><span class="n">ANSIColors</span><span class="o">.</span><span class="n">INTENSE_GREEN</span><span class="p">,</span>
</span><span class='line'>                <span class="n">reset</span><span class="o">=</span><span class="n">ANSIColors</span><span class="o">.</span><span class="n">RESET</span> <span class="o">+</span> <span class="n">ANSIColors</span><span class="o">.</span><span class="n">INTENSE_GREEN</span><span class="p">,</span>
</span><span class='line'>            <span class="p">),</span>
</span><span class='line'>        <span class="p">)</span>
</span><span class='line'>        <span class="n">set_theme</span><span class="p">(</span><span class="n">solarized_light_theme</span><span class="p">)</span>
</span><span class='line'>
</span><span class='line'><span class="n">_main</span><span class="p">()</span>  <span class="c"># _main avoids name collision, in case python -i is used</span>
</span><span class='line'><span class="k">del</span> <span class="n">_main</span>  <span class="c"># Delete _main from global scope</span>
</span></code></pre></td></tr></table></div></figure>


<p>Note that:</p>

<ol>
<li>I put all relevant code within a <code>_main</code> function so that the variables I set don&rsquo;t remain in the global scope of the Python REPL (they will by default)</li>
<li>I call the <code>_main</code> function and then delete the function afterward, again so the <code>_main</code> variable doesn&rsquo;t stay floating around in my REPL</li>
<li>I use <code>try</code>-<code>except</code>-<code>else</code> to ensure errors don&rsquo;t occur on Python 3.13 and below</li>
</ol>


<p>Also note that the syntax highlighting in the new REPL is <a href="https://github.com/python/cpython/issues/134953">not as fine-grained</a> as many other syntax highlighting tools.
I suspect that it may become a bit more granular over time, <strong>which may break the above code</strong>.</p>

<p>The <code>_colorize</code> module is currently an internal implementation detail and is deliberately undocumented.
Its API may change at any time, so <strong>the above code may break in Python 3.15</strong>.
If that happens, I&rsquo;ll just update my <code>PYTHONSTARTUP</code> file at that point.</p>

<h2>Packaging themes</h2>

<p>I&rsquo;ve stuck all of the above code in a <code>~/.startup.py</code> file and I set the <code>PYTHONSTARTUP</code> environment variable on my system to point to this file.</p>

<p>Instead of manually updating a startup file, is there any way to make these themes <em>installable</em>?</p>

<p>Well, if a <code>.pth</code> file is included in Python&rsquo;s <code>site-packages</code> directory, that file (which must be a single line) will be run whenever Python starts up.
In theory, a package could use such a file to import a module and then call a function that would set the color scheme for the REPL.
My <a href="https://github.com/treyhunner/dramatic">dramatic</a> package uses (<em>cough</em> abuses <em>cough</em>) <code>.pth</code> files in this way.</p>

<p>This sounds like a somewhat bad idea, but maybe not a <em>horrible</em> idea.</p>

<p>If you do this, let me know.</p>

<h2>What&rsquo;s your theme?</h2>

<p>Have you played with setting a theme in your own Python REPL?</p>

<p>What theme are you using?</p>
]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[My PyCon US 2025 recap]]></title>
    <link href="https://treyhunner.com/2025/06/pycon-us-2025-recap/"/>
    <updated>2025-06-11T16:45:00-07:00</updated>
    <id>https://treyhunner.com/2025/06/pycon-us-2025-recap</id>
    <content type="html"><![CDATA[<p>I&rsquo;m pretty much fully back to normal life after PyCon US 2025.</p>

<p>I started writing this post shortly after PyCon, got side-tracked, and now I&rsquo;m finally publishing it.</p>

<p><strong>My very quick recap</strong>: I spent a ton of time at PyCon chatting with folks and I <em>really</em> enjoyed it.
As Ned wrote, <a href="https://nedbatchelder.com/blog/202505/pycon_summer_camp.html">it&rsquo;s like summer camp</a>.</p>

<h2>My pre-conference (Monday and Tuesday)</h2>

<p>I flew in a couple days early (Monday May 12) because Breeze airlines had a non-stop flight from San Diego to Pittsburgh that only ran on Monday or Friday.
I listened to <a href="http://robertdputnam.com/the-upswing/">The Upswing</a> as I flew in and wondered <a href="https://mastodon.social/@treyhunner/114496799621377766">how we can make our community less individualistic</a>.</p>

<p>On Tuesday, I went on <a href="https://mastodon.social/@treyhunner/114495825740774796">a walking tour</a> of downtown Pittsburgh.
Then I ate Indian food for lunch at Sree&rsquo;s, which I chose because I saw they had great vegan lunch options and weren&rsquo;t open for dinner.
I was eating vegan during PyCon US (as I <em>mostly</em> do at home) and I remembered to do <a href="https://mastodon.social/@treyhunner/114506541091205849">some research on vegan dinner restaurants in the area</a> this year.</p>

<p>On Tuesday night, as CPython core devs started showing up in the Westin lobby, I went on a walk with Anthony Shaw.
Anthony asked whether I was up for &ldquo;an adventure&rdquo; and I said yes without asking questions.
We walked across the river to Randyland (a.k.a. &ldquo;the mattress factory&rdquo;).
It was fun, interesting, and quite odd.
We were the only ones there and had plenty of time to look around at all the interesting items and art pieces that Randy had collected.</p>

<p>Anthony and I did dinner at Condado&rsquo;s and ran into more conference friends at the bar (Kattni, Rose, and Kojo).</p>

<h2>My tutorial on decorators (Wednesday)</h2>

<p>Wednesday was my tutorial day.</p>

<p>Attendees used sticky notes during the exercise sections to note when they needed help.
I wrote about this system in my <a href="https://treyhunner.com/2025/05/how-to-give-a-great-pycon-tutorial/">blog post of tips for giving a good PyCon tutorial</a>.</p>

<p>The tutorial went well and the attendees seemed to enjoy it.</p>

<h2>The newcomer&rsquo;s orientation &amp; education summit (Thursday)</h2>

<p>Just before the opening reception on Thursday evening, Kojo, Sumana, and I ran the <a href="https://mastodon.social/@pycon@fosstodon.org/114512798321665686">newcomer&rsquo;s orientation</a>.
Figuring out what exactly we want to say to help orient folks to PyCon is always a bit challenging.
A first-time PyCon attendee gave me some ideas for how we could do it even better next year.
I&rsquo;ve taken notes and will revisit them later.</p>

<p>I also attended a few talks during education summit earlier in the day.
Reuven Lerner gave a talk on how he recommends his students use LLMs and I was pleased to hear many suggestions that are closely aligned with what I recommend as well as a few insights I hadn&rsquo;t heard before.</p>

<h2>My lightning talk (Friday)</h2>

<p>I gave a lightning talk on Friday evening (the first talk day).
I called it <a href="https://youtu.be/Uuhu-F05A7k?feature=shared&amp;t=3174">Systems, gates, and rage</a>.
It seemed to go over well.
Folks occasionally told me throughout the conference that they enjoyed it.
I won&rsquo;t spoil the topic of my lightning talk (you&rsquo;ll need to watch it) but it&rsquo;s a topic that I&rsquo;d been thinking about for a few months.</p>

<p>Rodrigo gave a <a href="https://mastodon.social/@treyhunner/114519939925398070">meta lightning talk</a> as the first talk of the first day.
I gave a meta lightning talk last year in the same slot.
I hope this becomes a tradition, where the first lightning talk is a talk about someone explaining how to give a lightning talk.</p>

<h2>The keynotes</h2>

<p>I really enjoyed the keynotes this year.</p>

<p><a href="https://youtu.be/ydVmzg_SJLw?feature=shared">Cory Doctorow&rsquo;s opening keynote</a> was <a href="https://mastodon.social/@treyhunner/114519939925398070"><em>really</em> thought-provoking</a> and <a href="https://youtu.be/Bglsof9b23k?feature=shared">Lynn Root&rsquo;s keynote</a> was on the importance <a href="https://mastodon.social/@treyhunner/114519939925398070">of &ldquo;play&rdquo;</a>, which is a topic I&rsquo;ve written about before to my <a href="https://pym.dev/newsletter">mailing list</a>.</p>

<p><a href="https://youtu.be/qog-dGVhSBI?feature=shared">The Marshall project</a> keynote and <a href="https://youtu.be/3UOLpTA7pRI?feature=shared">The Carpentries</a> keynote were also great, though I missed portions of each (beginning of one and end of another) and ended up watching the full videos online after the conference.</p>

<h2>The hallway track</h2>

<p>The &ldquo;hallway track&rdquo; is the way many PyCon attendees describe <strong>the discussions that happen organically in the hallway</strong>.</p>

<p>These discussions can happen at any time, including breaks, breakfast, lunch, and even during talks. I missed at least 3 talks this year because I was having a great discussion in the hallway and time got away from me.</p>

<p>PyCon&rsquo;s venue in Pittsburgh included a great <a href="https://mastodon.social/@treyhunner/114512971414412295">rooftop</a> so the <strong>rooftop track</strong> / <strong>garden track</strong> was a lovely spin in the hallway track this year.</p>

<h2>The open spaces</h2>

<p>The open spaces are a very important part of PyCon for me.</p>

<p>Like the hallway track, the open spaces are mostly (usually) about having a conversation.
Unlike the hallway track, the open spaces have a <strong>set topic</strong>.</p>

<p>I attended open spaces (and <a href="https://mastodon.social/@treyhunner/114526876252905533">ran my own</a>):</p>

<ul>
<li><strong>Education and Outreach Working Group Open Space</strong>: I&rsquo;m part of this newly revived work group and we held this to hear topics that community members are interested in seeing the PSF focus on more deliberately</li>
<li><strong>t-strings: Let&rsquo;s build powerful templating together</strong>: I am interested in the new t-string feature in Python 3.14 and wanted to listen to this conversation (I contributed <em>a little bit</em> too)</li>
<li><strong>Computer Assisted Reporting and Investigative journalism</strong>: I was curious to hear this conversation, which included journalists, government employees who respond to information requests, and many others</li>
<li><strong>Python Skill-Building</strong>: I ran this open space on the roof during lunch time to chat about different Python skill-building services, including my own (<a href="https://pym.dev">Python Morsels</a>)</li>
<li><strong>Ask a Friendly Meat-Loving Vegan</strong>: I also ran this open space, inspired by a conversation I had the night before in a hotel lobby about animal farming, veganism, nutrition, and other related topics I&rsquo;ve thought/read a lot about</li>
</ul>


<p>I enjoyed meeting folks with similar interests and having fun and productive conversations (or at least listening to interesting conversations) in those open spaces.</p>

<h2>Dinners, games, and hotel lobby conversations</h2>

<p>I ate at Bae Bae&rsquo;s, Rosewater Grill, and APTEKA during the conference.
I also ate at Condado&rsquo;s at least 3 nights.</p>

<p>APTEKA was a fun trek for <a href="https://mastodon.social/@pythonbynight@fosstodon.org/114525581821316666">vegan PierogiCon</a> with mostly non-vegan folks.
I&rsquo;m sure the pierogis weren&rsquo;t authentic, but we all loved the food.</p>

<p>After dinner every night I went to the Westin lobby and either played <a href="https://treyhunner.com/2015/06/cabo-card-game/">the Cabo card game</a> or chatted with folks (<a href="https://mastodon.social/@treyhunner/114509076754402356">1</a>, <a href="https://mastodon.social/@treyhunner/114514682814457694">2</a>, <a href="https://mastodon.social/@treyhunner/114525878624139679">3</a>).</p>

<h2>On missing talks</h2>

<p>The keynotes and lightning talks are usually my favorite parts of PyCon.
I tried to watch as many live keynotes and lightning talk sessions as I could this year.
The morning lightning talks were hard to make it to because I kept sleeping in late enough that I missed most of them.</p>

<p>Ultimately, I watched <em>very few</em> live talks.
I missed talks due to sleeping in, attending open spaces, visiting booths in the exhibit hall, getting lost in conversations in the hallway, and mid-day taking naps (in my hotel room, not during talks!).</p>

<p>Hynek has <a href="https://hynek.me/articles/hallway-track/">written about the downside of the hallway track and the importance of attending talks</a>.
I sympathize with Hynek&rsquo;s argument that the hallway track is a trade off and there are downsides to missing talks, for both attendees and speakers.
I am grateful that folks give talks and I want to support folks who do give talks, and yet, I often find myself attending few talks.</p>

<p>Ironically, the one talk I ended up taking a nap through (again, in my hotel room, not <em>in the talk</em>) was Hynek&rsquo;s.
My brain was fried by the time of his talk and I&rsquo;m glad I was able to watch it online the week after PyCon.
He apparently had a completely packed room as it was!</p>

<p>I did make sure to show up to a talk by a conference friend who I wanted to support (Michael Dahlberg&rsquo;s <a href="https://youtu.be/LseCKvrp6og?feature=shared">talk on honeybee swarms</a>).
I have also binge-watched a dozen or so talks online that I had planned to attend but missed during PyCon.</p>

<p>You can <a href="https://www.youtube.com/@PyConUS/videos">watch all talks online now</a> (at least I <em>think</em> they&rsquo;re all up now?).</p>

<h2>Attend a Python event</h2>

<p>I&rsquo;m looking forward to PyCon coming to Long Beach next year.
I&rsquo;m in San Diego and it will be nice to <em>not fly</em> to a PyCon for once!</p>

<p>I highly recommend attending local Python meetups, attending regional Python conferences, and/or attending PyCon US.
Most in-person Python community events are <em>pretty lovely</em>.
The Python community generally goes out of its way to be more welcoming than most tech events.</p>

<p>See you in Long Beach!</p>
]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Let's end kidney deaths: buy one of my kidneys]]></title>
    <link href="https://treyhunner.com/2025/05/lets-end-kidney-deaths/"/>
    <updated>2025-05-26T13:00:00-07:00</updated>
    <id>https://treyhunner.com/2025/05/lets-end-kidney-deaths</id>
    <content type="html"><![CDATA[<p>Those with kidney failure need dialysis.
Dialysis is <em>expensive</em>.
Dialysis care in the United States accounts for 7% of Medicare&rsquo;s budget and <strong>nearly 1% of the entire federal budget</strong> (<a href="https://strivehealth.com/news/patients-vs-profits-who-wins-in-the-traditional-u-s-dialysis-system/">yes</a>, <a href="https://pharm.ucsf.edu/kidney/need/statistics">really</a>).</p>

<p>Dialysis is <em>should</em> be a stop gap measure.
Ideally, dialysis is just an interim solution until a kidney donor is found.
Folks with kidney failure <em>really need</em> a new kidney, but most will never receive one.
So regular dialysis is a life-long state for many.</p>

<p>More than 100,000 patients in the United States are on the kidney transplant list, but fewer than 20,000 kidneys are transplanted from deceased donors each year and fewer than 10,000 kidneys are transplanted from living donors.
About <a href="https://reason.com/volokh/2025/04/10/end-kidney-deaths-act-reintroduced-in-congress/">40,000 people</a> die in the US each year from kidney failure.</p>

<h2>The beauty of non-directed kidney donations</h2>

<p>Most kidney donations are directed.</p>

<p>With <strong>directed kidney donations</strong>, a prospective donor states either:</p>

<ul>
<li>&ldquo;I&rsquo;m signing up to give a kidney to this friend/family member&rdquo;</li>
<li>Or &ldquo;I&rsquo;m signing up to eventually give a kidney to anyone as long as this friend/family member finds a matching donor in exchange for my donation&rdquo;</li>
</ul>


<p>A small minority of kidney donations are <strong>non-directed kidney donations</strong>.
Non-directed donors have discovered that they have 2 kidneys but only need one <em>and</em> that donating a kidney is relatively safe and it saves lives (lives, plural, due to kidney chains).
A sizable minority of prospective non-directed kidney donors <a href="https://pmc.ncbi.nlm.nih.gov/articles/PMC7500709/">don&rsquo;t make the cut</a>, often due to medical concerns.</p>

<p>When a non-directed kidney donor does successfully donate a kidney, that pretty much always unlocks a <a href="https://www.kidneyregistry.com/for-donors/kidney-donation-blog/what-is-a-kidney-donation-chain/">kidney donation chain</a>, which usually saves 3 or more lives!</p>

<p>Many people have altruistically donated one of their kidneys.
<a href="https://youtu.be/nhht9kslq04?feature=shared">Ned Brooks</a>, <a href="https://youtu.be/SNs0GKxkmpE?feature=shared">Allyssa Bates</a>, <a href="https://youtu.be/fi4gZpp6lKA?feature=shared">Dan Drew</a>, and <a href="https://youtu.be/XygJ0A2lopg?feature=shared">Rochel Smoller</a> all gave TED talks about their experience donating a kidney.
But <em>most</em> kidney donations are directed, meaning the donor has agreed to donate a kidney only if a loved one receives a kidney donation as a result.
That&rsquo;s the reason non-directed donations are able to unlock donation chains.</p>

<h2>Kidney donations are priceless (literally)</h2>

<p>Non-directed kidney donations save lives.
Thanks to the math behind kidney donation chains, non-directed donations often save many lives!</p>

<p>But kidney donations also mean less dialysis and dialysis costs money from all of us.
Remember, nearly 1% of the federal budget is spent on dialysis.</p>

<p>Interestingly, non-directed kidney donors don&rsquo;t receive any money for their donation.
The doctors, nurse, and all medical staff involved in a kidney transplant receive compensation for their work, but the donor, who is literally <em>donating an organ</em>, receives <strong>no compensation at all</strong>.</p>

<h2>The End Kidney Deaths Act</h2>

<p>What if we could save tens of thousands of lives every year, reduce the cost of dialysis to US taxpayers, and incentivize more non-directed kidney donations?</p>

<p>The <a href="https://www.congress.gov/bill/119th-congress/house-bill/2687/text">End Kidney Deaths Act</a>, which is currently going through Congress, is meant to do just that.</p>

<p>If the End Kidney Deaths Act passes, non-directed kidney donations would provide $50,000 of tax credits to the donor, broken up into $10,000 per year over 5 years.</p>

<h2>I might donate a kidney for $50,000</h2>

<p>I first heard someone argue that donating a kidney was a sensible act in 2020.</p>

<p>The argument goes like this:</p>

<ol>
<li>Donating a kidney saves multiple lives</li>
<li>The short-term downside of donating a kidney is largely lost work time</li>
<li>The long-term health risks involved with donating a kidney are minimal</li>
<li>Therefore you should donate a kidney</li>
</ol>


<p>I&rsquo;m somewhat convinced by this argument.
And yet I have not donated a kidney.</p>

<p>Why have I not donated a kidney?</p>

<p>Well, I run my own business and I don&rsquo;t think I would want to lose 2 weeks of work.
Also I don&rsquo;t <em>really</em> want to go through the process.
Plus, I can always donate a kidney <em>next year</em>, right?</p>

<p>I&rsquo;m choosing not to save someone&rsquo;s life because it&rsquo;s a large inconvenience for me.
That may sound selfish, but you&rsquo;re in the same situation that <em>I</em> am.</p>

<p>For $50,000 of tax credits, I would <strong>much more seriously</strong> consider donating a kidney.</p>

<h2>Let&rsquo;s think through the &ldquo;ick&rdquo; factor</h2>

<p>We&rsquo;re generally uncomfortable with the idea of paying another human for their body parts.
There are good reasons and bad reasons for this, but I don&rsquo;t believe the good reasons apply in this case&hellip; at least not enough to outweigh the lives that this bill would save.</p>

<p>Around 70% of the world&rsquo;s donated plasma comes from the US, largely due to the fact that plasma donors in the US are compensated for their donation.
There are ethical considerations around plasma donation and this industry should be better regulated.
Kathleen McLaughlin wrote a book about this: <a href="https://jacobin.com/2023/05/plasma-donation-industry-payment-inequality-poverty-big-pharma">interview</a> and <a href="https://jacobin.com/2023/08/blood-money-book-review-plasma-donation-exploitation-labor">book review</a>.
And yet, regulation aside, I think it&rsquo;s worth compensating plasma donors to reduce the world plasma shortage (<a href="https://www.npr.org/2021/05/14/996921658/blood-money">Planet Money episode</a>).</p>

<p>Another thing to note when comparing kidney donations to plasma donations: the <strong>End Kidney Deaths Act</strong> would compensate kidney donors via tax credits, <em>not</em> through private markets.
Each approach has pros and cons, but the &ldquo;this is exploitative&rdquo; argument is often applied to markets but rarely applied to tax credits.</p>

<p>You may be experiencing a &ldquo;this might not be a good idea&rdquo; reaction when thinking about paying non-directed kidney donors, even through tax credits.
It&rsquo;s worth thinking through why that is.
What is your brain telling you?
Which of the underlying arguments <em>against</em> this are worth considering?
And how do those arguments look when put within the bigger picture?</p>

<h2>Let&rsquo;s compensate altruistic kidney donors</h2>

<p>Non-directed kidney donations are extremely valuable.
I think we should incentivize these donations.
I hope Congress passes the <a href="https://www.congress.gov/bill/119th-congress/house-bill/2687/text">End Kidney Deaths Act</a>.</p>

<p>What do you think?</p>

<p>Is this something you would <a href="https://democracy.io">write your representative about</a>?</p>

<h2>Please join this effort</h2>

<p><a href="https://cnliberalism.org/posts/podcast-should-we-pay-kidney-donors-ft-elaine-perlman">The podcast episode</a> with Elaine Perlman is what inspired me to spend a couple hours researching and writing this post.</p>

<p>Since originally publishing this post, I clicked the &ldquo;join the team&rdquo; button at <a href="https://www.endkidneydeathsact.org">EndKidneyDeathsAct.org</a>.
The form took only a minute to fill out, I&rsquo;ve received a few emails since then, including an email thread with a few others in my congressional district, and I&rsquo;m now considering writing an op-ed in a newspaper about this.</p>

<p>If, like me, you feel that this is a <em>very</em> sensible idea, so much so that you would spend a few minutes or a few hours of time to try to make this happen, please get involved a bit more deeply.
You can click the &ldquo;contact your leaders&rdquo; button at <a href="https://www.endkidneydeathsact.org">EndKidneyDeathsAct.org</a> or, like me, you could click &ldquo;join the team&rdquo;.</p>

<p>I&rsquo;m glad to now be a small part of this very important effort.
I think you will be glad to be part of this too.</p>
]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Make a cutting room floor]]></title>
    <link href="https://treyhunner.com/2025/05/make-a-cutting-room-floor/"/>
    <updated>2025-05-22T07:40:00-07:00</updated>
    <id>https://treyhunner.com/2025/05/make-a-cutting-room-floor</id>
    <content type="html"><![CDATA[<p>All writers need a <strong>cutting room floor</strong>.</p>

<p>I have never written a book, an academic paper, or a journal article.
And yet I <em>do</em> write.</p>

<p>I write emails, I write blog posts, I write screencast scripts, I journal, I write talks, and I write curriculum for my Python trainings.</p>

<p>When I write, I need a place to put text that <em>may</em> not make the cut in a final draft.
I call this place the &ldquo;cutting room floor&rdquo;.</p>

<h2>How I write: draft, research, edit</h2>

<p>Before sharing what I mean by a &ldquo;cutting room floor&rdquo;, I need to explain <em>how</em> I write.</p>

<p>I tend to write in 2-3 stages:</p>

<ol>
<li>Draft: writing <em>more</em> words, ideally without deleting or modifying</li>
<li>Research: discovering facts playing with possible examples</li>
<li>Edit: deleting, rearranging, and modifying words</li>
</ol>


<p>When <strong>drafting</strong>, I try to barf out words quickly and, ideally, without any editing at all.
I often do this by setting a 25 - 60 minute timer and then writing thoughts as they pour out of my head.</p>

<p>When <strong>researching</strong>, I&rsquo;m looking things up online, trying out code in a text editor, and sometimes playing with ideas in an abstract way.
This phase sometimes looks like traditional &ldquo;research&rdquo; and sometimes includes a mix of research and brainstorming.</p>

<p>When <strong>editing</strong>, I&rsquo;m re-reading my words and attempting to turn them into something <em>good</em>.
The first draft is sometimes pretty close to what I ultimately end up with, but sometimes it&rsquo;s <em>very different</em> from what I eventually publish.</p>

<p>Sometimes I draft and then edit without any research.
Sometimes I research first.
And often I bounce back and forth between these different tasks.</p>

<p>I try to keep my drafting time <em>distinct</em> from my research and editing time.
When drafting, my thoughts aren&rsquo;t always sensible, but they&rsquo;re <em>something</em>.
It&rsquo;s often easier to edit a bunch of <em>something</em> into something <em>sensible</em> than it is to write something sensible in the first place.</p>

<p>It&rsquo;s <em>very</em> tempting to start researching or editing while I&rsquo;m drafting.
I try to counter that temptation by writing <code>TODO insert blah here</code> whenever I feel that something is needed but that <em>thing</em> needs further research before I can write a certain paragraph or so about it.</p>

<h2>The &ldquo;cutting room floor&rdquo; is about editing</h2>

<p>When I&rsquo;m <strong>editing</strong> my writing, I frequently find it challenging to remove certain sections.
I often feel like I wrote something important, even while acknowledging that it may not fit well within the larger piece I&rsquo;m writing.
I often don&rsquo;t realize that an idea or a bit of writing doesn&rsquo;t fit until I&rsquo;m far into editing.</p>

<p>The <strong>cutting room floor</strong> is a way to <strong>save</strong> a specific bit of writing while also acknowledging that I may never use it.</p>

<p>When using a &ldquo;cutting room floor&rdquo;, I select a paragraph or a whole section of text and I &ldquo;cut&rdquo; it out of my writing and then &ldquo;paste&rdquo; it at the bottom of the document under a &ldquo;Cutting room floor&rdquo; section.
Or sometimes I&rsquo;ll paste it in a separate file, called something like <code>cutting-room-floor.md</code>.</p>

<p>I <em>do</em> usually delete that file or that section from my text, but not until the end.
Even when my text is all version-controlled, it feels much less <em>final</em> and <em>scary</em> to move some text into a (possibly temporary) &ldquo;cutting room floor&rdquo; than it feels to delete the text entirely.</p>

<h2>Kill your darlings&hellip; or set them aside?</h2>

<p>The noun &ldquo;<a href="https://en.wikipedia.org/wiki/Deleted_scene#%22Cutting_room_floor%22">cutting room floor</a>&rdquo; relates to the verb &ldquo;<a href="https://idioms.thefreedictionary.com/Kill+Your+Darlings">kill your darlings</a>&rdquo;.</p>

<p>While &ldquo;kill your darlings&rdquo; may sound like a violent phrase, it essentially means &ldquo;don&rsquo;t be afraid to remove things you like&rdquo;.</p>

<p>In my <a href="https://trey.io/pyohio2024">PyOhio keynote</a> in 2024, I originally had a whole section on cognitive dissonance.
This section involved my own personal experience with cognitive dissonance and I spent a lot of time trying to make this section coherent, thoughtful, and impactful.
That was originally my favorite section of the talk, but I eventually <strong>deleted that entire section from the talk</strong>.</p>

<p>As I iterated versions of the talk, that &ldquo;darling&rdquo; section made less and less sense within the context of the whole talk.
At some point I realized that the section was only <em>in</em> the talk because it was a <strong>darling</strong> for me.</p>

<p>That section meant something special to me, but it just didn&rsquo;t fit.
Reluctantly, I moved it to the cutting room floor.
I kept iterating and the eventual talk was <em>better</em> because I cut that &ldquo;darling&rdquo; section out.</p>

<p>My <strong>cutting room floor</strong> allowed me to <strong>kill a darling</strong> by setting it to the side.
I could always add it back in later, but ultimately, I never did.
Once it was removed, I knew the talk made more sense.</p>

<h2>Allow yourself to delete everything</h2>

<p>Having a &ldquo;cutting room floor&rdquo; gives me <strong>permission to delete</strong> any section of any talk, screencast, or article I write.</p>

<p>A &ldquo;cut&rdquo; section isn&rsquo;t <em>gone</em>.
It&rsquo;s just been moved to the cutting room floor&hellip; possibly just temporarily!
I can always add the section back in later if I want.</p>

<p>The cutting room floor could be thought of as a sort of graveyard, but it can <em>also</em> be a place of inspiration.
I <em>rarely</em> add sections back into my talk after I cut them, but I <em>do</em> often spend time re-drafting a new section that replaces a section that I had cut.
Sometimes I&rsquo;ll even look at the text in my cutting room floor, borrowing the words I like as I write replacement text.</p>

<p>Instead of saying an emphatic &ldquo;NO&rdquo; by deleting permanently, I can say &ldquo;not right now&rdquo; by moving text to the cutting room floor.
For me, &ldquo;not right now&rdquo; is often a lot easier for me to say than &ldquo;no&rdquo;.</p>

<p>So during <em>your</em> editing session, give yourself permission to cut by <strong>making a cutting room floor</strong>.</p>
]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Tips for PyCon tutorial presenters]]></title>
    <link href="https://treyhunner.com/2025/05/how-to-give-a-great-pycon-tutorial/"/>
    <updated>2025-05-01T16:00:00-07:00</updated>
    <id>https://treyhunner.com/2025/05/how-to-give-a-great-pycon-tutorial</id>
    <content type="html"><![CDATA[<p>You&rsquo;ve submitted a tutorial to PyCon and it was accepted.
Now what?</p>

<p>In this post I&rsquo;ll be sharing my thoughts on giving a great PyCon tutorial.</p>

<h2>Screen readability 👓</h2>

<p>Be sure to consider readability when teaching a tutorial.
Talks usually involve slides and the default font size in presentation software is often pretty readable on a projector.
That&rsquo;s <em>not</em> usually the case for code editors, web browsers, and terminals.</p>

<p>Consider the size of the text you&rsquo;ll be displaying.
I tend to zoom in until my screen can just barely fit about 80 characters of text.</p>

<p>Also consider your color scheme.
When reading from a projector, black text on a white background is usually easier to read than white text on a black background.
So I recommend using a <strong>light mode</strong> color scheme even if you <em>usually</em> prefer dark mode.
And be sure to use a high contract color scheme!</p>

<p>Also consider how you&rsquo;ll share URLs.
Shortened URLs are easier to type.
QR codes can also help, but be sure to share the <em>actual</em> URL as some may not be able to scan QR codes.</p>

<h2>Engage attendees in attempted information retrieval 🧠</h2>

<p>Have you ever heard the phrase &ldquo;in one ear and the other&rdquo;?
That&rsquo;s how lectures usually work.
Lectures tend to focus on putting information into our heads.
Unfortunately, our brain isn&rsquo;t designed to remember information that&rsquo;s presented to it.</p>

<p>We don&rsquo;t learn by putting information into our heads; we learn by trying to retrieve information from our heads.
Information retrieval attempts, even somewhat unsuccessful ones, are <em>very important</em> for helping our brain retain information.</p>

<p>So, how will your tutorial engage attendees in the act of attempted information retrieval?</p>

<p>You could:</p>

<ul>
<li>Group quiz: &ldquo;what do you think might happen if&hellip;?&rdquo;</li>
<li>Silently quiz: present a quiz question every few minutes to work on individually</li>
<li>Take 10 minute exercise breaks every 20 minutes so attendees can write code to apply what they&rsquo;ve just seen</li>
</ul>


<p>Give attendees a way to know whether they &ldquo;did it right&rdquo; as well.
But also keep in mind that the focus of any sort of quizzing should be on <strong>taking a guess</strong>, not on getting a grade.</p>

<p>We don&rsquo;t learn by watching, but by <em>doing</em>, so I recommend planning your tutorial around a <em>lot</em> of recall.</p>

<h2>Prepare for the unprepared 😦</h2>

<p>Since your tutorial is going to be hands-on (see the previous section), you&rsquo;ll probably need your attendees to set things up on their machines.</p>

<p>You could set aside the first 30 minutes of your 3 hour tutorial for setup steps, but I wouldn&rsquo;t recommend that.
Setup will take some attendees 5 minutes and others 25 minutes.
Expecting folks who are quick to idle for many minutes isn&rsquo;t ideal.</p>

<p>If possible, I would recommend writing up prerequisite setup instructions and asking attendees to perform necessary setup steps <em>before</em> PyCon.
A week or two before PyCon, email those installation and/or setup instructions.
Then email those instructions a couple more times.
Yes, that may seem redundant, but redundant reminders can be very helpful.</p>

<p>Some attendees will register after you send out your first instructions email, some will ignore or skip over emails, and some may even register for your tutorial <em>on the day of your tutorial</em>.</p>

<p>It&rsquo;s helpful to give any unprepared attendees some escape hatches to get caught up.</p>

<p>For example, you could let attendees know that:</p>

<ol>
<li>You would like them to work through setup instructions by X date</li>
<li>You be available for asynchronous email-based assistance and they should email you if they get stuck</li>
<li>You will be in your tutorial room early and you can help anyone who also shows up 15 minutes early get setup</li>
</ol>


<p>Inevitably, somewhere around 10% of your attendees will <em>not</em> work through the setup instructions in full before the start of your tutorial.</p>

<p>Please <em>do</em> provide help to these unprepared attendees, but don&rsquo;t punish the prepared ones, at least not too much.
For example, if you have a 15 minute exercise section near the beginning of your tutorial, float around the room during that time and make sure everyone has their machines setup.</p>

<h2>Expect the unexpected 🚨</h2>

<p>What happens if an attendee&rsquo;s work laptop doesn&rsquo;t allow them to install or run certain programs?
What if an attendee brings a Chromebook and your installation instructions assume Linux, Mac, or Windows?
What if an attendee didn&rsquo;t bring a laptop?</p>

<p>What&rsquo;s your backup plan?</p>

<p>Can attendees use an online interface instead of working locally?
Do you have a spare laptop with a guest account they could use?
Could another attendee volunteer to have someone pair up with them?</p>

<p>And what happens if the Internet drops out?
Internet issues have plagued <em>many</em> a tutorial presenter in the past, especially for tutorials that involve dozens of attendees all downloading many megabytes at the same time.
Consider bringing a pre-loaded USB thumb drive or two as a backup plan.</p>

<h2>Have helpers, if you can 🧷</h2>

<p>Having an exercise &ldquo;break&rdquo; early on allows those in need of extra setup help to catch up.</p>

<p>I also like to encouraging attendees to try helping each other out.
This can set a sort of &ldquo;we&rsquo;re all in this together&rdquo; atmosphere.
When an attendee helps a neighbor understand the instructions for an exercise, that may also help them solidify their understanding as well.</p>

<p>If you can find a volunteer <strong>teaching assistant</strong> (TA) or two who can familiarize themselves with your material even <em>just a little bit</em>, that can be very handy.
The primary role of a TA is to simply to act as another set of eyes and ears for attendees who need a bit of extra help.
This is <em>especially</em> helpful during the first exercise section or two, when everyone is getting setup and wading into the material.</p>

<p>During exercise breaks in my recorded PyCon tutorials (e.g. <a href="https://youtu.be/_6U1XoxyyBY?feature=shared&amp;t=2310">here</a> or <a href="https://youtu.be/ixiRkUwPI2A?feature=shared&amp;t=1078">here</a> or <a href="https://youtu.be/6zu8lrYn6t8?feature=shared&amp;t=1605">here</a>) you may notice me and my TAs wondering around the room helping folks who have stickies up (more on that below) and generally checking in on folks who have confused expressions on their faces.</p>

<p>For both you <em>and</em> your helpers, consider what it means to be a helpful helper.
You may want to watch my <a href="https://treyhunner.com/mentoring/resources.html">Meaningful Mentoring Moments</a> talk or review some of the materials I link to in that talk.</p>

<h2>Seek advice from others 🧑‍🏫</h2>

<p>In each of my tutorials I give every attendee a sticky note.
I either put a sticky note at each chair before we begin, have a TA give a sticky notes to each person as they walk in, or I walk around the room and say hi to each person while giving them a sticky note.</p>

<p>After everyone has a sticky note, I explain their purpose: to flag down me (or a TA) during exercises when help is needed.
It&rsquo;s difficult to type on your keyboard while keeping a hand raised in the air for many many minutes.
Sticky notes allow attendees to signal that they need help <em>and</em> they allow me to quickly scan the room and see how many folks need help.</p>

<p>You probably wouldn&rsquo;t have thought of sticky notes.
I wouldn&rsquo;t have either!
I saw them used in an Open Hatch workshop many years ago and thought &ldquo;that&rsquo;s genius&rdquo;.</p>

<p>Ask for advice from others who have taught live tutorials, workshops, and classes.
You may not find every bit of advice helpful, but most of us who have taught before are happy to share what works for us.</p>

<h2>Envision the moment 💭</h2>

<p>If we learn solely from experience, then the first time we do something we&rsquo;d expect to fail miserably.
But that&rsquo;s (obviously) not what we <em>want</em> to happen.
We want our first tutorial to be a reasonably successful experience.</p>

<p>Fortunately, our brains are half decent reality simulators.
Let&rsquo;s use our brain&rsquo;s ability to simulate future possibilities to our advantage.</p>

<p>Imagine the world from the perspective of your attendees.
Where might they get stuck or frustrated?
If they don&rsquo;t feel comfortable speaking up, will you know about their frustrations?
Will you be deliberately polling the room to see how they&rsquo;re feeling?
When?
How often?
What will trigger you to poll the room?</p>

<p>Imagine when pulse checks might be helpful to see how your audience members are feeling.</p>

<p>Also, imagine how much of your curriculum you&rsquo;ll get through.
How might you meet your attendees' expectations if it takes twice the time to get through curriculum?
Is there anything that can be skipped?</p>

<p>What happens if you get to the end and you have 30 minutes left?
Could you add extra material that&rsquo;s <em>expected</em> to be skipped, but could be worked through if there&rsquo;s additional time?</p>

<p>What will the call-to-action be at the end of your tutorial?
Are there future projects attendees could work through?
Are there specific actions you would recommend attendees take next?</p>

<h2>Use checklists ✅</h2>

<p>One time I forgot to pack sticky notes and found myself frantically looking for a nearby office supplies store the morning of my tutorial.
And then I did that 2 more times. 😬</p>

<p>You can&rsquo;t keep everything in your head.
Use a checklist to keep yourself on track during your tutorial <em>and</em> leading up to your tutorial.</p>

<p>Here&rsquo;s a checklist with much of the advice in this post:</p>

<ol>
<li>Use a large font and a highly readable color scheme</li>
<li>Be sure your curriculum facilitates active recall</li>
<li>Nudge attendees to work through setup instructions before your tutorial</li>
<li>Be prepared to help a portion of the room with setup instructions <em>during</em> the tutorial</li>
<li>Try to find teaching assistants if you can</li>
<li>Wander around the room to help during exercise sections</li>
<li>Consider using sticky notes so attendees can flag down help during exercise sections</li>
<li>Ask other tutorial presenters and teachers for advice</li>
<li>Mentally walk through the day and consider what might go differently than planned</li>
</ol>


<h2>Have compassion 💖</h2>

<p>Doing hard things is hard.
Explaining hard things is even harder.</p>

<p>Learning isn&rsquo;t easy either.</p>

<p>Be kind to your attendees and <strong>be kind to yourself</strong>.
Mistakes will happen.
Try to keep moving forward and do your best to make good use of the time you have.</p>
]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Which social network are we using for PyCon US this year?]]></title>
    <link href="https://treyhunner.com/2025/04/which-social-network-are-we-using-for-pycon/"/>
    <updated>2025-04-18T11:45:00-07:00</updated>
    <id>https://treyhunner.com/2025/04/which-social-network-are-we-using-for-pycon</id>
    <content type="html"><![CDATA[<p>Last year I updated my <a href="https://treyhunner.com/2018/04/how-to-make-the-most-of-your-first-pycon/">having a great first PyCon</a> post to note that Mastodon would likely be more popular than Twitter at PyCon.</p>

<p>My guess was correct.
During PyCon US 2024, <a href="https://mastodon.social/@treyhunner/112453920848761679">Mastodon overtook Twitter</a> for the most posts on the <code>#PyConUS</code> hashtag.</p>

<p>In the fall of 2024, Bluesky really took off.
It currently seems like Bluesky is now a bit more popular than Mastodon for Python posts in general&hellip; but that doesn&rsquo;t necessarily mean it will be the most popular social media network during the conference.</p>

<p>This year I&rsquo;m guessing that Mastodon will still be the most popular network for <code>#PyConUS</code> posts, though I wouldn&rsquo;t be very surprised if Bluesky took the lead instead.</p>

<h2>What do the social networks think?</h2>

<p>I decided to pose this question to the various social networks via a poll. I made sure to share these polls using both the <code>#Python</code> and <code>#PyConUS</code> hashtags for visibility&rsquo;s sake.</p>

<p>The results?</p>

<ul>
<li><a href="https://www.linkedin.com/posts/treyhunner_pyconus-python-activity-7313960284581216256-DdzA/">LinkedIn poll</a>: LinkedIn isn&rsquo;t sure whether Twitter is the new Twitter</li>
<li><a href="https://x.com/treyhunner/status/1908187705965703323">Twitter poll</a> Twitter (mostly) knows that Bluesky is the new Twitter</li>
<li><a href="https://bsky.app/profile/trey.io/post/3llyswsgkut2h">Bluesky poll</a> Bluesky knows the really nerdy tech folks hang out on Mastodon</li>
<li><a href="https://mastodon.social/@treyhunner/114280595068705311">Mastodon poll</a> Mastodon loves itself above all else</li>
</ul>


<p>In other words, LinkedIn leans more toward Bluesky being the leader than Mastodon, Twitter leans toward Bluesky, but Bluesky and Mastodon both lean toward Mastodon being the leader during PyCon.</p>

<p>I didn&rsquo;t mention Threads or other platforms because they didn&rsquo;t seem like real contenders given where the most active PyCon-attending Python-posting folks seem to hang out in 2025.</p>

<h2>The actual results</h2>

<p>Here are the actual results of the polls.</p>

<h3>LinkedIn</h3>

<p><a href="https://www.linkedin.com/posts/treyhunner_pyconus-python-activity-7313960284581216256-DdzA/"><img src="https://treyhunner.com/images/pycon-poll-linkedin-2025.png" title="LinkedIn poll results" ></a></p>

<h3>Twitter</h3>

<p><a href="https://x.com/treyhunner/status/1908187705965703323"><img src="https://treyhunner.com/images/pycon-poll-twitter-2025.png" title="Twitter poll results" ></a></p>

<h3>Bluesky</h3>

<p><a href="https://bsky.app/profile/trey.io/post/3llyswsgkut2h"><img src="https://treyhunner.com/images/pycon-poll-bluesky-2025.png" title="Bluesky poll results" ></a></p>

<h3>Mastodon</h3>

<p><a href="https://mastodon.social/@treyhunner/114280595068705311"><img src="https://treyhunner.com/images/pycon-poll-mastodon-2025.png" title="Mastodon poll results" ></a></p>

<h2>We&rsquo;re still fragmented</h2>

<p>Unfortunately, regardless of which network is the leader this year during PyCon US, we&rsquo;re still going to be much more fragmented than we used to be. Twitter <em>was</em> the clear leader years ago and it very clearly isn&rsquo;t anymore&hellip; at least not for PyCon US conference chatter.</p>

<p>I&rsquo;ll be checking Mastodon and Bluesky and will post on <em>at least</em> Mastodon and possibly also Bluesky. I hope that other folks will also use one of these 2 social networks to organize dinners and gatherings! 🤞</p>

<p>Feel free to follow me <a href="https://mastodon.social/@treyhunner">on Mastodon</a> and <a href="https://mastodon.social/@treyhunner">Bluesky</a> during the conference.</p>

<p>Also let me know if you&rsquo;d like to join my <a href="https://bsky.app/profile/trey.io/post/3ln45jpzmik2c">PyCon US starter pack</a> on Bluesky.
Lists on Mastodon require following and I prefer not to follow everyone I meet at PyCon so, unfortunately, I probably won&rsquo;t have a Mastodon equivalent of this. 😢</p>

<p>I recommend checking the <code>#PyConUS</code> hashtag on both networks as well.
Note that you can <em>subscribe</em> to hashtags on Mastodon which is pretty neat!</p>

<h2>See you at PyCon!</h2>

<p>If this will be your first PyCon US, I recommend signing up for both Mastodon and Bluesky and checking the <code>#PyConUS</code> hashtag during the conference.</p>

<p>Also, be sure to see my <a href="https://treyhunner.com/2018/04/how-to-make-the-most-of-your-first-pycon/">post on having a great first PyCon</a> and <a href="https://mastodon.social/@treyhunner/112448459788776426">see this additional tips</a>.</p>
]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Django components: sometimes an include doesn't cut it]]></title>
    <link href="https://treyhunner.com/2025/03/django-components-sometimes-an-include-doesnt-cut-it/"/>
    <updated>2025-03-15T21:00:00-07:00</updated>
    <id>https://treyhunner.com/2025/03/django-components-sometimes-an-include-doesnt-cut-it</id>
    <content type="html"><![CDATA[<p>Have you ever wished that Django&rsquo;s <code>include</code> template tag could accept blocks of content?</p>

<p>I have.</p>

<p>Unfortunately, Django&rsquo;s <code>{% include %}</code> tag doesn&rsquo;t accept blocks of text.</p>

<p>Let&rsquo;s look at a few possible solutions to this problem.</p>

<h2>The Problem: Hack Include Workarounds</h2>

<p>Let&rsquo;s say we have HTML and CSS that make up a modal that is powered by Alpine.js and HTMX and we want to include this base modal template into many different templates for many different actions.</p>

<p>The problem is that the main content of our modal changes for different use cases.</p>

<p>We could try to fix this problem by breaking up our &ldquo;include&rdquo; into two parts (a top and a bottom).</p>

<p>Here&rsquo;s the top include (<code>_modal_top.html</code>):</p>

<figure class='code'><div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class='line-number'>1</span>
<span class='line-number'>2</span>
<span class='line-number'>3</span>
<span class='line-number'>4</span>
<span class='line-number'>5</span>
<span class='line-number'>6</span>
<span class='line-number'>7</span>
<span class='line-number'>8</span>
<span class='line-number'>9</span>
<span class='line-number'>10</span>
<span class='line-number'>11</span>
<span class='line-number'>12</span>
<span class='line-number'>13</span>
<span class='line-number'>14</span>
<span class='line-number'>15</span>
<span class='line-number'>16</span>
<span class='line-number'>17</span>
<span class='line-number'>18</span>
<span class='line-number'>19</span>
<span class='line-number'>20</span>
<span class='line-number'>21</span>
<span class='line-number'>22</span>
<span class='line-number'>23</span>
<span class='line-number'>24</span>
<span class='line-number'>25</span>
<span class='line-number'>26</span>
<span class='line-number'>27</span>
<span class='line-number'>28</span>
<span class='line-number'>29</span>
<span class='line-number'>30</span>
<span class='line-number'>31</span>
<span class='line-number'>32</span>
<span class='line-number'>33</span>
</pre></td><td class='code'><pre><code class=''><span class='line'>&lt;div
</span><span class='line'>  x-data="{}"
</span><span class='line'>  id="base-modal"
</span><span class='line'>  ="$dispatch('close-modal')"
</span><span class='line'>  x-on:keydown.escape.prevent.stop="$dispatch('close-modal')"
</span><span class='line'>  x-on:close-modal.stop="$el.remove()"
</span><span class='line'>  role="dialog"
</span><span class='line'>  aria-modal="true"
</span><span class='line'>  x-id="['modal-title']"
</span><span class='line'>  :aria-labelledby="$id('modal-title')"
</span><span class='line'>  class="tw-fixed tw-inset-0 tw-z-10 tw-overflow-y-auto"
</span><span class='line'>  style="z-index: 2000;"
</span><span class='line'>&gt;
</span><span class='line'>  &lt;!-- Overlay --&gt;
</span><span class='line'>  &lt;div x-transition.opacity class="tw-fixed tw-inset-0 tw-bg-black tw-bg-opacity-50"&gt;&lt;/div&gt;
</span><span class='line'>
</span><span class='line'>  &lt;!-- Panel --&gt;
</span><span class='line'>  &lt;form
</span><span class='line'>    id="modal-panel"
</span><span class='line'>    hx-=""
</span><span class='line'>    hx-select="#modal-panel"
</span><span class='line'>    hx-swap="outerHTML"
</span><span class='line'>    x-transition
</span><span class='line'>    x-on:click="$dispatch('close-modal')"
</span><span class='line'>    class="tw-relative tw-flex tw-min-h-screen tw-items-center tw-justify-center tw-p-4"
</span><span class='line'>  &gt;
</span><span class='line'>    &lt;div
</span><span class='line'>        x-on:click.stop
</span><span class='line'>        x-trap.noscroll.inert="true"
</span><span class='line'>        class="tw-relative tw-w-full tw-max-w-lg tw-overflow-y-auto tw-rounded-xl tw-bg-white tw-p-6 tw-shadow-lg"
</span><span class='line'>    &gt;
</span><span class='line'>      &lt;!-- Title --&gt;
</span><span class='line'>      &lt;h5 h:id="$id('modal-title')"&gt;&lt;/h5&gt;</span></code></pre></td></tr></table></div></figure>


<p>And here&rsquo;s the bottom include (<code>_modal_bottom.html</code>):</p>

<figure class='code'><div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class='line-number'>1</span>
<span class='line-number'>2</span>
<span class='line-number'>3</span>
</pre></td><td class='code'><pre><code class=''><span class='line'>&lt;/div&gt;
</span><span class='line'>  &lt;/form&gt;
</span><span class='line'>&lt;/div&gt;</span></code></pre></td></tr></table></div></figure>


<p>This is how we might use these modals:</p>

<figure class='code'><div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class='line-number'>1</span>
<span class='line-number'>2</span>
<span class='line-number'>3</span>
<span class='line-number'>4</span>
<span class='line-number'>5</span>
<span class='line-number'>6</span>
<span class='line-number'>7</span>
<span class='line-number'>8</span>
<span class='line-number'>9</span>
<span class='line-number'>10</span>
<span class='line-number'>11</span>
<span class='line-number'>12</span>
<span class='line-number'>13</span>
<span class='line-number'>14</span>
</pre></td><td class='code'><pre><code class=''><span class='line'>{% url "api:delete" object.pk as delete_url %}
</span><span class='line'>{% include "_modal_top.html" title="Delete Object" close_event="@solution-deleted.window" htmx_method="delete" htmx_action=delete_url %}
</span><span class='line'>
</span><span class='line'>&lt;div class="tw-mt-7 tw-text-gray-600"&gt;
</span><span class='line'>  &lt;p&gt;Are you sure you want to delete &lt;strong&gt;{{ object }}&lt;/strong&gt;?&lt;/p&gt;
</span><span class='line'>&lt;/div&gt;
</span><span class='line'>
</span><span class='line'>&lt;div class="tw-mt-9 tw-flex tw-justify-end tw-space-x-2"&gt;
</span><span class='line'>  &lt;button class="btn btn-danger" type="submit"&gt;Delete&lt;/button&gt;
</span><span class='line'>  &lt;button class="btn btn-secondary" type="reset" x-on:click.prevent="$dispatch('close-modal')" data-dismiss="modal"&gt;Cancel&lt;/button&gt;
</span><span class='line'>&lt;/div&gt;
</span><span class='line'>
</span><span class='line'>{% include "_modal_bottom.html" %}
</span><span class='line'></span></code></pre></td></tr></table></div></figure>


<p>This is pretty awful.</p>

<p>What other solutions are there?</p>

<h2>One Solution: Just Copy-Paste</h2>

<p>Instead of messy with includes, we could just copy-paste the HTML we need every time we need it:</p>

<figure class='code'><div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class='line-number'>1</span>
<span class='line-number'>2</span>
<span class='line-number'>3</span>
<span class='line-number'>4</span>
<span class='line-number'>5</span>
<span class='line-number'>6</span>
<span class='line-number'>7</span>
<span class='line-number'>8</span>
<span class='line-number'>9</span>
<span class='line-number'>10</span>
<span class='line-number'>11</span>
<span class='line-number'>12</span>
<span class='line-number'>13</span>
<span class='line-number'>14</span>
<span class='line-number'>15</span>
<span class='line-number'>16</span>
<span class='line-number'>17</span>
<span class='line-number'>18</span>
<span class='line-number'>19</span>
<span class='line-number'>20</span>
<span class='line-number'>21</span>
<span class='line-number'>22</span>
<span class='line-number'>23</span>
<span class='line-number'>24</span>
<span class='line-number'>25</span>
<span class='line-number'>26</span>
<span class='line-number'>27</span>
<span class='line-number'>28</span>
<span class='line-number'>29</span>
<span class='line-number'>30</span>
<span class='line-number'>31</span>
<span class='line-number'>32</span>
<span class='line-number'>33</span>
<span class='line-number'>34</span>
<span class='line-number'>35</span>
<span class='line-number'>36</span>
<span class='line-number'>37</span>
<span class='line-number'>38</span>
<span class='line-number'>39</span>
<span class='line-number'>40</span>
<span class='line-number'>41</span>
<span class='line-number'>42</span>
<span class='line-number'>43</span>
<span class='line-number'>44</span>
<span class='line-number'>45</span>
<span class='line-number'>46</span>
<span class='line-number'>47</span>
</pre></td><td class='code'><pre><code class=''><span class='line'>&lt;div
</span><span class='line'>  x-data="{}"
</span><span class='line'>  id="base-modal"
</span><span class='line'>  @solution-deleted.window="$dispatch('close-modal')"
</span><span class='line'>  x-on:keydown.escape.prevent.stop="$dispatch('close-modal')"
</span><span class='line'>  x-on:close-modal.stop="$el.remove()"
</span><span class='line'>  role="dialog"
</span><span class='line'>  aria-modal="true"
</span><span class='line'>  x-id="['modal-title']"
</span><span class='line'>  :aria-labelledby="$id('modal-title')"
</span><span class='line'>  class="tw-fixed tw-inset-0 tw-z-10 tw-overflow-y-auto"
</span><span class='line'>  style="z-index: 2000;"
</span><span class='line'>&gt;
</span><span class='line'>  &lt;!-- Overlay --&gt;
</span><span class='line'>  &lt;div x-transition.opacity class="tw-fixed tw-inset-0 tw-bg-black tw-bg-opacity-50"&gt;&lt;/div&gt;
</span><span class='line'>
</span><span class='line'>  &lt;!-- Panel --&gt;
</span><span class='line'>  &lt;form
</span><span class='line'>    id="modal-panel"
</span><span class='line'>    hx-delete="{% url "api:delete" object.pk %}"
</span><span class='line'>    hx-select="#modal-panel"
</span><span class='line'>    hx-swap="outerHTML"
</span><span class='line'>    x-transition
</span><span class='line'>    x-on:click="$dispatch('close-modal')"
</span><span class='line'>    class="tw-relative tw-flex tw-min-h-screen tw-items-center tw-justify-center tw-p-4"
</span><span class='line'>  &gt;
</span><span class='line'>    &lt;div
</span><span class='line'>        x-on:click.stop
</span><span class='line'>        x-trap.noscroll.inert="true"
</span><span class='line'>        class="tw-relative tw-w-full tw-max-w-lg tw-overflow-y-auto tw-rounded-xl tw-bg-white tw-p-6 tw-shadow-lg"
</span><span class='line'>    &gt;
</span><span class='line'>      &lt;!-- Title --&gt;
</span><span class='line'>      &lt;h5 h:id="$id('modal-title')"&gt;Delete Object&lt;/h5&gt;
</span><span class='line'>
</span><span class='line'>      &lt;!-- Content --&gt;
</span><span class='line'>      &lt;div class="tw-mt-7 tw-text-gray-600"&gt;
</span><span class='line'>        &lt;p&gt;Are you sure you want to delete &lt;strong&gt;{{ object }}&lt;/strong&gt;?&lt;/p&gt;
</span><span class='line'>      &lt;/div&gt;
</span><span class='line'>
</span><span class='line'>      &lt;!-- Buttons --&gt;
</span><span class='line'>      &lt;div class="tw-mt-9 tw-flex tw-justify-end tw-space-x-2"&gt;
</span><span class='line'>        &lt;button class="btn btn-danger" type="submit"&gt;Delete&lt;/button&gt;
</span><span class='line'>        &lt;button class="btn btn-secondary" type="reset" x-on:click.prevent="$dispatch('close-modal')" data-dismiss="modal"&gt;Cancel&lt;/button&gt;
</span><span class='line'>      &lt;/div&gt;
</span><span class='line'>    &lt;/div&gt;
</span><span class='line'>  &lt;/form&gt;
</span><span class='line'>&lt;/div&gt;</span></code></pre></td></tr></table></div></figure>


<p>Honestly, I think this solution isn&rsquo;t a bad one.
Yes it is repetitive, but it&rsquo;s <em>so</em> much easier to understand and maintain this big block of fairly straightforward HTML.</p>

<p>The biggest downside to this approach is that enhancements made to one of the styling and features of these various copy-pasted modals will likely diverge over time if we&rsquo;re not careful to update all of them whenever we update one of them.</p>

<h2>A Better Solution: Components</h2>

<p>If I was using a component-based front-end web framework, I might be tempted to push all this logic into that front-end framework.
But I&rsquo;m not using a component-based front-end front-end web framework <em>and</em> I don&rsquo;t want to be forced to push any component-ish logic into the front-end.</p>

<p>Fortunately, in 2025, Django has <a href="https://djangopackages.org/grids/g/components/">a number of component frameworks</a>.</p>

<p>If we setup <a href="https://django-cotton.com">django-cotton</a>, we could make a <code>cotton/modal.html</code> file like this:</p>

<figure class='code'><div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class='line-number'>1</span>
<span class='line-number'>2</span>
<span class='line-number'>3</span>
<span class='line-number'>4</span>
<span class='line-number'>5</span>
<span class='line-number'>6</span>
<span class='line-number'>7</span>
<span class='line-number'>8</span>
<span class='line-number'>9</span>
<span class='line-number'>10</span>
<span class='line-number'>11</span>
<span class='line-number'>12</span>
<span class='line-number'>13</span>
<span class='line-number'>14</span>
<span class='line-number'>15</span>
<span class='line-number'>16</span>
<span class='line-number'>17</span>
<span class='line-number'>18</span>
<span class='line-number'>19</span>
<span class='line-number'>20</span>
<span class='line-number'>21</span>
<span class='line-number'>22</span>
<span class='line-number'>23</span>
<span class='line-number'>24</span>
<span class='line-number'>25</span>
<span class='line-number'>26</span>
<span class='line-number'>27</span>
<span class='line-number'>28</span>
<span class='line-number'>29</span>
<span class='line-number'>30</span>
<span class='line-number'>31</span>
<span class='line-number'>32</span>
<span class='line-number'>33</span>
<span class='line-number'>34</span>
<span class='line-number'>35</span>
<span class='line-number'>36</span>
<span class='line-number'>37</span>
<span class='line-number'>38</span>
<span class='line-number'>39</span>
<span class='line-number'>40</span>
<span class='line-number'>41</span>
<span class='line-number'>42</span>
<span class='line-number'>43</span>
<span class='line-number'>44</span>
<span class='line-number'>45</span>
<span class='line-number'>46</span>
<span class='line-number'>47</span>
</pre></td><td class='code'><pre><code class=''><span class='line'>&lt;div
</span><span class='line'>  x-data="{}"
</span><span class='line'>  id="base-modal"
</span><span class='line'>  {{ close_event }}="$dispatch('close-modal')"
</span><span class='line'>  x-on:keydown.escape.prevent.stop="$dispatch('close-modal')"
</span><span class='line'>  x-on:close-modal.stop="$el.remove()"
</span><span class='line'>  role="dialog"
</span><span class='line'>  aria-modal="true"
</span><span class='line'>  x-id="['modal-title']"
</span><span class='line'>  :aria-labelledby="$id('modal-title')"
</span><span class='line'>  class="tw-fixed tw-inset-0 tw-z-10 tw-overflow-y-auto"
</span><span class='line'>  style="z-index: 2000;"
</span><span class='line'>&gt;
</span><span class='line'>  &lt;!-- Overlay --&gt;
</span><span class='line'>  &lt;div x-transition.opacity class="tw-fixed tw-inset-0 tw-bg-black tw-bg-opacity-50"&gt;&lt;/div&gt;
</span><span class='line'>
</span><span class='line'>  &lt;!-- Panel --&gt;
</span><span class='line'>  &lt;form
</span><span class='line'>    id="modal-panel"
</span><span class='line'>    hx-{{ htmx_method }}="{{ htmx_action }}"
</span><span class='line'>    hx-select="#modal-panel"
</span><span class='line'>    hx-swap="outerHTML"
</span><span class='line'>    x-transition
</span><span class='line'>    x-on:click="$dispatch('close-modal')"
</span><span class='line'>    class="tw-relative tw-flex tw-min-h-screen tw-items-center tw-justify-center tw-p-4"
</span><span class='line'>  &gt;
</span><span class='line'>    &lt;div
</span><span class='line'>        x-on:click.stop
</span><span class='line'>        x-trap.noscroll.inert="true"
</span><span class='line'>        class="tw-relative tw-w-full tw-max-w-lg tw-overflow-y-auto tw-rounded-xl tw-bg-white tw-p-6 tw-shadow-lg"
</span><span class='line'>    &gt;
</span><span class='line'>      &lt;!-- Title --&gt;
</span><span class='line'>      &lt;h5 h:id="$id('modal-title')"&gt;{{ title }}&lt;/h5&gt;
</span><span class='line'>
</span><span class='line'>      &lt;!-- Content --&gt;
</span><span class='line'>      &lt;div class="tw-mt-7 tw-text-gray-600"&gt;
</span><span class='line'>        {{ slot }}
</span><span class='line'>      &lt;/div&gt;
</span><span class='line'>
</span><span class='line'>      &lt;!-- Buttons --&gt;
</span><span class='line'>      &lt;div class="tw-mt-9 tw-flex tw-justify-end tw-space-x-2"&gt;
</span><span class='line'>        {{ buttons }}
</span><span class='line'>      &lt;/div&gt;
</span><span class='line'>
</span><span class='line'>    &lt;/div&gt;
</span><span class='line'>  &lt;/form&gt;
</span><span class='line'>&lt;/div&gt;</span></code></pre></td></tr></table></div></figure>


<p>We can then use our modal component like this:</p>

<figure class='code'><div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class='line-number'>1</span>
<span class='line-number'>2</span>
<span class='line-number'>3</span>
<span class='line-number'>4</span>
<span class='line-number'>5</span>
<span class='line-number'>6</span>
<span class='line-number'>7</span>
<span class='line-number'>8</span>
<span class='line-number'>9</span>
<span class='line-number'>10</span>
<span class='line-number'>11</span>
<span class='line-number'>12</span>
<span class='line-number'>13</span>
<span class='line-number'>14</span>
<span class='line-number'>15</span>
</pre></td><td class='code'><pre><code class=''><span class='line'>&lt;c-modal
</span><span class='line'>  title="Delete Object"
</span><span class='line'>  close_event="@solution-deleted.window"
</span><span class='line'>  htmx_method="delete"
</span><span class='line'>  htmx_action={% url "api:delete" object.pk %}
</span><span class='line'>&gt;
</span><span class='line'>  &lt;div class="tw-mt-7 tw-text-gray-600"&gt;
</span><span class='line'>    &lt;p&gt;Are you sure you want to delete &lt;strong&gt;{{ object }}&lt;/strong&gt;?&lt;/p&gt;
</span><span class='line'>  &lt;/div&gt;
</span><span class='line'>
</span><span class='line'>  &lt;c-slot name="buttons"&gt;
</span><span class='line'>    &lt;button class="btn btn-danger" type="submit"&gt;Delete&lt;/button&gt;
</span><span class='line'>    &lt;button class="btn btn-secondary" type="reset" x-on:click.prevent="$dispatch('close-modal')" data-dismiss="modal"&gt;Cancel&lt;/button&gt;
</span><span class='line'>  &lt;/c-slot&gt;
</span><span class='line'>&lt;/c-modal&gt;</span></code></pre></td></tr></table></div></figure>


<p>I find this a <em>lot</em> easier to read than the <code>include</code> approach and a lot easier to maintain than the copy-pasted approach.</p>

<h2>The Downsides: Too Much Magic</h2>

<p>The biggest downside I see to <a href="https://django-cotton.com">django-cotton</a> is that it&rsquo;s a bit magical.</p>

<p>If you see <code>&lt;c-some-name&gt;</code> in a template, you need to know that this includes things from <code>cotton/some_name.html</code>.</p>

<p>There are lots of <a href="https://en.wikipedia.org/wiki/Action_at_a_distance">action at a distance</a> issues that come up with Django, which can make it feel a bit magical but which are nonetheless worthwhile tradeoffs.
But this one also doesn&rsquo;t look like a Django template tag, filter, or variable.
That feels <em>very</em> magical to me.</p>

<p>I&rsquo;ve been enjoying trying out django-cotton over the past week and enjoying it.</p>

<p>Here are 2 other Django component libraries I have considered trying:</p>

<ul>
<li><a href="https://github.com/joshuadavidthomas/django-bird">django-bird</a></li>
<li><a href="https://github.com/mixxorz/slippers">slippers</a></li>
<li><a href="https://github.com/SmileyChris/django-includecontents">django-includecontents</a></li>
</ul>


<p>I doubt I will try these 3, as they require writing Python code for each new component, which I would rather avoid:</p>

<ul>
<li><a href="https://github.com/django-components/django-components">django-components</a></li>
<li><a href="https://github.com/Xzya/django-web-components">django-web-components</a></li>
<li><a href="https://github.com/rails-inspire-django/django-viewcomponent">django-viewcomponent</a></li>
</ul>


<p>All Django component libraries (except for django-cotton) disallow line breaks between passed-in attributes due to a limitation of Django&rsquo;s template tags (see below).</p>

<h2>The Future: Multi-line Django Template Tags?</h2>

<p>If Django&rsquo;s template tags could be wrapped over multiple lines, we could create a library that worked like this:</p>

<figure class='code'><div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class='line-number'>1</span>
<span class='line-number'>2</span>
<span class='line-number'>3</span>
<span class='line-number'>4</span>
<span class='line-number'>5</span>
<span class='line-number'>6</span>
<span class='line-number'>7</span>
<span class='line-number'>8</span>
<span class='line-number'>9</span>
<span class='line-number'>10</span>
<span class='line-number'>11</span>
<span class='line-number'>12</span>
<span class='line-number'>13</span>
<span class='line-number'>14</span>
<span class='line-number'>15</span>
<span class='line-number'>16</span>
</pre></td><td class='code'><pre><code class=''><span class='line'>{% url "api:delete" object.pk as delete_url %}
</span><span class='line'>{% component "_modal.html"
</span><span class='line'>  title="Delete Object"
</span><span class='line'>  close_event="@solution-deleted.window"
</span><span class='line'>  htmx_method="delete"
</span><span class='line'>  htmx_action=delete_ul
</span><span class='line'>%}
</span><span class='line'>  &lt;div class="tw-mt-7 tw-text-gray-600"&gt;
</span><span class='line'>    &lt;p&gt;Are you sure you want to delete &lt;strong&gt;{{ object }}&lt;/strong&gt;?&lt;/p&gt;
</span><span class='line'>  &lt;/div&gt;
</span><span class='line'>
</span><span class='line'>  {% slot "buttons" %}
</span><span class='line'>    &lt;button class="btn btn-danger" type="submit"&gt;Delete&lt;/button&gt;
</span><span class='line'>    &lt;button class="btn btn-secondary" type="reset" x-on:click.prevent="$dispatch('close-modal')" data-dismiss="modal"&gt;Cancel&lt;/button&gt;
</span><span class='line'>  {% endslot %}
</span><span class='line'>{% endcomponent %}</span></code></pre></td></tr></table></div></figure>


<p>But that first multi-line <code>{% component %}</code> tag is a big problem.
This is invalid in Django&rsquo;s template language because tags cannot have linebreaks within them (see <a href="https://code.djangoproject.com/ticket/8652">this old ticket</a>, <a href="https://forum.djangoproject.com/t/allow-newlines-inside-tags/36040">this discussion</a>, and <a href="https://code.djangoproject.com/ticket/35899">this new ticket</a>):</p>

<figure class='code'><div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class='line-number'>1</span>
<span class='line-number'>2</span>
<span class='line-number'>3</span>
<span class='line-number'>4</span>
<span class='line-number'>5</span>
<span class='line-number'>6</span>
</pre></td><td class='code'><pre><code class=''><span class='line'>{% component "_modal.html"
</span><span class='line'>  title="Delete Object"
</span><span class='line'>  close_event="@solution-deleted.window"
</span><span class='line'>  htmx_method="delete"
</span><span class='line'>  htmx_action=delete_ul
</span><span class='line'>%}</span></code></pre></td></tr></table></div></figure>


<p>Until Django&rsquo;s template language allows tags to span over multiple lines, we&rsquo;re stuck with hacks like the ones that django-cotton uses.</p>
]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[My default apps of 2024]]></title>
    <link href="https://treyhunner.com/2025/01/my-default-apps-of-2024/"/>
    <updated>2025-01-22T14:30:00-08:00</updated>
    <id>https://treyhunner.com/2025/01/my-default-apps-of-2024</id>
    <content type="html"><![CDATA[<p>Here are my default apps of 2024&hellip; in 2025.</p>

<p>Inspired by <a href="https://micro.webology.dev/2024/12/19/default-apps.html">Jeff&rsquo;s list</a>.</p>

<p>You can <a href="https://raindrop.io/treyhunner/referral-links-47224019">find my referral links here</a> for Libro, YNAB, or SavvyCal.
I&rsquo;d love a free audiobook if you end up switching from Audible to Libro.fm. 💗</p>

<ul>
<li>🌐 Browser: Vivaldi</li>
<li>🔍 Search: Kagi (sometimes free Perplexity)</li>
<li>🤖 LLM: Claude</li>
<li>📝 Notes: <a href="https://obsidian.md">Obsidian</a> + Vim</li>
<li>✅ To-Do: Obsidian</li>
<li>✍ Writing: Obsidian + Vim</li>
<li>🧑‍💻 Code Editor: Vim</li>
<li>📼 Screencasting: OBS + Kdenlive + <a href="https://github.com/treyhunner/dotfiles/blob/0ab0a3d2df45940e38aad5729f5cdc1c72932226/bin/caption">Whisper API</a> + <a href="https://github.com/treyhunner/dotfiles/blob/0ab0a3d2df45940e38aad5729f5cdc1c72932226/bin/normalize">Custom Python scripts</a></li>
<li>📚 Audiobooks: Libby (💖), Spotify (meh), Libro (💖), Audible (yuck)</li>
<li>🎤 Podcasts: Pocket Casts, though I wish I had a better alternative</li>
<li>🎵 Music: Spotify</li>
<li>📁 Cloud File Storage: a mix of Git, Dropbox, Google Drive</li>
<li>📜 Word Processing: Google Drive</li>
<li>📈 Spreadsheets: Google Drive, Airtable</li>
<li>💰 Budgeting and Personal Finance: YNAB</li>
<li>💬 Chat: Signal, Discord, Slack</li>
<li>📆 Scheduling + Booking: SavvyCal</li>
<li>📆 Calendar: Google Calendar</li>
<li>📹 Video Calls: Google Meet + Zoom</li>
<li>🔐 Password Management: <a href="https://bitwarden.com">Bitwarden</a></li>
<li>🔏 Multi-Factor Auth: <a href="https://play.google.com/store/apps/details?id=com.beemdevelopment.aegis">Aegis</a> (Android) + <a href="https://flathub.org/apps/com.belmoussaoui.Authenticator">Authenticator</a> (laptop)</li>
<li>🐚 Terminal: Gnome Terminal + Tmux + Tmuxstart + Starship + Direnv</li>
<li>🗃️ Version Control: Git</li>
<li>🖥️ DNS: Cloudflare</li>
<li>🔖 Bookmarks: <a href="https://raindrop.io">Raindrop.io</a></li>
<li>📮 Email: Gmail</li>
<li>🎒 Backups: Manual Calendar Event + Git</li>
</ul>

]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[My favorite audiobooks of 2024 (and also 2017 through 2023)]]></title>
    <link href="https://treyhunner.com/2024/12/my-favorite-audiobooks-of-2024/"/>
    <updated>2024-12-31T08:30:00-08:00</updated>
    <id>https://treyhunner.com/2024/12/my-favorite-audiobooks-of-2024</id>
    <content type="html"><![CDATA[<p>I listen to <em>many</em> audiobooks every year.
I wrote recaps of my favorites in <a href="https://treyhunner.com/2014/12/top-6-books-of-2014/">2014</a>, <a href="https://treyhunner.com/2015/12/my-favorite-audiobooks-of-2015/">2015</a>, and <a href="https://treyhunner.com/2017/01/my-favorite-audiobooks-of-2016/">2016</a> and then I stopped doing annual recaps.</p>

<p>After a 7 year hiatus, I&rsquo;m attempting to start this annual habit again, starting with audiobooks I read in 2024.
But first, I&rsquo;ll reflect on some of the books that have stuck with me from 2017 through 2023.</p>

<h2>Books that have stuck with me from 2017 to 2023</h2>

<p>Of the 206 books I read over the 7 years of 2017 through 2023, about 35 books really stuck with me and I would recommend reading all of them.</p>

<p>Each section in ordered roughly by how much I would generally recommend the book (the first book in each section I usually recommend more than the last).
I enjoyed all these books, but I didn&rsquo;t write a review for all of them.
I&rsquo;ve included links when I did write a review.</p>

<p>Really enjoyable Sci-Fi (and a bit of Fantasy) that&rsquo;s stuck with me:</p>

<ul>
<li><strong><a href="https://www.goodreads.com/review/show/4282500474">Dawn</a></strong> (and also <a href="https://www.goodreads.com/review/show/4940269595">Adulthood Rites</a>) by Octavia Butler</li>
<li><strong><a href="https://www.goodreads.com/review/show/5206254350">The Long Way to a Small, Angry Planet</a></strong> (&amp; <a href="https://www.goodreads.com/review/show/5309212833">A Closed and Common Orbit</a>) by Becky Chambers</li>
<li><strong>The Fifth Season</strong> (and the other 2 books in the series) by N.K. Jemisin</li>
<li><strong><a href="https://www.goodreads.com/review/show/5851342829">Hive Minds Give Good Hugs</a></strong> by Natalie Maher</li>
<li><strong>Parable of the Sower</strong> (and <a href="https://www.goodreads.com/review/show/4024406874">Parable of the Talents</a>) by Octavia Butler</li>
<li><strong>A Song for a New Day</strong> by Sarah Pinsker</li>
<li><strong>The City We Became</strong> by N.K. Jemisin</li>
</ul>


<p>Books on human progress (to extinguish your cynicism):</p>

<ul>
<li><strong>Enlightenment Now</strong> by Steven Pinker</li>
<li><strong><a href="https://www.goodreads.com/review/show/3578812646">Factfulness</a></strong> by Hans Rosling</li>
<li><strong><a href="https://www.goodreads.com/review/show/4173349209">The Fabric of Civilization: How Textiles Made the World</a></strong> by Virginia Postrel</li>
</ul>


<p>The book that has most shaped my charitable giving habits:</p>

<ul>
<li><strong><a href="https://www.goodreads.com/review/show/3194541050">The Life You Can Save</a></strong> by Peter Singer (<a href="https://www.thelifeyoucansave.org/booktopia/">free here</a>)</li>
</ul>


<p>My favorite self-improvement books:</p>

<ul>
<li><strong><a href="https://www.goodreads.com/review/show/3954320132">Mind Management, Not Time Management</a></strong> by David Kadavy</li>
<li><strong><a href="https://www.goodreads.com/review/show/3904322364">The Scout Mindset</a></strong> by Julia Galef</li>
<li><strong><a href="https://www.goodreads.com/review/show/2739759842">Deep Work</a></strong> by Cal Newport</li>
<li><strong><a href="https://www.goodreads.com/review/show/3103304459">The Joy of Movement</a></strong> by Kelly McGonigal</li>
<li><strong><a href="https://www.goodreads.com/review/show/5863937406">Learn Like a Pro</a></strong> by Barbara Oakley, Olav Schewe</li>
<li><strong>Atomic Habits</strong> by James Clear</li>
<li><strong><a href="https://www.goodreads.com/review/show/3175104288">Good Habits, Bad Habits</a></strong> by Wendy Wood</li>
</ul>


<p>Books on economics, government, and public policy that aren&rsquo;t about housing and city planning:</p>

<ul>
<li><strong><a href="https://www.goodreads.com/review/show/4698620684">Basic Economics: A Citizen&rsquo;s Guide to the Economy</a></strong> by Thomas Sowell</li>
<li><strong><a href="https://www.goodreads.com/review/show/5665700932">Recoding America</a></strong> by Jennifer Pahlka</li>
<li><strong><a href="https://www.goodreads.com/review/show/4022258349">Drug Use for Grown-Ups: Chasing Liberty in the Land of Fear</a></strong> by Carl L. Hart</li>
<li><strong>How Rights Went Wrong</strong> by Jamal Greene</li>
</ul>


<p>Books on housing and city planning that have shaped my YIMBY-ish views:</p>

<ul>
<li><strong><a href="https://www.goodreads.com/review/show/4296215248">Happy City</a></strong> by Charles Montgomery</li>
<li><strong><a href="https://www.goodreads.com/review/show/5559905236">Arbitrary Lines: How Zoning Broke the American City and How to</a></strong> by M. Nolan Gray</li>
<li><strong><a href="https://www.goodreads.com/review/show/4296213335">Walkable City: How Downtown Can Save America, One Step at a Time</a></strong> by Jeff Speck</li>
<li><strong><a href="https://www.goodreads.com/review/show/5716387928">Paved Paradise: How Parking Explains the World</a></strong> by Henry Grabar</li>
</ul>


<p>Books on xenophobia and other-ing that have particularly stuck with me:</p>

<ul>
<li><strong>Caste</strong> by Isabel Wilkerson</li>
<li><strong>The Sum of Us: What Racism Costs Everyone and How We Can Prosper Together</strong> by Heather McGhee</li>
</ul>


<p>Books that have impacted my <em>very gradual</em> journey toward veganism:</p>

<ul>
<li><strong><a href="https://www.goodreads.com/review/show/5908347928">Why We Love Dogs, Eat Pigs, and Wear Cows</a></strong> by Melanie Joy</li>
<li><strong><a href="https://www.goodreads.com/review/show/3597337038">The End of Animal Farming</a></strong> by Jacy Reese Anthis</li>
</ul>


<p>TV shows I&rsquo;ve (so far) liked more than the books they&rsquo;re based on:</p>

<ul>
<li>Silo (Apple TV)</li>
<li>The Power (Amazon Prime)</li>
<li>The Three-Body Problem (Netflix, though Amazon Prime has a much slower Chinese version)</li>
</ul>


<p>Note that the above books do not include books I listened to in 2014, 2015, and 2016.
See <a href="https://treyhunner.com/blog/categories/audiobooks/">my other audiobook posts here</a>.
Of those 3 years of books, the ones that I&rsquo;d recommend most are <strong>Success and Luck</strong> by Robert H. Frank (<a href="https://treyhunner.com/2017/01/my-favorite-audiobooks-of-2016/">2016</a>), <strong>Just Mercy</strong> by Bryan Stevenson (<a href="https://treyhunner.com/2014/12/top-6-books-of-2014/">2014</a>), <strong>Whistling Vivaldi</strong> by Claude M. Steele (<a href="https://treyhunner.com/2015/12/my-favorite-audiobooks-of-2015/">2015</a>), and <strong>Make it Stick</strong> by Peter C. Brown, Henry L. Roediger III, Mark A. McDaniel (<a href="https://treyhunner.com/2017/01/my-favorite-audiobooks-of-2016/">2016</a>).</p>

<p>Also note that all the links above point to Goodreads, which I don&rsquo;t recommend despite the fact that I use it.
I plan to eventually switch to <a href="https://thestorygraph.com">The Story Graph</a> for recording my reading activity, but I&rsquo;m awaiting <a href="https://roadmap.thestorygraph.com/features/posts/an-api">an API</a> so I can update my current reading-tracking system (which is based around a Google Sheet totaling hours read and other stats) to use it instead of Goodreads.</p>

<h2>My favorite audiobooks of 2024</h2>

<p>I read 41 audiobooks this year.</p>

<p>These are the ones I would most recommend listening to.</p>

<ul>
<li><strong>Best self-help book</strong>:

<ul>
<li>Eat, Drink, and Be Healthy: The Harvard Medical School Guide to Healthy Eating</li>
</ul>
</li>
<li><strong>Best page-turners</strong>:

<ul>
<li>Says Who? A Kinder, Funner Usage Guide for Everyone Who Cares About Words</li>
<li>A Little Devil in America: Notes in Praise of Black Performance</li>
</ul>
</li>
<li><strong>Most thought-provoking</strong>:

<ul>
<li>Land is a Big Deal: Why rent is too high, wages too low, and what we can do about it</li>
<li>The Myth of Left and Right: How the Political Spectrum Misleads and Harms America</li>
<li>Uncommon Sense Teaching: Practical Insights in Brain Science to Help Students Learn</li>
</ul>
</li>
<li><strong>Validated my current world view and I recommend read to challenge themselves</strong>:

<ul>
<li>Not the End of the World: How We Can Be the First Generation to Build a Sustainable Planet</li>
<li>Determined: A Science of Life without Free Will</li>
<li>Eating Animals</li>
</ul>
</li>
</ul>


<h3>Eat, Drink, and Be Healthy: The Harvard Medical School Guide to Healthy Eating</h3>

<p>This is the best book I&rsquo;ve read about nutrition.
This book contains no silver bullets; just evidence-backed advice.</p>

<p>If you&rsquo;re reading this in audiobook-form, note that the last portion of the book contains recipes, read out-loud for many minutes&hellip; so I would stop listening once the recipes start.
I&rsquo;m considering buying a hard copy for some of the recipes.</p>

<h3>Says Who? A Kinder, Funner Usage Guide for Everyone Who Cares About Words</h3>

<p>This book pleased my inner wordy and challenged my inner grammando (those terms are explained in the book).</p>

<p>If you enjoy poking at the English language, I think you&rsquo;ll find this a fun listen.</p>

<h3>A Little Devil in America: Notes in Praise of Black Performance</h3>

<p>This is a beautifully written book.</p>

<p>If you enjoy writing that&rsquo;s thought-provoking, emotional, and engrossing, read/listen to this.</p>

<h3>Land is a Big Deal: Why rent is too high, wages too low, and what we can do about it</h3>

<p>This book is all about implementing a &ldquo;land value tax&rdquo; and it does a good job of explaining what that means and what the consequences might be.
A land value tax feels like an extreme marriage of capitalism and socialism, as it would (sort of) abolish private ownership of land while encouraging the market to make the best use of each piece of land.
The subject of this book is <em>very</em> wonky so this book was a bit challenging at times, but I found it fairly accessible overall.</p>

<p>For the sake of affordable housing, loosening or removing zoning restrictions (see the book Arbitrary Lines mentioned above) and reducing regulatory requirements around construction seem more important than a land value tax.
But as far as taxes go, a land value tax does seem like the most justice-oriented and efficiency-oriented tax.</p>

<h3>The Myth of Left and Right: How the Political Spectrum Misleads and Harms America</h3>

<p>I prefer a good short book to a good long book and this audiobook very much justifies the time it took to read (4 hours).</p>

<p>If you frequently talk about or think about US politics, I would recommend reading this book.
The authors make a solid case that we do a disservice to political discourse when we use the words &ldquo;left&rdquo;, &ldquo;right&rdquo;, &ldquo;liberal&rdquo;, and &ldquo;conservative&rdquo;.</p>

<h3>Uncommon Sense Teaching: Practical Insights in Brain Science to Help Students Learn</h3>

<p>Much of this book was review for me: namely interleaved versus blocked practice, active versus passive learning, elaboration, and spaced repetition.
Even the parts that were review were helpful to hear again.
I also took notes from this book about &ldquo;learn it, link it&rdquo;, dopamine hits, comprehension checks, and pauses to consolidate.</p>

<p>This book included quite a bit more discussion about how the brain actually works than I remember hearing in previous books I&rsquo;ve read on learning and teaching.
It could be that these sections were simply more memorable than previous explanations I&rsquo;ve heard, as the explanations in this book heavily relied on memorable analogies and stories.</p>

<h3>Not the End of the World: How We Can Be the First Generation to Build a Sustainable Planet</h3>

<p>This book discusses what we focus on too much and too little when it comes to the problems and the solutions around climate change.
Hannah Ritchie calls out falsehoods and misconceptions, but she doesn&rsquo;t berate believers of these misconceptions.</p>

<p>We all care about solving global warming and sustainability.
Knowing more of the facts behind these concepts is the first step to working solutions.</p>

<p>This book definitely falls into the &ldquo;skepticism over cynicism&rdquo; category that I very much appreciate, as it inspires action instead of of encouraging inaction.</p>

<p>If you enjoy a Scottish accent, listen to the audiobook (Hannah Ritchie self-narrates).</p>

<h3>Determined: A Science of Life without Free Will</h3>

<p>Before reading this book, I hadn&rsquo;t considered that free will comes in shades and that as a society we have been gradually chipping away at the magnitude of free will we collectively believe in.</p>

<p>The first 9 chapters (especially 5 through 9) are a bit of a slog, as each focuses on disputing a different argument for free will.
I found the final 5 chapters (which focus on &ldquo;what do we do if there&rsquo;s no free will&rdquo;) the most interesting.</p>

<h3>Eating Animals</h3>

<p>This book was memorable in a somewhat brutal way.
I do not recommend reading this book while eating animal products, but I would recommend reading it.</p>

<p>There&rsquo;s quite a bit of navel-gazing, but also quite a few interviews with many a number of folks in and around the animal farming industry.</p>

<p>For a more hopeful take on animal welfare, see the book The End of Animal Farming (mentioned above).
Also see the book Why We Love Dogs, Eat Pigs, and Wear Cows (also mentioned above)</p>

<h2>Where to buy audiobooks (not Audible)</h2>

<p>Interested in listening to these in audiobook form?</p>

<p>Don&rsquo;t buy them from Audible.</p>

<p>Whenever possible, I recommend avoiding Audible because:</p>

<ol>
<li>Audible doesn&rsquo;t sell audiobooks; they sell the ability to play audiobooks through their app</li>
<li>Audible credits expire and also disappear upon cancellation, which is an awful <a href="https://en.wikipedia.org/wiki/Dark_pattern">dark pattern</a></li>
</ol>


<p>Cory Doctorow has <a href="https://doctorow.medium.com/why-none-of-my-books-are-available-on-audible-83cb182f2f91">written</a> about his dislike of Audible and has recorded <a href="https://craphound.com/news/2022/07/24/why-none-of-my-books-are-available-on-audible/">an audiobook against Audible</a> (which is ironically <a href="https://www.audible.com/pd/Why-None-of-My-Books-Are-Available-on-Audible-Audiobook/B0B7KH8KSD">also on Audible</a>).</p>

<p>If you enjoy audiobooks and pro-consumer practices, I recommend trying out <a href="https://libro.fm/referral?rf_code=lfm240965">Libro.fm</a> (that&rsquo;s a referral link which will give me one free audiobook if you subscribe).</p>

<p>Unfortunately, a few of my favorite audiobooks noted above are <em>only</em> available on Audible.</p>

<p>You can purchase <em>all</em> of the above books on Libro.fm <em>except</em> for <strong>Hive Minds Give Good Hugs</strong>, <strong>Recoding America</strong>, and <strong>Walkable City</strong>.
The book <strong>Land is a Big Deal</strong> is <em>technically</em> available outside of Audible, but I&rsquo;ve only found it available for direct purchase <a href="https://www.shacksimplepress.com/product-page/copy-of-land-is-a-big-deal-audio-version">from the publisher</a> and it&rsquo;s much more expensive that way.</p>

<p>For <em>most</em> of my audiobook-listening, I subscribe to <strong>Libro.fm</strong>, checkout books from my local library with <strong>Libby</strong>, and I&rsquo;ve also used <strong>Spotify</strong> to listen to a few shorter books.
For Audible-only books, I sign up for a subscription, buy licenses until my credit balance is 0, and then cancel.</p>

<h2>Have a recommendation?</h2>

<p>Have a question?
Have an audiobook recommendation for me?</p>

<p>Comment below.</p>
]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Lazy self-installing Python scripts with uv]]></title>
    <link href="https://treyhunner.com/2024/12/lazy-self-installing-python-scripts-with-uv/"/>
    <updated>2024-12-09T11:15:10-08:00</updated>
    <id>https://treyhunner.com/2024/12/lazy-self-installing-python-scripts-with-uv</id>
    <content type="html"><![CDATA[<p>I frequently find myself writing my own short command-line scripts in Python that help me with day-to-day tasks.</p>

<p>It&rsquo;s <em>so</em> easy to throw together a single-file Python command-line script and throw it in my <code>~/bin</code> directory!</p>

<p>Well&hellip; it&rsquo;s easy, <em>unless</em> the script requires anything outside of the Python standard library.</p>

<p>Recently I&rsquo;ve started using uv and my <em>primary</em> for use for it has been fixing Python&rsquo;s &ldquo;just manage the dependencies automatically&rdquo; problem.</p>

<p>I&rsquo;ll share how I&rsquo;ve been using uv&hellip; first first let&rsquo;s look at the problem.</p>

<h2>A script without dependencies</h2>

<p>If I have a Python script that I want to be easily usable from anywhere on my system, I typically follow these steps:</p>

<ol>
<li>Add an appropriate shebang line above the first line in the file (e.g. <code>#!/usr/bin/env python3</code>)</li>
<li>Set an executable bit on the file (<code>chmod a+x my_script.py</code>)</li>
<li>Place the script in a directory that&rsquo;s in my shell&rsquo;s <code>PATH</code> variable (e.g. <code>cp my_script.py ~/bin/my_script</code>)</li>
</ol>


<p>For example, here&rsquo;s a script I use to print out 80 zeroes (or a specific number of zeroes) to check whether my terminal&rsquo;s font size is large enough when I&rsquo;m teaching:</p>

<figure class='code'><figcaption><span></span></figcaption><div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class='line-number'>1</span>
<span class='line-number'>2</span>
<span class='line-number'>3</span>
<span class='line-number'>4</span>
<span class='line-number'>5</span>
<span class='line-number'>6</span>
</pre></td><td class='code'><pre><code class='python'><span class='line'><span class="c">#!/usr/bin/env python3</span>
</span><span class='line'><span class="kn">import</span> <span class="nn">sys</span>
</span><span class='line'>
</span><span class='line'><span class="n">numbers</span> <span class="o">=</span> <span class="n">sys</span><span class="o">.</span><span class="n">argv</span><span class="p">[</span><span class="mi">1</span><span class="p">:]</span> <span class="ow">or</span> <span class="p">[</span><span class="mi">80</span><span class="p">]</span>
</span><span class='line'><span class="k">for</span> <span class="n">n</span> <span class="ow">in</span> <span class="n">numbers</span><span class="p">:</span>
</span><span class='line'>    <span class="k">print</span><span class="p">(</span><span class="s">&quot;0&quot;</span> <span class="o">*</span> <span class="nb">int</span><span class="p">(</span><span class="n">n</span><span class="p">))</span>
</span></code></pre></td></tr></table></div></figure>


<p>This file lives at <code>/home/trey/bin/0</code> so I can run the command <code>0</code> from my system prompt to see 80 <code>0</code> characters printed in my terminal.</p>

<p>This works great!
But this script doesn&rsquo;t have any dependencies.</p>

<h2>The problem: a script with dependencies</h2>

<p>Here&rsquo;s a Python script that normalizes the audio of a given video file and writes a new audio-normalized version of the video to a new file:</p>

<figure class='code'><figcaption><span></span></figcaption><div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class='line-number'>1</span>
<span class='line-number'>2</span>
<span class='line-number'>3</span>
<span class='line-number'>4</span>
<span class='line-number'>5</span>
<span class='line-number'>6</span>
<span class='line-number'>7</span>
<span class='line-number'>8</span>
<span class='line-number'>9</span>
<span class='line-number'>10</span>
<span class='line-number'>11</span>
<span class='line-number'>12</span>
<span class='line-number'>13</span>
<span class='line-number'>14</span>
<span class='line-number'>15</span>
<span class='line-number'>16</span>
<span class='line-number'>17</span>
<span class='line-number'>18</span>
<span class='line-number'>19</span>
<span class='line-number'>20</span>
<span class='line-number'>21</span>
<span class='line-number'>22</span>
<span class='line-number'>23</span>
<span class='line-number'>24</span>
</pre></td><td class='code'><pre><code class='python'><span class='line'><span class="sd">&quot;&quot;&quot;Normalize audio in input video file.&quot;&quot;&quot;</span>
</span><span class='line'><span class="kn">from</span> <span class="nn">argparse</span> <span class="kn">import</span> <span class="n">ArgumentParser</span>
</span><span class='line'><span class="kn">from</span> <span class="nn">pathlib</span> <span class="kn">import</span> <span class="n">Path</span>
</span><span class='line'>
</span><span class='line'><span class="kn">from</span> <span class="nn">ffmpeg_normalize</span> <span class="kn">import</span> <span class="n">FFmpegNormalize</span>
</span><span class='line'>
</span><span class='line'>
</span><span class='line'><span class="k">def</span> <span class="nf">normalize_audio_for</span><span class="p">(</span><span class="n">video_path</span><span class="p">,</span> <span class="n">audio_normalized_path</span><span class="p">):</span>
</span><span class='line'>    <span class="sd">&quot;&quot;&quot;Return audio-normalized video file saved in the given directory.&quot;&quot;&quot;</span>
</span><span class='line'>    <span class="n">ffmpeg_normalize</span> <span class="o">=</span> <span class="n">FFmpegNormalize</span><span class="p">(</span><span class="n">audio_codec</span><span class="o">=</span><span class="s">&quot;aac&quot;</span><span class="p">,</span> <span class="n">audio_bitrate</span><span class="o">=</span><span class="s">&quot;192k&quot;</span><span class="p">,</span> <span class="n">target_level</span><span class="o">=-</span><span class="mi">17</span><span class="p">)</span>
</span><span class='line'>    <span class="n">ffmpeg_normalize</span><span class="o">.</span><span class="n">add_media_file</span><span class="p">(</span><span class="nb">str</span><span class="p">(</span><span class="n">video_path</span><span class="p">),</span> <span class="n">audio_normalized_path</span><span class="p">)</span>
</span><span class='line'>    <span class="n">ffmpeg_normalize</span><span class="o">.</span><span class="n">run_normalization</span><span class="p">()</span>
</span><span class='line'>
</span><span class='line'>
</span><span class='line'><span class="k">def</span> <span class="nf">main</span><span class="p">():</span>
</span><span class='line'>    <span class="n">parser</span> <span class="o">=</span> <span class="n">ArgumentParser</span><span class="p">()</span>
</span><span class='line'>    <span class="n">parser</span><span class="o">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s">&quot;video_file&quot;</span><span class="p">,</span> <span class="nb">type</span><span class="o">=</span><span class="n">Path</span><span class="p">)</span>
</span><span class='line'>    <span class="n">parser</span><span class="o">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s">&quot;output_file&quot;</span><span class="p">,</span> <span class="nb">type</span><span class="o">=</span><span class="n">Path</span><span class="p">)</span>
</span><span class='line'>    <span class="n">args</span> <span class="o">=</span> <span class="n">parser</span><span class="o">.</span><span class="n">parse_args</span><span class="p">()</span>
</span><span class='line'>    <span class="n">normalize_audio_for</span><span class="p">(</span><span class="n">args</span><span class="o">.</span><span class="n">video_file</span><span class="p">,</span> <span class="n">args</span><span class="o">.</span><span class="n">output_file</span><span class="p">)</span>
</span><span class='line'>
</span><span class='line'>
</span><span class='line'><span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">&quot;__main__&quot;</span><span class="p">:</span>
</span><span class='line'>    <span class="n">main</span><span class="p">()</span>
</span></code></pre></td></tr></table></div></figure>


<p>This script depends on the <a href="https://github.com/slhck/ffmpeg-normalize">ffmpeg-normalize</a> Python package and the <a href="https://ffmpeg.org">ffmpeg</a> utility.
I already have <code>ffmpeg</code> installed, but I prefer <em>not</em> to globally install Python packages.
I install all Python packages within virtual environments and I install global Python scripts using <a href="https://pipx.pypa.io">pipx</a>.</p>

<p>At this point I <em>could</em> choose to either:</p>

<ol>
<li>Create a virtual environment, install <code>ffmpeg-normalize</code> in it, and put a shebang line referencing that virtual environment&rsquo;s Python binary at the top of my script file</li>
<li>Turn my script into a <code>pip</code>-installable Python package with a <code>pyproject.toml</code> that lists <code>ffmpeg-normalize</code> as a dependency and use <code>pipx</code> to install it</li>
</ol>


<p>That first solution requires me to keep track of virtual environments that exist for specific scripts to work.
That sounds painful.</p>

<p>The second solution involves making a Python package and then upgrading that Python package whenever I need to make a change to this script.
That&rsquo;s definitely going to be painful.</p>

<h2>The solution: let uv handle it</h2>

<p>A few months ago, my friend <a href="https://micro.webology.dev">Jeff Triplett</a> showed me that <code>uv</code> can work within a shebang line and can read a special comment at the top of a Python file that tells uv which Python version to run a script with and which dependencies it needs.</p>

<p>Here&rsquo;s a shebang line that would work for the above script:</p>

<figure class='code'><figcaption><span></span></figcaption><div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class='line-number'>1</span>
<span class='line-number'>2</span>
<span class='line-number'>3</span>
<span class='line-number'>4</span>
<span class='line-number'>5</span>
<span class='line-number'>6</span>
<span class='line-number'>7</span>
</pre></td><td class='code'><pre><code class='python'><span class='line'><span class="c">#!/usr/bin/env -S uv run --script</span>
</span><span class='line'><span class="c"># /// script</span>
</span><span class='line'><span class="c"># requires-python = &quot;&gt;=3.12&quot;</span>
</span><span class='line'><span class="c"># dependencies = [</span>
</span><span class='line'><span class="c">#     &quot;ffmpeg-normalize&quot;,</span>
</span><span class='line'><span class="c"># ]</span>
</span><span class='line'><span class="c"># ///</span>
</span></code></pre></td></tr></table></div></figure>


<p>That tells uv that this script should be run on Python 3.12 and that it depends on the <code>ffmpeg-normalize</code> package.</p>

<p>Neat&hellip; but what does that do?</p>

<p>Well, the first time this script is run, uv will create a virtual environment for it, install <code>ffmpeg-normalize</code> into that venv, and then run the script:</p>

<figure class='code'><figcaption><span></span></figcaption><div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class='line-number'>1</span>
<span class='line-number'>2</span>
<span class='line-number'>3</span>
<span class='line-number'>4</span>
<span class='line-number'>5</span>
</pre></td><td class='code'><pre><code class='bash'><span class='line'><span class="nv">$ </span>normalize
</span><span class='line'>Reading inline script metadata from <span class="sb">`</span>/home/trey/bin/normalize<span class="sb">`</span>
</span><span class='line'>Installed <span class="m">4</span> packages in 5ms
</span><span class='line'>usage: normalize <span class="o">[</span>-h<span class="o">]</span> video_file output_file
</span><span class='line'>normalize: error: the following arguments are required: video_file, output_file
</span></code></pre></td></tr></table></div></figure>


<p>Every time the script is run after that, uv finds and reuses the same virtual environment:</p>

<figure class='code'><figcaption><span></span></figcaption><div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class='line-number'>1</span>
<span class='line-number'>2</span>
<span class='line-number'>3</span>
<span class='line-number'>4</span>
</pre></td><td class='code'><pre><code class='bash'><span class='line'><span class="nv">$ </span>normalize
</span><span class='line'>Reading inline script metadata from <span class="sb">`</span>/home/trey/bin/normalize<span class="sb">`</span>
</span><span class='line'>usage: normalize <span class="o">[</span>-h<span class="o">]</span> video_file output_file
</span><span class='line'>normalize: error: the following arguments are required: video_file, output_file
</span></code></pre></td></tr></table></div></figure>


<p>Each time uv runs the script, it quickly checks that all listed dependencies are properly installed with their correct versions.</p>

<p>Another script I use this for is <a href="https://github.com/treyhunner/dotfiles/blob/main/bin/caption">caption</a>, which uses whisper (via the Open AI API) to quickly caption my screencasts just after I record and edit them.
The caption quality very rarely need more than a very minor edit or two (for my personal accent of English at least) even for technical like &ldquo;dunder method&rdquo; and via the API the captions generate very quickly.</p>

<p>See the <a href="https://packaging.python.org/en/latest/specifications/inline-script-metadata/">inline script metadata</a> page of the Python packaging users guide for more details on that format that uv is using (honestly I always just copy-paste an example myself).</p>

<h2>uv everywhere?</h2>

<p>I haven&rsquo;t yet fully embraced uv everywhere.</p>

<p>I don&rsquo;t manage my Python projects with uv, though I do use it to create new virtual environments (with <code>--seed</code> to ensure the <code>pip</code> command is available) as a <a href="https://treyhunner.com/2024/10/switching-from-virtualenvwrapper-to-direnv-starship-and-uv/">virtualenvwrapper replacement, along with direnv</a>.</p>

<p>I have also started using <a href="https://docs.astral.sh/uv/concepts/tools/">uv tool</a> as a <a href="https://pipx.pypa.io">pipx</a> replacement and I&rsquo;ve considered replacing <a href="https://pipx.pypa.io/stable/">pyenv</a> with uv.</p>

<h2>uv instead of pipx</h2>

<p>When I want to install a command-line tool that happens to be Python powered, I used to do this:</p>

<figure class='code'><figcaption><span></span></figcaption><div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class='line-number'>1</span>
</pre></td><td class='code'><pre><code class='bash'><span class='line'><span class="nv">$ </span>pipx countdown-cli
</span></code></pre></td></tr></table></div></figure>


<p>Now I do this instead:</p>

<figure class='code'><figcaption><span></span></figcaption><div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class='line-number'>1</span>
</pre></td><td class='code'><pre><code class='bash'><span class='line'><span class="nv">$ </span>uv tool install countdown-cli
</span></code></pre></td></tr></table></div></figure>


<p>Either way, I end up with a <code>countdown</code> script in my <code>PATH</code> that automatically uses its own separate virtual environment for its dependencies:</p>

<figure class='code'><figcaption><span></span></figcaption><div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class='line-number'>1</span>
<span class='line-number'>2</span>
<span class='line-number'>3</span>
<span class='line-number'>4</span>
<span class='line-number'>5</span>
<span class='line-number'>6</span>
<span class='line-number'>7</span>
<span class='line-number'>8</span>
<span class='line-number'>9</span>
<span class='line-number'>10</span>
<span class='line-number'>11</span>
<span class='line-number'>12</span>
<span class='line-number'>13</span>
<span class='line-number'>14</span>
<span class='line-number'>15</span>
<span class='line-number'>16</span>
</pre></td><td class='code'><pre><code class='bash'><span class='line'><span class="nv">$ </span>countdown --help
</span><span class='line'>Usage: countdown <span class="o">[</span>OPTIONS<span class="o">]</span> DURATION
</span><span class='line'>
</span><span class='line'>  Countdown from the given duration to 0.
</span><span class='line'>
</span><span class='line'>  DURATION should be a number followed by m or s <span class="k">for</span> minutes or seconds.
</span><span class='line'>
</span><span class='line'>  Examples of DURATION:
</span><span class='line'>
</span><span class='line'>  - 5m <span class="o">(</span><span class="m">5</span> minutes<span class="o">)</span>
</span><span class='line'>  - 45s <span class="o">(</span><span class="m">45</span> seconds<span class="o">)</span>
</span><span class='line'>  - 2m30s <span class="o">(</span><span class="m">2</span> minutes and <span class="m">30</span> seconds<span class="o">)</span>
</span><span class='line'>
</span><span class='line'>Options:
</span><span class='line'>  --version  Show the version and exit.
</span><span class='line'>  --help     Show this message and exit.
</span></code></pre></td></tr></table></div></figure>


<h2>uv instead of pyenv</h2>

<p>For years, I&rsquo;ve used pyenv to manage multiple versions of Python on my machine.</p>

<figure class='code'><figcaption><span></span></figcaption><div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class='line-number'>1</span>
</pre></td><td class='code'><pre><code class='bash'><span class='line'><span class="nv">$ </span>pyenv install 3.13.0
</span></code></pre></td></tr></table></div></figure>


<p>Now I could do this:</p>

<figure class='code'><figcaption><span></span></figcaption><div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class='line-number'>1</span>
</pre></td><td class='code'><pre><code class='bash'><span class='line'><span class="nv">$ </span>uv python install --preview 3.13.0
</span></code></pre></td></tr></table></div></figure>


<p>Or I could make a <code>~/.config/uv/uv.toml</code> file containing this:</p>

<figure class='code'><figcaption><span></span></figcaption><div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class='line-number'>1</span>
</pre></td><td class='code'><pre><code class='bash'><span class='line'><span class="nv">preview</span> <span class="o">=</span> <span class="nb">true</span>
</span></code></pre></td></tr></table></div></figure>


<p>And then run the same thing without the <code>--preview</code> flag:</p>

<figure class='code'><figcaption><span></span></figcaption><div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class='line-number'>1</span>
</pre></td><td class='code'><pre><code class='bash'><span class='line'><span class="nv">$ </span>uv python install 3.13.0
</span></code></pre></td></tr></table></div></figure>


<p>This puts a <code>python3.13</code> binary in my <code>~/.local/bin directory</code>, which is on my <code>PATH</code>.</p>

<p>Why &ldquo;preview&rdquo;?
Well, without it uv doesn&rsquo;t (<a href="https://github.com/astral-sh/uv/issues/6265#issuecomment-2461107903">yet</a>) place <code>python3.13</code> in my <code>PATH</code> by default, as this feature is currently in testing/development.</p>

<h2>Self-installing Python scripts are the big win</h2>

<p>I still prefer pyenv for its ability to <a href="https://treyhunner.com/2024/05/installing-a-custom-python-build-with-pyenv/">install custom Python builds</a> and I don&rsquo;t have a preference between <code>uv tool</code> and <code>pipx</code>.</p>

<p>The biggest win that I&rsquo;ve experienced from uv so far is the ability to run an executable script and have any necessary dependencies install automagically.</p>

<p>This doesn&rsquo;t mean that I <em>never</em> make Python package out of my Python scripts anymore&hellip; but I do so much more rarely.
I used to create a Python package out of a script as soon as it required third-party dependencies.
Now my &ldquo;do I <em>really</em> need to turn this into a proper package&rdquo; bar is set much higher.</p>
]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[New Python Jumpstart course]]></title>
    <link href="https://treyhunner.com/2024/11/new-python-jumpstart-course/"/>
    <updated>2024-11-25T08:08:08-08:00</updated>
    <id>https://treyhunner.com/2024/11/new-python-jumpstart-course</id>
    <content type="html"><![CDATA[<p>I&rsquo;ve just recently launched a self-paced introduction to Python that is <strong>extremely hands-on</strong>.
It&rsquo;s called <strong><a href="https://pym.dev/courses/jumpstart/overview">Python Jumpstart</a></strong> and it&rsquo;s based on introductory Python curriculum that I have been iterating on for years.</p>

<h2>Learn Python by writing Python code ✍</h2>

<p>We <em>do not</em> learn by putting information into the brain.
Our brains simply don&rsquo;t retain knowledge that way.</p>

<p>Learning happens from repeatedly attempting to retrieve information <em>from</em> the brain.
When it comes to Python, that means <strong>writing Python code</strong>.</p>

<p>So unlike most Python courses, Python Jumpstart is <em>not</em> focused on watching videos and rewriting code seen within videos.
Instead, this new Python course is based around <strong>learning Python by writing Python code</strong>.</p>

<h2>How Python Jumpstart is structured 🔬</h2>

<p>Python Jumpstart includes learning Python by solving <strong>46 short Python exercises</strong>.</p>

<p>Before each exercise, you&rsquo;ll watch a 5 minute video explaining a new Python topic.
You&rsquo;ll then attempt the exercise to put one or more Python topics into practice.</p>

<p>You won&rsquo;t solve many of the exercises on your first try and <em>that&rsquo;s okay</em>.
These exercises are designed to be revisited a few times over the course of weeks, until you&rsquo;re satisfied with your solution.</p>

<h2>This is not a sprint 📆</h2>

<p>This structured path to Python proficiency, includes:</p>

<ul>
<li>Carefully crafted exercises that build real understanding</li>
<li>Short, detailed explanations that <em>won&rsquo;t</em> waste your time</li>
<li>A proven teaching approach that focuses on active learning</li>
<li>Spaced repetition to help concepts stick</li>
</ul>


<p>This is not a course for impatient or passive learners.
Learning takes <strong>repetitive effort</strong> spaced over many days and this course is structured to embrace that fact.</p>

<p>If you spend <strong>30 minutes</strong> on Python Jumpstart each day, I estimate that you&rsquo;ll complete this course in <strong>about 7 weeks</strong>.
You&rsquo;ll spend the large majority of that time writing Python code.</p>

<p>This course <em>will</em> take time, but it will be time well-spent.</p>

<h2>Launch week special: 50% off until December 2 ⏰</h2>

<p>Through Monday December 2, 2024, you can get lifetime access to <a href="https://pym.dev/courses/jumpstart/overview">Python Jumpstart</a> for <strong>$99</strong>.
After this launch week, Python Jumpstart will be <strong>$199</strong>.</p>

<p>Ready to jumpstart your Python learning journey?</p>

<p><a href="https://pym.dev/courses/jumpstart/overview" class="subscribe-btn form-big">Get Python Jumpstart for $99</a></p>
]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Python Black Friday &amp; Cyber Monday sales (2024)]]></title>
    <link href="https://treyhunner.com/2024/11/python-black-friday-and-cyber-monday-sales-2024/"/>
    <updated>2024-11-20T11:00:00-08:00</updated>
    <id>https://treyhunner.com/2024/11/python-black-friday-and-cyber-monday-sales-2024</id>
    <content type="html"><![CDATA[<p>Ready for some Python skill-building sales?</p>

<p>This is my <strong><a href="https://treyhunner.com/blog/categories/sales/">seventh</a></strong> annual compilation of <strong>Python learning deals</strong>.</p>

<h2>Lots of Python sales</h2>

<p>Here are Python-related sales that are live right now:</p>

<ul>
<li><strong><a href="https://www.pythonmorsels.com/courses/jumpstart/overview/">Python Jumpstart</a></strong> with Python Morsels: <strong>50% off</strong> my brand new Python course, an introduction to Python that&rsquo;s <em>very</em> hands-on (<strong>$99</strong> instead of <strong>$199</strong>)</li>
<li><strong><a href="http://talkpython.fm/black-friday">Talk Python</a></strong>: the annual everything bundle includes all courses for $240</li>
<li><strong><a href="https://courses.dataschool.io/black-friday">Data School</a></strong> 40% off all Kevin&rsquo;s courses or get a bundle with all 5 of his courses</li>
<li><strong><a href="https://store.metasnake.com/?coupon=EMAIL40">Matt Harrison</a></strong>: get 25% off with GIVING25 plus every purchase will open a product for a scholarship recipient</li>
<li><strong><a href="https://lernerpython.com/">Reuven Lerner</a></strong>: get 20% off <a href="https://www.bambooweekly.com/bf2024">Bamboo Weekly</a> or 30% off any of his Weekly Python Exercise <a href="https://lernerpython.com">courses</a> with code BF2024</li>
<li><strong><a href="https://courses.pythontest.com">Brian Okken</a></strong>: is offering 20% off his courses with code TURKEYSALE2024</li>
<li><strong><a href="https://mathspp.gumroad.com/">Rodrigo</a></strong> 50% off Rodrigo&rsquo;s <a href="https://mathspp.gumroad.com/l/all-books-bundle/BF24">all books bundle</a> with code <code>BF24</code></li>
<li><strong><a href="https://www.blog.pythonlibrary.org">Mike Driscoll</a></strong>: 35% off Mike&rsquo;s Python <a href="https://driscollis.gumroad.com/">books</a> and <a href="https://www.teachmepython.com/">courses</a> with code <code>BF24</code></li>
<li><strong><a href="https://testdriven.io/bundle/flask-black-friday/?ref=blackfridaydealsdotdev">Test Driven</a></strong>: get the Flask course bundle for 30% off</li>
<li><strong><a href="https://thepythoncodingplace.com/membership/">The Python Coding Place</a></strong>: 40% off <a href="https://thepythoncodingplace.thinkific.com/enroll/2906653?coupon=black2024">The Python Coding Book</a> and 40% off a lifetime membership to <a href="https://thepythoncodingplace.thinkific.com/cart/add_product/2731141?price_id=3865919&amp;coupon=black2024">The Python Coding Place</a> with code <code>black2024</code></li>
<li><strong><a href="https://learnbyexample.gumroad.com">Sundeep Agarwal</a></strong>: ~50% off Sundeep&rsquo;s <a href="https://learnbyexample.gumroad.com/l/all-books/FestiveOffer">all book</a> and <a href="https://learnbyexample.gumroad.com/l/python-bundle/FestiveOffer">Python</a> bundles with code <code>FestiveOffer</code></li>
<li><strong><a href="https://benjaminb.gumroad.com/l/xjgtb/bLACK60">Benjamin Bennett Alexander</a></strong>: 60% off his Mastering Python Fundamentals book with code <code>BLACK60</code></li>
<li><strong><a href="https://learning.oreilly.com/signup/?promotion_code=CYBERWEEK24">O'Reilly Media</a></strong>: 40% off the first year with code <code>CYBERWEEK24</code> ($299 instead of $499)</li>
<li><strong><a href="http://Matplotlib-journey.com">Matplotlib Journey</a></strong>: a Python dataviz course that has a pre-launch promo right now (69€ instead of 149€)</li>
<li><strong><a href="https://nodeledge.ai/courses/python-done-right">Python Done Right</a></strong>: beta sale on this course ($99 instead of the eventual price of $299)</li>
<li><strong><a href="https://pragprog.com/">Pragmatic Bookshelf</a></strong>: 40% off sale with code <code>turkeysale2024</code></li>
<li><strong><a href="https://nostarch.com/catalog/python">No Starch</a></strong>: 35% off with code <code>BLACKHATFRIDAY</code> (Crash Course, Automate The Boring Stuff, etc.)</li>
<li><strong><a href="https://www.manning.com/catalog#section-50">Manning</a></strong> is offering 50% off if you buy two items</li>
</ul>


<h2>Even more sales</h2>

<p>Also see Adam Johnson&rsquo;s <a href="https://adamj.eu/tech/2024/11/18/django-black-friday-deals-2024/">Django-related Deals for Black Friday 2024</a> for sales on Adam&rsquo;s books, courses from the folks at Test Driven, Django templates, and various other Django-related deals.</p>

<p>And for non-Python/Django Python deals, see the <a href="https://github.com/trungdq88/Awesome-Black-Friday-Cyber-Monday#readme">Awesome Black Friday / Cyber Monday deals</a> GitHub repository and the <a href="https://blackfridaydeals.dev">BlackFridayDeals.dev</a> website.</p>

<p>If you know of another sale (or a likely sale) <strong>please comment below</strong> or email me.</p>

<h2>Read more about Python Jumpstart</h2>

<p>Want to read more about my new self-paced Intro to Python course that&rsquo;s on sale?
See <a href="https://treyhunner.com/2024/11/new-python-jumpstart-course/">this blog post</a> on why I made Python Jumpstart and how it&rsquo;s structured.
Get it by Monday to <strong>save $100</strong>.</p>
]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Adding keyboard shortcuts to the Python REPL]]></title>
    <link href="https://treyhunner.com/2024/10/adding-keyboard-shortcuts-to-the-python-repl/"/>
    <updated>2024-10-28T07:15:00-07:00</updated>
    <id>https://treyhunner.com/2024/10/adding-keyboard-shortcuts-to-the-python-repl</id>
    <content type="html"><![CDATA[<p>I talked about the new Python 3.13 REPL <a href="https://treyhunner.com/2024/05/my-favorite-python-3-dot-13-feature/">a few months ago</a> and <a href="https://www.pythonmorsels.com/python-313-whats-new/">after 3.13 was released</a>.
I think it&rsquo;s <strong>awesome</strong>.</p>

<p>I&rsquo;d like to share a secret feature within the Python 3.13 REPL which I&rsquo;ve been finding useful recently: <strong>adding custom keyboard shortcuts</strong>.</p>

<p>This feature involves a <code>PYTHONSTARTUP</code> file, use of an unsupported Python module, and dynamically evaluating code.</p>

<p>In short, we may be getting ourselves into trouble.
But the result is <em>very</em> neat!</p>

<p>Thanks to Łukasz Llanga for inspiring this post via his excellent <a href="https://youtu.be/dK6HGcSb60Y?si=jWPEa8BcdYGnW9l6">EuroPython keynote talk</a>.</p>

<h2>The goal: keyboard shortcuts in the REPL</h2>

<p>First, I&rsquo;d like to explain the end result.</p>

<p>Let&rsquo;s say I&rsquo;m in the Python REPL on my machine and I&rsquo;ve typed <code>numbers =</code>:</p>

<figure class='code'><figcaption><span></span></figcaption><div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class='line-number'>1</span>
</pre></td><td class='code'><pre><code class='pycon'><span class='line'><span class="gp">&gt;&gt;&gt; </span><span class="n">numbers</span> <span class="o">=</span>
</span></code></pre></td></tr></table></div></figure>


<p>I can now hit <code>Ctrl-N</code> to enter a list of numbers I often use while teaching (<a href="https://en.wikipedia.org/wiki/Lucas_number">Lucas numbers</a>):</p>

<figure class='code'><figcaption><span></span></figcaption><div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class='line-number'>1</span>
</pre></td><td class='code'><pre><code class='pycon'><span class='line'><span class="go">numbers = [2, 1, 3, 4, 7, 11, 18, 29]</span>
</span></code></pre></td></tr></table></div></figure>


<p>That saved me some typing!</p>

<h2>Getting a prototype working</h2>

<p>First, let&rsquo;s try out an example command.</p>

<p>Copy-paste this into your Python 3.13 REPL:</p>

<figure class='code'><figcaption><span></span></figcaption><div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class='line-number'>1</span>
<span class='line-number'>2</span>
<span class='line-number'>3</span>
<span class='line-number'>4</span>
<span class='line-number'>5</span>
<span class='line-number'>6</span>
<span class='line-number'>7</span>
<span class='line-number'>8</span>
<span class='line-number'>9</span>
<span class='line-number'>10</span>
<span class='line-number'>11</span>
</pre></td><td class='code'><pre><code class='python'><span class='line'><span class="kn">from</span> <span class="nn">_pyrepl.simple_interact</span> <span class="kn">import</span> <span class="n">_get_reader</span>
</span><span class='line'><span class="kn">from</span> <span class="nn">_pyrepl.commands</span> <span class="kn">import</span> <span class="n">Command</span>
</span><span class='line'>
</span><span class='line'><span class="k">class</span> <span class="nc">Lucas</span><span class="p">(</span><span class="n">Command</span><span class="p">):</span>
</span><span class='line'>
</span><span class='line'>    <span class="k">def</span> <span class="nf">do</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
</span><span class='line'>        <span class="bp">self</span><span class="o">.</span><span class="n">reader</span><span class="o">.</span><span class="n">insert</span><span class="p">(</span><span class="s">&quot;[2, 1, 3, 4, 7, 11, 18, 29]&quot;</span><span class="p">)</span>
</span><span class='line'>
</span><span class='line'><span class="n">reader</span> <span class="o">=</span> <span class="n">_get_reader</span><span class="p">()</span>
</span><span class='line'><span class="n">reader</span><span class="o">.</span><span class="n">commands</span><span class="p">[</span><span class="s">&quot;lucas&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">Lucas</span>
</span><span class='line'><span class="n">reader</span><span class="o">.</span><span class="n">bind</span><span class="p">(</span><span class="s">r&quot;\C-n&quot;</span><span class="p">,</span> <span class="s">&quot;lucas&quot;</span><span class="p">)</span>
</span></code></pre></td></tr></table></div></figure>


<p>Now hit <code>Ctrl-N</code>.</p>

<p>If all worked as planned, you should see that list of numbers entered into the REPL.</p>

<p>Cool!
Now let&rsquo;s generalize this trick and make Python run our code whenever it starts.</p>

<p>But first&hellip; a disclaimer.</p>

<h2>Here be dragons 🐉</h2>

<p>Notice that <code>_</code> prefix in the <code>_pyrepl</code> module that we&rsquo;re importing from?
That means this module is officially unsupported.</p>

<p>The <code>_pyrepl</code> module is an implementation detail and its implementation may change at any time in future Python versions.</p>

<p>In other words: <code>_pyrepl</code> is designed to be used by <em>Python&rsquo;s standard library modules</em> and not anyone else.
That means that we should assume this code will break in a future Python version.</p>

<p>Will that stop us from playing with this module for the fun of it?</p>

<p>It won&rsquo;t.</p>

<h2>Creating a <code>PYTHONSTARTUP</code> file</h2>

<p>So we&rsquo;ve made <em>one</em> custom key combination for ourselves.
How can we setup this command automatically whenever the Python REPL starts?</p>

<p>We need a <code>PYTHONSTARTUP</code> file.</p>

<p>When Python launches, if it sees a <code>PYTHONSTARTUP</code> environment variable it will treat that environment variable as a Python file to run on startup.</p>

<p>I&rsquo;ve made a <code>/home/trey/.python_startup.py</code> file and I&rsquo;ve set this environment variable in my shell&rsquo;s configuration file (<code>~/.zshrc</code>):</p>

<figure class='code'><figcaption><span></span></figcaption><div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class='line-number'>1</span>
</pre></td><td class='code'><pre><code class='bash'><span class='line'><span class="nb">export </span><span class="nv">PYTHONSTARTUP</span><span class="o">=</span><span class="nv">$HOME</span>/.python_startup.py
</span></code></pre></td></tr></table></div></figure>


<p>To start, we could put our single custom command in this file:</p>

<figure class='code'><figcaption><span></span></figcaption><div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class='line-number'>1</span>
<span class='line-number'>2</span>
<span class='line-number'>3</span>
<span class='line-number'>4</span>
<span class='line-number'>5</span>
<span class='line-number'>6</span>
<span class='line-number'>7</span>
<span class='line-number'>8</span>
<span class='line-number'>9</span>
<span class='line-number'>10</span>
<span class='line-number'>11</span>
<span class='line-number'>12</span>
<span class='line-number'>13</span>
</pre></td><td class='code'><pre><code class='python'><span class='line'><span class="k">try</span><span class="p">:</span>
</span><span class='line'>    <span class="kn">from</span> <span class="nn">_pyrepl.simple_interact</span> <span class="kn">import</span> <span class="n">_get_reader</span>
</span><span class='line'>    <span class="kn">from</span> <span class="nn">_pyrepl.commands</span> <span class="kn">import</span> <span class="n">Command</span>
</span><span class='line'><span class="k">except</span> <span class="ne">ImportError</span><span class="p">:</span>
</span><span class='line'>    <span class="k">pass</span>  <span class="c"># Not in the new pyrepl OR _pyrepl implementation changed</span>
</span><span class='line'><span class="k">else</span><span class="p">:</span>
</span><span class='line'>    <span class="k">class</span> <span class="nc">Lucas</span><span class="p">(</span><span class="n">Command</span><span class="p">):</span>
</span><span class='line'>        <span class="k">def</span> <span class="nf">do</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
</span><span class='line'>            <span class="bp">self</span><span class="o">.</span><span class="n">reader</span><span class="o">.</span><span class="n">insert</span><span class="p">(</span><span class="s">&quot;[2, 1, 3, 4, 7, 11, 18, 29]&quot;</span><span class="p">)</span>
</span><span class='line'>
</span><span class='line'>    <span class="n">reader</span> <span class="o">=</span> <span class="n">_get_reader</span><span class="p">()</span>
</span><span class='line'>    <span class="n">reader</span><span class="o">.</span><span class="n">commands</span><span class="p">[</span><span class="s">&quot;lucas&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">Lucas</span>
</span><span class='line'>    <span class="n">reader</span><span class="o">.</span><span class="n">bind</span><span class="p">(</span><span class="s">r&quot;\C-n&quot;</span><span class="p">,</span> <span class="s">&quot;lucas&quot;</span><span class="p">)</span>
</span></code></pre></td></tr></table></div></figure>


<p>Note that I&rsquo;ve stuck our code in a <code>try</code>-<code>except</code> block.
Our code <em>only</em> runs if those <code>_pyrepl</code> imports succeed.</p>

<p>Note that this <em>might</em> still raise an exception when Python starts <em>if</em> the reader object&rsquo;s <code>command</code> attribute or <code>bind</code> method change in a way that breaks our code.</p>

<p>Personally, I&rsquo;d like to see those breaking changes occur print out a traceback the next time I upgrade Python.
So I&rsquo;m going to leave those last few lines <em>without</em> their own catch-all exception handler.</p>

<h2>Generalizing the code</h2>

<p>Here&rsquo;s a <code>PYTHONSTARTUP</code> file with a more generalized solution:</p>

<figure class='code'><figcaption><span></span></figcaption><div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class='line-number'>1</span>
<span class='line-number'>2</span>
<span class='line-number'>3</span>
<span class='line-number'>4</span>
<span class='line-number'>5</span>
<span class='line-number'>6</span>
<span class='line-number'>7</span>
<span class='line-number'>8</span>
<span class='line-number'>9</span>
<span class='line-number'>10</span>
<span class='line-number'>11</span>
<span class='line-number'>12</span>
<span class='line-number'>13</span>
<span class='line-number'>14</span>
<span class='line-number'>15</span>
<span class='line-number'>16</span>
<span class='line-number'>17</span>
<span class='line-number'>18</span>
<span class='line-number'>19</span>
<span class='line-number'>20</span>
<span class='line-number'>21</span>
<span class='line-number'>22</span>
<span class='line-number'>23</span>
<span class='line-number'>24</span>
<span class='line-number'>25</span>
</pre></td><td class='code'><pre><code class='python'><span class='line'><span class="k">try</span><span class="p">:</span>
</span><span class='line'>    <span class="kn">from</span> <span class="nn">_pyrepl.simple_interact</span> <span class="kn">import</span> <span class="n">_get_reader</span>
</span><span class='line'>    <span class="kn">from</span> <span class="nn">_pyrepl.commands</span> <span class="kn">import</span> <span class="n">Command</span>
</span><span class='line'><span class="k">except</span> <span class="ne">ImportError</span><span class="p">:</span>
</span><span class='line'>    <span class="k">pass</span>
</span><span class='line'><span class="k">else</span><span class="p">:</span>
</span><span class='line'>    <span class="c"># Hack the new Python 3.13 REPL!</span>
</span><span class='line'>    <span class="n">cmds</span> <span class="o">=</span> <span class="p">{</span>
</span><span class='line'>        <span class="s">r&quot;\C-n&quot;</span><span class="p">:</span> <span class="s">&quot;[2, 1, 3, 4, 7, 11, 18, 29]&quot;</span><span class="p">,</span>
</span><span class='line'>        <span class="s">r&quot;\C-f&quot;</span><span class="p">:</span> <span class="s">&#39;[&quot;apples&quot;, &quot;oranges&quot;, &quot;bananas&quot;, &quot;strawberries&quot;, &quot;pears&quot;]&#39;</span><span class="p">,</span>
</span><span class='line'>    <span class="p">}</span>
</span><span class='line'>    <span class="kn">from</span> <span class="nn">textwrap</span> <span class="kn">import</span> <span class="n">dedent</span>
</span><span class='line'>    <span class="n">reader</span> <span class="o">=</span> <span class="n">_get_reader</span><span class="p">()</span>
</span><span class='line'>    <span class="k">for</span> <span class="n">n</span><span class="p">,</span> <span class="p">(</span><span class="n">key</span><span class="p">,</span> <span class="n">text</span><span class="p">)</span> <span class="ow">in</span> <span class="nb">enumerate</span><span class="p">(</span><span class="n">cmds</span><span class="o">.</span><span class="n">items</span><span class="p">(),</span> <span class="n">start</span><span class="o">=</span><span class="mi">1</span><span class="p">):</span>
</span><span class='line'>        <span class="n">name</span> <span class="o">=</span> <span class="n">f</span><span class="s">&quot;CustomCommand{n}&quot;</span>
</span><span class='line'>        <span class="k">exec</span><span class="p">(</span><span class="n">dedent</span><span class="p">(</span><span class="n">f</span><span class="s">&quot;&quot;&quot;</span>
</span><span class='line'><span class="s">            class _cmds:</span>
</span><span class='line'><span class="s">                class {name}(Command):</span>
</span><span class='line'><span class="s">                    def do(self):</span>
</span><span class='line'><span class="s">                        self.reader.insert({text!r})</span>
</span><span class='line'><span class="s">                reader.commands[{name!r}] = {name}</span>
</span><span class='line'><span class="s">                reader.bind({key!r}, {name!r})</span>
</span><span class='line'><span class="s">        &quot;&quot;&quot;</span><span class="p">))</span>
</span><span class='line'>    <span class="c"># Clean up all the new variables</span>
</span><span class='line'>    <span class="k">del</span> <span class="n">_get_reader</span><span class="p">,</span> <span class="n">Command</span><span class="p">,</span> <span class="n">dedent</span><span class="p">,</span> <span class="n">reader</span><span class="p">,</span> <span class="n">cmds</span><span class="p">,</span> <span class="n">text</span><span class="p">,</span> <span class="n">key</span><span class="p">,</span> <span class="n">name</span><span class="p">,</span> <span class="n">_cmds</span><span class="p">,</span> <span class="n">n</span>
</span></code></pre></td></tr></table></div></figure>


<p>This version uses a dictionary to map keyboard shortcuts to the text they should insert.</p>

<p>Note that we&rsquo;re repeatedly building up a string of <code>Command</code> subclasses for each shortcut, using <code>exec</code> to execute the code for that custom <code>Command</code> subclass, and then binding the keyboard shortcut to that new command class.</p>

<p>At the end we then delete all the variables we&rsquo;ve made so our REPL will start the clean global environment we normally expect it to have:</p>

<figure class='code'><figcaption><span></span></figcaption><div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class='line-number'>1</span>
<span class='line-number'>2</span>
<span class='line-number'>3</span>
<span class='line-number'>4</span>
</pre></td><td class='code'><pre><code class='pycon'><span class='line'><span class="go">Python 3.13.0 (main, Oct  8 2024, 10:37:56) [GCC 11.4.0] on linux</span>
</span><span class='line'><span class="go">Type &quot;help&quot;, &quot;copyright&quot;, &quot;credits&quot; or &quot;license&quot; for more information.</span>
</span><span class='line'><span class="gp">&gt;&gt;&gt; </span><span class="nb">dir</span><span class="p">()</span>
</span><span class='line'><span class="go">[&#39;__annotations__&#39;, &#39;__builtins__&#39;, &#39;__cached__&#39;, &#39;__doc__&#39;, &#39;__file__&#39;, &#39;__loader__&#39;, &#39;__name__&#39;, &#39;__package__&#39;, &#39;__spec__&#39;]</span>
</span></code></pre></td></tr></table></div></figure>


<p>Is this messy?</p>

<p>Yes.</p>

<p>Is that a needless use of a dictionary that could have been a list of 2-item tuples instead?</p>

<p>Yes.</p>

<p>Does this work?</p>

<p>Yes.</p>

<h2>Doing more interesting and risky stuff</h2>

<p>Note that there are many keyboard shortcuts that may cause weird behaviors if you bind them.</p>

<p>For example, if you bind <code>Ctrl-i</code>, your binding may trigger every time you try to indent.
And if you try to bind <code>Ctrl-m</code>, your binding may be ignored because this is equivalent to hitting the <code>Enter</code> key.</p>

<p>So be sure to test your REPL carefully after each new binding you try to invent.</p>

<p>If you want to do something more interesting, you could poke around in the <code>_pyrepl</code> package to see what existing code you can use/abuse.</p>

<p>For example, here&rsquo;s a very hacky way of making a binding to <code>Ctrl-x</code> followed by <code>Ctrl-r</code> to make this import <code>subprocess</code>, type in a <code>subprocess.run</code> line, and move your cursor between the empty string within the <code>run</code> call:</p>

<figure class='code'><figcaption><span></span></figcaption><div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class='line-number'>1</span>
<span class='line-number'>2</span>
<span class='line-number'>3</span>
<span class='line-number'>4</span>
<span class='line-number'>5</span>
<span class='line-number'>6</span>
<span class='line-number'>7</span>
<span class='line-number'>8</span>
<span class='line-number'>9</span>
<span class='line-number'>10</span>
<span class='line-number'>11</span>
<span class='line-number'>12</span>
</pre></td><td class='code'><pre><code class='python'><span class='line'><span class="k">class</span> <span class="nc">_cmds</span><span class="p">:</span>
</span><span class='line'>    <span class="k">class</span> <span class="nc">Run</span><span class="p">(</span><span class="n">Command</span><span class="p">):</span>
</span><span class='line'>        <span class="k">def</span> <span class="nf">do</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
</span><span class='line'>            <span class="kn">from</span> <span class="nn">_pyrepl.commands</span> <span class="kn">import</span> <span class="n">backward_kill_word</span><span class="p">,</span> <span class="n">left</span>
</span><span class='line'>            <span class="n">backward_kill_word</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">reader</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">event_name</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">event</span><span class="p">)</span><span class="o">.</span><span class="n">do</span><span class="p">()</span>
</span><span class='line'>            <span class="bp">self</span><span class="o">.</span><span class="n">reader</span><span class="o">.</span><span class="n">insert</span><span class="p">(</span><span class="s">&quot;import subprocess</span><span class="se">\n</span><span class="s">&quot;</span><span class="p">)</span>
</span><span class='line'>            <span class="n">code</span> <span class="o">=</span> <span class="s">&#39;subprocess.run(&quot;&quot;, shell=True)&#39;</span>
</span><span class='line'>            <span class="bp">self</span><span class="o">.</span><span class="n">reader</span><span class="o">.</span><span class="n">insert</span><span class="p">(</span><span class="n">code</span><span class="p">)</span>
</span><span class='line'>            <span class="k">for</span> <span class="n">_</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">code</span><span class="p">)</span> <span class="o">-</span> <span class="n">code</span><span class="o">.</span><span class="n">index</span><span class="p">(</span><span class="s">&#39;&quot;&quot;&#39;</span><span class="p">)</span> <span class="o">-</span> <span class="mi">1</span><span class="p">):</span>
</span><span class='line'>                <span class="n">left</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">reader</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">event_name</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">event</span><span class="p">)</span><span class="o">.</span><span class="n">do</span><span class="p">()</span>
</span><span class='line'><span class="n">reader</span><span class="o">.</span><span class="n">commands</span><span class="p">[</span><span class="s">&quot;subprocess_run&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">_cmds</span><span class="o">.</span><span class="n">Run</span>
</span><span class='line'><span class="n">reader</span><span class="o">.</span><span class="n">bind</span><span class="p">(</span><span class="s">r&quot;\C-x\C-r&quot;</span><span class="p">,</span> <span class="s">&quot;subprocess_run&quot;</span><span class="p">)</span>
</span></code></pre></td></tr></table></div></figure>


<h2>What keyboard shortcuts are available?</h2>

<p>As you play with customizing keyboard shortcuts, you&rsquo;ll likely notice that many key combinations result in strange and undesirable behavior when overridden.</p>

<p>For example, overriding <code>Ctrl-J</code> will also override the <code>Enter</code> key&hellip; at least it does in my terminal.</p>

<p>I&rsquo;ll list the key combinations that seem unproblematic on my setup with Gnome Terminal in Ubuntu Linux.</p>

<p>Here are <code>Control</code> key shortcuts that seem to be complete unused in the Python REPL:</p>

<ul>
<li><code>Ctrl-N</code></li>
<li><code>Ctrl-O</code></li>
<li><code>Ctrl-P</code></li>
<li><code>Ctrl-Q</code></li>
<li><code>Ctrl-S</code></li>
<li><code>Ctrl-V</code></li>
</ul>


<p>Note that overriding <code>Ctrl-H</code> is often an alternative to the backspace key</p>

<p>Here are <code>Alt</code>/<code>Meta</code> key shortcuts that appear unused on my machine:</p>

<ul>
<li><code>Alt-A</code></li>
<li><code>Alt-E</code></li>
<li><code>Alt-G</code></li>
<li><code>Alt-H</code></li>
<li><code>Alt-I</code></li>
<li><code>Alt-J</code></li>
<li><code>Alt-K</code></li>
<li><code>Alt-M</code></li>
<li><code>Alt-N</code></li>
<li><code>Alt-O</code></li>
<li><code>Alt-P</code></li>
<li><code>Alt-Q</code></li>
<li><code>Alt-S</code></li>
<li><code>Alt-V</code></li>
<li><code>Alt-W</code></li>
<li><code>Alt-X</code></li>
<li><code>Alt-Z</code></li>
</ul>


<p>You can add an <code>Alt</code> shortcut by using <code>\M</code> (for &ldquo;meta&rdquo;).
So <code>r"\M-a"</code> would capture <code>Alt-A</code> just as <code>r"\C-a"</code> would capture <code>Ctrl-A</code>.</p>

<p>Here are keyboard shortcuts that <em>can</em> be customized but you might want to consider whether the current default behavior is worth losing:</p>

<ul>
<li><code>Alt-B</code>: backward word (same as <code>Ctrl-Left</code>)</li>
<li><code>Alt-C</code>: capitalize word (does nothing on my machine&hellip;)</li>
<li><code>Alt-D</code>: kill word (delete to end of word)</li>
<li><code>Alt-F</code>: forward word (same as <code>Ctrl-Right</code>)</li>
<li><code>Alt-L</code>: downcase word (does nothing on my machine&hellip;)</li>
<li><code>Alt-U</code>: upcase word (does nothing on my machine&hellip;)</li>
<li><code>Alt-Y</code>: yank pop</li>
<li><code>Ctrl-A</code>: beginning of line (like the <code>Home</code> key)</li>
<li><code>Ctrl-B</code>: left (like the <code>Left</code> key)</li>
<li><code>Ctrl-E</code>: end of line (like the <code>End</code> key)</li>
<li><code>Ctrl-F</code>: right (like the <code>Right</code> key)</li>
<li><code>Ctrl-G</code>: cancel</li>
<li><code>Ctrl-H</code>: backspace (same as the <code>Backspace</code> key)</li>
<li><code>Ctrl-K</code>: kill line (delete to end of line)</li>
<li><code>Ctrl-T</code>: transpose characters</li>
<li><code>Ctrl-U</code>: line discard (delete to beginning of line)</li>
<li><code>Ctrl-W</code>: word discard (delete to beginning of word)</li>
<li><code>Ctrl-Y</code>: yank</li>
<li><code>Alt-R</code>: restore history (within history mode)</li>
</ul>


<h2>What fun have you found in <code>_pyrepl</code>?</h2>

<p>Find something fun while playing with the <code>_pyrepl</code> package&rsquo;s inner-workings?</p>

<p>I&rsquo;d love to hear about it!
Comment below to share what you found.</p>
]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Django and the Python 3.13 REPL]]></title>
    <link href="https://treyhunner.com/2024/10/django-and-the-new-python-3-dot-13-repl/"/>
    <updated>2024-10-13T21:03:32-07:00</updated>
    <id>https://treyhunner.com/2024/10/django-and-the-new-python-3-dot-13-repl</id>
    <content type="html"><![CDATA[<p>Your new Django project uses Python 3.13.</p>

<p>You&rsquo;re really looking forward to using the new REPL&hellip; but <code>python manage.py shell</code> just shows the same old Python REPL.
What gives?</p>

<p>Well, Django&rsquo;s management shell uses Python&rsquo;s <a href="https://docs.python.org/3/library/code.html">code</a> module to launch a custom REPL, but the <code>code</code> module doesn&rsquo;t (<a href="https://github.com/python/cpython/issues/119512">yet</a>) use the new Python REPL.</p>

<p>So you&rsquo;re out of luck&hellip; or are you?</p>

<h2>How stable do you need your <code>shell</code> command to be?</h2>

<p>The new Python REPL&rsquo;s code lives in a <a href="https://github.com/python/cpython/tree/v3.13.0/Lib/_pyrepl">_pyrepl</a> package.
Surely there must be some way to launch the new REPL using that <code>_pyrepl</code> package!</p>

<p>First, note the <code>_</code> before that package name.
It&rsquo;s <code>_pyrepl</code>, not <code>pyrepl</code>.</p>

<p>Any solution that relies on this module may break in future Python releases.</p>

<p>So&hellip; should we give up on looking for a solution, if we can&rsquo;t get a &ldquo;stable&rdquo; one?</p>

<p>I don&rsquo;t think so.</p>

<p>My <code>shell</code> command doesn&rsquo;t usually <em>need</em> to be stable in more than one version of Python at a time.
So I&rsquo;m fine with a solution that <em>attempts</em> to use the new REPL and then falls back to the old REPL if it fails.</p>

<h2>A working solution</h2>

<p>So, let&rsquo;s look at a working solution.</p>

<p>Stick <a href="https://pym.dev/p/2zqeq/">this code</a> in a <code>management/commands/shell.py</code> file within one of your Django apps:</p>

<figure class='code'><figcaption><span></span></figcaption><div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class='line-number'>1</span>
<span class='line-number'>2</span>
<span class='line-number'>3</span>
<span class='line-number'>4</span>
<span class='line-number'>5</span>
<span class='line-number'>6</span>
<span class='line-number'>7</span>
<span class='line-number'>8</span>
<span class='line-number'>9</span>
<span class='line-number'>10</span>
</pre></td><td class='code'><pre><code class='python'><span class='line'><span class="sd">&quot;&quot;&quot;Python 3.13 REPL support using the unsupported _pyrepl module.&quot;&quot;&quot;</span>
</span><span class='line'><span class="kn">from</span> <span class="nn">django.core.management.commands.shell</span> <span class="kn">import</span> <span class="n">Command</span> <span class="k">as</span> <span class="n">BaseShellCommand</span>
</span><span class='line'>
</span><span class='line'>
</span><span class='line'><span class="k">class</span> <span class="nc">Command</span><span class="p">(</span><span class="n">BaseShellCommand</span><span class="p">):</span>
</span><span class='line'>    <span class="n">shells</span> <span class="o">=</span> <span class="p">[</span><span class="s">&quot;ipython&quot;</span><span class="p">,</span> <span class="s">&quot;bpython&quot;</span><span class="p">,</span> <span class="s">&quot;pyrepl&quot;</span><span class="p">,</span> <span class="s">&quot;python&quot;</span><span class="p">]</span>
</span><span class='line'>
</span><span class='line'>    <span class="k">def</span> <span class="nf">pyrepl</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">options</span><span class="p">):</span>
</span><span class='line'>        <span class="kn">from</span> <span class="nn">_pyrepl.main</span> <span class="kn">import</span> <span class="n">interactive_console</span>
</span><span class='line'>        <span class="n">interactive_console</span><span class="p">()</span>
</span></code></pre></td></tr></table></div></figure>


<h2>How it works</h2>

<p>Django&rsquo;s <code>shell</code> command has made it very simple to add support for your favorite REPL of choice.</p>

<p><a href="https://github.com/django/django/blob/5.1.2/django/core/management/commands/shell.py">The code for the <code>shell</code> command</a> loops through the <code>shells</code> list and attempts to run a method with that name on its own class.
If an <code>ImportError</code> is raised then it attempts the next command, stopping once no exception occurs.</p>

<p>Our new command will try to use IPython and bpython if they&rsquo;re installed and then it will try the new Python 3.13 REPL followed by the old Python REPL.</p>

<p>If Python 3.14 breaks our import by moving the <code>interactive_console</code> function, then an <code>ImportError</code> will be raised, causing us to fall back to the old REPL after we upgrade to Python 3.14 one day.
If instead, the <code>interactive_console</code> function&rsquo;s usage changes (maybe it will require arguments) then our <code>shell</code> command will completely break and we&rsquo;ll need to manually fix it when we upgrade to Python 3.14.</p>

<h2>What&rsquo;s so great about the new REPL?</h2>

<p>If you&rsquo;re already using IPython or BPython as your REPL and you&rsquo;re enjoying them, I would stick with them.</p>

<p>Third-party libraries move faster than Python itself and they&rsquo;re often more feature-rich.
IPython has about 20 years worth of feature development and it has features that the built-in Python REPL will likely never have.</p>

<p>If you&rsquo;re using the default Python REPL though, this new REPL is a <em>huge</em> upgrade.
I&rsquo;ve been using it as my default REPL since May and I <em>love</em> it.
See <a href="https://pym.dev/python-313-whats-new/">my screencast on Python 3.13</a> for my favorite features in the new REPL.</p>

<p><strong>P.S. for Python Morsels users</strong>: if you want to try using that <code>code</code> module, check out the (fairly advanced) <a href="https://www.pythonmorsels.com/exercises/3efdd9e172a346d08679ec39419ed822/?level=advanced">replr</a> or (even more advanced) <a href="https://www.pythonmorsels.com/exercises/5800cdcbbc5b4936b3e253dc15050480/?level=advanced">replsync</a> exercises.</p>
]]></content>
  </entry>
  
</feed>
