<?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>2024-12-31T08:11:31-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[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.10</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>
  
  <entry>
    <title type="html"><![CDATA[Switching from virtualenvwrapper to direnv, Starship, and uv]]></title>
    <link href="https://treyhunner.com/2024/10/switching-from-virtualenvwrapper-to-direnv-starship-and-uv/"/>
    <updated>2024-10-03T14:00:00-07:00</updated>
    <id>https://treyhunner.com/2024/10/switching-from-virtualenvwrapper-to-direnv-starship-and-uv</id>
    <content type="html"><![CDATA[<p>Earlier this week I considered whether I should finally switch away from <a href="https://virtualenvwrapper.readthedocs.io">virtualenvwrapper</a> to using local <code>.venv</code> managed by <a href="https://direnv.net">direnv</a>.</p>

<p>I&rsquo;ve never seriously used direnv, but I&rsquo;ve been hearing <a href="https://micro.webology.dev/2024/03/13/on-environment-variables.html">Jeff</a> and <a href="https://hynek.me/til/python-project-local-venvs/">Hynek</a> talk about their use of direnv for a while.</p>

<p>After a few days, I&rsquo;ve finally stumbled into a setup that works great for me.
I&rsquo;d like to note the basics of this setup as well as some fancy additions that are specific to my own use case.</p>

<h2>My old virtualenvwrapper workflow</h2>

<p>First, I&rsquo;d like to note my <em>old</em> workflow that I&rsquo;m trying to roughly recreate:</p>

<ol>
<li>I type <code>mkvenv3 &lt;project_name&gt;</code> to create a new virtual environment for the current project directory and activate it</li>
<li>I type <code>workon &lt;project_name&gt;</code> when I want to workon that project: this activates the correct virtual environment <em>and</em> changes to the project directory</li>
</ol>


<p>The initial setup I thought of allows me to:</p>

<ol>
<li>Run <code>echo layout python &gt; .envrc &amp;&amp; direnv allow</code> to create a virtual environment for the current project and activate it</li>
<li>Change directories into the project directory to automatically activate the virtual environment</li>
</ol>


<p>The more complex setup I eventually settled on allows me to:</p>

<ol>
<li>Run <code>venv &lt;project_name&gt;</code> to create a virtual environment for the current project and activate it</li>
<li>Run <code>workon &lt;project_name&gt;</code> to change directories into the project (which automatically activates the virtual environment)</li>
</ol>


<h2>The initial setup</h2>

<p>First, I <a href="https://direnv.net">installed direnv</a> and added this to my <code>~/.zshrc</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='bash'><span class='line'><span class="nb">eval</span> <span class="s2">&quot;$(direnv hook zsh)&quot;</span>
</span></code></pre></td></tr></table></div></figure>


<p>Then whenever I wanted to create a virtual environment for a new project I created a <code>.envrc</code> file in that directory, which looked 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>
</pre></td><td class='code'><pre><code class='bash'><span class='line'>layout python
</span></code></pre></td></tr></table></div></figure>


<p>Then I ran <code>direnv allow</code> to allow, as <code>direnv</code> instructed me to, to allow the new virtual environment to be automatically created and activated.</p>

<p>That&rsquo;s pretty much it.</p>

<p>Unfortunately, I did not like this initial setup.</p>

<h2>No shell prompt?</h2>

<p>The first problem was that the virtual environment&rsquo;s prompt didn&rsquo;t show up in my shell prompt.
This is due to a <a href="https://github.com/direnv/direnv/issues/529">direnv not allowing modification of the <code>PS1</code> shell prompt</a>.
That means I&rsquo;d need to modify my shell configuration to show the correct virtual environment name myself.</p>

<p>So I added this to my <code>~/.zshrc</code> file to show the virtual environment name at the beginning of my prompt:</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='bash'><span class='line'><span class="c"># Add direnv-activated venv to prompt</span>
</span><span class='line'>show_virtual_env<span class="o">()</span> <span class="o">{</span>
</span><span class='line'>  <span class="k">if</span> <span class="o">[[</span> -n <span class="s2">&quot;$VIRTUAL_ENV_PROMPT&quot;</span> <span class="o">&amp;&amp;</span> -n <span class="s2">&quot;$DIRENV_DIR&quot;</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span><span class='line'>    <span class="nb">echo</span> <span class="s2">&quot;($(basename $VIRTUAL_ENV_PROMPT)) &quot;</span>
</span><span class='line'>  <span class="k">fi</span>
</span><span class='line'><span class="o">}</span>
</span><span class='line'><span class="nv">PS1</span><span class="o">=</span><span class="s1">&#39;$(show_virtual_env)&#39;</span><span class="nv">$PS1</span>
</span></code></pre></td></tr></table></div></figure>


<h2>Wrong virtual environment directory</h2>

<p>The next problem was that the virtual environment was placed in <code>.direnv/python3.12</code>.
I wanted each virtual environment to be in a <code>.venv</code> directory instead.</p>

<p>To do that, I made a <code>.config/direnv/direnvrc</code> file that customized the python layout:</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>
</pre></td><td class='code'><pre><code class='bash'><span class='line'>layout_python<span class="o">()</span> <span class="o">{</span>
</span><span class='line'>    <span class="k">if</span> <span class="o">[[</span> -d <span class="s2">&quot;.venv&quot;</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span><span class='line'>        <span class="nv">VIRTUAL_ENV</span><span class="o">=</span><span class="s2">&quot;$(pwd)/.venv&quot;</span>
</span><span class='line'>    <span class="k">fi</span>
</span><span class='line'>
</span><span class='line'>    <span class="k">if</span> <span class="o">[[</span> -z <span class="nv">$VIRTUAL_ENV</span> <span class="o">||</span> ! -d <span class="nv">$VIRTUAL_ENV</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span><span class='line'>        log_status <span class="s2">&quot;No virtual environment exists. Executing \`python -m venv .venv\`.&quot;</span>
</span><span class='line'>        python -m venv .venv
</span><span class='line'>        <span class="nv">VIRTUAL_ENV</span><span class="o">=</span><span class="s2">&quot;$(pwd)/.venv&quot;</span>
</span><span class='line'>    <span class="k">fi</span>
</span><span class='line'>
</span><span class='line'>    <span class="c"># Activate the virtual environment</span>
</span><span class='line'>    . <span class="nv">$VIRTUAL_ENV</span>/bin/activate
</span><span class='line'><span class="o">}</span>
</span></code></pre></td></tr></table></div></figure>


<h2>Loading, unloading, loading, unloading&hellip;</h2>

<p>I also didn&rsquo;t like the loading and unloading messages that showed up each time I changed directories.
I removed those by clearing the <code>DIRENV_LOG_FORMAT</code> variable in my <code>~/.zshrc</code> configuration:</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">DIRENV_LOG_FORMAT</span><span class="o">=</span>
</span></code></pre></td></tr></table></div></figure>


<h2>The more advanced setup</h2>

<p>I don&rsquo;t like it when all my virtual environment prompts show up as <code>.venv</code>.
I want ever prompt to be the name of the actual project&hellip; which is usually the directory name.</p>

<p>I also <em>really</em> wanted to be able to type <code>venv</code> to create a new virtual environment, activate it, and create the <code>.envrc</code> file for my <em>automatically</em>.</p>

<p>Additionally, I thought it would be really handy if I could type <code>workon &lt;project_name&gt;</code> to change directories to a specific project.</p>

<p>I made two aliases in my <code>~/.zshrc</code> configuration for all of 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>
<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>
<span class='line-number'>48</span>
<span class='line-number'>49</span>
<span class='line-number'>50</span>
<span class='line-number'>51</span>
<span class='line-number'>52</span>
<span class='line-number'>53</span>
<span class='line-number'>54</span>
<span class='line-number'>55</span>
</pre></td><td class='code'><pre><code class='bash'><span class='line'>venv<span class="o">()</span> <span class="o">{</span>
</span><span class='line'>    <span class="nb">local </span><span class="nv">venv_name</span><span class="o">=</span><span class="k">${</span><span class="nv">1</span><span class="k">:-$(</span>basename <span class="s2">&quot;$PWD&quot;</span><span class="k">)}</span>
</span><span class='line'>    <span class="nb">local </span><span class="nv">projects_file</span><span class="o">=</span><span class="s2">&quot;$HOME/.projects&quot;</span>
</span><span class='line'>
</span><span class='line'>    <span class="c"># Check if .envrc already exists</span>
</span><span class='line'>    <span class="k">if</span> <span class="o">[</span> -f .envrc <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
</span><span class='line'>        <span class="nb">echo</span> <span class="s2">&quot;Error: .envrc already exists&quot;</span> &gt;<span class="p">&amp;</span>2
</span><span class='line'>        <span class="k">return</span> 1
</span><span class='line'>    <span class="k">fi</span>
</span><span class='line'>
</span><span class='line'>    <span class="c"># Create venv</span>
</span><span class='line'>    <span class="k">if</span> ! python3 -m venv --prompt <span class="s2">&quot;$venv_name&quot;</span><span class="p">;</span> <span class="k">then</span>
</span><span class='line'>        <span class="nb">echo</span> <span class="s2">&quot;Error: Failed to create venv&quot;</span> &gt;<span class="p">&amp;</span>2
</span><span class='line'>        <span class="k">return</span> 1
</span><span class='line'>    <span class="k">fi</span>
</span><span class='line'>
</span><span class='line'>    <span class="c"># Create .envrc</span>
</span><span class='line'>    <span class="nb">echo</span> <span class="s2">&quot;layout python&quot;</span> &gt; .envrc
</span><span class='line'>
</span><span class='line'>    <span class="c"># Append project name and directory to projects file</span>
</span><span class='line'>    <span class="nb">echo</span> <span class="s2">&quot;${venv_name} = ${PWD}&quot;</span> &gt;&gt; <span class="nv">$projects_file</span>
</span><span class='line'>
</span><span class='line'>    <span class="c"># Allow direnv to immediately activate the virtual environment</span>
</span><span class='line'>    direnv allow
</span><span class='line'><span class="o">}</span>
</span><span class='line'>
</span><span class='line'>workon<span class="o">()</span> <span class="o">{</span>
</span><span class='line'>    <span class="nb">local </span><span class="nv">project_name</span><span class="o">=</span><span class="s2">&quot;$1&quot;</span>
</span><span class='line'>    <span class="nb">local </span><span class="nv">projects_file</span><span class="o">=</span><span class="s2">&quot;$HOME/.projects&quot;</span>
</span><span class='line'>    <span class="nb">local </span>project_dir
</span><span class='line'>
</span><span class='line'>    <span class="c"># Check for projects config file</span>
</span><span class='line'>    <span class="k">if</span> <span class="o">[[</span> ! -f <span class="s2">&quot;$projects_file&quot;</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span><span class='line'>        <span class="nb">echo</span> <span class="s2">&quot;Error: $projects_file not found&quot;</span> &gt;<span class="p">&amp;</span>2
</span><span class='line'>        <span class="k">return</span> 1
</span><span class='line'>    <span class="k">fi</span>
</span><span class='line'>
</span><span class='line'>    <span class="c"># Get the project directory for the given project name</span>
</span><span class='line'>    <span class="nv">project_dir</span><span class="o">=</span><span class="k">$(</span>grep -E <span class="s2">&quot;^$project_name\s*=&quot;</span> <span class="s2">&quot;$projects_file&quot;</span> <span class="p">|</span> sed <span class="s1">&#39;s/^[^=]*=\s*//&#39;</span><span class="k">)</span>
</span><span class='line'>
</span><span class='line'>    <span class="c"># Ensure a project directory was found</span>
</span><span class='line'>    <span class="k">if</span> <span class="o">[[</span> -z <span class="s2">&quot;$project_dir&quot;</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span><span class='line'>        <span class="nb">echo</span> <span class="s2">&quot;Error: Project &#39;$project_name&#39; not found in $projects_file&quot;</span> &gt;<span class="p">&amp;</span>2
</span><span class='line'>        <span class="k">return</span> 1
</span><span class='line'>    <span class="k">fi</span>
</span><span class='line'>
</span><span class='line'>    <span class="c"># Ensure the project directory exists</span>
</span><span class='line'>    <span class="k">if</span> <span class="o">[[</span> ! -d <span class="s2">&quot;$project_dir&quot;</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span><span class='line'>        <span class="nb">echo</span> <span class="s2">&quot;Error: Directory $project_dir does not exist&quot;</span> &gt;<span class="p">&amp;</span>2
</span><span class='line'>        <span class="k">return</span> 1
</span><span class='line'>    <span class="k">fi</span>
</span><span class='line'>
</span><span class='line'>    <span class="c"># Change directories</span>
</span><span class='line'>    <span class="nb">cd</span> <span class="s2">&quot;$project_dir&quot;</span>
</span><span class='line'><span class="o">}</span>
</span></code></pre></td></tr></table></div></figure>


<p>Now I can type this to create a <code>.venv</code> virtual environment in my current directory, which has a prompt named after the current directory, activate it, and create a <code>.envrc</code> file which will automatically activate that virtual environment (thanks to that <code>~/.config/direnv/direnvrc</code> file) whenever I change into 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>
</pre></td><td class='code'><pre><code class='bash'><span class='line'><span class="nv">$ </span>venv
</span></code></pre></td></tr></table></div></figure>


<p>If I wanted to customized the prompt name for the virtual environment, 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>venv my_project
</span></code></pre></td></tr></table></div></figure>


<p>When I wanted to start working on that project later, I can either change into that directory <em>or</em> if I&rsquo;m feeling lazy I can simply type:</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>workon my_project
</span></code></pre></td></tr></table></div></figure>


<p>That reads from my <code>~/.projects</code> file to look up the project directory to switch to.</p>

<h2>Switching to uv</h2>

<p>I also decided to try using <a href="https://docs.astral.sh/uv/">uv</a> for all of this, since it&rsquo;s faster at creating virtual environments.
One benefit of <code>uv</code> is that it tries to select the correct Python version for the project, if it sees a version noted in a <code>pyproject.toml</code> file.</p>

<p>Another benefit of using <code>uv</code>, is that I should also be able to update the <code>venv</code> to use a specific version of Python with something like <code>--python 3.12</code>.</p>

<p>Here are the updated shell aliases for the <code>~/.zshrc</code> for <code>uv</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>
<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='bash'><span class='line'>venv<span class="o">()</span> <span class="o">{</span>
</span><span class='line'>    <span class="nb">local </span>venv_name
</span><span class='line'>    <span class="nb">local </span><span class="nv">dir_name</span><span class="o">=</span><span class="k">$(</span>basename <span class="s2">&quot;$PWD&quot;</span><span class="k">)</span>
</span><span class='line'>
</span><span class='line'>    <span class="c"># If there are no arguments or the last argument starts with a dash, use dir_name</span>
</span><span class='line'>    <span class="k">if</span> <span class="o">[</span> <span class="nv">$# </span>-eq <span class="m">0</span> <span class="o">]</span> <span class="o">||</span> <span class="o">[[</span> <span class="s2">&quot;${!#}&quot;</span> <span class="o">==</span> -* <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span><span class='line'>        <span class="nv">venv_name</span><span class="o">=</span><span class="s2">&quot;$dir_name&quot;</span>
</span><span class='line'>    <span class="k">else</span>
</span><span class='line'>        <span class="nv">venv_name</span><span class="o">=</span><span class="s2">&quot;${!#}&quot;</span>
</span><span class='line'>        <span class="nb">set</span> -- <span class="s2">&quot;${@:1:$#-1}&quot;</span>
</span><span class='line'>    <span class="k">fi</span>
</span><span class='line'>
</span><span class='line'>    <span class="c"># Check if .envrc already exists</span>
</span><span class='line'>    <span class="k">if</span> <span class="o">[</span> -f .envrc <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
</span><span class='line'>        <span class="nb">echo</span> <span class="s2">&quot;Error: .envrc already exists&quot;</span> &gt;<span class="p">&amp;</span>2
</span><span class='line'>        <span class="k">return</span> 1
</span><span class='line'>    <span class="k">fi</span>
</span><span class='line'>
</span><span class='line'>    <span class="c"># Create venv using uv with all passed arguments</span>
</span><span class='line'>    <span class="k">if</span> ! uv venv --seed --prompt <span class="s2">&quot;$@&quot;</span> <span class="s2">&quot;$venv_name&quot;</span><span class="p">;</span> <span class="k">then</span>
</span><span class='line'>        <span class="nb">echo</span> <span class="s2">&quot;Error: Failed to create venv&quot;</span> &gt;<span class="p">&amp;</span>2
</span><span class='line'>        <span class="k">return</span> 1
</span><span class='line'>    <span class="k">fi</span>
</span><span class='line'>
</span><span class='line'>    <span class="c"># Create .envrc</span>
</span><span class='line'>    <span class="nb">echo</span> <span class="s2">&quot;layout python&quot;</span> &gt; .envrc
</span><span class='line'>
</span><span class='line'>    <span class="c"># Append to ~/.projects</span>
</span><span class='line'>    <span class="nb">echo</span> <span class="s2">&quot;${venv_name} = ${PWD}&quot;</span> &gt;&gt; ~/.projects
</span><span class='line'>
</span><span class='line'>    <span class="c"># Allow direnv to immediately activate the virtual environment</span>
</span><span class='line'>    direnv allow
</span><span class='line'><span class="o">}</span>
</span></code></pre></td></tr></table></div></figure>


<h2>Switching to starship</h2>

<p>I also decided to try out using <a href="https://starship.rs">Starship</a> to customize my shell this week.</p>

<p>I added this to my <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">eval</span> <span class="s2">&quot;$(starship init zsh)&quot;</span>
</span></code></pre></td></tr></table></div></figure>


<p>And removed this, which is no longer needed since Starship will be managing the shell for me:</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='bash'><span class='line'><span class="c"># Add direnv-activated venv to prompt</span>
</span><span class='line'>show_virtual_env<span class="o">()</span> <span class="o">{</span>
</span><span class='line'>  <span class="k">if</span> <span class="o">[[</span> -n <span class="s2">&quot;$VIRTUAL_ENV_PROMPT&quot;</span> <span class="o">&amp;&amp;</span> -n <span class="s2">&quot;$DIRENV_DIR&quot;</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span><span class='line'>    <span class="nb">echo</span> <span class="s2">&quot;($(basename $VIRTUAL_ENV_PROMPT)) &quot;</span>
</span><span class='line'>  <span class="k">fi</span>
</span><span class='line'><span class="o">}</span>
</span><span class='line'><span class="nv">PS1</span><span class="o">=</span><span class="s1">&#39;$(show_virtual_env)&#39;</span><span class="nv">$PS1</span>
</span></code></pre></td></tr></table></div></figure>


<p>I also switched my <code>python</code> layout for direnv to just set the <code>$VIRTUAL_ENV</code> variable and add the <code>$VIRTUAL_ENV/bin</code> directory to my <code>PATH</code>, since the <code>$VIRTUAL_ENV_PROMPT</code> variable isn&rsquo;t needed for Starship to pick up the prompt:</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'>layout_python<span class="o">()</span> <span class="o">{</span>
</span><span class='line'>    <span class="nv">VIRTUAL_ENV</span><span class="o">=</span><span class="s2">&quot;$(pwd)/.venv&quot;</span>
</span><span class='line'>    PATH_add <span class="s2">&quot;$VIRTUAL_ENV/bin&quot;</span>
</span><span class='line'>    <span class="nb">export </span>VIRTUAL_ENV
</span><span class='line'><span class="o">}</span>
</span></code></pre></td></tr></table></div></figure>


<p>I also made a <em>very</em> boring Starship configuration in <code>~/.config/starship.toml</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>
<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>
</pre></td><td class='code'><pre><code class='toml'><span class='line'><span class="n">format</span> <span class="o">=</span> <span class="s">&quot;&quot;&quot;</span>
</span><span class='line'><span class="s">$python\</span>
</span><span class='line'><span class="s">$directory\</span>
</span><span class='line'><span class="s">$git_branch\</span>
</span><span class='line'><span class="s">$git_state\</span>
</span><span class='line'><span class="s">$character&quot;&quot;&quot;</span>
</span><span class='line'>
</span><span class='line'><span class="n">add_newline</span> <span class="o">=</span> <span class="kc">false</span>
</span><span class='line'>
</span><span class='line'><span class="p">[</span><span class="n">python</span><span class="p">]</span>
</span><span class='line'><span class="n">format</span> <span class="o">=</span> <span class="err">&#39;</span><span class="p">([(</span><span class="err">\</span><span class="p">(</span><span class="err">$</span><span class="n">virtualenv</span><span class="err">\</span><span class="p">)</span> <span class="p">)](</span><span class="err">$</span><span class="n">style</span><span class="p">))</span><span class="err">&#39;</span>
</span><span class='line'><span class="n">style</span> <span class="o">=</span> <span class="s">&quot;bright-black&quot;</span>
</span><span class='line'>
</span><span class='line'><span class="p">[</span><span class="n">directory</span><span class="p">]</span>
</span><span class='line'><span class="n">style</span> <span class="o">=</span> <span class="s">&quot;bright-blue&quot;</span>
</span><span class='line'>
</span><span class='line'><span class="p">[</span><span class="n">character</span><span class="p">]</span>
</span><span class='line'><span class="n">success_symbol</span> <span class="o">=</span> <span class="s">&quot;[\\$](black)&quot;</span>
</span><span class='line'><span class="n">error_symbol</span> <span class="o">=</span> <span class="s">&quot;[\\$](bright-red)&quot;</span>
</span><span class='line'><span class="n">vimcmd_symbol</span> <span class="o">=</span> <span class="s">&quot;[❮](green)&quot;</span>
</span><span class='line'>
</span><span class='line'><span class="p">[</span><span class="n">git_branch</span><span class="p">]</span>
</span><span class='line'><span class="n">format</span> <span class="o">=</span> <span class="s">&quot;[$symbol$branch]($style) &quot;</span>
</span><span class='line'><span class="n">style</span> <span class="o">=</span> <span class="s">&quot;bright-purple&quot;</span>
</span><span class='line'>
</span><span class='line'><span class="p">[</span><span class="n">git_state</span><span class="p">]</span>
</span><span class='line'><span class="n">format</span> <span class="o">=</span> <span class="err">&#39;\</span><span class="p">([</span><span class="err">$</span><span class="n">state</span><span class="p">(</span> <span class="err">$</span><span class="n">progress_current</span><span class="err">/$</span><span class="n">progress_total</span><span class="p">)](</span><span class="err">$</span><span class="n">style</span><span class="p">)</span><span class="err">\</span><span class="p">)</span> <span class="err">&#39;</span>
</span><span class='line'><span class="n">style</span> <span class="o">=</span> <span class="s">&quot;purple&quot;</span>
</span><span class='line'>
</span><span class='line'><span class="p">[</span><span class="n">cmd_duration</span><span class="p">.</span><span class="n">disabled</span><span class="p">]</span>
</span></code></pre></td></tr></table></div></figure>


<p>I setup such a boring configuration because when I&rsquo;m teaching, I don&rsquo;t want my students to be confused or distracted by a prompt that has considerably more information in it than <em>their</em> default prompt may have.</p>

<p>The biggest downside of switching to Starship has been my own earworm-oriented brain.
As I update my Starship configuration files, I&rsquo;ve repeatedly heard David Bowie singing &ldquo;I&rsquo;m a Starmaaan&rdquo;. 🎶</p>

<h2>Ground control to major TOML</h2>

<p>After all of that, I realized that I could additionally use different Starship configurations for different directories by putting a <code>STARSHIP_CONFIG</code> variable in specific layouts.
After that realization, I made my configuration even <em>more</em> vanilla and made some alternative configurations in my <code>~/.config/direnv/direnvrc</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>
<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='bash'><span class='line'>layout_python<span class="o">()</span> <span class="o">{</span>
</span><span class='line'>    <span class="nv">VIRTUAL_ENV</span><span class="o">=</span><span class="s2">&quot;$(pwd)/.venv&quot;</span>
</span><span class='line'>
</span><span class='line'>    PATH_add <span class="s2">&quot;$VIRTUAL_ENV/bin&quot;</span>
</span><span class='line'>    <span class="nb">export </span>VIRTUAL_ENV
</span><span class='line'>
</span><span class='line'>    <span class="nb">export </span><span class="nv">STARSHIP_CONFIG</span><span class="o">=</span>/home/trey/.config/starship/python.toml
</span><span class='line'><span class="o">}</span>
</span><span class='line'>
</span><span class='line'>layout_git<span class="o">()</span> <span class="o">{</span>
</span><span class='line'>    <span class="nb">export </span><span class="nv">STARSHIP_CONFIG</span><span class="o">=</span>/home/trey/.config/starship/git.toml
</span><span class='line'><span class="o">}</span>
</span></code></pre></td></tr></table></div></figure>


<p>Those other two configuration files are fancier, as I have no concern about them distracting my students since I&rsquo;ll never be within those directories while teaching.</p>

<p>You can find those files in <a href="https://github.com/treyhunner/dotfiles">my dotfiles repository</a>.</p>

<h2>The necessary tools</h2>

<p>So I replaced virtualenvwrapper with direnv, uv, and Starship.
Though direnv is doing most of the important work here.
The use of uv and Starship were just bonuses.</p>

<p>I <em>am</em> also hoping to eventually replace my pipx use with uv and once uv supports <a href="https://github.com/astral-sh/uv/issues/6265">adding python3.x commands</a> to my <code>PATH</code>, I may replace my use of pyenv with uv as well.</p>

<p>Thanks to all who <a href="https://mastodon.social/@treyhunner/113232640710715449">participated in my Mastodon thread</a> as I fumbled through discovering this setup.</p>
]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[10-Week Hands-On Python Course]]></title>
    <link href="https://treyhunner.com/2024/08/python-high-five/"/>
    <updated>2024-08-20T14:20:00-07:00</updated>
    <id>https://treyhunner.com/2024/08/python-high-five</id>
    <content type="html"><![CDATA[<p>Ever wished you could take an <strong>Intro to Python training</strong> with me, but you don&rsquo;t work for a company with a generous training budget?
I&rsquo;m running a Python-learning program just for this situation.</p>

<p><a href="https://www.pythonmorsels.com/high-five/">Python High Five</a> is a 10-week Python jumpstart program that starts <strong>this September</strong>.</p>

<h2>Set aside the time to learn ⌚</h2>

<p>One of the biggest problems for folks starting to learn Python is <strong>setting aside the time</strong>.
And even if you <em>do</em> manage to set aside the time, you&rsquo;ll often hit a roadblock where you feel <strong>confused</strong>.</p>

<p>Python High Five is a way to keep a <strong>daily learning habit</strong> <em>and</em> to <strong>get help</strong> when find yourself stuck.</p>

<p>This program is based around <strong>daily practice</strong>.
Monday through Friday you&rsquo;ll pick <strong>30 minutes</strong> from your schedule, at any time that works you.
During those 30 minutes, you&rsquo;ll watch a 5 minute video, work on the day&rsquo;s exercise, and reflect on your progress.</p>

<h2>The most effective learning is hands-on 🖐️</h2>

<p>Python High Five is all about learning through <strong>writing Python code</strong>.
Each week we&rsquo;ll dive deeper into Python, building upon what we&rsquo;ve learned so far.</p>

<p>When you find yourself stuck you can get help through an asynchronous <strong>group chat</strong> and weekly <strong>office hour</strong> sessions.
In addition to our weekly office hours together, I&rsquo;ll check the chat each day, respond to questions, and provide guidance.</p>

<h2>Proven learning techniques behind the scenes 📝</h2>

<p>The daily check-ins allow for daily <strong>accountability</strong>.
The group chat also provides both a community of peers to rely on, and <strong>guidance from an experienced Python trainer</strong> (me).</p>

<p>We&rsquo;ll also be using proven learning techniques behind the scenes:</p>

<ul>
<li><strong>Retrieval practice</strong>: you don&rsquo;t learn by putting information into your head, but by trying to take it out; for Python learning, that means writing code.</li>
<li><strong>Spaced repetition</strong>: cramming is less effective than learning spaced out over time, which is why we&rsquo;ll spend 30 minutes each weekday instead of spending a few hours every week.</li>
<li><strong>Interleaving</strong>: each day&rsquo;s exercise isn&rsquo;t predictably themed because a bit of unpredictability can be <em>really</em> improve learning outcomes.</li>
<li><strong>Elaboration</strong>: your daily check-in isn&rsquo;t <em>just</em> about reflection: it&rsquo;s also a helpful learning tool!</li>
</ul>


<p>Plus, we&rsquo;ll be working through curriculum I&rsquo;ve been developing and iterating on for many years.
I have taught these topics in many different settings to folks from <em>many</em> different backgrounds.</p>

<h2>Form a daily learning habit 🔁</h2>

<p>Any 10-week program will be <em>just the start</em> of a Python learning habit.
You&rsquo;ll need to keep up your Python after Python High Five ends, either by promptly applying your skills to a new project or diving deeper into Python with continued daily practice.</p>

<p>That&rsquo;s why I&rsquo;m offering an <strong>80% discount</strong> for High Five attendees on one year of <a href="https://www.pythonmorsels.com">Python Morsels</a>, which is my skill-building service designed to help <strong>deepen your Python skills every week</strong>.
You can <a href="https://www.pythonmorsels.com/high-five/#morsels">see more details on that here</a>.</p>

<h2>Ready to start your Python journey? ⛰️</h2>

<p>Are you ready to start your Python journey with a solid foundation?</p>

<p><a href="https://www.pythonmorsels.com/high-five/">Read more about Python High Five</a> and decide whether this is for you.</p>

<p>Keep in mind that while the program begins on September 9, <strong>enrollment closes on August 31</strong>.
So <a href="https://www.pythonmorsels.com/high-five/#faq">check the FAQs</a> and if you have additional questions, be sure to email me soon!</p>
]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Quickly find the right datetime format code for your date]]></title>
    <link href="https://treyhunner.com/2024/08/find-the-datetime-format-code-for-your-date/"/>
    <updated>2024-08-05T11:30:00-07:00</updated>
    <id>https://treyhunner.com/2024/08/find-the-datetime-format-code-for-your-date</id>
    <content type="html"><![CDATA[<p>I often find myself with a string representing a date and time and the need to create a format string that will parse this string into a <code>datetime</code> object.</p>

<p>I decided to make a tool that solves this problem for me: <a href="https://pym.dev/strptime">https://pym.dev/strptime</a></p>

<h2>Finding the code to parse a date format with <code>strptime</code></h2>

<p>Here&rsquo;s how I&rsquo;m now using this new tool.</p>

<p>I find a date string in a random spreadsheet or log file that I need to parse.
For example, the string <code>30-Jun-2024 20:09</code>, which I recently found in a spreadsheet.</p>

<p>I then paste the string into the tool and watch the format appear:</p>

<p><a href="https://pym.dev/strptime"><img src="https://treyhunner.com/images/strptime-1.png"></a></p>

<p>Then I click on the date format to copy-paste it.
That&rsquo;s it!</p>

<p>This tool works by cycling through a number of common formats.
It also works for dates without a time, like <code>Jul 1, 2024</code>.</p>

<p><a href="https://pym.dev/strptime"><img src="https://treyhunner.com/images/strptime-2.png"></a></p>

<p>This input field works great when you&rsquo;re in need of a code for the <code>datetime</code> class&rsquo;s <code>strptime</code> method (which <em>parses</em> dates).
But what if you need a code for <code>strftime</code> (for <em>formatting</em> dates)?</p>

<h2>Finding the code to format a date with <code>strftime</code></h2>

<p>If you don&rsquo;t have a date but instead want to <em>construct</em> a date in a specific common format, scroll down the page a bit.</p>

<p>This page includes a table of common formats.</p>

<p><a href="https://pym.dev/strptime#formats"><img src="https://treyhunner.com/images/strptime-table.png"></a></p>

<p>Click on the format to copy it.
That&rsquo;s it.</p>

<h2>Playing with format codes</h2>

<p>What if you have a date format already but you&rsquo;re not sure what it represents?</p>

<p>Paste it in the box!</p>

<p>For example if you&rsquo;re wondering what the <code>%B</code> in <code>%B %d, %Y</code> means, paste it in to see what that represent with the current date and time:</p>

<p><a href="https://pym.dev/strptime"><img src="https://treyhunner.com/images/strptime-reverse.png"></a></p>

<h2>Other features</h2>

<p>There are a few other hidden features in this tool:</p>

<ul>
<li>After a date or date format is pasted, if it corresponds to one of the formats listed in the table of common formats, that row will be highlighted</li>
<li>Hitting the <code>Enter</code> key anywhere on the page will select the input field</li>
<li>Clicking on a date within the format table will fill that date into the input box</li>
<li>The bottom of the page includes links to other useful datetime formatting/parsing tools as well as a link to the relevant Python documentation</li>
</ul>


<h2>Thoughts? Feature requests?</h2>

<p>What do you think of this tool?</p>

<p>Is this something you&rsquo;d bookmark and use often?
Is this missing a key feature that you would need for it to be valuable for your use?</p>

<p>Are there date and time formats you&rsquo;d like to see that don&rsquo;t seem to be supported yet?</p>

<p>Comment or email me to let me know!</p>
]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Why does "python -m json" not work? Why is it "json.tool"?]]></title>
    <link href="https://treyhunner.com/2024/08/why-does-python-m-json-not-work/"/>
    <updated>2024-08-01T14:00:00-07:00</updated>
    <id>https://treyhunner.com/2024/08/why-does-python-m-json-not-work</id>
    <content type="html"><![CDATA[<p><strong>Update</strong>: upon the encouragement of a few CPython core team members, I <a href="https://github.com/python/cpython/pull/122884">opened a pull request</a> to add this to Python 3.14.</p>

<p>Have you ever used Python&rsquo;s <code>json.tool</code> command-line interface?</p>

<p>You can run <code>python -m json.tool</code> against a JSON file and Python will print a nicely formatted version of the 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>
</pre></td><td class='code'><pre><code class='bash'><span class='line'>python -m json.tool example.json
</span><span class='line'><span class="o">[</span>
</span><span class='line'>    1,
</span><span class='line'>    2,
</span><span class='line'>    3,
</span><span class='line'>    4
</span><span class='line'><span class="o">]</span>
</span></code></pre></td></tr></table></div></figure>


<p>Why do we need to run <code>json.tool</code> instead of <code>json</code>?</p>

<h2>The history of <code>python -m</code></h2>

<p>Python 3.5 added the ability to run a module as a command-line script using the <code>-m</code> argument (see <a href="https://peps.python.org/pep-0338/">PEP 338</a>) which was implemented in Python 2.5.
While that feature was being an additional feature/bug was accidentally added, alowing packages to be run with the <code>-m</code> flag as well.
When a package was run with the <code>-m</code> flag, the package&rsquo;s <code>__init__.py</code> file would be run, with the <code>__name__</code> variable set to <code>__main__</code>.</p>

<p>This accidental feature/bug was <a href="https://github.com/python/cpython/issues/47000">removed in Python 2.6</a>.</p>

<p>It wasn&rsquo;t until Python 2.7 that the ability to run <code>python -m package</code> was re-added (see below).</p>

<h2>The history of the <code>json</code> module</h2>

<p>The <code>json</code> module was <a href="https://docs.python.org/3/whatsnew/2.6.html#the-json-module-javascript-object-notation">added in Python 2.6</a>.
It was based on the third-party <code>simplejson</code> library.</p>

<p>That library originally relied on the fact that Python packages could be run with the <code>-m</code> flag to run the package&rsquo;s <code>__init__.py</code> file with <code>__name__</code> set to <code>__main__</code> (see <a href="https://github.com/simplejson/simplejson/blob/v1.8.1/simplejson/__init__.py#L368">this code from version 1.8.1</a>).</p>

<p>When <code>simplejson</code> was added to Python as the <code>json</code> module in Python 2.6, this bug/feature could no longer be relied upon as it was fixed in Python 2.6.
To continue allowing for a nice command-line interface, it was decided that running a <code>tool</code> submodule would be the way to add a command-line interface to the <code>json</code> package (<a href="https://github.com/simplejson/simplejson/commit/74d9c5c4c4339db47dfa86bf37858cae80ed3776">discussion here</a>).</p>

<p>Python 2.7 <a href="https://docs.python.org/2.7/using/cmdline.html?highlight=__main__#cmdoption-m">added the ability to run any package as a command-line tool</a>.
The package would simply need a <code>__main__.py</code> file within it, which Python would run as the entry point to the package.</p>

<p>At this point, <code>json.tool</code> had already been added in Python 2.6 and no attempt was made (as far as I can tell) to allow <code>python -m json</code> to work instead.
Once you&rsquo;ve added a feature, removing or changing it can be painful.</p>

<h2>Could we make <code>python -m json</code> work today?</h2>

<p>We could.
We would just need to <a href="https://github.com/treyhunner/cpython/commit/1226315e2df0d4229558734d5f0d50f1386a025e">rename <code>tool.py</code> to <code>__main__.py</code></a>.
To allow <code>json.tool</code> to still work <em>also</em>, would could <a href="https://github.com/python/cpython/commit/7ce95d21886c7ad5278c07c1a20cda5bebab4731">make a new <code>tool.py</code> module</a> that simply imports <code>json.__main__</code>.</p>

<p>We could even go so far as to <a href="https://github.com/treyhunner/cpython/commit/ae4ca62346c690e1c6aaf1ccfed37069984b5d67">deprecate <code>json.tool</code></a> if we wanted to.</p>

<p>Should we do this though? 🤔</p>

<h2>Should we make <code>python -m json</code> work?</h2>

<p>I don&rsquo;t know about you, but I would rather type <code>python -m json</code> than <code>python -m json.tool</code>.
It&rsquo;s more memorable <em>and</em> easier to guess, if you&rsquo;re not someone who has memorized <a href="https://www.pythonmorsels.com/cli-tools/">all the command-line tools hiding in Python</a>.</p>

<p>But would this actually be used?
I mean, don&rsquo;t people just use the <a href="https://jqlang.github.io/jq/">jq</a> tool instead?</p>

<p>Well, a <code>sqlite3</code> shell was <a href="https://docs.python.org/3/library/sqlite3.html#command-line-interface">added to Python 3.12</a> despite the fact that third-party interactive sqlite prompts are fairly common.</p>

<p>It is pretty handy to have a access to a tool within an unfamiliar environment where installing yet-another-tool might pose a problem.
Think Docker, a locked-down Windows machine, or any computer without network or with network restrictions.</p>

<p>Personally, I&rsquo;d like to see <code>python3 -m json</code> work.
I can&rsquo;t think of any big downsides.
Can you?</p>

<p><strong>Update</strong>: <a href="https://github.com/python/cpython/pull/122884">pull request opened</a>.</p>

<h2>Too long; didn&rsquo;t read</h2>

<p>The &ldquo;too long didn&rsquo;t read version&rdquo;:</p>

<ul>
<li>Python 2.5 added support for the <code>-m</code> argument for <em>modules</em>, but not <em>packages</em></li>
<li>A third-party <code>simplejson</code> app existed with a nice CLI that relied on a <code>-m</code> implementation bug allowing packages to be run using <code>-m</code></li>
<li>Python 2.6 fixed that implementation quirk and broke the previous ability to run <code>python -m simplejson</code></li>
<li>Python 2.6 also added the <code>json</code> module, which was based on this third-party <code>simplejson</code> package</li>
<li>Since <code>python -m json</code> couldn&rsquo;t work anymore, the ability to run <code>python -m json.tool</code> was added</li>
<li>Python 2.7 added official support for <code>python -m some_package</code> by running a <code>__main__</code> submodule</li>
<li>The <code>json.tool</code> module already existed in Python 2.6 and the ability to run <code>python -m json</code> was (as far as I can tell) never seriously considered</li>
</ul>


<h2>All thanks to git history and issue trackers</h2>

<p>I discovered this by noting <a href="https://github.com/simplejson/simplejson/commit/74d9c5c4c4339db47dfa86bf37858cae80ed3776">the first commit</a> that added the <code>tool</code> submodule to <code>simplejson</code>, which notes the fact that this was for consistency with the new <code>json</code> standard library module.</p>

<p>Thank you git history.
And thank you to the folks who brought us the <code>simplejson</code> library, the <code>json</code> module, and the ability to use <code>-m</code> on both a module and a package!</p>

<p>Also, thank you to Alyssa Coghlan, Hugo van Kemenade, and Adam Turner for reviewing my pull request to add this feature to Python 3.14. 💖</p>
]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[GPT and Claude from your URL bar]]></title>
    <link href="https://treyhunner.com/2024/07/chatgpt-and-claude-from-your-browser-url-bar/"/>
    <updated>2024-07-11T09:00:00-07:00</updated>
    <id>https://treyhunner.com/2024/07/chatgpt-and-claude-from-your-browser-url-bar</id>
    <content type="html"><![CDATA[<p>I&rsquo;ve been playing with using <a href="https://kagi.com">Kagi</a> and <a href="https://www.perplexity.ai">Perplexity</a> as my browser&rsquo;s default search engine recently.</p>

<p>Currently I have Kagi setup as my default search engine and I added a <a href="https://help.kagi.com/kagi/features/bangs.html#custom-bangs">custom bang</a> command in my Kagi account so that <strong>!x</strong> will trigger a search with Perplexity.</p>

<p>This got me thinking&hellip; could I trigger a new <a href="https://chatgpt.com">Chat GPT</a> or <a href="https://claude.ai">Claude</a> chat from my browser&rsquo;s URL bar?</p>

<h2>How browsers query search engines</h2>

<p>Whether you&rsquo;re using <a href="https://superuser.com/questions/1772248/how-to-add-custom-search-engine-to-chrome">Chrome</a>, <a href="https://support.mozilla.org/en-US/kb/change-your-default-search-settings-firefox">Firefox</a>, or some other browser (I&rsquo;m trying <a href="https://vivaldi.com/blog/search-favorite-websites-quickly/">Vivaldi</a>) you can add custom search engines to your browser.</p>

<p>Custom search engines use a template URL that looks something like this:</p>

<p><code>https://duckduckgo.com/?q=%s</code></p>

<p>Note that this URL is a <em>template</em> because of that <code>%s</code>.
That <code>%s</code> bit will be replaced by the actual query you search for.</p>

<p>So if Chat GPT and Claude have URLs that allow creating a new chat via a URL&rsquo;s query string (the thing after the <code>?</code>), then they could be used as search engines.</p>

<p>And it turns out&hellip; they do!</p>

<h2>Querying Chat GPT via a URL</h2>

<p>To query Chat GPT via a URL query string, use:</p>

<p><code>https://chatgpt.com/?q=%s</code></p>

<p>Set that URL as a custom &ldquo;search engine&rdquo; in your browser.
Then whenever you&rsquo;d like to start a new Chat GPT conversation, select your browser&rsquo;s URL bar, type the search engine prefix followed by a space, type your query, and hit Enter!</p>

<h2>Querying Claude via a URL</h2>

<p>To query Claude via a URL query string, use:</p>

<p><code>https://claude.ai/new?q=%s</code></p>

<p>It works the same way as the Chat GPT URL.</p>

<h2>Querying Perplexity via a URL</h2>

<p>This may be a bit redundant, as you may have thought to use Perplexity can be used as a &ldquo;search engine&rdquo; in your browser, but here&rsquo;s the URL in case you&rsquo;re looking for it:</p>

<p><code>https://www.perplexity.ai/search?q=%s</code></p>

<h2>Kagi bangs</h2>

<p>If you&rsquo;re a <a href="https://kagi.com">Kagi</a> user, you can setup each of these as a custom bang.</p>

<p><strong>Chat GPT</strong></p>

<p><img src="https://treyhunner.com/images/kagi-chatgpt-bang.png" title="Screenshot of Chat GPT bang settings for Chat GPT" ></p>

<p><strong>Claude</strong></p>

<p><img src="https://treyhunner.com/images/kagi-claude-bang.png" title="Screenshot of Chat GPT bang settings for Claude" ></p>

<p><strong>Perplexity</strong></p>

<p><img src="https://treyhunner.com/images/kagi-perplexity-bang.png" title="Screenshot of Perplexity bang settings for Kagi" ></p>

<p>I&rsquo;m not sure what bangs I ultimately want to use in the long-term.
Right now I have <strong>!x</strong> set to query Perplexity, <strong>!c</strong> set to query Chat GPT, and <strong>!l</strong> set to query Claude.</p>

<p>I was considering <strong>!g</strong> for Chat <strong>G</strong>PT but that searches Google already and I was considering <strong>!c</strong> for Claude, but I had already set that to search Chat GPT, so I went with <strong>!l</strong> for C<strong>l</strong>aude&hellip; or maybe it&rsquo;s <strong>!l</strong> for <strong>L</strong>LM. 🤔</p>

<h2>No Gemini &ldquo;search&rdquo; support</h2>

<p>So Claude, Chat GPT, and Kagi can all initiate new conversations directly from your browser&rsquo;s URL bar using these 3 URLs:</p>

<ul>
<li><code>https://chatgpt.com/?q=%s</code></li>
<li><code>https://claude.ai/new?q=%s</code></li>
<li><code>https://www.perplexity.ai/search?q=%s</code></li>
</ul>


<p>As far as I can tell, Gemini does not support starting a conversation via a query string.
Maybe this is related to Google embedding a similar feature <a href="https://chromeunboxed.com/chromes-url-bar-is-getting-quick-access-to-gemini-ai/">directly into Chrome</a>. 🤷</p>
]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[A beautiful Python monstrosity]]></title>
    <link href="https://treyhunner.com/2024/06/a-beautiful-python-monstrosity/"/>
    <updated>2024-06-08T14:30:00-07:00</updated>
    <id>https://treyhunner.com/2024/06/a-beautiful-python-monstrosity</id>
    <content type="html"><![CDATA[<p>Creating performance tests for <a href="https://www.pythonmorsels.com">Python Morsels</a> exercises is a frequent annoyance</p>

<p>I loathe writing automated tests for performance-related exercises because they&rsquo;re <em>always</em> flaky.
How flaky depends on the exercise, what I&rsquo;m testing, and the time variability inherent in the particular Python features that a learner might use.</p>

<p>I came up with a solution for flaky tests recently, but it also makes my tests less readable.
I then came up with a tool to improve the readability, but that has its own trade-offs.</p>

<p>The code I eventually came up with is a <strong>beautiful Python monstrosity</strong>.</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="nd">@attempt_n_times</span><span class="p">(</span><span class="mi">10</span><span class="p">)</span>
</span><span class='line'><span class="k">def</span> <span class="nf">_</span><span class="p">():</span>
</span><span class='line'>    <span class="n">nonlocal</span> <span class="n">micro_time</span><span class="p">,</span> <span class="n">tiny_time</span>
</span><span class='line'>    <span class="n">micro_time</span> <span class="o">=</span> <span class="n">time</span><span class="p">(</span><span class="n">micro_numbers</span><span class="p">)</span>
</span><span class='line'>    <span class="n">tiny_time</span> <span class="o">=</span> <span class="n">time</span><span class="p">(</span><span class="n">tiny_numbers</span><span class="p">)</span>
</span><span class='line'>    <span class="bp">self</span><span class="o">.</span><span class="n">assertLess</span><span class="p">(</span><span class="n">tiny_time</span><span class="p">,</span> <span class="n">micro_time</span><span class="o">*</span><span class="n">n</span><span class="p">)</span>
</span></code></pre></td></tr></table></div></figure>


<p>I&rsquo;ll explain what that code does, but first let&rsquo;s talk about why it&rsquo;s needed.</p>

<h2>The flaky performance tests</h2>

<p>My flaky performance tests initially looked 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>
</pre></td><td class='code'><pre><code class='python'><span class='line'><span class="k">def</span> <span class="nf">test_some_test</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
</span><span class='line'>    <span class="n">n</span><span class="p">,</span> <span class="n">m</span> <span class="o">=</span> <span class="mf">2.45</span><span class="p">,</span> <span class="mf">2.04</span>
</span><span class='line'>
</span><span class='line'>    <span class="n">micro_time</span> <span class="o">=</span> <span class="n">time</span><span class="p">(</span><span class="n">micro_numbers</span><span class="p">)</span>
</span><span class='line'>    <span class="n">tiny_time</span> <span class="o">=</span> <span class="n">time</span><span class="p">(</span><span class="n">tiny_numbers</span><span class="p">)</span>
</span><span class='line'>    <span class="bp">self</span><span class="o">.</span><span class="n">assertLess</span><span class="p">(</span><span class="n">tiny_time</span><span class="p">,</span> <span class="n">micro_time</span><span class="o">*</span><span class="n">n</span><span class="p">)</span>
</span><span class='line'>
</span><span class='line'>    <span class="n">small_time</span> <span class="o">=</span> <span class="n">time</span><span class="p">(</span><span class="n">small_numbers</span><span class="p">)</span>
</span><span class='line'>    <span class="bp">self</span><span class="o">.</span><span class="n">assertLess</span><span class="p">(</span><span class="n">small_time</span><span class="p">,</span> <span class="n">tiny_time</span><span class="o">*</span><span class="n">n</span><span class="p">)</span>
</span><span class='line'>    <span class="bp">self</span><span class="o">.</span><span class="n">assertLess</span><span class="p">(</span><span class="n">small_time</span><span class="p">,</span> <span class="n">micro_time</span><span class="o">*</span><span class="n">n</span><span class="o">*</span><span class="n">m</span><span class="p">)</span>
</span><span class='line'>
</span><span class='line'>    <span class="n">medium_time</span> <span class="o">=</span> <span class="n">time</span><span class="p">(</span><span class="n">medium_numbers</span><span class="p">)</span>
</span><span class='line'>    <span class="bp">self</span><span class="o">.</span><span class="n">assertLess</span><span class="p">(</span><span class="n">medium_time</span><span class="p">,</span> <span class="n">micro_time</span><span class="o">*</span><span class="n">n</span><span class="o">*</span><span class="n">m</span><span class="o">*</span><span class="n">m</span><span class="p">)</span>
</span><span class='line'>    <span class="bp">self</span><span class="o">.</span><span class="n">assertLess</span><span class="p">(</span><span class="n">medium_time</span><span class="p">,</span> <span class="n">tiny_time</span><span class="o">*</span><span class="n">n</span><span class="o">*</span><span class="n">m</span><span class="p">)</span>
</span><span class='line'>    <span class="bp">self</span><span class="o">.</span><span class="n">assertLess</span><span class="p">(</span><span class="n">medium_time</span><span class="p">,</span> <span class="n">small_time</span><span class="o">*</span><span class="n">n</span><span class="p">)</span>
</span></code></pre></td></tr></table></div></figure>


<p>The first block runs a performance test for the user&rsquo;s function on a very small list and on a slightly larger list and then asserting that the slightly larger list didn&rsquo;t take <em>too</em> much longer to run.
The next two blocks run the same code on even larger lists and make further assertions about the relative times that the code took to run.</p>

<p>This roughly approximates the <a href="https://www.pythonmorsels.com/time-complexities/">time complexity</a> of this code.</p>

<h2>Running performance checks in a loop</h2>

<p>These performance checks need to:</p>

<ol>
<li>Predictably fail for inefficient solutions</li>
<li>Predictably pass for efficient solutions</li>
<li>Run <em>fast</em> (within just a few seconds) even when the code is inefficient</li>
<li>Avoid the use of <code>threading</code> because they&rsquo;ll be running on WebAssembly in the browser</li>
<li>Run consistently on pretty much any computer</li>
</ol>


<p>These 5 requirements together have caused me countless headaches.
I get the tests passing well, but they don&rsquo;t always fail when they should.
I get the tests failing and passing when they should, but then they&rsquo;re too slow.
And so on&hellip;</p>

<p>Notice the <code>n</code> and <code>m</code> factors in the above assertions:</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='python'><span class='line'><span class="bp">self</span><span class="o">.</span><span class="n">assertLess</span><span class="p">(</span><span class="n">small_time</span><span class="p">,</span> <span class="n">micro_time</span><span class="o">*</span><span class="n">n</span><span class="o">*</span><span class="n">m</span><span class="p">)</span>
</span></code></pre></td></tr></table></div></figure>


<p>If <code>n</code> and <code>m</code> are too big, we&rsquo;ll get false positives (tests passing when they should fail).
If <code>n</code> and <code>m</code> are too small, we&rsquo;ll get false negatives (tests failing when they should pass).</p>

<p>To avoid both <a href="https://en.wikipedia.org/wiki/Type_I_and_type_II_errors">Type I and Type II errors</a>, I decided to keep <code>n</code> and <code>m</code> small but attempt the assertion block multiple times.</p>

<p>Here&rsquo;s the (far less flaky) revised 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>
<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='python'><span class='line'><span class="k">def</span> <span class="nf">test_some_test</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
</span><span class='line'>    <span class="n">n</span><span class="p">,</span> <span class="n">m</span> <span class="o">=</span> <span class="mf">2.45</span><span class="p">,</span> <span class="mf">2.04</span>
</span><span class='line'>
</span><span class='line'>    <span class="k">for</span> <span class="n">attempts_left</span> <span class="ow">in</span> <span class="nb">reversed</span><span class="p">(</span><span class="nb">range</span><span class="p">(</span><span class="mi">10</span><span class="p">)):</span>
</span><span class='line'>        <span class="k">try</span><span class="p">:</span>
</span><span class='line'>            <span class="n">micro_time</span> <span class="o">=</span> <span class="n">time</span><span class="p">(</span><span class="n">micro_numbers</span><span class="p">)</span>
</span><span class='line'>            <span class="n">tiny_time</span> <span class="o">=</span> <span class="n">time</span><span class="p">(</span><span class="n">tiny_numbers</span><span class="p">)</span>
</span><span class='line'>            <span class="bp">self</span><span class="o">.</span><span class="n">assertLess</span><span class="p">(</span><span class="n">tiny_time</span><span class="p">,</span> <span class="n">micro_time</span><span class="o">*</span><span class="n">n</span><span class="p">)</span>
</span><span class='line'>            <span class="k">break</span>
</span><span class='line'>        <span class="k">except</span> <span class="ne">AssertionError</span><span class="p">:</span>
</span><span class='line'>            <span class="k">if</span> <span class="n">attempts_left</span> <span class="o">==</span> <span class="mi">0</span><span class="p">:</span>
</span><span class='line'>                <span class="k">raise</span>
</span><span class='line'>
</span><span class='line'>    <span class="k">for</span> <span class="n">attempts_left</span> <span class="ow">in</span> <span class="nb">reversed</span><span class="p">(</span><span class="nb">range</span><span class="p">(</span><span class="mi">5</span><span class="p">)):</span>
</span><span class='line'>        <span class="k">try</span><span class="p">:</span>
</span><span class='line'>            <span class="n">small_time</span> <span class="o">=</span> <span class="n">time</span><span class="p">(</span><span class="n">small_numbers</span><span class="p">)</span>
</span><span class='line'>            <span class="bp">self</span><span class="o">.</span><span class="n">assertLess</span><span class="p">(</span><span class="n">small_time</span><span class="p">,</span> <span class="n">tiny_time</span><span class="o">*</span><span class="n">n</span><span class="p">)</span>
</span><span class='line'>            <span class="bp">self</span><span class="o">.</span><span class="n">assertLess</span><span class="p">(</span><span class="n">small_time</span><span class="p">,</span> <span class="n">micro_time</span><span class="o">*</span><span class="n">n</span><span class="o">*</span><span class="n">m</span><span class="p">)</span>
</span><span class='line'>            <span class="k">break</span>
</span><span class='line'>        <span class="k">except</span> <span class="ne">AssertionError</span><span class="p">:</span>
</span><span class='line'>            <span class="k">if</span> <span class="n">attempts_left</span> <span class="o">==</span> <span class="mi">0</span><span class="p">:</span>
</span><span class='line'>                <span class="k">raise</span>
</span><span class='line'>
</span><span class='line'><span class="k">for</span> <span class="n">attempts_left</span> <span class="ow">in</span> <span class="nb">reversed</span><span class="p">(</span><span class="nb">range</span><span class="p">(</span><span class="mi">3</span><span class="p">)):</span>
</span><span class='line'>    <span class="k">try</span><span class="p">:</span>
</span><span class='line'>        <span class="n">medium_time</span> <span class="o">=</span> <span class="n">time</span><span class="p">(</span><span class="n">medium_numbers</span><span class="p">)</span>
</span><span class='line'>        <span class="bp">self</span><span class="o">.</span><span class="n">assertLess</span><span class="p">(</span><span class="n">medium_time</span><span class="p">,</span> <span class="n">micro_time</span><span class="o">*</span><span class="n">n</span><span class="o">*</span><span class="n">m</span><span class="o">*</span><span class="n">m</span><span class="p">)</span>
</span><span class='line'>        <span class="bp">self</span><span class="o">.</span><span class="n">assertLess</span><span class="p">(</span><span class="n">medium_time</span><span class="p">,</span> <span class="n">tiny_time</span><span class="o">*</span><span class="n">n</span><span class="o">*</span><span class="n">m</span><span class="p">)</span>
</span><span class='line'>        <span class="bp">self</span><span class="o">.</span><span class="n">assertLess</span><span class="p">(</span><span class="n">medium_time</span><span class="p">,</span> <span class="n">small_time</span><span class="o">*</span><span class="n">n</span><span class="p">)</span>
</span><span class='line'>        <span class="k">break</span>
</span><span class='line'>    <span class="k">except</span> <span class="ne">AssertionError</span><span class="p">:</span>
</span><span class='line'>        <span class="k">if</span> <span class="n">attempts_left</span> <span class="o">==</span> <span class="mi">0</span><span class="p">:</span>
</span><span class='line'>            <span class="k">raise</span>
</span></code></pre></td></tr></table></div></figure>


<p>The <code>for</code> loop runs the code multiple times, the <code>break</code> statement stops the code as soon as the assertions all pass, and the <code>except</code> and <code>if</code> ensure that any assertion errors are suppressed until/unless we&rsquo;re on the final iteration of the loop.</p>

<p>Let&rsquo;s call this a <code>for</code>-<code>try</code>-<code>break</code>-<code>except</code>-<code>if</code>-<code>raise</code> pattern.
It&rsquo;s an absurdly verbose name fitting of absurdly verbose code.</p>

<p>This <code>for</code>-<code>try</code>-<code>break</code>-<code>except</code>-<code>if</code>-<code>raise</code> pattern works pretty well!
But it&rsquo;s not pretty.</p>

<p>Like many programmers, I believe that <strong>Don&rsquo;t Repeat Yourself</strong> (<a href="https://en.wikipedia.org/wiki/Don%27t_repeat_yourself">DRY</a>) need not apply to tests.
Tests are <em>allowed</em> to be repetitive if <a href="https://stackoverflow.com/questions/6453235/what-does-damp-not-dry-mean-when-talking-about-unit-tests">the verbosity improves readability</a>.</p>

<p>But there is <em>so much noise</em> in that code!
I decided that removing some noise might improve readability.
So I devised a helper utility to reduce the repetition.</p>

<h2>In search of a solution</h2>

<p>While pondering the repetitive noise in this code, I wondered what Python features I could use to abstract away this <code>for</code>-<code>try</code>-<code>break</code>-<code>except</code>-<code>if</code>-<code>raise</code> pattern.</p>

<p>Could I make a context manager and use a <code>with</code> block?
That might help with the <code>try</code>-<code>except</code>, but context managers can&rsquo;t run their code block multiple times, so that wouldn&rsquo;t help with the <code>for</code> and the <code>break</code>.
So a context manager is out.</p>

<p>Could I abstract this away into a looping helper by implementing a generator function?
We <em>are</em> looping and generator functions <em>can</em> <code>break</code> early.
But, a generator function can&rsquo;t catch an exception that&rsquo;s raised within the <em>body</em> of a loop.
So a generator function wouldn&rsquo;t work either.</p>

<p>What about a decorator? 🤔</p>

<p>Context managers and decorators both sandwich a block of code.
But decorators sandwich functions and they have the power to run the same function <em>repeatedly</em>.
A decorator might work!</p>

<p>Here&rsquo;s a decorator that will run a given function up to 10 times (until no <code>AssertionError</code> is raised):</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>
</pre></td><td class='code'><pre><code class='python'><span class='line'><span class="k">def</span> <span class="nf">try_10_times</span><span class="p">(</span><span class="n">function</span><span class="p">):</span>
</span><span class='line'>    <span class="k">def</span> <span class="nf">wrapper</span><span class="p">():</span>
</span><span class='line'>        <span class="k">for</span> <span class="n">attempts_left</span> <span class="ow">in</span> <span class="nb">reversed</span><span class="p">(</span><span class="nb">range</span><span class="p">(</span><span class="mi">10</span><span class="p">)):</span>
</span><span class='line'>            <span class="k">try</span><span class="p">:</span>
</span><span class='line'>                <span class="k">return</span> <span class="n">function</span><span class="p">()</span>
</span><span class='line'>            <span class="k">except</span> <span class="ne">AssertionError</span><span class="p">:</span>
</span><span class='line'>                <span class="k">if</span> <span class="n">attempts_left</span> <span class="o">==</span> <span class="mi">0</span><span class="p">:</span>
</span><span class='line'>                    <span class="k">raise</span>
</span><span class='line'>    <span class="k">return</span> <span class="n">wrapper</span>
</span></code></pre></td></tr></table></div></figure>


<p>To use this decorator, we would need to define a function and then call that function:</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="nd">@try_10_times</span>
</span><span class='line'><span class="k">def</span> <span class="nf">assertions</span><span class="p">():</span>
</span><span class='line'>    <span class="n">micro_time</span> <span class="o">=</span> <span class="n">time</span><span class="p">(</span><span class="n">micro_numbers</span><span class="p">)</span>
</span><span class='line'>    <span class="n">tiny_time</span> <span class="o">=</span> <span class="n">time</span><span class="p">(</span><span class="n">tiny_numbers</span><span class="p">)</span>
</span><span class='line'>    <span class="bp">self</span><span class="o">.</span><span class="n">assertLess</span><span class="p">(</span><span class="n">tiny_time</span><span class="p">,</span> <span class="n">micro_time</span><span class="o">*</span><span class="n">n</span><span class="p">)</span>
</span><span class='line'>
</span><span class='line'><span class="n">assertions</span><span class="p">()</span>
</span></code></pre></td></tr></table></div></figure>


<p>This isn&rsquo;t <em>quite</em> good enough though&hellip;</p>

<ol>
<li>We need a pattern to run code N times (not necessarily exactly 10)</li>
<li>We reference the variables defined in each block in later blocks, so <code>micro_time</code> and <code>tiny_time</code> will need to be available <em>outside</em> that function</li>
<li>We need this function to run just one time right after it&rsquo;s defined&hellip; could we do that automatically?</li>
</ol>


<p>All 3 of these problems are solvable:</p>

<ol>
<li>We need a decorator that accepts arguments</li>
<li>We need to use <em>rarely seen</em> <a href="https://stackoverflow.com/a/1261961/98187"><code>nonlocal</code></a> statement</li>
<li>We could have the decorator automatically call the decorated function</li>
</ol>


<h2>The final <em>weird</em> decorator</h2>

<p>Here&rsquo;s the decorator I ended up with:</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="k">def</span> <span class="nf">attempt_n_times</span><span class="p">(</span><span class="n">n</span><span class="p">):</span>
</span><span class='line'>    <span class="sd">&quot;&quot;&quot;</span>
</span><span class='line'><span class="sd">    Run tests multiple times if assertions are raised.</span>
</span><span class='line'>
</span><span class='line'><span class="sd">    Allows for more forgiving tests when assertions may be a bit flaky.</span>
</span><span class='line'><span class="sd">    &quot;&quot;&quot;</span>
</span><span class='line'>    <span class="k">def</span> <span class="nf">decorator</span><span class="p">(</span><span class="n">function</span><span class="p">):</span>
</span><span class='line'>        <span class="sd">&quot;&quot;&quot;This looks like a decorator, but it actually runs the function!&quot;&quot;&quot;</span>
</span><span class='line'>        <span class="k">for</span> <span class="n">attempts_left</span> <span class="ow">in</span> <span class="nb">reversed</span><span class="p">(</span><span class="nb">range</span><span class="p">(</span><span class="n">n</span><span class="p">)):</span>
</span><span class='line'>            <span class="k">try</span><span class="p">:</span>
</span><span class='line'>                <span class="k">return</span> <span class="n">function</span><span class="p">()</span>
</span><span class='line'>            <span class="k">except</span> <span class="ne">AssertionError</span><span class="p">:</span>
</span><span class='line'>                <span class="k">if</span> <span class="n">attempts_left</span> <span class="o">==</span> <span class="mi">0</span><span class="p">:</span>
</span><span class='line'>                    <span class="k">raise</span>
</span><span class='line'>    <span class="k">return</span> <span class="n">decorator</span>
</span></code></pre></td></tr></table></div></figure>


<p>This decorator accepts an <code>n</code> argument which determines the maximum number of times the decorated function should be called.
The decorator then <em>calls</em> the function repeatedly in a <code>for</code> loop and a <code>try</code>-<code>except</code> block.
As soon as an <code>AssertionError</code> is <em>not</em> raised during one of these function calls, the looping stops.</p>

<p>The <em>weirdest</em> part about this decorator is that it calls the decorated function.
Note that the <code>decorator</code> function doesn&rsquo;t define a <code>wrapper</code> function within itself&hellip; it just runs code right away!</p>

<h2>The resulting beautiful Python monstrosity</h2>

<p>Here&rsquo;s the final refactored test 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">test_some_test</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
</span><span class='line'>    <span class="n">n</span><span class="p">,</span> <span class="n">m</span> <span class="o">=</span> <span class="mf">2.45</span><span class="p">,</span> <span class="mf">2.04</span>
</span><span class='line'>    <span class="n">micro_time</span> <span class="o">=</span> <span class="n">tiny_time</span> <span class="o">=</span> <span class="n">small_time</span> <span class="o">=</span> <span class="n">medium_time</span> <span class="o">=</span> <span class="mi">0</span>
</span><span class='line'>
</span><span class='line'>    <span class="nd">@attempt_n_times</span><span class="p">(</span><span class="mi">10</span><span class="p">)</span>
</span><span class='line'>    <span class="k">def</span> <span class="nf">_</span><span class="p">():</span>
</span><span class='line'>        <span class="n">nonlocal</span> <span class="n">micro_time</span><span class="p">,</span> <span class="n">tiny_time</span>
</span><span class='line'>        <span class="n">micro_time</span> <span class="o">=</span> <span class="n">time</span><span class="p">(</span><span class="n">micro_numbers</span><span class="p">)</span>
</span><span class='line'>        <span class="n">tiny_time</span> <span class="o">=</span> <span class="n">time</span><span class="p">(</span><span class="n">tiny_numbers</span><span class="p">)</span>
</span><span class='line'>        <span class="bp">self</span><span class="o">.</span><span class="n">assertLess</span><span class="p">(</span><span class="n">tiny_time</span><span class="p">,</span> <span class="n">micro_time</span><span class="o">*</span><span class="n">n</span><span class="p">)</span>
</span><span class='line'>
</span><span class='line'>    <span class="nd">@attempt_n_times</span><span class="p">(</span><span class="mi">5</span><span class="p">)</span>
</span><span class='line'>    <span class="k">def</span> <span class="nf">_</span><span class="p">():</span>
</span><span class='line'>        <span class="n">nonlocal</span> <span class="n">small_time</span>
</span><span class='line'>        <span class="n">small_time</span> <span class="o">=</span> <span class="n">time</span><span class="p">(</span><span class="n">small_numbers</span><span class="p">)</span>
</span><span class='line'>        <span class="bp">self</span><span class="o">.</span><span class="n">assertLess</span><span class="p">(</span><span class="n">small_time</span><span class="p">,</span> <span class="n">tiny_time</span><span class="o">*</span><span class="n">n</span><span class="p">)</span>
</span><span class='line'>        <span class="bp">self</span><span class="o">.</span><span class="n">assertLess</span><span class="p">(</span><span class="n">small_time</span><span class="p">,</span> <span class="n">micro_time</span><span class="o">*</span><span class="n">n</span><span class="o">*</span><span class="n">m</span><span class="p">)</span>
</span><span class='line'>
</span><span class='line'>    <span class="nd">@attempt_n_times</span><span class="p">(</span><span class="mi">3</span><span class="p">)</span>
</span><span class='line'>    <span class="k">def</span> <span class="nf">_</span><span class="p">():</span>
</span><span class='line'>        <span class="n">nonlocal</span> <span class="n">medium_time</span>
</span><span class='line'>        <span class="n">medium_time</span> <span class="o">=</span> <span class="n">time</span><span class="p">(</span><span class="n">medium_numbers</span><span class="p">)</span>
</span><span class='line'>        <span class="bp">self</span><span class="o">.</span><span class="n">assertLess</span><span class="p">(</span><span class="n">medium_time</span><span class="p">,</span> <span class="n">micro_time</span><span class="o">*</span><span class="n">n</span><span class="o">*</span><span class="n">m</span><span class="o">*</span><span class="n">m</span><span class="p">)</span>
</span><span class='line'>        <span class="bp">self</span><span class="o">.</span><span class="n">assertLess</span><span class="p">(</span><span class="n">medium_time</span><span class="p">,</span> <span class="n">tiny_time</span><span class="o">*</span><span class="n">n</span><span class="o">*</span><span class="n">m</span><span class="p">)</span>
</span><span class='line'>        <span class="bp">self</span><span class="o">.</span><span class="n">assertLess</span><span class="p">(</span><span class="n">medium_time</span><span class="p">,</span> <span class="n">small_time</span><span class="o">*</span><span class="n">n</span><span class="p">)</span>
</span></code></pre></td></tr></table></div></figure>


<p>The <code>attempt_n_times</code> decorator <strong>immediately calls the function it decorates</strong>.
Each function is defined and immediately called one or more times, in a <code>try</code>-<code>except</code> block within a loop.</p>

<p>That&rsquo;s why we&rsquo;ve named these functions with the <a href="https://stackoverflow.com/questions/36315309/how-does-python-throw-away-variable-work">throwaway</a> <code>_</code> name: <strong>we don&rsquo;t care about the name of a function we&rsquo;re never going to refer to again</strong>.</p>

<p>Also note the use of the <code>nonlocal</code> statement.
Each function in Python has its own scope and all assignments <a href="https://www.pythonmorsels.com/local-and-global-variables/#assigning-to-local-and-global-variables">assign to the local scope</a> by default.
That <code>nonlocal</code> variable pulls those variables to the scope of the outer function instead.</p>

<p>Compare the above code to the code just before this refactor:</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>
</pre></td><td class='code'><pre><code class='python'><span class='line'><span class="k">def</span> <span class="nf">test_some_test</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
</span><span class='line'>    <span class="n">n</span><span class="p">,</span> <span class="n">m</span> <span class="o">=</span> <span class="mf">2.45</span><span class="p">,</span> <span class="mf">2.04</span>
</span><span class='line'>
</span><span class='line'>    <span class="k">for</span> <span class="n">attempts_left</span> <span class="ow">in</span> <span class="nb">reversed</span><span class="p">(</span><span class="nb">range</span><span class="p">(</span><span class="mi">10</span><span class="p">)):</span>
</span><span class='line'>        <span class="k">try</span><span class="p">:</span>
</span><span class='line'>            <span class="n">micro_time</span> <span class="o">=</span> <span class="n">time</span><span class="p">(</span><span class="n">micro_numbers</span><span class="p">)</span>
</span><span class='line'>            <span class="n">tiny_time</span> <span class="o">=</span> <span class="n">time</span><span class="p">(</span><span class="n">tiny_numbers</span><span class="p">)</span>
</span><span class='line'>            <span class="bp">self</span><span class="o">.</span><span class="n">assertLess</span><span class="p">(</span><span class="n">tiny_time</span><span class="p">,</span> <span class="n">micro_time</span><span class="o">*</span><span class="n">n</span><span class="p">)</span>
</span><span class='line'>            <span class="k">break</span>
</span><span class='line'>        <span class="k">except</span> <span class="ne">AssertionError</span><span class="p">:</span>
</span><span class='line'>            <span class="k">if</span> <span class="n">attempts_left</span> <span class="o">==</span> <span class="mi">0</span><span class="p">:</span>
</span><span class='line'>                <span class="k">raise</span>
</span><span class='line'>
</span><span class='line'>    <span class="k">for</span> <span class="n">attempts_left</span> <span class="ow">in</span> <span class="nb">reversed</span><span class="p">(</span><span class="nb">range</span><span class="p">(</span><span class="mi">5</span><span class="p">)):</span>
</span><span class='line'>        <span class="k">try</span><span class="p">:</span>
</span><span class='line'>            <span class="n">small_time</span> <span class="o">=</span> <span class="n">time</span><span class="p">(</span><span class="n">small_numbers</span><span class="p">)</span>
</span><span class='line'>            <span class="bp">self</span><span class="o">.</span><span class="n">assertLess</span><span class="p">(</span><span class="n">small_time</span><span class="p">,</span> <span class="n">tiny_time</span><span class="o">*</span><span class="n">n</span><span class="p">)</span>
</span><span class='line'>            <span class="bp">self</span><span class="o">.</span><span class="n">assertLess</span><span class="p">(</span><span class="n">small_time</span><span class="p">,</span> <span class="n">micro_time</span><span class="o">*</span><span class="n">n</span><span class="o">*</span><span class="n">m</span><span class="p">)</span>
</span><span class='line'>            <span class="k">break</span>
</span><span class='line'>        <span class="k">except</span> <span class="ne">AssertionError</span><span class="p">:</span>
</span><span class='line'>            <span class="k">if</span> <span class="n">attempts_left</span> <span class="o">==</span> <span class="mi">0</span><span class="p">:</span>
</span><span class='line'>                <span class="k">raise</span>
</span><span class='line'>
</span><span class='line'>    <span class="k">for</span> <span class="n">attempts_left</span> <span class="ow">in</span> <span class="nb">reversed</span><span class="p">(</span><span class="nb">range</span><span class="p">(</span><span class="mi">3</span><span class="p">)):</span>
</span><span class='line'>        <span class="k">try</span><span class="p">:</span>
</span><span class='line'>            <span class="n">medium_time</span> <span class="o">=</span> <span class="n">time</span><span class="p">(</span><span class="n">medium_numbers</span><span class="p">)</span>
</span><span class='line'>            <span class="bp">self</span><span class="o">.</span><span class="n">assertLess</span><span class="p">(</span><span class="n">medium_time</span><span class="p">,</span> <span class="n">micro_time</span><span class="o">*</span><span class="n">n</span><span class="o">*</span><span class="n">m</span><span class="o">*</span><span class="n">m</span><span class="p">)</span>
</span><span class='line'>            <span class="bp">self</span><span class="o">.</span><span class="n">assertLess</span><span class="p">(</span><span class="n">medium_time</span><span class="p">,</span> <span class="n">tiny_time</span><span class="o">*</span><span class="n">n</span><span class="o">*</span><span class="n">m</span><span class="p">)</span>
</span><span class='line'>            <span class="bp">self</span><span class="o">.</span><span class="n">assertLess</span><span class="p">(</span><span class="n">medium_time</span><span class="p">,</span> <span class="n">small_time</span><span class="o">*</span><span class="n">n</span><span class="p">)</span>
</span><span class='line'>            <span class="k">break</span>
</span><span class='line'>        <span class="k">except</span> <span class="ne">AssertionError</span><span class="p">:</span>
</span><span class='line'>            <span class="k">if</span> <span class="n">attempts_left</span> <span class="o">==</span> <span class="mi">0</span><span class="p">:</span>
</span><span class='line'>                <span class="k">raise</span>
</span></code></pre></td></tr></table></div></figure>


<p>I find the refactored version easier to skim.</p>

<p>But that <code>attempt_n_times</code> decorator <em>does</em> abuse the decorator syntax.
Decorators aren&rsquo;t <em>meant</em> to call the function they&rsquo;re decorating.</p>

<p>Is this misuse of decorators worth it?</p>

<h2>Is this worth it?</h2>

<p>Decorators aren&rsquo;t supposed to immediately call the function they decorate.
But there&rsquo;s nothing stopping them from doing so.
I feel that I&rsquo;ve traded &ldquo;normal code&rdquo; for a beautiful monstrosity that&rsquo;s easier to skim at a glance.</p>

<p>The <code>attempt_n_times</code> decorator is pretending that it&rsquo;s a block-level tool by using a function because there&rsquo;s no other way to invent such a tool in Python.</p>

<p>I think abstracting away the <code>for</code>-<code>try</code>-<code>break</code>-<code>except</code>-<code>if</code>-<code>raise</code> pattern was worth it, even though I ended up abusing Python&rsquo;s decorator syntax in the process.</p>

<p>What do you think?
Was that <code>attempt_n_times</code> abstraction worth it?</p>
]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[PyCon 2024 Reflection]]></title>
    <link href="https://treyhunner.com/2024/05/pycon-2024-reflection/"/>
    <updated>2024-05-28T13:00:00-07:00</updated>
    <id>https://treyhunner.com/2024/05/pycon-2024-reflection</id>
    <content type="html"><![CDATA[<p>I traveled back home from PyCon US 2024 last week.
This is my reflection on my time at PyCon.</p>

<h2>Attempting to eat vegan</h2>

<p>Since 2020, I&rsquo;ve been <a href="https://mastodon.social/@treyhunner/111794737871397453">gradually eating more plant-based</a> and <a href="https://mastodon.social/@treyhunner/111982459215543497">a few months ago</a> I decided to take PyCon as an opportunity to attempt exclusively vegan eating outside my own home.
As <a href="https://mastodon.social/@treyhunner/112493037289419028">I noted on Mastodon</a>, it was a challenge and I failed every day at least once but I found the experience worthwhile.
Our food system is <em>very</em> dairy-oriented.</p>

<h2>Staying hydrated and fed</h2>

<p>One of the first things I did before heading to the convention center was walk to Target and buy snacks and drinks.
When at PyCon, I prefer to spend 30 minutes and $20 to have a backup plan for last minute hydration and calories (even if not the <em>greatest</em> calories).
I never quite know when I might sleep through breakfast, find lunch lacking, or wish I&rsquo;d eaten more dinner.</p>

<h2>A tutorial, an orientation, a lightning talk, and open spaces</h2>

<p>My responsibilities at PyCon this year included teaching a tutorial and helping run the Newcomer&rsquo;s Orientation with <a href="https://x.com/KojoIdrissa">Kojo</a> and <a href="https://social.coop/@brainwane">Sumana</a>.</p>

<p><a href="https://mastodon.social/@treyhunner/112479031063220835">Yngve and Marie</a> offered to act as teaching assistants during my tutorial and I was very grateful for their help!
<a href="https://x.com/mathsppblog">Rodrigo</a> and Krishna also offered to TA just before my tutorial started and I was extra grateful to have even more help than I&rsquo;d expected.
The attendees were mostly better prepared than I expected they would be, which was also great.
It&rsquo;s always great to spend less time on setup and more time exploring Python together.</p>

<p>The newcomer&rsquo;s orientation the next day went well.
We kept it fairly brief and were able to address about 10 minutes of audience questions before the opening reception started.</p>

<p>Once my PyCon responsibilities completed, I invented a few more (light) responsibilities for myself. 😅
I signed up to give a lightning talk on how to give a lightning talk.
They slotted it as the first talk of the first lightning talk session on Friday night.
I kept this talk pretty much the same as the one <a href="https://youtu.be/aNHBr7q-KVw?feature=shared&amp;t=915">I presented DjangoCon 2016</a>.
I could have made the transitions fancier, but I decided to embrace the idea of simplicity with the hope that audience members might think &ldquo;look if that first speaker can give such a simple and succinct presentation, maybe I can too.&rdquo;</p>

<p>On Saturday I ran <a href="https://mastodon.social/@treyhunner/112457077109815019">an open space on Python Learning</a>.
Some of you showed up because you&rsquo;re on my mailing list or you&rsquo;re paying Python Morsels subscribers.
Many folks showed up because the topic was interesting, either as a learner or as a teacher.
I really enjoyed the round-table-style conversation we had.</p>

<p>I also ran a <a href="https://mastodon.social/@treyhunner/112468136603503893">Cabo Card game open space</a> during lunch on Sunday on the 4th floor rooftop.
Cabo is my usual conference ice breaker game and I played it at least a few nights <a href="https://mas.to/@davidism/112465501797531611">in The Westin lobby</a> as well.</p>

<h2>Seeing conference friends, old and new</h2>

<p>For me, PyCon is largely about having conversations.
The talks and tutorials are great for starting me thinking about an idea.
The hallway track, open spaces, and meals are great for continuing conversations about those ideas (or <em>other</em> ideas).</p>

<p>My first morning in Pittsburgh, I chatted with <a href="https://www.linkedin.com/in/naomiceder/">Naomi</a> and <a href="https://x.com/reuvenmlerner/">Reuven</a>.
I&rsquo;m glad I ran into them before the conference kicked off because (as often happens at PyCon) I only very briefly saw either of them during the rest of PyCon!</p>

<p>After my tutorial that afternoon, I did dinner with Marie, Yngve, and Rodrigo at Rosewater Mediterranean (good vegan options, assuming you enjoy falafel and various sauces).
As sometimes happens at PyCon, another PyCon attendee, Sachin, joined our table because we noticed him eating on his own at a table near us and invited him to join us.</p>

<p>On Saturday, <a href="https://hachyderm.io/@melaniearbor">Melanie</a>, <a href="https://mas.to/@davidism">David</a>, <a href="https://x.com/kjaymiller">Jay</a>, and I had a sort of mini San Diego Python study group reunion dinner before inviting folks to join us for <a href="https://mastodon.social/@treyhunner/112465503888730383">Cabo and Knucklebones</a> one night.
The 4 of us originally met each other (along with <a href="https://hachyderm.io/@willingc">Carol</a> and other wonderful Python folks) at the San Diego Python study group about 10 years ago.</p>

<p>I had some wonderful conversations about ways to improve the Python documentation over dinner (at Nicky&rsquo;s Thai) on Sunday night with <em>so</em> many docs-concerned folks who I highly respect.
I&rsquo;m really excited that Python has <a href="https://peps.python.org/pep-0732/">the documentation editorial board</a> and I&rsquo;m hopeful that that board, with the help of many others community members, will usher in big improvements to the documentation in the coming years.</p>

<p>I also met a number of Internet acquaintances IRL for the first time at PyCon.
I met <a href="https://www.linkedin.com/in/tereza-iofciu/">Tereza</a> and <a href="https://www.linkedin.com/in/jessica0greene/">Jessica</a>, who I know from our work in the PSF Code of Conduct workgroup.
I met <a href="https://fosstodon.org/@slott56">Steve Lott</a>, who I originally knew as <a href="https://stackoverflow.com/users/10661/s-lott">a prolific question-answerer</a>.
I also met <a href="https://github.com/hugovk">Hugo</a>, a CPython core dev, the Python 3.14 &amp; 3.15 release manager, and a <a href="https://mastodon.social/@hugovk">social media user</a> (which is how I&rsquo;ve primarily interacted with him because the Internet is occasionally lovely).
I was also very excited to meet many <a href="https://www.pythonmorsels.com">Python Morsels members</a> as well as folks who know me through <a href="https://www.pythonmorsels.com/newsletter/">my weekly Python tips newsletter</a>.</p>

<p>I was grateful to chat with <a href="https://mastodon.social/@hynek">Hynek</a> and <a href="https://mastodon.social/@AlSweigart">Al</a> about creating talks, YouTube videos, and other online content.
I also enjoyed chatting with <a href="https://mastodon.social/@glyph">Glyph</a> a bit about our experiences consulting and training and (in hindsight) wished I&rsquo;d planned an open space for either consultants or trainers, both of which have been held at PyCon before but it just takes someone to stick it on the open space board.</p>

<p>Many folks I only saw very briefly (I said a quick hi and bye to <a href="https://aeracode.org/@andrew">Andrew</a> over lunch during the sprints) and some I didn&rsquo;t see at all (<a href="https://mastodon.social/@frank@frankwiles.social">Frank</a> was at PyCon but we never ran into each other).
Some I essentially saw through playing a few rounds of Cabo (<a href="https://social.coop/@Yhg1s">Thomas</a> and <a href="https://hachyderm.io/@ethantyping">Ethan</a> among many others).
We also ran into at least 4 other PyCon attendees in the airport on Tuesday afternoon, including <a href="https://www.linkedin.com/in/bbelderbos/">Bob</a> and <a href="https://www.linkedin.com/in/juliansequeira/">Julian</a>, who it&rsquo;s always a pleasure to see.</p>

<h2>A Mastodon-oriented PyCon</h2>

<p>On Thursday night I had the feeling that the number of Mastodon posts I saw on the <strong>#PyConUS</strong> hashtag was greater than the number of Twitter posts.
I (very unscientifically) counted up the number of posts I was seeing on each and found that my perception was correct: <a href="https://mastodon.social/@treyhunner/112453920848761679">Mastodon seemed to slightly overtake Twitter at PyCon this year</a>.</p>

<p>Over dinner on Wednesday, I tried to convince Marie, Yngve, and Rodrigo to get Mastodon accounts just to follow the hashtag during PyCon.
I succeeded: <a href="https://mastodon.social/@treyhunner/112479031063220835">Marie and Yngve</a> and <a href="https://fosstodon.org/@davep/112458736212760528">Rodrigo</a>!</p>

<p>Mastodon will never be <em>the</em> social media platform.
Its decentralized nature is too much of a barrier for many folks.
However, it does seem to be used by <em>enough</em> somewhat nerdy Python folks to now be one the most used social media platform for PyCon posting.</p>

<h2>The talks</h2>

<p>I ended up spending little time in the talks during PyCon.
This wasn&rsquo;t on purpose.
I just happened to attend many open spaces, take personal breaks, and end up in hallway conversations often.
I did see many of the lightning talks live, as well as <a href="https://x.com/kjaymiller">Jay</a>, <a href="https://simonwillison.net/@simon">Simon</a>, and <a href="https://social.coop/@brainwane">Sumana</a>&rsquo;s keynotes (all of them were exceptional) and the opening and closing remarks.
I also watched a few talks from my hotel room while taking breaks.</p>

<p>While I&rsquo;m often a bit light on my talk load at PyCon, I do recommend folks attend a good handful of live talks during PyCon, <a href="https://mastodon.social/@jonafato/112514979873634457">as Jon</a> and <a href="https://hynek.me/articles/hallway-track/">others</a> recommend.
I wish I had seen more talks live.
I also wish I had attended a few open spaces that I missed.</p>

<p>At any one time, I know that I&rsquo;m always missing about 90% of what&rsquo;s scheduled during PyCon (if you include the talks <em>and</em> the open spaces).
That&rsquo;s assuming I don&rsquo;t ditch the conference entirely for a few hours and <a href="https://social.coop/@bitprophet/112452662184950234">walk across a bridge</a> or <a href="https://mastodon.social/@AlSweigart/112514009252817862">ride a funicular</a> (neither of which I did, as I stuck around the venue the whole time this year).
I am glad I saw, did, and talked about everything I did, but there&rsquo;s always something I wish I&rsquo;d seen/done!</p>

<h2>The sprints</h2>

<p>Thanks to the documentation dinner, I had a couple documentation-related ideas in mind on the first day of sprints.
But I&rsquo;m also <em>really</em> excited about the new Python REPL coming in Python 3.13 (<a href="https://x.com/treyhunner/status/1720185049461801371">in case</a> you <a href="https://treyhunner.com/2024/05/installing-a-custom-python-build-with-pyenv/">can&rsquo;t tell</a> from <a href="https://treyhunner.com/2024/05/my-favorite-python-3-dot-13-feature/">how much</a> I <a href="https://x.com/treyhunner/status/1788307498715554160">talk</a> about <a href="https://www.linkedin.com/posts/treyhunner_python-activity-7194082747315859456-qQjk/?utm_source=share&amp;utm_medium=member_desktop">it</a>), so I sprinted on that instead.
<a href="https://github.com/ambv">Łukasz</a> assigned me the task of researching keyboard shortcuts that the new REPL is missing (compared to the current one on Linux and Mac) so <a href="https://github.com/python/cpython/issues/119034#issuecomment-2121142576">I spent some time researching that</a>.
I got to see the REPL running on <a href="https://fosstodon.org/@tonybaloney/112477098396842900">Anthony&rsquo;s laptop</a> on Windows and I am <em>so excited</em> that Windows support will be included before 3.13.0 lands! 🎉</p>

<p>Partly inspired by <a href="https://youtu.be/RL3HFj5SDqI?t=1549">Carol Willing&rsquo;s PyCon preview message</a>, I also thanked <a href="https://github.com/pablogsal">Pablo</a>, <a href="https://github.com/ambv">Łukasz</a>, and <a href="https://github.com/lysnikolaou">Lysandros</a> in-person for all their work on the new Python REPL. 🤗</p>

<h2>Until next year</h2>

<p>I&rsquo;ll be <a href="https://www.pyohio.org/2024/program/speakers/keynote-speakers/#trey-hunner">keynoting at PyOhio</a> this year.</p>

<p>Besides PyOhio, I&rsquo;m not sure whether I&rsquo;ll make it to another conference until PyCon US next year.
I&rsquo;d love to attend all of them, but I do have work and personal goals that need accomplishing too!</p>

<p>I hope to see you at PyCon US 2025!
In the meantime, if you&rsquo;re wishing we&rsquo;d exchanged contact details or met in-person, please feel free to stay in touch through <a href="https://mastodon.social/@treyhunner">Mastodon</a>, <a href="https://mastodon.social/@treyhunner">LinkedIn</a>, <a href="https://pym.dev/newsletter">my weekly emails</a>, <a href="https://www.youtube.com/@PythonMorsels">YouTube</a>, or <a href="http://twitter.com/treyhunner">Twitter</a>.</p>

<p><img class="no-border" src="https://treyhunner.com/images/pycon2024-pic.jpg"  alt="Yellow bridges over a river just outside the PyCon 2024 conference center in Pittsburgh"></p>
]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[The new REPL in Python 3.13]]></title>
    <link href="https://treyhunner.com/2024/05/my-favorite-python-3-dot-13-feature/"/>
    <updated>2024-05-08T13:30:00-07:00</updated>
    <id>https://treyhunner.com/2024/05/my-favorite-python-3-dot-13-feature</id>
    <content type="html"><![CDATA[<p>Python 3.13 just hit feature freeze with <a href="https://www.python.org/downloads/release/python-3130b1/">the first beta release today</a>.</p>

<p>Just before the feature freeze, a shiny new feature was added: <strong>a brand new Python REPL</strong>. ✨</p>

<p>This new Python REPL is the feature I&rsquo;m most looking forward to using while teaching after 3.13.0 final is released later this year.
In terms of improving my quality of life while teaching Python, this new REPL may be my favorite feature since f-strings were added in Python 3.6.</p>

<p>I&rsquo;d like to share what&rsquo;s so great about this new REPL and what additional improvements I&rsquo;m hoping we might see in future Python releases.</p>

<p>None of these features will be ground breaking for folks who are already using <a href="https://github.com/ipython/ipython">IPython</a> day-to-day.
This new REPL really shines when you can&rsquo;t or shouldn&rsquo;t install PyPI packages (as when teaching a brand new Pythonistas in a locked-down corporate environment).</p>

<h2>Little niceties</h2>

<p>The first thing you&rsquo;ll notice when you launch the new REPL is the colored prompt.</p>

<p><img src="https://treyhunner.com/images/new-repl-intro.gif"></p>

<p>You may also notice that as you type a block of code, after the first indented line, the next line will be auto-indented!
Additionally, hitting the Tab key inserts 4 spaces now, which means there&rsquo;s no more need to ever hit <code>Space Space Space Space</code> to indent ever again.</p>

<p>At this point you might be thinking, &ldquo;wait did I accidentally launch ptpython or some other alternate REPL?&rdquo;
But it gets even better!</p>

<h2>You can &ldquo;exit&rdquo; now</h2>

<p>Have you ever typed <code>exit</code> at the Python REPL?
If so, you&rsquo;ve seen a message 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>
</pre></td><td class='code'><pre><code class='pycon'><span class='line'><span class="gp">&gt;&gt;&gt; </span><span class="nb">exit</span>
</span><span class='line'><span class="go">Use exit() or Ctrl-D (i.e. EOF) to exit</span>
</span></code></pre></td></tr></table></div></figure>


<p>That feels a bit silly, doesn&rsquo;t it?
Well, typing <code>exit</code> will exit immediately.</p>

<p><img src="https://treyhunner.com/images/new-repl-exit.gif"></p>

<p>Typing <code>help</code> also enters help mode now (previously you needed to call <code>help()</code> as a function).</p>

<h2>Block-level history</h2>

<p>The feature that will make the biggest different in my own usage of the Python REPL is block-level history.</p>

<p><img src="https://treyhunner.com/images/new-repl-block.gif"></p>

<p>I make typos all the time while teaching.
I also often want to re-run a specific block of code with a couple small changes.</p>

<p>The old-style Python REPL stores history line-by-line.
So editing a block of code in the old REPL required hitting the up arrow many times, hitting Enter, hitting the up arrow many more times, hitting Enter, etc. until each line in a block was chosen.
At the same time you also needed to make sure to edit your changes along the way&hellip; or you&rsquo;ll end up re-running the same block with the same typo as before!</p>

<p>The ability to edit a previously typed <em>block</em> of code is huge for me.
For certain sections of my Python curriculum, I hop into <a href="https://github.com/prompt-toolkit/ptpython">ptpython</a> or <a href="https://github.com/ipython/ipython">IPython</a> specifically for this feature.
Now I&rsquo;ll be able to use the default Python REPL instead.</p>

<h2>Pasting code <em>just works</em></h2>

<p>The next big feature for me is the ability to paste code.</p>

<p>Check this out:</p>

<p><img src="https://treyhunner.com/images/new-repl-paste.gif"></p>

<p>Not impressed?
Well, watch what happens when we paste that same block of code into the old Python REPL:</p>

<p><img src="https://treyhunner.com/images/old-repl-paste.gif"></p>

<p>The old REPL treated pasted text the same as manually typed text.
When two consecutive newlines were encountered in the old REPL, it would end the current block of code because it assumed the Enter key had been pressed twice.</p>

<p>The new REPL supports <a href="https://en.wikipedia.org/wiki/Bracketed-paste">bracketed paste</a>, which is was invented in 2002 and has since been adopted by all modern terminal emulators.</p>

<h2>No Windows support? Curses!</h2>

<p><strong>EDIT</strong>: This whole section is now irrelevant!
During the PyCon US sprints in late May 2024, the <code>readline</code> and <code>curses</code> dependencies were removed and <a href="https://mastodon.social/@tonybaloney@fosstodon.org/112477098540793635">Windows support</a> will be included in the second beta release of Python 3.13.0! 🎉</p>

<p>Unfortunately, this new REPL <strike>doesn&rsquo;t currently work on Windows</strike>.
This new REPL relies on the <code>curses</code> and <code>readline</code> modules, neither of which are available on Windows.</p>

<p>The <a href="https://pym.dev/repl">in-browser Python REPL</a> on Python Morsels also won&rsquo;t be able to use the new REPL because readline and curses aren&rsquo;t available in the WebAssembly Python build.</p>

<h2>Beta test Python 3.13 to try out the new REPL 💖</h2>

<p>Huge thanks to Pablo Galindo Salgado, Łukasz Langa, and Lysandros Nikolaou <a href="https://docs.python.org/3.13/whatsnew/3.13.html">for implementing this new feature</a>!
And thanks to Michael Hudson-Doyle and Armin Rigo for implementing the original version of this REPL, which was <a href="https://github.com/pypy/pyrepl">heavily borrowed from PyPy&rsquo;s pyrepl project</a>.</p>

<p>The new Python REPL coming in 3.13 is a major improvement over the old REPL.</p>

<p>Want to try out this new REPL?
Download and install <a href="https://www.python.org/downloads/release/python-3130b1/">Python 3.13.0 beta 1</a>!</p>

<p>Beta testing new Python releases helps the Python core team ensure the final release of 3.13.0 is as stable and functional as possible.
If you notice a bug, <a href="https://github.com/python/cpython/issues">check the issue tracker</a> to see if it&rsquo;s been reported yet and if not report it!</p>
]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Installing a custom Python build with pyenv]]></title>
    <link href="https://treyhunner.com/2024/05/installing-a-custom-python-build-with-pyenv/"/>
    <updated>2024-05-03T21:26:23-07:00</updated>
    <id>https://treyhunner.com/2024/05/installing-a-custom-python-build-with-pyenv</id>
    <content type="html"><![CDATA[<p>I am <em>so</em> excited about the new Python REPL that will <em>likely</em> land in Python 3.13.
I&rsquo;ve been following <a href="https://github.com/python/cpython/pull/111567">this CPython pull request</a> since I heard <a href="https://github.com/pablogsal">Pablo</a> and <a href="https://lukasz.langa.pl">Łukasz</a> announce their work on the new Python REPL <a href="https://twitter.com/treyhunner/status/1720186574032531780">in episode 1</a> of their new <a href="https://podcasts.apple.com/us/podcast/core-py/id1712665877">core.py podcast</a>.</p>

<h2>Github notifications? 🤔</h2>

<p>That pull request was quiet for many months, but in the last couple weeks, I started seeing email notifications in my inbox about it.
I&rsquo;ve never fancied myself a competent C developer and I try to steer clear from understanding TTY magic, so I have <em>no idea</em> what most of the commits do.
But seeing activity on this pull request rejuvenated my excitement about this upcoming feature!</p>

<p>I also remember reading that the Python 3.13 feature freeze is coming up soon, so I&rsquo;ve been silently cheering for that PR to make the cut before the deadline.</p>

<p>In the last few days, I decided that I should try committing to use this new REPL locally as my default Python environment.
When I type <code>python</code> on my machine, I want to live in this new shiny REPL.
I figure this will make it easier to spot bugs that might not have been noticed yet&hellip; though honestly it&rsquo;ll mostly just allow me to try out this fancy new REPL first-hand.</p>

<h2>Installing a custom CPython build in pyenv</h2>

<p>I use pyenv to manage the many Python versions I have installed on my machine.
I wondered whether it was possible to install a custom build of CPython with pyenv.</p>

<p>Instead of going to the pyenv documentation to figure out an answer, I argued with an AI until it gave me a working answer.
I tried a few AI systems at first, but Claude seemed to give me the most promising-looking answer so it was the one I argued with for 5-10 minutes until I got a working solution.</p>

<p>First, I created this <code>~/.pyenv/plugins/python-build/share/python-build/3.13.0-pyrepl</code> file:</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'>prefer_openssl11
</span><span class='line'>export PYTHON_BUILD_CONFIGURE_WITH_OPENSSL=1
</span><span class='line'>install_package "pyrepl" "https://github.com/pablogsal/cpython/archive/pyrepl.tar.gz" standard verify_py39 ensurepip</span></code></pre></td></tr></table></div></figure>


<p>Then I ran this command, which took a couple minutes:</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-pyrepl
</span></code></pre></td></tr></table></div></figure>


<p>After that, <code>pyenv versions</code> showed a new <code>3.13.0-pyrepl</code> version:</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>
</pre></td><td class='code'><pre><code class='bash'><span class='line'><span class="nv">$ </span>pyenv versions
</span><span class='line'>  system
</span><span class='line'>* 3.8.18 <span class="o">(</span><span class="nb">set </span>by /home/trey/.pyenv/version<span class="o">)</span>
</span><span class='line'>* 3.9.18 <span class="o">(</span><span class="nb">set </span>by /home/trey/.pyenv/version<span class="o">)</span>
</span><span class='line'>* 3.10.13 <span class="o">(</span><span class="nb">set </span>by /home/trey/.pyenv/version<span class="o">)</span>
</span><span class='line'>* 3.11.6 <span class="o">(</span><span class="nb">set </span>by /home/trey/.pyenv/version<span class="o">)</span>
</span><span class='line'>* 3.12.0 <span class="o">(</span><span class="nb">set </span>by /home/trey/.pyenv/version<span class="o">)</span>
</span><span class='line'>  3.13.0-pyrepl
</span></code></pre></td></tr></table></div></figure>


<p>I then added <code>3.13.0-pyrepl</code> to the top of my <code>~/.pyenv/version</code> file to make this my <em>default</em> Python:</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='bash'><span class='line'>3.13.0-pyrepl
</span><span class='line'>3.12.0
</span><span class='line'>3.11.6
</span><span class='line'>3.10.13
</span><span class='line'>3.9.18
</span><span class='line'>3.8.18
</span></code></pre></td></tr></table></div></figure>


<p>And it worked!
Tying <code>python</code> showed the new colorful prompt.</p>

<p>Is is a bad idea to make this not-even-beta version of CPython the default Python on my machine?
I have no idea.
Everything&rsquo;s been fine for the last 10 hours at least&hellip; 🤷</p>

<p>If you ever need to try installing a custom CPython build with pyenv, maybe the above instructions will work.
They&rsquo;re mostly generated by a large language model that didn&rsquo;t give me a working answer until the third response&hellip; so feel free to let me know if it&rsquo;s all wrong (or all right?).</p>

<p>After this adventure, I checked my podcast feed this evening only to realize that there&rsquo;s <a href="https://mastodon.social/@ambv/112378026608575109">a new core.py episode</a> all about exactly this feature!
If you&rsquo;d like to hear some core developers nerd out about CPython development, give core.py a listen.
You don&rsquo;t need to understand how CPython development works to enjoy their enthusiasm. 💖</p>
]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[10 years of Python conferences]]></title>
    <link href="https://treyhunner.com/2024/04/10-years-of-python-conferences/"/>
    <updated>2024-04-27T11:45:00-07:00</updated>
    <id>https://treyhunner.com/2024/04/10-years-of-python-conferences</id>
    <content type="html"><![CDATA[<p>10 years and 10 days ago I flew home from my very first Python conference.</p>

<p>I left a few days into the PyCon US 2014 sprints and I remember feeling a bit like summer camp was ending.
I&rsquo;d played board games, contributed to an open source project, seen tons of talks, and met a <em>ton</em> of people.</p>

<h2>My first Python conference: PyCon US 2014</h2>

<p>PyCon 2014 was the first Python conference I attended.</p>

<p>At the start of the conference I only knew a handful of San Diegans.
I left having met many more folks.
Some of the folks I met I knew from online forums, GitHub repos, or videos
I met Kenneth Love, Baptiste Mispelon, Carl Meyer, Eric Holscher in-person, among many others.
Most folks I met I had never encountered online, but I was glad to have met in person.</p>

<p>For the most part, I had no idea who anyone was, what they did with Python, or what they might be interested in talking about.
I also had no idea what most of the various non-talk activities were.
I found out about the Education Summit and hadn&rsquo;t realized that it required pre-registration.
The open spaces are one of my favorite parts of PyCon and I didn&rsquo;t even they existed until PyCon 2015.</p>

<p>I <em>did</em> stay for a couple days of the sprints and I was grateful for that.
Most of the memorable human connections I had were during the sprints.
I helped <a href="https://pyvideo.org">PyVideo</a> upgrade their code base from Python 2 to Python 3 (this was before Will and Sheila <a href="https://pyvideo.org/pages/thanks-will-and-sheila.html">stepped down as maintainers</a>).
Will guided me through the code base and seemed grateful for the help.</p>

<p>I also got the idea to write front-end JavaScript tests for Django during the sprints and eventually <a href="https://github.com/django/deps/blob/main/final/0003-javascript-tests.rst">started that process</a> after PyCon thanks to Carl Meyer&rsquo;s guidance.</p>

<h2>Attending regional conferences and DjangoCon</h2>

<p>In fall 2014, I attended Django BarCamp at the Eventbrite office.
That was my first exposure to the idea of an &ldquo;unconference&rdquo;&hellip; which I kept in mind when I spotted the open spaces board at PyCon 2015.</p>

<p>Before coming back to Montreal for <a href="https://twitter.com/treyhunner/status/585969805796212736">PyCon 2015</a>, I emailed Harry Percival to ask if he could use a teaching assistant during his tutorial on writing tests. His reply was much more enthusiastic than I expected: &ldquo;YES YES OH GOD YES THANK YOU THANK YOU THANK YOU TREY&rdquo;.
I was very honored to be able to help Harry, as my testing workflow was <em>heavily</em> inspired by many blog posts he&rsquo;d written about testing best practices in Django.</p>

<p>I coached at my first Django Girls event in 2015 in Ensenada and then my second at <a href="https://twitter.com/algosuna/status/641421944005378048">DjangoCon 2015</a> in Austin. I gave my first lightning talk at DjangoCon 2015, comparing modern JavaScript to Python. It was a lightning talk I had given at the San Diego JavaScript and <a href="https://www.sandiegopython.org">San Diego Python</a> meetups.</p>

<p>In 2016, I attended PyTennessee in Nashville. I remember attending a dinner of of about a dozen folks who spoke at the conference. I was grateful to get to chat with so many folks whose talks I&rsquo;d attended.</p>

<h2>Presenting talks and tutorials</h2>

<p>I presented my first conference tutorial at <a href="https://twitter.com/treyhunner/status/737069721292439552">PyCon 2016</a> in Portland and <a href="https://pyvideo.org/djangocon-us-2016/readability-counts.html">my first talk</a> at <a href="https://twitter.com/asendecka/status/756851845012844544">DjangoCon US 2016</a> in Philadelphia.
I had been presenting lightning talks every few months at my local Python and JavaScript meetups for a few years by then and I had hosted free workshops at my local meetup and paid workshops for training clients.</p>

<p>Having presented locally helped, but presenting on a big stage is always scary.</p>

<h2>Volunteering</h2>

<p>I volunteered at some of my first few conferences and found that I really enjoyed it.
I especially enjoyed running the registration desk, as you&rsquo;re often the first helpful face that people see coming into the conference.</p>

<p>During PyCon 2016, 2017, and 2018, I co-chaired the open spaces thanks to Anna Ossowski inviting me to help.
I had first attended open spaces during PyCon 2015 and I <em>loved</em> them.
Talks are great, but so are discussions!</p>

<p>I also ran for the PSF board of directors in 2016 and ended up serving on the board for a few years before stepping down.
After my board terms, I volunteered for the PSF Code of Conduct working group for about 6 years.
I didn&rsquo;t even know what the PSF <em>was</em> until PyCon 2015!</p>

<h2>A <em>lot</em> of travel&hellip; maybe too much</h2>

<p>After DjangoCon 2016, I went a bit conference-wild.
I attended <a href="https://twitter.com/treyhunner/status/829412120370565120">PyTennessee 2017</a>, <a href="https://twitter.com/PythonChat/status/845052805375295488">PyCaribbean 2017</a> in Puerto Rico, <a href="https://twitter.com/loooorenanicole/status/866382369619562496">PyCon US 2017</a> in Portland, <a href="https://twitter.com/asteracode/status/893274761727361024">PyCon Australia 2017</a> in Melbourne, <a href="https://twitter.com/TobiasMcNulty/status/896570680933703681">DjangoCon 2017</a> in Spokane, <a href="https://twitter.com/treyhunner/status/916890505157468160">PyGotham 2017</a> in NYC, and <a href="https://twitter.com/treyhunner/status/937441419966296064">North Bay Python 2017</a> in Petaluma.</p>

<p>In 2018 I sponsored <a href="https://twitter.com/treyhunner/status/962862235734495232">PyTennessee</a> and <a href="https://twitter.com/jmwatt3/status/1023713820752183296">PyOhio</a> and spoke at both.
I passed out chocolate chip cookies at PyTennessee as a way to announce the launch of <a href="https://www.pythonmorsels.com">Python Morsels</a>.
I also attended <a href="https://twitter.com/treyhunner/status/996881842820272128">PyCon 2018</a> in Cleveland, <a href="https://www.flickr.com/photos/144080672@N05/31764542868/">DjangoCon 2018</a> in San Diego, <a href="https://twitter.com/treyhunner/status/1048340952451047425">PyGotham 2018</a>, and <a href="https://twitter.com/nnja/status/1058589935618318336">North Bay Python 2018</a>.</p>

<p>I slowed down <em>a bit</em> in 2019, with just <a href="https://twitter.com/LaylaSells_cshs/status/1100058649227972609">PyCascades</a> (Seattle), <a href="https://twitter.com/mariatta/status/1125534406851084288">PyCon US</a> (Cleveland), <a href="https://twitter.com/juliansequeira/status/1157106403598749698">PyCon Australia</a> (Sydney), and DjangoCon US (San Diego, which is home for me).</p>

<h2>Since the pandemic</h2>

<p>Since the start of the pandemic, I&rsquo;ve attended <a href="https://twitter.com/treyhunner/status/1519724687890128899">PyCon US 2022</a>, DjangoCon 2022 in San Diego (in my city for the <em>third</em> time!) and <a href="https://mastodon.social/@treyhunner/110243968672457367">PyCon US 2023</a>.
Traveling is more challenging for me than it used to be, but I hope to attend more regional conferences again soon.</p>

<p>Between client work, I&rsquo;ve been focusing less on conferences and more on blog posts (<a href="https://www.pythonmorsels.com/articles/">over here</a>), <a href="https://www.youtube.com/@PythonMorsels">screencasts</a>, my <a href="https://www.pythonmorsels.com/newsletter/">weekly Python tips</a> emails, and (of course) on <a href="https://www.pythonmorsels.com">Python Morsels</a>.</p>

<h2>My journey started locally</h2>

<p>I became part of the Python community before I knew I was part of it.</p>

<p>I started using Python professionally in December 2009 and I attended my first San Diego Python meetup in March 2012.
I met the organizers, gave some lightning talks, attended Saturday study group sessions (thanks Carol Willing, Alain Domissy, and others for running these), and volunteered to help organize meetups, study groups, and workshops.</p>

<p>By 2014, I had learned from folks online and in-person and I had helped out at my local Python meetup.
I had even made a few contributions to some small Django packages I relied on heavily.</p>

<p>I was encouraged to attend PyCon 2014 by others who were attending (thanks Carol, Micah, and Paul among others).
The conference was well-worth the occasional feeling of overwhelm.</p>

<h2>We&rsquo;re all just people</h2>

<p>The biggest thing I&rsquo;ve repeatedly learned over the past decade of Python conferences is that we&rsquo;re all just people.</p>

<p>Carol Willing keynoted PyCon US 2023.
But I met Carol as a kind Python user in San Diego who started the first Python study group meetings in Pangea Bakery on Convoy Street.</p>

<p>Jay Miller will be keynoting PyCon US 2024.
But I met Jay as an attendee of the Python study group, who was enthusiastic about both learning and teaching others.</p>

<p>My partner, Melanie Arbor, keynoted DjangoCon 2022 along with Jay Miller.
When I met Melanie, she was new to Python and was very eager to both learn and help others.</p>

<p>David Lord has made a huge impact on the maintenance of Flask and other Pallets projects.
I met David as a Python study group attendee who was an enthusiastic StackOverflow contributor.</p>

<p>I learned a ton from Brandon Rhodes, Ned Batchelder, Russell Keith-Magee, and many others from online videos, forums, and open source projects before I ever met them.
But each of them are also just Python-loving people like the rest of us.
Russell gives good hugs, Ned is an organizer of his local Python meetup, and Brandon wears the same brand of shoes as me.</p>

<p>We all have people we&rsquo;ve learned from, we suffer from feelings of inadequacy, we get grumpy sometimes, and we care about the Python language and community in big and small ways.</p>

<h2>What&rsquo;s next for you?</h2>

<p>Will you attend a local meetup?
Or will you attend an online social event?</p>

<p>If so, consider asking the organize if you can present a 5 minute lightning talk at a future event.
As I <a href="https://youtu.be/aNHBr7q-KVw?si=Fryj6Ez4Cw7q-RAq">noted in a DjangoCon 2016 lightning talk</a>, lightning talks are a great way to connect with folks.</p>

<p>Will you attend a Python conference one day?
See <a href="https://treyhunner.com/2018/04/how-to-make-the-most-of-your-first-pycon/">having a great first PyCon</a> when/if you do.</p>

<p>Remember that we&rsquo;re all just people though.
Some may have a bit more experience (whether at speaking, contributing to open source, or something else), but we&rsquo;re just people.</p>
]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Python Black Friday &amp; Cyber Monday sales (2023)]]></title>
    <link href="https://treyhunner.com/2023/11/python-black-friday-and-cyber-monday-sales-2023/"/>
    <updated>2023-11-20T08:00:00-08:00</updated>
    <id>https://treyhunner.com/2023/11/python-black-friday-and-cyber-monday-sales-2023</id>
    <content type="html"><![CDATA[<p>It&rsquo;s time for my annual compilation post of <strong>Python learning deals</strong>.
I&rsquo;ve been compiling Python-related Black Friday &amp; Cyber Monday sales <a href="https://treyhunner.com/blog/categories/sales/">since 2018</a> and 2023&rsquo;s Python-related sales are coming up.</p>

<h2>Lifetime Python Morsels access for the price of two years</h2>

<p>I&rsquo;m kicking things off with <a href="https://www.pythonmorsels.com/lifetime-access-sale/">my sale</a> on Python Morsels.
Python Morsels helps developers <strong>deepen their Python skills</strong> in a way that day-to-day coding simply can&rsquo;t.</p>

<p>Python Morsels is designed for:</p>

<ul>
<li>experienced developers frustrated with gaps in their Python knowledge</li>
<li>self-taught programmers seeking courage and confidence in their Python abilities</li>
<li>experienced Python developers hoping to dive even deeper</li>
</ul>


<p>If you saw yourself in that list and you plan to use Python heavily for at least a few more years, I highly recommend checking out <a href="https://www.pythonmorsels.com/lifetime-access-sale/">the Python Morsels sale</a>.</p>

<p>From now through November 27, you can get <strong>lifetime access</strong> to Python Morsels for a one-time fee.
Python Morsels usually costs <strong>$240/year</strong> but lifetime access is <strong>only $480</strong>.
This is the best sale I&rsquo;ve ever offered on Python Morsels and I&rsquo;m guessing this might be the best Python-related deal this year.</p>

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

<h2>On sale now</h2>

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

<ul>
<li><strong><a href="https://www.pythonmorsels.com/lifetime-access-sale/">Python Morsels</a></strong>: lifetime access to my Python skill-building platform for the price of 2 years</li>
<li><strong><a href="https://learning.oreilly.com/signup/?promotion_code=CYBERWEEK23">O'Reilly Media</a></strong>: the first year is $200 off with the coupon <code>CYBERWEEK23</code> ($299 instead of $499)</li>
<li><strong><a href="http://talkpython.fm/black-friday">Talk Python</a></strong>: 50% off 5 of their courses</li>
<li><strong><a href="https://courses.dataschool.io/black-friday">Data School</a></strong>: 40% off all Kevin Markham&rsquo;s courses</li>
<li><strong><a href="https://courses.pythontest.com/p/complete-pytest-course?code=BLACKFRIDAY">Brian Okken</a></strong>: 50% off pytest course and community access with coupon code <code>BLACKFRIDAY</code> (ends Nov 30)</li>
<li><strong><a href="https://lernerpython.com/bfcm-2023/">Reuven Lerner</a></strong>: 40% off Reuven&rsquo;s courses and 25% off a new membership he&rsquo;s launching</li>
<li><strong><a href="https://www.linkedin.com/feed/update/urn:li:activity:7133217889460883456/">Matt Harrison</a></strong>: 20% off Matt&rsquo;s corporate training</li>
<li><strong><a href="https://learnbyexample.gumroad.com">Sundeep Agarwal</a></strong>: around 70% 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> and his <a href="https://learnbyexample.gumroad.com/l/py_regex/FestiveOffer">regex</a> book is free</li>
<li><strong><a href="https://www.blog.pythonlibrary.org">Mike Driscoll</a></strong>: 33% 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>black2023</code></li>
<li><strong><a href="https://thepythoncodingplace.com/membership/">Stephen Gruppetta</a></strong>: 70% off pre-sale on his new Python membership ($95 instead of $395)</li>
<li><strong><a href="https://mathspp.gumroad.com/">Rodrigo</a></strong>: 40% discount on Rodrigo&rsquo;s upcoming <a href="https://mathspp.gumroad.com/l/pythonbootcamp?code=bootcampbf23">bootcamp</a> and on his <a href="https://mathspp.gumroad.com/l/comprehending-comprehensions?code=presale">comprehensions course</a></li>
<li><strong><a href="https://nostarch.com/catalog/python">No Starch</a></strong>: 35% off with code <code>DEALS4DAYS</code> (Crash Course, Automate The Boring Stuff, etc.)</li>
<li><strong><a href="https://pragprog.com/">Pragmatic Bookshelf</a></strong>: 40% off <a href="https://pragprog.com/titles/bopytest2/python-testing-with-pytest-second-edition/">the pytest book</a> and all other books with code <code>turkeycode2023</code></li>
<li><strong><a href="https://www.manning.com/catalog#section-50">Manning</a></strong> 50% off eBooks, 40% off print books</li>
<li><strong><a href="https://udemy.com">Udemy</a></strong>: various <a href="https://www.udemy.com/topic/python/">Python courses</a> are on sale right now</li>
</ul>


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

<h2>Django sales</h2>

<p>Adam Johnson is also compiling many <strong>Django-related Black Friday and Cyber Monday sales</strong> via a <a href="https://adamj.eu/tech/2023/11/20/django-black-friday-deals-2023/">Django sales post</a>.</p>

<h2>More developer-oriented deals</h2>

<p>For even more Black Friday deals for software developers, see <a href="https://blackfridaydeals.dev">BlackFridayDeals.dev</a>, which I believe launched this year.</p>

<h2>Go get yourself some deals!</h2>

<p>Go hop on those sales! (But make sure to put an event in your calendar to actually use what you purchase. 😉)
And if you have questions about the <a href="https://www.pythonmorsels.com/lifetime-access-sale/"><strong>Python Morsels Cyber Monday sale</strong></a> 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>

<p>Happy Python-ing!</p>
]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Python Morsels Cyber Monday sale]]></title>
    <link href="https://treyhunner.com/2022/11/python-morsels-cyber-monday-sale/"/>
    <updated>2022-11-25T08:30:00-08:00</updated>
    <id>https://treyhunner.com/2022/11/python-morsels-cyber-monday-sale</id>
    <content type="html"><![CDATA[<p>Python Morsels helps Python users <strong>sharpen their Python skills</strong> in a way that writing production code doesn&rsquo;t. If you are:</p>

<ul>
<li>an experienced developer, frustrated with <strong>gaps in your Python knowledge</strong></li>
<li>a self-taught programmer seeking <strong>courage and confidence</strong> in your Python abilities</li>
<li>or an intermediate-level Python learner trying to <strong>deepen your Python skills</strong></li>
</ul>


<p>&hellip;a weekly Python Morsels habit can help you make <strong>consistent progress</strong> and <strong>noticeable growth</strong> in <strong>just a few months</strong>.</p>

<p>Python Morsels is <strong>on sale</strong> through Cyber Monday. <a href="https://www.pythonmorsels.com/all-python-exercises-and-screencasts/">Subscribe</a> now to <strong><a href="https://www.pythonmorsels.com/pricing/">save up to $108 per year</a></strong>.</p>

<h2>Day-to-day coding isn&rsquo;t purposeful learning</h2>

<p>If you write Python frequently, you likely learn new things all the time.
The learning you get from day-to-day coding is messy and unpredictable. Yes, learning happens, but gradually.</p>

<p>What if you could <strong>learn something unexpected about Python</strong> in <strong>just 30 minutes</strong> a week?</p>

<p>That&rsquo;s what Python Morsels is designed to do: push you <em>just</em> outside your comfort zone to <strong>discover something new</strong> without requiring a big time sink.</p>

<blockquote><p>The time I spent working on Python Morsels problems translates into saved time programming for work. And it&rsquo;s not a grind - it&rsquo;s actually fun. I&rsquo;ve learned advanced Python concepts that I would have never had the opportunity to use in my day to day work.
<br><span style="float: right;">
— Eric Pederson, <a href="https://www.pythonmorsels.com/testimonials/#tag:short_on_time">Python Morsels user</a></span>
<br></p></blockquote>

<div class="clearfix"></div>


<h2>Guided Python practice every single week</h2>

<p>Python Morsels is <strong>quite different</strong> from many other Python learning systems: you tell me your Python skill level (from <strong>novice</strong> to <strong>advanced</strong>) and I send you small tasks to help you sharpen your Python skills.</p>

<p>Every Monday, you&rsquo;ll receive an email from me with:</p>

<ul>
<li>a short screencast to watch (or read)</li>
<li>a multi-part exercise to move you outside your comfort zone (often achievable in 30 minutes)</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>If you&rsquo;d like to nudge your learning in a specific direction, you can always work through a topic-specific exercise path, or watch one of my many screencast series.</p>

<h2>Does this actually work?</h2>

<p>If you use Python Morsels even semi-regularly, I’m confident your Python skills will improve.</p>

<p>Here&rsquo;s what Python Morsels users have to say:</p>

<blockquote><p>I was hesitant about paying for Python Morsels given how many free learning resources there are. But it was definitely worth it. I&rsquo;ve learnt more from Python Morsels than anything else, by far.
<br><span style="float: right;">
— Cosmo Grant</span>
<br></p></blockquote>

<div class="clearfix"></div>


<blockquote><p>During my study of Python, I used various programming challenge sites. I can say for sure that this is the best challenge site I have ever come across.
<br><span style="float: right;">
— Bartosz Chojnacki</span>
<br></p></blockquote>

<div class="clearfix"></div>


<p>Not sure? <a href="https://www.pythonmorsels.com/testimonials/">Read more from Python Morsels users here</a>.</p>

<h2>Lock-in your $200/year subscription</h2>

<p>Python Morsels currently includes <strong>over 150 screencasts and articles</strong> and <strong>nearly 200 exercises</strong>, each of which links to over a dozen helpful resources.</p>

<p>Subscribe before November 29, 2022 to lock-in your subscription at $200/year.</p>

<p><a href="https://www.pythonmorsels.com/pricing" class="subscribe-btn form-big">Subscribe to Python Morsels 💰</a></p>
]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Python Black Friday &amp; Cyber Monday sales (2022)]]></title>
    <link href="https://treyhunner.com/2022/11/python-black-friday-and-cyber-monday-sales-2022/"/>
    <updated>2022-11-22T10:15:00-08:00</updated>
    <id>https://treyhunner.com/2022/11/python-black-friday-and-cyber-monday-sales-2022</id>
    <content type="html"><![CDATA[<p>It&rsquo;s that time of year again… time for my annual compilation post of <strong>Black Friday and Cyber Monday deals for learning Python</strong>.</p>

<h2>Save up to $108 a year on Python Morsels</h2>

<p>Of course I&rsquo;m going to kick things off with my own sale. 😉</p>

<p><a href="https://trey.io/cyber-monday-sale-2022">Python Morsels</a> helps developers <strong>deepen their Python skills</strong> in a way that day-to-day coding simply can&rsquo;t.</p>

<p>Python Morsels is specifically crafted for:</p>

<ul>
<li>experienced developers frustrated with gaps in their Python knowledge</li>
<li>self-taught programmers seeking courage and confidence in their Python abilities</li>
<li>intermediate-level Python learners trying to deepen skills</li>
</ul>


<p>If you saw yourself in that list, subscribe now before prices increase on November 29, 2022!</p>

<p><a href="https://trey.io/cyber-monday-sale-2022" class="subscribe-btn form-big">💰 See the Python Morsels sale</a></p>

<h2>Python books, courses, templates, and exercises</h2>

<p>There are a <em>lot</em> of Python-related sales going on this year.
Note that some of the below sales include courses, some include books, some include templates (Itamar&rsquo;s Docker templates for example) and some include a mix of different learning products.</p>

<ul>
<li><strong><a href="https://store.lerner.co.il/?coupon=BF2022">Reuven Lerner</a></strong>: Reuven&rsquo;s Python courses are 40% off this week with the coupon <code>BF2022</code></li>
<li><strong><a href="http://talkpython.fm/black-friday">Talk Python</a></strong>: Get all Talk Python courses in one $249 bundle</li>
<li><strong>Sundeep Agarwal</strong>: Sundeep&rsquo;s <a href="https://learnbyexample.gumroad.com/l/all-books/FestiveOffer">all books bundle is 64% off</a> (it&rsquo;s $10), the <a href="https://learnbyexample.gumroad.com/l/python-bundle/FestiveOffer">Learn by example Python bundle</a> is 80% off (it&rsquo;s $3), and <a href="https://learnbyexample.gumroad.com/l/py_projects/FestiveOffer">Practice Python Projects</a> is free!</li>
<li><strong><a href="https://store.metasnake.com/?coupon=PANDAS30">Matt Harrison</a></strong>: Matt&rsquo;s offering 30% off his Effective Pandas book on Friday <strong>only</strong></li>
<li><strong><a href="https://pythonspeed.com/products/docker/">Itamar Turner-Trauring</a></strong>: Itamar&rsquo;s Docker packaging products for Python are all 25% off through November with the code <code>FALL22</code></li>
<li><strong><a href="https://www.blog.pythonlibrary.org/2022/11/22/python-black-friday-cyber-monday-sales-2022/">Mike Driscoll</a></strong>: Mike is offering $10 off any of his books this year with the coupon code <code>black2022</code></li>
<li><strong><a href="https://nostarch.com/catalog/python">No Starch</a></strong>: books are 35% off with the coupon <code>HOLIDAYDEALS</code></li>
<li><strong><a href="https://pragprog.com/">Pragmatic Bookshelf</a></strong>: save 40% on Brian Okken&rsquo;s PyTest book or any other Pragmatic Bookshelf book with the coupon code <code>turkeysale2022</code></li>
<li><strong><a href="https://udemy.com">Udemy</a></strong>: various Python courses are also on sale on Udemy right now, including Al Sweigart&rsquo;s <a href="https://www.udemy.com/course/automate/">Automate the Boring Stuff with Python course</a></li>
</ul>


<h2>Python learning subscriptions</h2>

<p>I use a subscription model for <a href="https://trey.io/cyber-monday-sale-2022">Python Morsels</a> because subscriptions (when done well) can encourage habitual learning, which is often more effective than binge-learning.
But Python Morsels isn&rsquo;t the only subscription-based Python learning platform.</p>

<p>Here sales on other learning subscriptions:</p>

<ul>
<li><strong><a href="https://www.oreilly.com/online-learning/cyber-monday-2022.html">O'Reilly Media</a></strong> subscriptions are $200 off with the coupon <a href="https://www.oreilly.com/online-learning/cyber-monday-2022.html">CYBERWEEK22</a></li>
<li><strong><a href="https://www.dunderdata.com/black-friday">Dunder Data</a></strong> subscriptions (by Ted Petrou) are 40% off (normally $399), plus an extra 50% off for completing 3 certificates within 3 months</li>
<li><strong><a href="https://www.datacamp.com/promo/black-friday-2022">DataCamp</a></strong> has a 50% off sale on their annual subscriptions right now as well</li>
</ul>


<p>Also here&rsquo;s a Python-related service that&rsquo;s on sale (a subscription product, not a learning service):</p>

<ul>
<li><strong>Sourcery</strong>: <a href="https://sourcery.ai/pricing/">Sourcery Pro</a> is 33% off for the first 12 months with coupon <code>BLACKFRIDAY2022</code></li>
</ul>


<h2>Django sales</h2>

<p>Adam Johnson compiled many <a href="https://adamj.eu/tech/2022/11/21/django-black-friday-deals-2022/"><strong>Django-related Black Friday and Cyber Monday sales</strong></a>.</p>

<p>Here&rsquo;s a quick summary:</p>

<ul>
<li>Will Vincent is offering 50% off a bundle for newer Django developers (<a href="https://wsvincent.gumroad.com/l/bhylo/blackfriday2022">sale</a>)</li>
<li>Adam Johnson is offering 50% off his books for experienced Django developers (<a href="https://adamj.eu/tech/2022/11/21/django-black-friday-deals-2022/">announcement</a>)</li>
<li>Test Driven is selling a discounted bundle of courses on Django REST Framework, Celery, and search (<a href="https://testdriven.io/bundle/django-black-friday/">sale</a>)</li>
</ul>


<p>Plus other discounted books, apps, templates, and services from others: <a href="https://adamj.eu/tech/2022/11/21/django-black-friday-deals-2022/">read Adam&rsquo;s full post</a> for more details on the Django-related sales this year.</p>

<h2>Go get yourself some deals!</h2>

<p>Go hop on those sales! (But make sure to put an event in your calendar to actually use what you purchase. 😉)</p>

<p>And if you have questions about the <a href="https://trey.io/cyber-monday-sale-2022"><strong>Python Morsels Cyber Monday sale</strong></a> 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>

<p>Happy Python-ing!</p>
]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Overlooked facts about variables and objects in Python: it's all about pointers]]></title>
    <link href="https://treyhunner.com/2022/03/variables-objects-and-pointers-in-python/"/>
    <updated>2022-03-29T08:00:00-07:00</updated>
    <id>https://treyhunner.com/2022/03/variables-objects-and-pointers-in-python</id>
    <content type="html"><![CDATA[<p><em>This article was originally published <a href="https://www.pythonmorsels.com/pointers/">on Python Morsels</a>.</em></p>

<p>In Python, variables and data structures <strong>don&rsquo;t contain objects</strong>.
This fact is both commonly overlooked and tricky to internalize.</p>

<p>You can happily use Python for years without really understanding the below concepts, but this knowledge can certainly help alleviate <em>many</em> common Python gotchas.</p>

<p>Table of Contents:</p>

<ul data-toc=".entry-content"></ul>


<h2>Terminology</h2>

<p>Let&rsquo;s start with by introducing some terminology.
The last few definitions likely won&rsquo;t make sense until we define them in more detail later on.</p>

<p><strong>Object</strong> (a.k.a. <strong>value</strong>): a &ldquo;thing&rdquo;.
Lists, dictionaries, strings, numbers, tuples, functions, and modules are all objects.
&ldquo;Object&rdquo; defies definition because <a href="https://www.pythonmorsels.com/topics/everything-is-an-object/">everything is an object in Python</a>.</p>

<p><strong>Variable</strong> (a.k.a. <strong>name</strong>): a name used to refer to an object.</p>

<p><strong>Pointer</strong> (a.k.a. <strong>reference</strong>): describes where an object lives (often shown visually as an arrow)</p>

<p><strong>Equality</strong>: whether two objects represent the same data</p>

<p><strong>Identity</strong>: whether two pointers refer to the same object</p>

<p>These terms are best understood by their relationships to each other and that&rsquo;s the primarily purpose of this article.</p>

<h2>Python&rsquo;s variables are pointers, not buckets</h2>

<p>Variables in Python are not buckets containing things; they&rsquo;re <strong>pointers</strong> (they <em>point</em> to objects).</p>

<p>The word &ldquo;pointer&rdquo; may sound scary, but a lot of that scariness comes from related concepts (e.g. dereferencing) which aren&rsquo;t relevant in Python.
In Python a pointer just represents <strong>the connection between a variable and an objects</strong>.</p>

<p>Imagine <strong>variables</strong> living in <em>variable land</em> and <strong>objects</strong> living in <em>object land</em>.
A <strong>pointer</strong> is a little arrow that connects each variable to the object it <strong>points to</strong>.</p>

<p><img class="no-radius full-width" src="https://treyhunner.com/images/variable-diagram-different-values.svg" title="Diagram showing variables on the left and objects on the right, with arrows between each. The numbers variable points to a list. The numbers2 variable points to a separate list. The name variable points to a string." ></p>

<p>This above diagram represents the state of our Python process after running 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>
</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 class="p">[</span><span class="mi">2</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">7</span><span class="p">]</span>
</span><span class='line'><span class="gp">&gt;&gt;&gt; </span><span class="n">numbers2</span> <span class="o">=</span> <span class="p">[</span><span class="mi">11</span><span class="p">,</span> <span class="mi">18</span><span class="p">,</span> <span class="mi">29</span><span class="p">]</span>
</span><span class='line'><span class="gp">&gt;&gt;&gt; </span><span class="n">name</span> <span class="o">=</span> <span class="s">&quot;Trey&quot;</span>
</span></code></pre></td></tr></table></div></figure>


<p>If the word <strong>pointer</strong> scares you, use the word <strong>reference</strong> instead.
Whenever you see pointer-based phrases in this article, do a mental translation to a reference-based phrase:</p>

<ul>
<li><strong>pointer</strong> &rArr; <strong>reference</strong></li>
<li><strong>point to</strong> &rArr; <strong>refer to</strong></li>
<li><strong>pointed to</strong> &rArr; <strong>referenced</strong></li>
<li><strong>point X to Y</strong> &rArr; <strong>cause X to refer to Y</strong></li>
</ul>


<h2>Assignments point a variable to an object</h2>

<p>Assignment statements point a variable to an object.
That&rsquo;s it.</p>

<p>If we run 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>
</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 class="p">[</span><span class="mi">2</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">7</span><span class="p">]</span>
</span><span class='line'><span class="gp">&gt;&gt;&gt; </span><span class="n">numbers2</span> <span class="o">=</span> <span class="n">numbers</span>
</span><span class='line'><span class="gp">&gt;&gt;&gt; </span><span class="n">name</span> <span class="o">=</span> <span class="s">&quot;Trey&quot;</span>
</span></code></pre></td></tr></table></div></figure>


<p>The state of our variables and objects would look like this:</p>

<p><img class="no-radius full-width" src="https://treyhunner.com/images/variable-diagram-same-value.svg" title="Diagram showing variables on the left and objects on the right, with arrows between each. The numbers and numbers2 variables have arrows coming out of them pointing to the same list. The name variable points to a string." ></p>

<p>Note that <code>numbers</code> and <code>numbers2</code> <strong>point to the same object</strong>.
If we <em>change</em> that object, both variables will seem to &ldquo;see&rdquo; that change:</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='pycon'><span class='line'><span class="gp">&gt;&gt;&gt; </span><span class="n">numbers</span><span class="o">.</span><span class="n">pop</span><span class="p">()</span>
</span><span class='line'><span class="go">7</span>
</span><span class='line'><span class="gp">&gt;&gt;&gt; </span><span class="n">numbers</span>
</span><span class='line'><span class="go">[2, 1, 3, 4]</span>
</span><span class='line'><span class="gp">&gt;&gt;&gt; </span><span class="n">numbers2</span>
</span><span class='line'><span class="go">[2, 1, 3, 4]</span>
</span></code></pre></td></tr></table></div></figure>


<p>That strangeness was all due to this assignment statement:</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">numbers2</span> <span class="o">=</span> <span class="n">numbers</span>
</span></code></pre></td></tr></table></div></figure>


<p>Assignment statements don&rsquo;t copy anything: they just point a variable to an object.
So assigning one variable to another variable just <strong>points two variables to the same object</strong>.</p>

<h2>The 2 types of &ldquo;change&rdquo; in Python</h2>

<p>Python has 2 distinct types of &ldquo;change&rdquo;:</p>

<ol>
<li><strong>Assignment</strong> changes a variable (it changes <em>which</em> object it points to)</li>
<li><strong>Mutation</strong> changes an object (which any number of variables might point to)</li>
</ol>


<p>The word &ldquo;change&rdquo; is often ambiguous.
The phrase &ldquo;we changed <code>x</code>&rdquo; could mean &ldquo;we re-assigned <code>x</code>&rdquo; or it might mean &ldquo;we mutated the object <code>x</code> points to&rdquo;.</p>

<p><strong>Mutations change objects</strong>, not variables.
But variables <em>point to</em> objects.
So if another variable points to an object that <em>we&rsquo;ve just mutated</em>, that other variable will reflect the same change; not because the variable changed but because <strong>the object it points to</strong> changed.</p>

<h2>Equality compares objects and identity compares pointers</h2>

<p>Python&rsquo;s <code>==</code> operator checks that two objects <strong>represent the same data</strong> (a.k.a. <strong>equality</strong>):</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="gp">&gt;&gt;&gt; </span><span class="n">my_numbers</span> <span class="o">=</span> <span class="p">[</span><span class="mi">2</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mi">4</span><span class="p">]</span>
</span><span class='line'><span class="gp">&gt;&gt;&gt; </span><span class="n">your_numbers</span> <span class="o">=</span> <span class="p">[</span><span class="mi">2</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mi">4</span><span class="p">]</span>
</span><span class='line'><span class="gp">&gt;&gt;&gt; </span><span class="n">my_numbers</span> <span class="o">==</span> <span class="n">your_numbers</span>
</span><span class='line'><span class="go">True</span>
</span></code></pre></td></tr></table></div></figure>


<p>Python&rsquo;s <code>is</code> operator checks whether two objects <strong>are the same object</strong> (a.k.a. <strong>identity</strong>):</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='pycon'><span class='line'><span class="gp">&gt;&gt;&gt; </span><span class="n">my_numbers</span> <span class="ow">is</span> <span class="n">your_numbers</span>
</span><span class='line'><span class="go">False</span>
</span></code></pre></td></tr></table></div></figure>


<p>The variables <code>my_numbers</code> and <code>your_numbers</code> point to <strong>objects representing the same data</strong>, but the objects they point to <strong>are not the same object</strong>.</p>

<p>So changing one object doesn&rsquo;t change the other:</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='pycon'><span class='line'><span class="gp">&gt;&gt;&gt; </span><span class="n">my_numbers</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">=</span> <span class="mi">7</span>
</span><span class='line'><span class="gp">&gt;&gt;&gt; </span><span class="n">my_numbers</span> <span class="o">==</span> <span class="n">your_numbers</span>
</span><span class='line'><span class="go">False</span>
</span></code></pre></td></tr></table></div></figure>


<p>If two variables point to the same object:</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='pycon'><span class='line'><span class="gp">&gt;&gt;&gt; </span><span class="n">my_numbers_again</span> <span class="o">=</span> <span class="n">my_numbers</span>
</span><span class='line'><span class="gp">&gt;&gt;&gt; </span><span class="n">my_numbers</span> <span class="ow">is</span> <span class="n">my_numbers_again</span>
</span><span class='line'><span class="go">True</span>
</span></code></pre></td></tr></table></div></figure>


<p>Changing the object one variable points also changes the object the other points to because they both point to the same object:</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='pycon'><span class='line'><span class="gp">&gt;&gt;&gt; </span><span class="n">my_numbers_again</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="mi">7</span><span class="p">)</span>
</span><span class='line'><span class="gp">&gt;&gt;&gt; </span><span class="n">my_numbers_again</span>
</span><span class='line'><span class="go">[2, 1, 3, 4, 7]</span>
</span><span class='line'><span class="gp">&gt;&gt;&gt; </span><span class="n">my_numbers</span>
</span><span class='line'><span class="go">[2, 1, 3, 4, 7]</span>
</span></code></pre></td></tr></table></div></figure>


<p>The <code>==</code> operator checks for <strong>equality</strong> and the <code>is</code> operator checks for <strong>identity</strong>.
This distinction between identity and equality exists because variables <strong>don&rsquo;t contain objects</strong>, they <strong>point to objects</strong>.</p>

<p>In Python equality checks are very common and <a href="https://www.pythonmorsels.com/topics/equality-vs-identity/">identity checks are very rare</a>.</p>

<h2>There&rsquo;s no exception for immutable objects</h2>

<p>But wait, modifying a number <em>doesn&rsquo;t</em> change other variables pointing to the same number, right?</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='pycon'><span class='line'><span class="gp">&gt;&gt;&gt; </span><span class="n">n</span> <span class="o">=</span> <span class="mi">3</span>
</span><span class='line'><span class="gp">&gt;&gt;&gt; </span><span class="n">m</span> <span class="o">=</span> <span class="n">n</span>  <span class="c"># n and m point to the same number</span>
</span><span class='line'><span class="gp">&gt;&gt;&gt; </span><span class="n">n</span> <span class="o">+=</span> <span class="mi">2</span>
</span><span class='line'><span class="gp">&gt;&gt;&gt; </span><span class="n">n</span>  <span class="c"># n has changed</span>
</span><span class='line'><span class="go">5</span>
</span><span class='line'><span class="gp">&gt;&gt;&gt; </span><span class="n">m</span>  <span class="c"># but m hasn&#39;t changed!</span>
</span><span class='line'><span class="go">3</span>
</span></code></pre></td></tr></table></div></figure>


<p>Well, <strong>modifying a number is not possible</strong> in Python.
Numbers and strings are both <strong>immutable</strong>, meaning you can&rsquo;t mutate them.
You <strong>cannot change</strong> an immutable object.</p>

<p>So what about that <code>+=</code> operator above?
Didn&rsquo;t that mutate a number?
(It didn&rsquo;t.)</p>

<p>With immutable objects, these two statements are equivalent:</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='pycon'><span class='line'><span class="gp">&gt;&gt;&gt; </span><span class="n">n</span> <span class="o">+=</span> <span class="mi">2</span>
</span><span class='line'><span class="gp">&gt;&gt;&gt; </span><span class="n">n</span> <span class="o">=</span> <span class="n">n</span> <span class="o">+</span> <span class="mi">2</span>
</span></code></pre></td></tr></table></div></figure>


<p>For immutable objects, augmented assignments (<code>+=</code>, <code>*=</code>, <code>%=</code>, etc.) perform an operation (which returns a new object) and then do an assignment (to that new object).</p>

<p>Any operation you might <em>think</em> changes a string or a number instead returns a new object.
Any operation on an immutable object always <strong>returns a new object</strong> instead of modifying the original.</p>

<h2>Data structures contain pointers</h2>

<p>Like variables, data structures <strong>don&rsquo;t contain objects</strong>, they <strong>contain pointers to objects</strong>.</p>

<p>Let&rsquo;s say we make a list-of-lists:</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">matrix</span> <span class="o">=</span> <span class="p">[[</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">],</span> <span class="p">[</span><span class="mi">4</span><span class="p">,</span> <span class="mi">5</span><span class="p">,</span> <span class="mi">6</span><span class="p">],</span> <span class="p">[</span><span class="mi">7</span><span class="p">,</span> <span class="mi">8</span><span class="p">,</span> <span class="mi">9</span><span class="p">]]</span>
</span></code></pre></td></tr></table></div></figure>


<p>And then we make a variable pointing to the second list in our list-of-lists:</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='pycon'><span class='line'><span class="gp">&gt;&gt;&gt; </span><span class="n">row</span> <span class="o">=</span> <span class="n">matrix</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span>
</span><span class='line'><span class="gp">&gt;&gt;&gt; </span><span class="n">row</span>
</span><span class='line'><span class="go">[4, 5, 6]</span>
</span></code></pre></td></tr></table></div></figure>


<p>The state of our variables and objects now looks like this:</p>

<p><img class="no-radius full-width" src="https://treyhunner.com/images/data-structures-diagram.svg" title="Diagram showing matrix variable which points to a list of 3 items. Each item has an arrow coming out of it, pointing to a separate list. Each of these sublists has 3 elements which each point to a separate integer object. There's also a row variable which points to a list that's also pointed to by index 1 of the matrix list." ></p>

<p>Our <code>row</code> variable <strong>points to the same object</strong> as index <code>1</code> in our <code>matrix</code> list:</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='pycon'><span class='line'><span class="gp">&gt;&gt;&gt; </span><span class="n">row</span> <span class="ow">is</span> <span class="n">matrix</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span>
</span><span class='line'><span class="go">True</span>
</span></code></pre></td></tr></table></div></figure>


<p>So if we mutate the list that <code>row</code> points to:</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">row</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">=</span> <span class="mi">1000</span>
</span></code></pre></td></tr></table></div></figure>


<p>We&rsquo;ll see that change in both places:</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="gp">&gt;&gt;&gt; </span><span class="n">row</span>
</span><span class='line'><span class="go">[1000, 5, 6]</span>
</span><span class='line'><span class="gp">&gt;&gt;&gt; </span><span class="n">matrix</span>
</span><span class='line'><span class="go">[[1, 2, 3], [1000, 5, 6], [7, 8, 9]]</span>
</span></code></pre></td></tr></table></div></figure>


<p>It&rsquo;s common to speak of data structures &ldquo;containing&rdquo; objects, but they actually only contain pointers to objects.</p>

<h2>Function arguments act like assignment statements</h2>

<p>Function calls also perform assignments.</p>

<p>If you mutate an object that was passed-in to your function, you&rsquo;ve mutated the original object:</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>
</pre></td><td class='code'><pre><code class='pycon'><span class='line'><span class="gp">&gt;&gt;&gt; </span><span class="k">def</span> <span class="nf">smallest_n</span><span class="p">(</span><span class="n">items</span><span class="p">,</span> <span class="n">n</span><span class="p">):</span>
</span><span class='line'><span class="gp">... </span>    <span class="n">items</span><span class="o">.</span><span class="n">sort</span><span class="p">()</span>  <span class="c"># This mutates the list (it sorts in-place)</span>
</span><span class='line'><span class="gp">... </span>    <span class="k">return</span> <span class="n">items</span><span class="p">[:</span><span class="n">n</span><span class="p">]</span>
</span><span class='line'><span class="gp">...</span>
</span><span class='line'><span class="gp">&gt;&gt;&gt; </span><span class="n">numbers</span> <span class="o">=</span> <span class="p">[</span><span class="mi">29</span><span class="p">,</span> <span class="mi">7</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">11</span><span class="p">,</span> <span class="mi">18</span><span class="p">,</span> <span class="mi">2</span><span class="p">]</span>
</span><span class='line'><span class="gp">&gt;&gt;&gt; </span><span class="n">smallest_n</span><span class="p">(</span><span class="n">numbers</span><span class="p">,</span> <span class="mi">4</span><span class="p">)</span>
</span><span class='line'><span class="go">[1, 2, 4, 7]</span>
</span><span class='line'><span class="gp">&gt;&gt;&gt; </span><span class="n">numbers</span>
</span><span class='line'><span class="go">[1, 2, 4, 7, 11, 18, 29]</span>
</span></code></pre></td></tr></table></div></figure>


<p>But if you reassign a variable to a different object, the original object will not change:</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>
</pre></td><td class='code'><pre><code class='pycon'><span class='line'><span class="gp">&gt;&gt;&gt; </span><span class="k">def</span> <span class="nf">smallest_n</span><span class="p">(</span><span class="n">items</span><span class="p">,</span> <span class="n">n</span><span class="p">):</span>
</span><span class='line'><span class="gp">... </span>    <span class="n">items</span> <span class="o">=</span> <span class="nb">sorted</span><span class="p">(</span><span class="n">items</span><span class="p">)</span>  <span class="c"># this makes a new list (original is unchanged)</span>
</span><span class='line'><span class="gp">... </span>    <span class="k">return</span> <span class="n">items</span><span class="p">[:</span><span class="n">n</span><span class="p">]</span>
</span><span class='line'><span class="gp">...</span>
</span><span class='line'><span class="gp">&gt;&gt;&gt; </span><span class="n">numbers</span> <span class="o">=</span> <span class="p">[</span><span class="mi">29</span><span class="p">,</span> <span class="mi">7</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">11</span><span class="p">,</span> <span class="mi">18</span><span class="p">,</span> <span class="mi">2</span><span class="p">]</span>
</span><span class='line'><span class="gp">&gt;&gt;&gt; </span><span class="n">smallest_n</span><span class="p">(</span><span class="n">numbers</span><span class="p">,</span> <span class="mi">4</span><span class="p">)</span>
</span><span class='line'><span class="go">[1, 2, 4, 7]</span>
</span><span class='line'><span class="gp">&gt;&gt;&gt; </span><span class="n">numbers</span>
</span><span class='line'><span class="go">[29, 7, 1, 4, 11, 18, 2]</span>
</span></code></pre></td></tr></table></div></figure>


<p>We&rsquo;re reassigning the <code>items</code> variable here.
That reassignment changes <em>which</em> object the <code>items</code> variable points to, but it doesn&rsquo;t change the original object.</p>

<p>We <strong>changed an object</strong> in the first case and we <strong>changed a variable</strong> in the second case.</p>

<p>Here&rsquo;s another example you&rsquo;ll sometimes see:</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='python'><span class='line'><span class="k">class</span> <span class="nc">Widget</span><span class="p">:</span>
</span><span class='line'>    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">attrs</span><span class="o">=</span><span class="p">(),</span> <span class="n">choices</span><span class="o">=</span><span class="p">()):</span>
</span><span class='line'>        <span class="bp">self</span><span class="o">.</span><span class="n">attrs</span> <span class="o">=</span> <span class="nb">list</span><span class="p">(</span><span class="n">attrs</span><span class="p">)</span>
</span><span class='line'>        <span class="bp">self</span><span class="o">.</span><span class="n">choices</span> <span class="o">=</span> <span class="nb">list</span><span class="p">(</span><span class="n">choices</span><span class="p">)</span>
</span></code></pre></td></tr></table></div></figure>


<p>Class <a href="https://www.pythonmorsels.com/topics/what-is-init/">initializer methods</a> often copy iterables given to them by making a new list out of their items.
This allows the class to accept any iterable (not just lists) and decouples the original iterable from the class (modifying these lists won&rsquo;t upset the original caller).
The above example was <a href="https://github.com/django/django/blob/4.0.2/django/forms/widgets.py#L560,L565">borrowed from Django</a>.</p>

<p><strong>Don&rsquo;t mutate the objects</strong> passed-in to your function unless the function caller expects you to.</p>

<h2>Copies are shallow and that&rsquo;s usually okay</h2>

<p>Need to copy a list in Python?</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 class="p">[</span><span class="mi">2000</span><span class="p">,</span> <span class="mi">1000</span><span class="p">,</span> <span class="mi">3000</span><span class="p">]</span>
</span></code></pre></td></tr></table></div></figure>


<p>You could call the <code>copy</code> method (if you&rsquo;re certain your iterable is a list):</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">my_numbers</span> <span class="o">=</span> <span class="n">numbers</span><span class="o">.</span><span class="n">copy</span><span class="p">()</span>
</span></code></pre></td></tr></table></div></figure>


<p>Or you could pass it to the <code>list</code> constructor (this works on <strong>any iterable</strong>):</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">my_numbers</span> <span class="o">=</span> <span class="nb">list</span><span class="p">(</span><span class="n">numbers</span><span class="p">)</span>
</span></code></pre></td></tr></table></div></figure>


<p>Both of these techniques make a new list which <strong>points to the same objects</strong> as the original list.</p>

<p>The two lists are distinct, but the objects within them are the same:</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="gp">&gt;&gt;&gt; </span><span class="n">numbers</span> <span class="ow">is</span> <span class="n">my_numbers</span>
</span><span class='line'><span class="go">False</span>
</span><span class='line'><span class="gp">&gt;&gt;&gt; </span><span class="n">numbers</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="ow">is</span> <span class="n">my_numbers</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span>
</span><span class='line'><span class="go">True</span>
</span></code></pre></td></tr></table></div></figure>


<p>Since integers (and all numbers) are immutable in Python we don&rsquo;t really care that each list contains the same objects because we can&rsquo;t mutate those objects anyway.</p>

<p>With mutable objects, this distinction matters.
This makes two list-of-lists which each contain pointers to the same three lists:</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='pycon'><span class='line'><span class="gp">&gt;&gt;&gt; </span><span class="n">matrix</span> <span class="o">=</span> <span class="p">[[</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">],</span> <span class="p">[</span><span class="mi">4</span><span class="p">,</span> <span class="mi">5</span><span class="p">,</span> <span class="mi">6</span><span class="p">],</span> <span class="p">[</span><span class="mi">7</span><span class="p">,</span> <span class="mi">8</span><span class="p">,</span> <span class="mi">9</span><span class="p">]]</span>
</span><span class='line'><span class="gp">&gt;&gt;&gt; </span><span class="n">new_matrix</span> <span class="o">=</span> <span class="nb">list</span><span class="p">(</span><span class="n">matrix</span><span class="p">)</span>
</span></code></pre></td></tr></table></div></figure>


<p>These two lists aren&rsquo;t the same, but each item within them is the same:</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="gp">&gt;&gt;&gt; </span><span class="n">matrix</span> <span class="ow">is</span> <span class="n">new_matrix</span>
</span><span class='line'><span class="go">False</span>
</span><span class='line'><span class="gp">&gt;&gt;&gt; </span><span class="n">matrix</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="ow">is</span> <span class="n">new_matrix</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span>
</span><span class='line'><span class="go">True</span>
</span></code></pre></td></tr></table></div></figure>


<p>Here&rsquo;s a rather complex visual representation of these two objects and the pointers they contain:</p>

<p><img class="no-radius full-width" src="https://treyhunner.com/images/data-structures-same-pointers-diagram.svg" title="Diagram showing matrix variable which points to a list of 3 items and a new_matrix variable which points to a separate list of 3 items. Each corresponding item in each of these matrix and new_matrix lists points to the same sublist." ></p>

<p>So if we mutate the first item in one list, it&rsquo;ll mutate the same item within the other list:</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='pycon'><span class='line'><span class="gp">&gt;&gt;&gt; </span><span class="n">matrix</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="mi">100</span><span class="p">)</span>
</span><span class='line'><span class="gp">&gt;&gt;&gt; </span><span class="n">matrix</span>
</span><span class='line'><span class="go">[[1, 2, 3, 100], [4, 5, 6], [7, 8, 9]]</span>
</span><span class='line'><span class="gp">&gt;&gt;&gt; </span><span class="n">new_matrix</span>
</span><span class='line'><span class="go">[[1, 2, 3, 100], [4, 5, 6], [7, 8, 9]]</span>
</span></code></pre></td></tr></table></div></figure>


<p>When you copy an object in Python, if that object <strong>points to other objects</strong>, you&rsquo;ll copy pointers to those other objects instead of copying the objects themselves.</p>

<p>New Python programmers respond to this behavior by sprinkling <code>copy.deepcopy</code> into their code.
The <code>deepcopy</code> function attempts to recursively copy an object along with all objects it points to.</p>

<p>Sometimes new Python programmers will use <code>deepcopy</code> to recursively copy data structures:</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>
</pre></td><td class='code'><pre><code class='python'><span class='line'><span class="kn">from</span> <span class="nn">copy</span> <span class="kn">import</span> <span class="n">deepcopy</span>
</span><span class='line'><span class="kn">from</span> <span class="nn">datetime</span> <span class="kn">import</span> <span class="n">datetime</span>
</span><span class='line'>
</span><span class='line'><span class="n">tweet_data</span> <span class="o">=</span> <span class="p">[{</span><span class="s">&quot;date&quot;</span><span class="p">:</span> <span class="s">&quot;Feb 04 2014&quot;</span><span class="p">,</span> <span class="s">&quot;text&quot;</span><span class="p">:</span> <span class="s">&quot;Hi Twitter&quot;</span><span class="p">},</span> <span class="p">{</span><span class="s">&quot;date&quot;</span><span class="p">:</span> <span class="s">&quot;Apr 16 2014&quot;</span><span class="p">,</span> <span class="s">&quot;text&quot;</span><span class="p">:</span> <span class="s">&quot;At #pycon2014&quot;</span><span class="p">}]</span>
</span><span class='line'>
</span><span class='line'><span class="c"># Parse date strings into datetime objects</span>
</span><span class='line'><span class="n">processed_data</span> <span class="o">=</span> <span class="n">deepcopy</span><span class="p">(</span><span class="n">tweet_data</span><span class="p">)</span>
</span><span class='line'><span class="k">for</span> <span class="n">tweet</span> <span class="ow">in</span> <span class="n">processed_data</span><span class="p">:</span>
</span><span class='line'>    <span class="n">tweet</span><span class="p">[</span><span class="s">&quot;date&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">datetime</span><span class="o">.</span><span class="n">strptime</span><span class="p">(</span><span class="n">tweet</span><span class="p">[</span><span class="s">&quot;date&quot;</span><span class="p">],</span> <span class="s">&quot;%b </span><span class="si">%d</span><span class="s"> %Y&quot;</span><span class="p">)</span>
</span></code></pre></td></tr></table></div></figure>


<p>But in Python, we often prefer to make new objects instead of mutating existing objects.
So we could entirely remove that <code>deepcopy</code> usage above by making a new list of new dictionaries instead of deep-copying our old list-of-dictionaries.</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='python'><span class='line'><span class="c"># Parse date strings into datetime objects</span>
</span><span class='line'><span class="n">processed_data</span> <span class="o">=</span> <span class="p">[</span>
</span><span class='line'>    <span class="p">{</span><span class="o">**</span><span class="n">tweet</span><span class="p">,</span> <span class="s">&quot;date&quot;</span><span class="p">:</span> <span class="n">datetime</span><span class="o">.</span><span class="n">strptime</span><span class="p">(</span><span class="n">tweet</span><span class="p">[</span><span class="s">&quot;date&quot;</span><span class="p">],</span> <span class="s">&quot;%b </span><span class="si">%d</span><span class="s"> %Y&quot;</span><span class="p">)}</span>
</span><span class='line'>    <span class="k">for</span> <span class="n">tweet</span> <span class="ow">in</span> <span class="n">tweet_data</span>
</span><span class='line'><span class="p">]</span>
</span></code></pre></td></tr></table></div></figure>


<p>We tend to prefer shallow copies in Python.
If you <strong>don&rsquo;t mutate objects that don&rsquo;t belong to you</strong> you usually won&rsquo;t have any need for <code>deepcopy</code>.</p>

<p>The <code>deepcopy</code> function certainly has its uses, but it&rsquo;s often unnecessary.
&ldquo;How to avoid using <code>deepcopy</code>&rdquo; warrants a separate discussion in a future article.</p>

<h2>Summary</h2>

<p>Variables in Python are not buckets containing things; they&rsquo;re <strong>pointers</strong> (they <em>point</em> to objects).</p>

<p>Python&rsquo;s model of variables and objects boils down to two primary rules:</p>

<ol>
<li><strong>Mutation</strong> changes an object</li>
<li><strong>Assignment</strong> points a variable to an object</li>
</ol>


<p>As well as these corollary rules:</p>

<ol>
<li><strong>Reassigning</strong> a variable points it to <strong>a different object</strong>, leaving the original object unchanged</li>
<li><strong>Assignments don&rsquo;t copy</strong> anything, so it&rsquo;s up to you to copy objects as needed</li>
</ol>


<p>Furthermore, data structures work the same way: lists and dictionaries container <strong>pointers to objects</strong> rather than the objects themselves.
And attributes work the same way: <strong>attributes point to objects</strong> (just like any variable points to an object).
So <strong>objects cannot contain objects in Python</strong> (they can only <em>point to</em> objects).</p>

<p>And note that while <strong>mutations change objects</strong> (not variables), multiple variables <em>can</em> point to the same object.
If two variables point to the same object changes to that object will be seen when accessing either variable (because they both point to <em>the same</em> object).</p>

<p>For more on this topic see:</p>

<ul>
<li>My <a href="https://www.pythonmorsels.com/topics/playlist/assignment-and-mutation/">screencast series on Assignments and Mutation in Python</a></li>
<li>Ned Batchelder&rsquo;s <a href="https://nedbatchelder.com/text/names1.html">Python Names and Values</a> talk</li>
<li>Brandon Rhodes' <a href="https://pyvideo.org/pyohio-2011/pyohio-2011-names-objects-and-plummeting-from.html">Names, Objects, and Plummeting From The Cliff</a> talk</li>
</ul>


<p>This mental model of Python is tricky to internalize so it&rsquo;s okay if it still feels confusing!
Python&rsquo;s features and best practices <em>often</em> nudge us toward &ldquo;doing the right thing&rdquo; automatically.
But if your code is acting strangely, it might be due to changing an object you didn&rsquo;t mean to change.</p>
]]></content>
  </entry>
  
</feed>
