<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0"><channel><title><![CDATA[Mostly Python]]></title><description><![CDATA[I write mostly about Python, and other topics that come up in the course of my programming and writing work. New posts on Thursdays, and some Tuesdays as well.]]></description><link>https://mostlypython.substack.com</link><image><url>https://substackcdn.com/image/fetch/$s_!lf6I!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F4ac4a977-0380-4396-8bbc-5215a0dbbb9b_500x500.png</url><title>Mostly Python</title><link>https://mostlypython.substack.com</link></image><generator>Substack</generator><lastBuildDate>Wed, 13 May 2026 08:00:51 GMT</lastBuildDate><atom:link href="https://mostlypython.substack.com/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[Eric Matthes]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[mostlypython@substack.com]]></webMaster><itunes:owner><itunes:email><![CDATA[mostlypython@substack.com]]></itunes:email><itunes:name><![CDATA[Eric Matthes]]></itunes:name></itunes:owner><itunes:author><![CDATA[Eric Matthes]]></itunes:author><googleplay:owner><![CDATA[mostlypython@substack.com]]></googleplay:owner><googleplay:email><![CDATA[mostlypython@substack.com]]></googleplay:email><googleplay:author><![CDATA[Eric Matthes]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[Amusing logical errors]]></title><description><![CDATA[MP 86: And the value of small utility functions.]]></description><link>https://mostlypython.substack.com/p/amusing-logical-errors</link><guid isPermaLink="false">https://mostlypython.substack.com/p/amusing-logical-errors</guid><dc:creator><![CDATA[Eric Matthes]]></dc:creator><pubDate>Thu, 07 Mar 2024 17:30:42 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!_Lz4!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb1dbfda9-35ca-4c2a-b97d-b0ae544442a4_754x446.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><strong>Note:</strong><em> I was hoping to send this week&#8217;s post from <a href="https://ghost.org">Ghost</a>. But it&#8217;s been a busy week, and I need a little more time to manage the transition off of Substack. (I don&#8217;t recommend trying to do a site migration while trying to sell your house in preparation for a 5,000-mile move.)</em></p><div><hr></div><p>As programmers, we choose to spend a significant part of our lives chasing bugs. I think it&#8217;s probably fair to say we spend more time staring at code that&#8217;s broken in some way, than code that&#8217;s working exactly as it&#8217;s supposed to.</p><p>In this post I&#8217;ll share some of the work I&#8217;ve been doing to move this newsletter to Ghost, and how it relates to troubleshooting errors in the early stages of a project.</p><h3>Two kinds of bugs</h3><p>Broadly speaking, there are two kinds of bugs. One class of bugs causes code to break. At some point while executing the code in a project, Python encounters an error it can&#8217;t resolve effectively enough to continue running. This kind of error leads to a traceback, which hopefully has enough information to point you toward the source of the issue.</p><p>The second kind of bug is a <em>logical error</em>. In this case, the program is able to complete its execution. If an issue arises during the program&#8217;s execution, Python can find a way around it without generating a traceback. These bugs are often harder to sort out. They usually depend on a deeper understanding of the problem you&#8217;re trying to solve, and a detailed understanding of how the code addresses that problem.</p><h3>Back to code blocks</h3><p>Early last year I wrote a long <a href="https://www.mostlypython.com/p/improving-code-blocks-in-substack">post</a> about how Substack should improve their code blocks if they want to support good technical writing. They haven&#8217;t done anything to address this, and one of the main reasons I&#8217;m moving off of Substack is that it&#8217;s not designed well for technical writing.</p><p>As an example, here&#8217;s a block of code I want to use in an upcoming post:</p><pre><code>from pathlib import Path

<strong>path = Path("coffees.txt")</strong>
contents = path.read_text()

print(contents)</code></pre><p>This is about as much as you can do with a code block on Substack. There&#8217;s no syntax highlighting, and there&#8217;s no built-in way to indicate which lines you&#8217;re focusing on in the text. Most of us would never put up with this kind of limitation in a text editor or IDE. The lack of modern styling also limits how clearly you can talk about specific lines of code.</p><p>With its default settings, Ghost doesn&#8217;t do much better in email posts:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!OZvX!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fab359ce6-8bed-4bab-991e-a93216b44a62_589x226.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!OZvX!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fab359ce6-8bed-4bab-991e-a93216b44a62_589x226.png 424w, https://substackcdn.com/image/fetch/$s_!OZvX!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fab359ce6-8bed-4bab-991e-a93216b44a62_589x226.png 848w, https://substackcdn.com/image/fetch/$s_!OZvX!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fab359ce6-8bed-4bab-991e-a93216b44a62_589x226.png 1272w, https://substackcdn.com/image/fetch/$s_!OZvX!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fab359ce6-8bed-4bab-991e-a93216b44a62_589x226.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!OZvX!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fab359ce6-8bed-4bab-991e-a93216b44a62_589x226.png" width="589" height="226" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ab359ce6-8bed-4bab-991e-a93216b44a62_589x226.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:226,&quot;width&quot;:589,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:17135,&quot;alt&quot;:&quot;code block with plain white text on black background&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="code block with plain white text on black background" title="code block with plain white text on black background" srcset="https://substackcdn.com/image/fetch/$s_!OZvX!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fab359ce6-8bed-4bab-991e-a93216b44a62_589x226.png 424w, https://substackcdn.com/image/fetch/$s_!OZvX!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fab359ce6-8bed-4bab-991e-a93216b44a62_589x226.png 848w, https://substackcdn.com/image/fetch/$s_!OZvX!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fab359ce6-8bed-4bab-991e-a93216b44a62_589x226.png 1272w, https://substackcdn.com/image/fetch/$s_!OZvX!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fab359ce6-8bed-4bab-991e-a93216b44a62_589x226.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><figcaption class="image-caption">By default, Ghost&#8217;s code blocks are quite plain as well. They do make it easy to show the file that a listing refers to.</figcaption></figure></div><p>There are two things Ghost does better out of the box. First, you can add captions to code blocks, which makes it easier to show which file a listing refers to.</p><p>Second, Ghost lets you do more with the web version of each post. For example, you can apply syntax highlighting to the online versions of all posts:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!xL-f!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff95b9c0d-498e-4dd3-a216-05a8b9cf81d2_749x224.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!xL-f!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff95b9c0d-498e-4dd3-a216-05a8b9cf81d2_749x224.png 424w, https://substackcdn.com/image/fetch/$s_!xL-f!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff95b9c0d-498e-4dd3-a216-05a8b9cf81d2_749x224.png 848w, https://substackcdn.com/image/fetch/$s_!xL-f!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff95b9c0d-498e-4dd3-a216-05a8b9cf81d2_749x224.png 1272w, https://substackcdn.com/image/fetch/$s_!xL-f!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff95b9c0d-498e-4dd3-a216-05a8b9cf81d2_749x224.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!xL-f!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff95b9c0d-498e-4dd3-a216-05a8b9cf81d2_749x224.png" width="749" height="224" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f95b9c0d-498e-4dd3-a216-05a8b9cf81d2_749x224.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:224,&quot;width&quot;:749,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:18128,&quot;alt&quot;:&quot;code block with Python-specific syntax highlighting&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="code block with Python-specific syntax highlighting" title="code block with Python-specific syntax highlighting" srcset="https://substackcdn.com/image/fetch/$s_!xL-f!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff95b9c0d-498e-4dd3-a216-05a8b9cf81d2_749x224.png 424w, https://substackcdn.com/image/fetch/$s_!xL-f!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff95b9c0d-498e-4dd3-a216-05a8b9cf81d2_749x224.png 848w, https://substackcdn.com/image/fetch/$s_!xL-f!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff95b9c0d-498e-4dd3-a216-05a8b9cf81d2_749x224.png 1272w, https://substackcdn.com/image/fetch/$s_!xL-f!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff95b9c0d-498e-4dd3-a216-05a8b9cf81d2_749x224.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><figcaption class="image-caption">Ghost does allow you to apply syntax highlighting to the web version of each post.</figcaption></figure></div><p>This comes from a recognition that almost all browsers today can render the styles necessary for syntax highlighting.</p><h3>Bringing styled code blocks to email posts</h3><p>If possible, I&#8217;d like to render the code blocks in email posts as effectively as they&#8217;re rendered in the browser. Because Ghost is open source, it should be possible to customize emails enough to achieve this.</p><p>Like many open projects, Ghost has an API. Once a post is drafted, you can manipulate it programmatically through the API. Over the past week, I&#8217;ve been working on a script that does the following:</p><ul><li><p>Retrieve a draft post from my instance of Ghost;</p></li><li><p>Convert every code block to an HTML block;</p></li><li><p>Parse the contents of each code block, and apply inline styles using <a href="https://pygments.org">Pygments</a>;</p></li><li><p>Push a copy of the modified post back to my instance of Ghost.</p></li></ul><p>It&#8217;s a bit more complicated than that, and if it ends up working I&#8217;ll write a post that covers the entire process in detail. But so far, it seems to be working. Here&#8217;s the same code block shown earlier, after being run through this extra processing:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!YBHq!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F233485c7-cce2-4331-bb56-8c713f7c2b91_623x225.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!YBHq!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F233485c7-cce2-4331-bb56-8c713f7c2b91_623x225.png 424w, https://substackcdn.com/image/fetch/$s_!YBHq!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F233485c7-cce2-4331-bb56-8c713f7c2b91_623x225.png 848w, https://substackcdn.com/image/fetch/$s_!YBHq!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F233485c7-cce2-4331-bb56-8c713f7c2b91_623x225.png 1272w, https://substackcdn.com/image/fetch/$s_!YBHq!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F233485c7-cce2-4331-bb56-8c713f7c2b91_623x225.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!YBHq!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F233485c7-cce2-4331-bb56-8c713f7c2b91_623x225.png" width="623" height="225" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/233485c7-cce2-4331-bb56-8c713f7c2b91_623x225.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:225,&quot;width&quot;:623,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:20157,&quot;alt&quot;:&quot;code block from an email sent through Ghost, with syntax highlighting&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="code block from an email sent through Ghost, with syntax highlighting" title="code block from an email sent through Ghost, with syntax highlighting" srcset="https://substackcdn.com/image/fetch/$s_!YBHq!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F233485c7-cce2-4331-bb56-8c713f7c2b91_623x225.png 424w, https://substackcdn.com/image/fetch/$s_!YBHq!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F233485c7-cce2-4331-bb56-8c713f7c2b91_623x225.png 848w, https://substackcdn.com/image/fetch/$s_!YBHq!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F233485c7-cce2-4331-bb56-8c713f7c2b91_623x225.png 1272w, https://substackcdn.com/image/fetch/$s_!YBHq!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F233485c7-cce2-4331-bb56-8c713f7c2b91_623x225.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><figcaption class="image-caption">With a bit of processing, it&#8217;s possible to generate fully-styled code blocks in emails through Ghost.</figcaption></figure></div><p>This is a screenshot of a test email sent from Ghost. The email contains an actual code block, not an image of highlighted code. You can copy the code straight from the code block, and paste it into an editor. I believe screen readers can parse the code as well as they parse any code listing online.</p><p>Some newsletter writers are embedding images of code blocks in their emails to achieve this same effect. But that approach requires clicking a link to an external site if you want to work with the actual code. The image-based approach is also much less accessible, as screen readers can&#8217;t parse the actual code. You can&#8217;t enlarge the text or do any other manipulation that depends on access to raw text content.</p><h3>Taking it one step further: Highlighting lines</h3><p>One of the most important things to communicate in a technical post is how code changes as you progress from a simple example to a more complex, fully implemented program. On Substack, you have to rely on something like bold text for that.</p><p>What most writers <em>want</em> to do is highlight the lines that have changed in a listing. I&#8217;m trying to add one more feature to the Ghost processing script. It will examine the first line in each listing, looking for a directive such as <code>hl-lines=[1, 3, 5,6,7]</code>. If it finds a directive like this, it will insert additional styling to highlight these lines.</p><p>I almost have this working. One of the challenges is that code blocks look something like this when represented as strings:</p><pre><code>from pathlib import Path\n\npath = Path("coffees.txt")</code></pre><p>After being run through a highlighter like Pygments, they look more like this:</p><pre><code>&lt;span...&gt;from&lt;/span&gt; &lt;span...&gt;pathlib&lt;/span&gt;&lt;span...&gt;import&lt;/span&gt;
 Path\n\npath &lt;span...&gt;=&lt;/span&gt; Path(&lt;span...&gt;"coffees.txt"&lt;/span&gt;</code></pre><p>Basically, every element that needs to be styled is wrapped in <code>&lt;span&gt;&lt;/span&gt;</code> tags.</p><p>To highlight individual lines, you need to insert new tags at some of the line breaks. But syntax highlighters that don&#8217;t deal with lines as individual units don&#8217;t care where newline characters end up. After running text through a highlighter, you end up with line breaks in the middle of sections that have consistent styling:</p><pre><code>&lt;span...&gt;import&lt;/span&gt; Path\n\npath &lt;span...&gt;...</code></pre><p>At one point I was trying to parse this kind of text. I needed to preserve the styling that Pygments had generated, and add my own style to highlight some lines. Here&#8217;s what I saw when I ran the parser:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!_Lz4!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb1dbfda9-35ca-4c2a-b97d-b0ae544442a4_754x446.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!_Lz4!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb1dbfda9-35ca-4c2a-b97d-b0ae544442a4_754x446.png 424w, https://substackcdn.com/image/fetch/$s_!_Lz4!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb1dbfda9-35ca-4c2a-b97d-b0ae544442a4_754x446.png 848w, https://substackcdn.com/image/fetch/$s_!_Lz4!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb1dbfda9-35ca-4c2a-b97d-b0ae544442a4_754x446.png 1272w, https://substackcdn.com/image/fetch/$s_!_Lz4!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb1dbfda9-35ca-4c2a-b97d-b0ae544442a4_754x446.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!_Lz4!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb1dbfda9-35ca-4c2a-b97d-b0ae544442a4_754x446.png" width="754" height="446" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b1dbfda9-35ca-4c2a-b97d-b0ae544442a4_754x446.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:446,&quot;width&quot;:754,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:40082,&quot;alt&quot;:&quot;code block with many repeated words, such as \&quot;path Path path Path\&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="code block with many repeated words, such as &quot;path Path path Path&quot;" title="code block with many repeated words, such as &quot;path Path path Path&quot;" srcset="https://substackcdn.com/image/fetch/$s_!_Lz4!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb1dbfda9-35ca-4c2a-b97d-b0ae544442a4_754x446.png 424w, https://substackcdn.com/image/fetch/$s_!_Lz4!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb1dbfda9-35ca-4c2a-b97d-b0ae544442a4_754x446.png 848w, https://substackcdn.com/image/fetch/$s_!_Lz4!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb1dbfda9-35ca-4c2a-b97d-b0ae544442a4_754x446.png 1272w, https://substackcdn.com/image/fetch/$s_!_Lz4!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb1dbfda9-35ca-4c2a-b97d-b0ae544442a4_754x446.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Parsing structured text is easy when the structure aligns with your goals. When the structure doesn&#8217;t match your goals, it&#8217;s much harder.</figcaption></figure></div><p>That&#8217;s a lot of paths! And a lot of <code>read_text()</code> calls as well. :)</p><p>There are a number of things to notice here. First of all, as a programmer, I&#8217;ve always been amused to see output like this. I imagine many non-programmers think we just write some code and it either works or doesn&#8217;t work. I don&#8217;t think most people realize we see half-broken output like this on a regular basis.</p><p>This kind of output is also a little deflating, because as programmers I think we tend to be optimists. Whenever we run a program, we hope to see correct output. Seeing incorrect output is a bit of a letdown, because it means we&#8217;re not done yet.</p><p>But there are glimmers of hope in the output shown here. Some lines are highlighted, and the highlighted section contains some of the code I wanted to see highlighted. Also, there seems to be a pattern in the incorrect output. All the content we want to see is there, but some of it is repeated. There&#8217;s almost certainly a systematic mistake I&#8217;m making, that once corrected will generate the correct output.</p><h3>The value of utility functions</h3><p>I haven&#8217;t implemented a fix quite yet, but I have a good idea about what to try next. My problem arose in part because <a href="https://beautiful-soup-4.readthedocs.io/en/latest/">Beautiful Soup</a> manages elements of a web page as a tree, made of nodes called <em>tags</em>. Some of the newline characters were embedded within a single tag, and I tried to split those tags up into several independent parts. I tried making copies of the tags and then adjusting the strings associated with each one. However, the new tags all pointed at the same string attribute that the original tag had. Changing the content of any one tag changed the content of the original tag, and all the new tags that had been created as well. That&#8217;s where the repetition in the rendered code block came from.</p><p>My post parser is a new project, and right now it&#8217;s a big mess of parts that mostly works. Rather than continuing to try to troubleshoot this issue within the context of the whole project, I&#8217;m going to pull the tag-splitting work out into a separate utility function. The function will take in a tag with embedded newlines, and return a sequence of tags with the newline characters isolated in individual tags. I&#8217;ll write a test for the new function, and then call the new function from the main project. This is the kind of work I was planning to do later, but doing it now will make my life a little easier today, and save future me some refactoring work.</p><p>If you can isolate problematic code, it&#8217;s almost always easier to troubleshoot, easier to use in a larger project, and easier to maintain as you run into more edge cases.</p><h3>Conclusions</h3><p>Most code blocks in emails look about the same as they did in the early 2000s. <a href="https://hashnode.com">Hashnode</a> is one of the first platforms I&#8217;ve seen that has rejected the idea that emails can&#8217;t contain nicely styled code blocks, and I appreciate their work in this area. Modern code blocks are more visually appealing, but more importantly they support clear communication about technical topics.<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-1" href="#footnote-1" target="_self">1</a></p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!ARc7!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F65eeb1e2-89fd-49d4-a91f-3fbc674e374c_1199x253.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!ARc7!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F65eeb1e2-89fd-49d4-a91f-3fbc674e374c_1199x253.png 424w, https://substackcdn.com/image/fetch/$s_!ARc7!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F65eeb1e2-89fd-49d4-a91f-3fbc674e374c_1199x253.png 848w, https://substackcdn.com/image/fetch/$s_!ARc7!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F65eeb1e2-89fd-49d4-a91f-3fbc674e374c_1199x253.png 1272w, https://substackcdn.com/image/fetch/$s_!ARc7!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F65eeb1e2-89fd-49d4-a91f-3fbc674e374c_1199x253.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!ARc7!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F65eeb1e2-89fd-49d4-a91f-3fbc674e374c_1199x253.png" width="1199" height="253" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/65eeb1e2-89fd-49d4-a91f-3fbc674e374c_1199x253.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:253,&quot;width&quot;:1199,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:63128,&quot;alt&quot;:&quot;screenshot from Hashnode's home page, showing syntax highlighting in a code block&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="screenshot from Hashnode's home page, showing syntax highlighting in a code block" title="screenshot from Hashnode's home page, showing syntax highlighting in a code block" srcset="https://substackcdn.com/image/fetch/$s_!ARc7!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F65eeb1e2-89fd-49d4-a91f-3fbc674e374c_1199x253.png 424w, https://substackcdn.com/image/fetch/$s_!ARc7!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F65eeb1e2-89fd-49d4-a91f-3fbc674e374c_1199x253.png 848w, https://substackcdn.com/image/fetch/$s_!ARc7!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F65eeb1e2-89fd-49d4-a91f-3fbc674e374c_1199x253.png 1272w, https://substackcdn.com/image/fetch/$s_!ARc7!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F65eeb1e2-89fd-49d4-a91f-3fbc674e374c_1199x253.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><figcaption class="image-caption">Hashnode is the only newsletter platform I&#8217;m aware of that includes support for syntax highlighting in code blocks by default.</figcaption></figure></div><p>I&#8217;m hoping to send next week&#8217;s newsletter out from Ghost, and it should have much better code blocks than what you&#8217;ve been seeing previously on <em>Mostly Python</em>. If you see better code blocks in the next few posts, you&#8217;ll have a good idea of what kind of work has gone into making them possible. :)</p><p><strong>Note:</strong><em> When the first post goes out from Ghost, I&#8217;m still planning to send a final post from Substack shortly afterwards to let people know it&#8217;s been sent, and how to reach out if you can&#8217;t find it.</em></p><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-1" href="#footnote-anchor-1" class="footnote-number" contenteditable="false" target="_self">1</a><div class="footnote-content"><p>I briefly considered using Hashnode, but they seem to be targeting developer blogs at companies. I know some individuals are using them, but I don&#8217;t think that&#8217;s the primary use case for Hashnode. It&#8217;s also not an open platform, and I don&#8217;t want to have to deal with another migration if they take their platform in a different direction at some point.</p></div></div>]]></content:encoded></item><item><title><![CDATA[Leaving Substack]]></title><description><![CDATA[MP 85: I'll be moving to Ghost next week, because it's a better fit for Mostly Python.]]></description><link>https://mostlypython.substack.com/p/leaving-substack</link><guid isPermaLink="false">https://mostlypython.substack.com/p/leaving-substack</guid><dc:creator><![CDATA[Eric Matthes]]></dc:creator><pubDate>Thu, 29 Feb 2024 17:30:19 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!zAn-!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F07ee04ea-0ed8-44e7-a3b8-c1bde6c96090_870x697.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I&#8217;ve enjoyed writing <em>Mostly Python</em> over the past year, and plan to continue the newsletter indefinitely. I really enjoy a mix of working on various projects, writing about aspects of those projects that might be of interest to others, and exploring many aspects of Python itself in depth.</p><p>As much as I&#8217;ve enjoyed writing about Python and other technical topics, I&#8217;ve grown quite dissatisfied with Substack as a hosting platform. Starting next week, I&#8217;ll be using Ghost as a hosting service. The content of this newsletter won&#8217;t be changing, but the look and feel will be a little different.</p><p>In this post I&#8217;ll share some of the reasoning behind this change. I&#8217;ll also share some thoughts about how this relates to the choices programmers have to make about the platforms and services we rely on for many real-world projects.</p><h3>What&#8217;s wrong with Substack?</h3><p>I started using Substack because I was looking for a simple newsletter platform that would let me focus on writing. That&#8217;s what Substack was in its original form, but now they seem to be focusing on growth for growth&#8217;s sake. They also seem to be evolving into a social media platform that thrives on the kind of conflicts and divisiveness that has driven people away from other platforms.<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-1" href="#footnote-1" target="_self">1</a></p><p>I just want to write a good technical newsletter, and Substack is no longer the most appropriate platform on which to do that.</p><h3>Why Ghost?</h3><p>If you haven&#8217;t heard of it, <a href="https://ghost.org">Ghost</a> is a platform similar to what Substack was when it started. Just like with Substack, you can write a newsletter that becomes an online archive. You can write posts that go out over email, and others that are only available online. Ghost has been around for over ten years, and hasn&#8217;t tried to implement the kinds of social features that Substack has.</p><p>One of the strongest reasons to move to Ghost is that it&#8217;s built on top of open source software. The names are a bit confusing; there are several things people mean when they say &#8220;Ghost&#8221;:</p><ul><li><p>The open source project <a href="https://github.com/TryGhost/Ghost">Ghost</a>, which is a framework for &#8220;modern publishing&#8221;. It supports blogging, newsletters, and managing free and paid content. Because the core framework is open source, you can either self-host your publication or use a managed service.</p></li><li><p>The managed hosting service <a href="https://ghost.org/pricing/">Ghost Pro</a>.</p></li><li><p>The non-profit organization <a href="https://ghost.org">Ghost</a>, which operates the Ghost Pro managed service. Revenue from operating the managed service supports ongoing development of the open source framework.</p></li></ul><p>I&#8217;m going to use the managed version, because I want to focus on writing without having to do a bunch of sysadmin work. Because of its basis in open source, I feel much more confident that Ghost will be a long-term home for <em>Mostly Python</em> than I ever felt with Substack.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!zAn-!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F07ee04ea-0ed8-44e7-a3b8-c1bde6c96090_870x697.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!zAn-!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F07ee04ea-0ed8-44e7-a3b8-c1bde6c96090_870x697.png 424w, https://substackcdn.com/image/fetch/$s_!zAn-!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F07ee04ea-0ed8-44e7-a3b8-c1bde6c96090_870x697.png 848w, https://substackcdn.com/image/fetch/$s_!zAn-!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F07ee04ea-0ed8-44e7-a3b8-c1bde6c96090_870x697.png 1272w, https://substackcdn.com/image/fetch/$s_!zAn-!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F07ee04ea-0ed8-44e7-a3b8-c1bde6c96090_870x697.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!zAn-!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F07ee04ea-0ed8-44e7-a3b8-c1bde6c96090_870x697.png" width="870" height="697" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/07ee04ea-0ed8-44e7-a3b8-c1bde6c96090_870x697.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:697,&quot;width&quot;:870,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:95504,&quot;alt&quot;:&quot;\&quot;Quickstart install\&quot; instructions for running your own instance of Ghost.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="&quot;Quickstart install&quot; instructions for running your own instance of Ghost." title="&quot;Quickstart install&quot; instructions for running your own instance of Ghost." srcset="https://substackcdn.com/image/fetch/$s_!zAn-!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F07ee04ea-0ed8-44e7-a3b8-c1bde6c96090_870x697.png 424w, https://substackcdn.com/image/fetch/$s_!zAn-!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F07ee04ea-0ed8-44e7-a3b8-c1bde6c96090_870x697.png 848w, https://substackcdn.com/image/fetch/$s_!zAn-!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F07ee04ea-0ed8-44e7-a3b8-c1bde6c96090_870x697.png 1272w, https://substackcdn.com/image/fetch/$s_!zAn-!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F07ee04ea-0ed8-44e7-a3b8-c1bde6c96090_870x697.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption"><a href="https://github.com/TryGhost/Ghost">Ghost</a> is an actively maintained open source project. I&#8217;m going to use a managed hosting service for now, but it&#8217;s nice to know that self-hosting is an option if I ever need to got that route.</figcaption></figure></div><p>At this point it&#8217;s hard to choose a closed platform if an equivalent platform is available that&#8217;s built on open source. I&#8217;ve watched so many closed platforms and services take one or more of the following paths:</p><ul><li><p>Introduce ads in an effort to monetize user-generated content;</p></li><li><p>Adopt dark patterns that pull users into higher usage levels than they originally intended;</p></li><li><p>Add features that aren&#8217;t aligned with the original purpose of the platform or service, and slowly push people into using those features;</p></li><li><p>Close off features and services that were central to the original platform&#8217;s appeal.</p></li></ul><p>If Ghost Pro happens to fall into any of these patterns, there are two possibilities other than just moving to an entirely different platform. Since Ghost itself is open source, anyone can build a competing hosting service that leaves out the problematic features. Migrating between platforms that use the same underlying framework is much easier than migrating between different proprietary platforms. If a reasonable managed service doesn&#8217;t appear, I can always choose to self-host the service. I don&#8217;t particularly <em>want</em> to do that, but I may choose that option over migrating yet again Again, I don&#8217;t think I&#8217;ll need to move off of Ghost because it&#8217;s not susceptible to the same kinds of pressures that Substack is.<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-2" href="#footnote-2" target="_self">2</a></p><h3>The bigger picture</h3><p>Most people reading this post probably aren&#8217;t planning to start a newsletter anytime soon. But many people <em>are</em> making decisions on a regular basis about which services to pay for, and which service providers to trust. When you&#8217;re working through these kinds of decisions, consider each of the following questions:</p><h4>Can you export your critical data?</h4><p>I knew Substack might not be a long term fit, but I was willing to give it a try because they allow you to export all your critical data. Writers &#8220;own&#8221; their content and subscriber list, and can export those at any time. Platforms that support exporting, and allow you to retain full ownership of your data, make it easy to implement effective backup routines <em>and</em> give you a way out of vendor lock-in.</p><p>Make sure you run an export early on, and look at <em>exactly</em> what&#8217;s in that export. While &#8220;you own your data and you can leave at any time&#8221; sounds nice, it&#8217;s probably a lot of work to process that data in a way that it can be used on another platform. Also, platforms don&#8217;t always keep their export services fully up to date. You might rely on a platform&#8217;s export function, and then when it comes time to use that data (for backup or migration purposes), find that some critical elements were never included in the export.</p><h4>What&#8217;s the platform&#8217;s business model?</h4><p>Every platform and service costs money to run. If the platform is charging enough for its core services, it&#8217;s more likely that they&#8217;ll be able to continue offering those services without trying to push unwanted &#8220;features&#8221; on the user base.<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-3" href="#footnote-3" target="_self">3</a></p><h4>How have they funded operations so far?</h4><p>Understanding a platform&#8217;s funding history can give you a sense of how reliable their service is likely to be. For example, Substack has raised on the order of $100 million in venture capital. Now they need to generate enough revenue to pay that original investment back, while covering current operating expenses, and making a profit on top of that. If they can&#8217;t do that, they&#8217;ll need to seek even more funding, get acquired by a larger company, or fold. Sometimes a company will drift from its original mission in pursuit of a business model that&#8217;s profitable enough to pay off their earlier investment rounds.</p><p>The fact that a company has sought venture funding is not a bad thing in and of itself. It&#8217;s one way that promising companies can grow quickly, while they&#8217;re still establishing sustainable revenue streams. If funding levels align with a valid business model, the company may become sustainable. If a company has grown rapidly without a clearly sustainable business model however, that company&#8217;s platform can be much less reliable in the long term.</p><p>If you&#8217;ll only need a company&#8217;s services for a short time, none of this matters too much. But if you&#8217;re planning to rely on a service over a period of years, considering these kinds of details can help you make an informed decision about which platforms to trust.</p><h4>What&#8217;s the public perception of the company?</h4><p>In some areas, public perception doesn&#8217;t mean a whole lot. Nobody really likes Oracle, but that company is probably going to be around for a long time. People have serious frustrations with Adobe&#8217;s subscription plans, but their services are pretty reliable if you choose to use them.</p><p>A platform that requires a large user base, who could leave at any time, is a much different story. Platforms collapse and get acquired on a regular basis. Sometimes that plays out as a company closing suddenly, and sometimes the company does things over time that drive more and more users away. If the platform you&#8217;re choosing is susceptible to these dynamics, look at some of the public statements its leaders are making. Try to get a sense of whether they might be creating problems for themselves in the near to mid term future, or if they inspire confidence in their leadership and decision-making abilities. </p><p>Every time Substack has experienced conflict around their policies, they&#8217;ve doubled down on the policies that led to those conflicts in the first place. I&#8217;ve spent a good part of my life dealing with conflict, and I&#8217;m happy to address it when necessary. But when a company or platform fosters conflict because it increases engagement, I want no part of that.</p><h3>Conclusions</h3><p>I&#8217;ve really enjoyed writing <em>Mostly Python</em> over the past year, and look forward to many years of continuing to do so. Next Thursday&#8217;s post will have the same kind of content you&#8217;re used to seeing here; it will just look a little different.</p><p>I&#8217;ll send one last email out from Substack after publishing from Ghost for the first time. That email will include a way to reach out if something goes wrong in the transition. If you&#8217;re a paid subscriber, Substack and Ghost both use Stripe for billing, so your subscription should carry over smoothly.</p><p>Thank you for joining me here on Substack, and I hope you&#8217;ll stay with me through this transition as well.</p><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-1" href="#footnote-anchor-1" class="footnote-number" contenteditable="false" target="_self">1</a><div class="footnote-content"><p>What started as a simple newsletter delivery platform is turning into a haphazardly built social media site. Last year Substack introduced <em>Notes</em>, its Twitter clone. They said they were building Notes without moderation, because they trust users to moderate themselves. Last fall Substack was called out quite publicly for allowing literal Nazi content on their platform. When they finally responded to an open letter from Substack writers, they basically said they don&#8217;t like Nazis but believe that any level of moderation equates to censorship. This free-speech absolutism is disingenuous; every public communication platform has to deal with moderation. If you&#8217;re interested in more specific thoughts on all of this, I wrote a more <a href="https://www.mostlypython.com/p/substacks-nazi-problem">detailed overview</a> of Substack&#8217;s issues previously.</p><p>Substack continues to build &#8220;features&#8221; that are aimed at simply inflating everyone&#8217;s subscriber numbers and connectedness on the platform. They&#8217;re starting to adopt dark patterns that lead people to subscribe to more newsletters than they intend to, and follow more people on Notes than they intended to as well. If you&#8217;ve been around the internet for a while, you&#8217;ve seen this play out before. What starts as a clean platform develops into one with more and more calls to &#8220;engage&#8221; and less of a focus on actual content.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-2" href="#footnote-anchor-2" class="footnote-number" contenteditable="false" target="_self">2</a><div class="footnote-content"><p>If you happen to be considering self-hosting Ghost for one of your projects, Molly White wrote a fantastically <a href="https://www.citationneeded.news/substack-to-self-hosted-ghost/">detailed post</a> about her initial setup.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-3" href="#footnote-anchor-3" class="footnote-number" contenteditable="false" target="_self">3</a><div class="footnote-content"><p>Substack&#8217;s main revenue source is that it keeps 10% of all revenue from paid subscriptions. That means they&#8217;re under constant pressure to increase the number of paid subscriptions, and the amount that people are paying for those subscriptions. Ghost, and most other newsletter platforms, charge volume-based usage fees. That means those platforms can let individual writers choose how and when to prompt readers to consider a paid subscription. </p><p>It&#8217;s much easier to avoid the temptation to implement dark patterns if you have a sustainable business model in the first place.</p></div></div>]]></content:encoded></item><item><title><![CDATA[Testing a book's code: Overall conclusions]]></title><description><![CDATA[MP 84: Final takeaways from a detailed look at testing.]]></description><link>https://mostlypython.substack.com/p/testing-a-books-code-overall-conclusions</link><guid isPermaLink="false">https://mostlypython.substack.com/p/testing-a-books-code-overall-conclusions</guid><dc:creator><![CDATA[Eric Matthes]]></dc:creator><pubDate>Thu, 22 Feb 2024 17:30:18 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F535dffec-3303-40c6-a101-440465f3e6e7_1346x362.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><strong>Note:</strong> <em>This is the final post in a <a href="https://www.mostlypython.com/p/testing-a-books-code-696">series</a> about testing. This concluding post is free to everyone, and the earlier posts in the series will be unlocked over the coming weeks. Thank you to everyone who supports my ongoing work on Mostly Python.</em></p><div><hr></div><p>I learned about testing fairly late in my career as a programmer. It wasn&#8217;t introduced in any of the classes I took when I was younger, and the intro and intermediate books I read over the years didn&#8217;t mention it in any prominent way. As soon as I learned the basics of testing, I wished I had learned about it earlier.</p><p>Testing is often thought of as something separate from development, but it doesn&#8217;t have to be. Testing a project effectively is an interesting challenge, and often pushes you to understand your project in new and meaningful ways.</p><p>I&#8217;ve had the good fortune to get to maintain a comprehensive introductory Python <a href="https://ehmatthes.github.io/pcc_3e/">book</a> for almost a decade now. That&#8217;s a time period where building out a thorough test suite is worth the effort. The challenges that came up in this work highlight some aspects of testing that don&#8217;t necessarily come up when developing a test suite for a more traditional software project. In this final post I&#8217;ll highlight some of these takeaways.</p><h3>General takeaways</h3><p>There are a number of big-picture takeaways from this work:</p><h4>Think about your testing constraints</h4><p>When you&#8217;re in charge of writing a test suite, you end up looking at your project in a different way. Instead of thinking about the <em>people</em> who use your project, you have to write a <em>program</em> that uses your project. If your project wasn&#8217;t designed to be used in a fully automated way, you&#8217;ll have to find a way to exercise the code from within the test suite. Sometimes you can change the actual project to make it easier to test, but sometimes there are very good reasons to keep the project as is, and come up with a test suite that handles the current structure of the project.</p><p>Code for a book is a perfect example of this kind of situation. When someone writes code for a book, they tend to prioritize simple examples that highlight the topic that&#8217;s being taught. Often times this means leaving out some elements that we&#8217;d include in a real-world project, which would distract people from learning about a specific topic. </p><p>In a more traditional project, you might have to test code that&#8217;s already being used in critical ways, or by a significant user base. It might be necessary to test the code as is, before making any changes just for the purposes of testing. Learning to recognize the constraints you&#8217;re working with is an important skill in testing.</p><h4>Think about possibilities</h4><p>All that said, you can be quite creative in developing your test suite. As an author, I can&#8217;t change any of the code I&#8217;m testing. But I can copy that code to a new repository or a temp directory, and do anything I want with it from that point forward.</p><p>In many testing tutorials and references, we work with code that generates text output. It&#8217;s often somewhat straightforward to verify whether text output is correct. However, most real-world output is more varied than that. If you&#8217;re working with text output, maybe there&#8217;s some elements of the text that&#8217;s always changing, such as timestamps. Maybe you&#8217;re working with image output, or HTML, or even sound files. It&#8217;s not always obvious how to verify that non-text output is &#8220;correct&#8221;.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!5FVY!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcfe6d866-8023-4f2d-9afd-3276a474ec1d_1576x1004.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!5FVY!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcfe6d866-8023-4f2d-9afd-3276a474ec1d_1576x1004.png 424w, https://substackcdn.com/image/fetch/$s_!5FVY!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcfe6d866-8023-4f2d-9afd-3276a474ec1d_1576x1004.png 848w, https://substackcdn.com/image/fetch/$s_!5FVY!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcfe6d866-8023-4f2d-9afd-3276a474ec1d_1576x1004.png 1272w, https://substackcdn.com/image/fetch/$s_!5FVY!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcfe6d866-8023-4f2d-9afd-3276a474ec1d_1576x1004.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!5FVY!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcfe6d866-8023-4f2d-9afd-3276a474ec1d_1576x1004.png" width="1456" height="928" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/cfe6d866-8023-4f2d-9afd-3276a474ec1d_1576x1004.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:928,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:98280,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!5FVY!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcfe6d866-8023-4f2d-9afd-3276a474ec1d_1576x1004.png 424w, https://substackcdn.com/image/fetch/$s_!5FVY!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcfe6d866-8023-4f2d-9afd-3276a474ec1d_1576x1004.png 848w, https://substackcdn.com/image/fetch/$s_!5FVY!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcfe6d866-8023-4f2d-9afd-3276a474ec1d_1576x1004.png 1272w, https://substackcdn.com/image/fetch/$s_!5FVY!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcfe6d866-8023-4f2d-9afd-3276a474ec1d_1576x1004.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">This is a plot of the most popular Python repos on GitHub. It changes slightly every day, because these projects are always accumulating more stars. Figuring out how to test output that&#8217;s static in some ways, but dynamic in others, is an interesting but ultimately solvable challenge.</figcaption></figure></div><p>I think a lot of testing resources talk about assertions where all output is either <em>correct</em> or <em>incorrect</em>. But I&#8217;ve run into many situations, especially when testing cross-platform code, where this is the actual question that needs to be asked:</p><blockquote><p><em>Is this output close enough to what it&#8217;s supposed to be?</em></p></blockquote><p>It&#8217;s not particularly difficult to write assertions that check whether output is &#8220;good enough&#8221;, but I don&#8217;t see it covered all that often. For example when testing output in the form of a plot image, it was satisfying to come up with an <a href="https://www.mostlypython.com/p/testing-a-books-code-part-5-testing#&#167;testing-a-plot-image-that-changes-regularly">approach</a> that looks at the average color values of the entire plot, rather than trying to make assertions about the actual data represented in the plot. I have no idea if this is the <em>best</em> approach to testing graphical output that can vary over time, but it&#8217;s certainly good enough for my purposes at this point.</p><h4>Think about dependencies</h4><p>The standard view of testing is that we should test against all versions of the language that the project currently supports, and multiple versions of its main dependencies. But this dependency matrix can grow quite large. Although there are tools that help you manage testing against that entire matrix, you don&#8217;t always need to test against the entire matrix.</p><p>In the context of testing code from a book, I&#8217;m primarily interested in testing against release candidates of new versions of Python, and major libraries such as Pygame, Matplotlib, Plotly, and Django. I don&#8217;t need to test against a full matrix of different versions of these libraries; instead I need to be able to name the specific version that I want to test against during the current test run.</p><p>I&#8217;m working on a deployment-focused <a href="https://django-simple-deploy.readthedocs.io/en/latest/">project</a> that probably doesn&#8217;t need to maintain backwards compatibility as carefully as many other projects. This project should only be run once against any given target project. It shouldn&#8217;t be an issue for someone to just install the latest version and then carry out their deployment. The test suite for that project is being developed with a focus on testing the current version of the library, and probably won&#8217;t do extensive testing against previously-released versions.</p><h4>Think about test artifacts</h4><p>This point deserves a post, or even a series, of its own. A test suite runs your project in various ways. As such, it can create any artifacts needed for automated testing, and any artifacts that you might want to open and interact with manually.</p><p>For example, it might be possible to write a plot image to memory instead of to a file, and avoid the overhead of reading and writing files during a test suite. But I sometimes want to go to the temp directory used by the test suite, and open up plot images to look at them. Even if tests pass, I might want to see what the plot looks like under current testing conditions. If a test fails I might want to look at the image that was generated during the test run, instead of having to run the project manually as part of my debugging work. I&#8217;ll say this over and over again, a good test suite can act as a <em>development tool</em>, not just a <em>verification tool</em>.</p><div class="pullquote"><p>I&#8217;ll say this over and over again, a good test suite can act as a development tool, not just a <em>verification tool</em>.</p></div><h4>Think about goals</h4><p>For people who haven&#8217;t done a lot of testing work, the perception can be that testing is entirely about the binary question &#8220;Does this code work correctly or not?&#8221; But a well-developed test suite should answer a specific set of questions that you have, which meet your goals in developing and maintaining the project:</p><blockquote><p><em>What parts of this project work as expected?</em></p><p><em>What parts are starting to work differently than expected?</em></p><p><em>If part of this project is behaving differently than it used to, what is that differen</em>ce?</p></blockquote><p>For example if a page in a web project fails to load, is the error on that particular page, or was there an error in a different part of the project that indirectly affects other pages? When this kind of bug occurs, will your test suite lead you efficiently to the root cause, or will it focus on the symptom of the underlying problem? I haven&#8217;t always known that a test would help me in this way, but I&#8217;ve certainly seen some testing approaches generate more helpful information than others.</p><p>There&#8217;s a tendency to think that tests should run as fast as possible. But we&#8217;re actually after a balance between speed, test coverage, and information generated. If you focus exclusively on speed, you might miss out on generating some information and artifacts that can be really helpful in debugging and development work. If you generate too much information, you might make it difficult to use the output of your test suite.</p><h4>Make sure your tests can fail</h4><p>Sometimes we focus so much on the desired outcome of tests passing, that we forget to <a href="https://www.mostlypython.com/i/137400936/can-a-test-fail">make sure</a> a test can fail. If you haven&#8217;t seen a failure in a while, consider introducing an intentional bug to see if the expected failures appear.</p><h3>pytest-specific takeaways</h3><p><a href="https://docs.pytest.org/en/7.1.x/contents.html">pytest</a> is a fantastic library. It&#8217;s one of those amazing libraries that&#8217;s simple enough in its most basic usage that people entirely new to testing should start using it. At the same time, it&#8217;s flexible and powerful enough that it&#8217;s perfectly appropriate to use in large, fully-deployed commercial projects.</p><p>You can get started with pytest from a simple tutorial. But there are lots of specific things to learn about it that will help you write exactly the kind of test suite that suits your needs. Also, there are a number of ways to run your tests, so you can use them in exactly the way you need at any given time.</p><h4>Use parametrization</h4><p>At first glance, parametrization seems like a way to write less code. But that&#8217;s just one benefit. The real power of parametrization comes from developing a highly consistent way to run a series of similar tests. In the context of this test suite, two test functions <a href="https://www.mostlypython.com/i/137400936/parametrizing-tests">resulted</a> in 60 tests being run.</p><p>Once you&#8217;re familiar with the concept and syntax of parametrization, it lets you generate a large number of tests by writing a small number of test functions. While appearing more complex to people unfamiliar with parametrization, it&#8217;s actually a much more maintainable way to develop a growing test suite.</p><h4>Use fixtures</h4><p>Fixtures are used to carry out the setup work required for test functions. Fixtures can do something, such as building a virtual environment for a set of tests. They can also <a href="https://www.mostlypython.com/i/137400936/getting-pythoncmd-from-a-fixture">return</a> resources needed for tests. By defining a fixture&#8217;s <a href="https://docs.pytest.org/en/6.2.x/fixture.html#scope-sharing-fixtures-across-classes-modules-packages-or-session">scope</a>, it can be run once for the entire test session. Or, it can be run for specific modules, classes, or functions.</p><p>As with many features of pytest, you can start out by writing a simple fixture that carries out a small task or creates a small resource for several test functions. As your experience and understanding grows, you can use fixtures in increasingly complex and powerful ways.</p><h4>Use parallel test execution</h4><p>By installing the <code>pytest-xdist</code> <a href="https://pytest-xdist.readthedocs.io/en/stable/">package</a>, you can run many kinds of tests <a href="https://www.mostlypython.com/i/137559667/parallelizing-tests">in parallel</a>. If your tests are starting to take a noticeable amount of time to run, see if a simple call to <code>pytest -n auto</code> speeds up your test run.</p><p>Parallel tests don&#8217;t always work well if your tests access external resources, or have complex setup steps. But you can install <code>pytest-xdist</code> and run a subset of your tests in parallel, and run tests that don&#8217;t work well in parallel separately.</p><h4>Use custom CLI args to enhance your test suite</h4><p>In this series I used a set of custom CLI args to <a href="https://www.mostlypython.com/i/137461912/testing-different-pygame-versions">specify</a> exactly which version of a library to use during the test run. This is particularly useful when testing against release candidates of third-party libraries.</p><p>You can use CLI args in all kinds of ways. For example, imagine your test suite generates a large number of image files and then compares those against a set of reference images. An update to a library you use changes the output images slightly, but in an acceptable way. You could go into your test&#8217;s temp directory, check the images, and then manually copy them over to your <em>reference_files/</em> directory. But if you do this on a regular basis, you could write a CLI arg that lets you run a command like this:</p><pre><code>$ <strong>pytest tests/test_image_output.py &#8212;-update-reference-files</strong></code></pre><p>You could then write code that copies each newly-generated image file to <em>reference_files/</em> when this flag is included. </p><h4>Learn to use pytest&#8217;s built-in CLI args</h4><p>pytest has a <a href="https://docs.pytest.org/en/6.2.x/usage.html">significant number</a> of CLI args that you can use to run exactly the tests you want, generating exactly the kind of output you want on any given run. For example, here are a few of my most-used arguments:</p><ul><li><p>The <code>-k</code> argument lets you specify tests matching a specific pattern. This can include test function names or parts of names, and parameters as well.</p></li><li><p>The <code>-x</code> argument stops at the first test failure.</p></li><li><p>The <code>&#8212;-lf</code> argument re-runs the last test that failed.</p></li><li><p>The <code>-s</code> flag tells pytest to show the output that was captured during test execution.</p></li><li><p>I recently learned about the <code>&#8212;-setup-plan</code> argument, which shows exactly what pytest will run, in what order. This doesn&#8217;t just show what order the tests will be run in. It also shows when each fixture will be called, and how often they&#8217;ll be called. You can see how it runs fixtures before test functions, and how it uses parametrization to repeat tests with different parameters. It&#8217;s an amazingly helpful tool for understanding more complex aspects of pytest.</p></li></ul><h4>Understand the role of multiple <em>conftest.py</em> files</h4><p>You can write a <em>conftest.py</em> file in any directory within your test suite, including your project&#8217;s root directory. This lets you structure your test suite efficiently. For example if a fixture is used by multiple modules, you can write that fixture once in a <em>conftest.py</em> file in the directory containing those modules. You don&#8217;t need a copy of the fixture in every test module.</p><p>Some people don&#8217;t like seeing multiple <em>conftest.py</em> files, because it can be less clear about where fixtures used in a test function are coming from. A good principle to minimize confusion is to place fixtures as close as possible to the tests that use them. For example if you have tests in a subdirectory, put the fixtures that are only used by those tests in that directory&#8217;s <em>conftest.py</em>. Don&#8217;t dump all of your fixtures into your root directory&#8217;s <em>conftest.py</em> file.</p><h3>Minor takeaways</h3><p>If you&#8217;ve made it this far, you must be interested in testing! Here&#8217;s a few more smaller things that are worth mentioning:</p><h4>Consider making assertions about your tests</h4><p>At the heart of every test is an assertion about your project&#8217;s behavior or output. But if there&#8217;s any complexity to your test suite&#8217;s setup, consider <a href="https://www.mostlypython.com/i/137706731/setting-up-the-test-environment">writing</a> assertions about that setup work itself. This is especially important to consider if there&#8217;s a chance the setup work might not be completed correctly, in a way that would still allow the tests to pass. Or, if a step in the setup work fails you might want to just exit the test suite and deal with that setup failure.</p><h4>Consider modifying a copy of your project&#8217;s code</h4><p>Some projects are more &#8220;testable&#8221; than others. If your code seems untestable as currently written, you should probably restructure your code so it&#8217;s easier to test the overall project and its component parts. But if you can&#8217;t modify your code for some reason, consider making a copy of all or part of the project, modifying the copy, and then making assertions against the modified code. For example you might <a href="https://www.mostlypython.com/i/137559667/rewriting-the-file-before-testing-it">modify</a> your code to write output to a file instead of displaying it on the screen. Then you can make assertions about the contents of that file.</p><h4>Consider testing a subset of your project&#8217;s codebase</h4><p>Chasing &#8220;100% test coverage&#8221; can be an exercise in over-optimization. There may well be other things that are more worth pursuing than complete test coverage of your codebase. Identify the most critical aspects of your project, and make sure to test those behaviors and sections. Also, if some sections of code are repetitive and a successful test implies correct behavior for the repeated parts, ask yourself if it&#8217;s <a href="https://www.mostlypython.com/i/137602222/testing-hn-programs">reasonable</a> to only test one or several of the repeated behaviors.</p><h4>Don&#8217;t refactor just to refactor</h4><p>Most of us are taught to refactor code that appears multiple times in a project. In a test suite, that&#8217;s much less important. Repetition is okay in testing, because it&#8217;s often helpful to have enough context in a test function to assess a test failure without having to trace through a complex hierarchy of code. Refactor your tests as needed to understand and <a href="https://www.mostlypython.com/i/137706731/running-on-windows">maintain</a> the evolving test suite, but don&#8217;t refactor just because you see repetition in your tests.</p><h4>Use a seed to test random code</h4><p>If your project uses randomness at all, consider <a href="https://www.mostlypython.com/i/137559667/testing-code-that-deals-with-randomness">using</a> a random seed to get repeatable pseudorandom output.</p><h4>Be wary of metadata, and other minor file differences</h4><p>If you&#8217;re comparing newly-generated files against a set of reference files, be wary of metadata. In several projects now, I&#8217;ve spent a lot of time trying to figure out why two seemingly identical files fail a comparison test. <a href="https://www.mostlypython.com/i/137559667/down-the-image-comparison-rabbit-hole">Sometimes</a> it&#8217;s a piece of metadata generated by a library used in generating the output file. Sometimes it&#8217;s a minor difference in how output is written on different operating systems. If files appear identical but fail a comparison test, figure out how to view the metadata on your system and take a look at it for your test file and your reference file.</p><h4>Write your tests on one OS, and then generalize to others early</h4><p>If your test suite will need to pass on multiple operating systems, write a small number of tests that work on your main OS. Before expanding your suite too significantly, run those tests on the other OSes you need to support, and <a href="https://www.mostlypython.com/i/137400936/running-on-windows">figure out</a> how to make them pass there as well. You can save yourself some refactoring work and some complexity by discovering cross-platform issues early on. You might learn more about how your project and its dependencies work on different OSes as well.</p><h3>Conclusions</h3><p>There are a lot of tutorials out there about testing example projects. But testing a real-world project often brings up challenges that don&#8217;t come up in clean sample projects. Most people reading this series probably aren't writing technical books, but the issues that arose in developing this suite are comparable to the issues that people face when developing a test suite for a real-world project.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!tHpd!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F535dffec-3303-40c6-a101-440465f3e6e7_1346x362.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!tHpd!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F535dffec-3303-40c6-a101-440465f3e6e7_1346x362.png 424w, https://substackcdn.com/image/fetch/$s_!tHpd!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F535dffec-3303-40c6-a101-440465f3e6e7_1346x362.png 848w, https://substackcdn.com/image/fetch/$s_!tHpd!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F535dffec-3303-40c6-a101-440465f3e6e7_1346x362.png 1272w, https://substackcdn.com/image/fetch/$s_!tHpd!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F535dffec-3303-40c6-a101-440465f3e6e7_1346x362.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!tHpd!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F535dffec-3303-40c6-a101-440465f3e6e7_1346x362.png" width="1346" height="362" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/535dffec-3303-40c6-a101-440465f3e6e7_1346x362.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:362,&quot;width&quot;:1346,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:70520,&quot;alt&quot;:&quot;terminal output showing passing tests&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="terminal output showing passing tests" title="terminal output showing passing tests" srcset="https://substackcdn.com/image/fetch/$s_!tHpd!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F535dffec-3303-40c6-a101-440465f3e6e7_1346x362.png 424w, https://substackcdn.com/image/fetch/$s_!tHpd!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F535dffec-3303-40c6-a101-440465f3e6e7_1346x362.png 848w, https://substackcdn.com/image/fetch/$s_!tHpd!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F535dffec-3303-40c6-a101-440465f3e6e7_1346x362.png 1272w, https://substackcdn.com/image/fetch/$s_!tHpd!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F535dffec-3303-40c6-a101-440465f3e6e7_1346x362.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Those green dots representing passing tests are very reassuring, and really do help me sleep better at  night.</figcaption></figure></div><p>I love testing, because it helps me sleep better at night. If I&#8217;ve tested my code against a release candidate of a library, I don&#8217;t worry about a slew of bug reports when that new version comes out. If I have to put a project aside for a while, it&#8217;s easier to pick up work on that project months later, or even years later, if it has at least a basic test suite. When resuming work on the project, I can run the tests and see if anything has broken for any reason in that project&#8217;s down time.</p><p>I also love hearing people&#8217;s testing stories, and hearing the questions people have about testing. If you have stories about test suites that saved you at some point, or tests that were frustrating in some way, please share it. If you have questions about how to approach some aspect of testing, I&#8217;d love to hear that as well. If your question is better asked privately than in comments, feel free to reply to this email. I can&#8217;t promise to have an answer, but I&#8217;ll be happy to share any thoughts I have that might help.</p>]]></content:encoded></item><item><title><![CDATA[Testing a book's code, part 6: Testing a Django project]]></title><description><![CDATA[MP 83: Testing a simple but nontrivial Django project, from a reader's perspective.]]></description><link>https://mostlypython.substack.com/p/testing-a-books-code-part-6-testing</link><guid isPermaLink="false">https://mostlypython.substack.com/p/testing-a-books-code-part-6-testing</guid><dc:creator><![CDATA[Eric Matthes]]></dc:creator><pubDate>Thu, 15 Feb 2024 17:30:19 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!M9zJ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff1bdd10d-3ec7-44c3-98b5-9f2d80f2ee82_990x651.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><strong>Note:</strong> <em>This is the sixth post in a <a href="https://www.mostlypython.com/p/testing-a-books-code-696">series</a> about testing. This post will only be available to paid subscribers for the first 6 weeks. After that it will be available to everyone. Thank you to everyone who supports my ongoing work on Mostly Python.</em></p><div><hr></div><p>The final project in <em>Python Crash Course</em> is a simple, but nontrivial Django project. It&#8217;s called <em>Learning Log</em>, and it lets users create their own online learning journals.</p><p>When testing a typical Django project, the focus is usually on testing the internal code in the project. I&#8217;m not really interested in that kind of testing here. Instead, I want to know that this project works on new versions of Django, which come out every 8(!) months. That&#8217;s a quick lifecycle, but it&#8217;s manageable because Django takes stability quite seriously.</p><p>In this post we&#8217;ll develop a single test function for the <em>Learning Log</em> project. It will create a copy of the project, build a separate virtual environment just for testing this project, and test whether the overall project functions as it&#8217;s supposed to.</p><h3>Setting up the test environment</h3><p>A Django project really needs its own virtual environment. I want this test to reflect how readers run the Learning Log project, so the test is going to make a fresh environment just for this project on every run. It&#8217;s then going to take the same actions a reader would when interacting with the project.</p><h4>Copy project files to temp directory</h4><p>Here&#8217;s the <a href="https://github.com/ehmatthes/mp_testing_pcc_3e/blob/7bdc8b837bc96b4e57b3fe0a5765c7f9acb8aa75/tests/test_django_project.py#L11">first part</a> of <em>test_django_project.py</em>:</p>
      <p>
          <a href="https://mostlypython.substack.com/p/testing-a-books-code-part-6-testing">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Testing a book's code, part 5: Testing Plotly data visualizations]]></title><description><![CDATA[MP 82: Testing complex HTML output, and output that varies based on API calls.]]></description><link>https://mostlypython.substack.com/p/testing-a-books-code-part-5-testing</link><guid isPermaLink="false">https://mostlypython.substack.com/p/testing-a-books-code-part-5-testing</guid><dc:creator><![CDATA[Eric Matthes]]></dc:creator><pubDate>Thu, 08 Feb 2024 17:30:12 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb77decd6-63fe-46c2-acd0-91f7ce484b99_1576x1004.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><strong>Note:</strong> <em>This is the fifth post in a <a href="https://www.mostlypython.com/p/testing-a-books-code-696">series</a> about testing. This post, and the ones that follow will only be available to paid subscribers for the first 6 weeks. After that they will be available to everyone. Thank you to everyone who supports my ongoing work on Mostly Python.</em></p><div><hr></div><p>In the <a href="https://www.mostlypython.com/p/testing-a-books-code-part-4-testing">previous post</a> we tested the output of programs that use Matplotlib to generate plots. Most of that work focused on testing output images, but we also dealt with randomness and metadata.</p><p>In this post we&#8217;ll test programs that use Plotly to generate visualizations. We&#8217;ll use some of what we learned in the Matplotlib tests, but we&#8217;ll also run into some new challenges in this test suite. For example, what do you do when two 3.4MB HTML files differ by only 100 characters?</p><h3>Rolling dice</h3><p>The first three programs we need to test simulate rolling a variety of dice. Here&#8217;s the structure of the <a href="https://github.com/ehmatthes/pcc_3e/blob/main/chapter_15/rolling_dice/die_visual.py">first program</a>:</p><pre><code># Create a D6.
die = Die()

# Make some rolls, and store results in a list.
results = []
for roll_num in range(1000):
    ...

# Analyze the results.
frequencies = []
...

# Visualize the results.
title = "Results of Rolling One D6 1,000 Times"
labels = {'x': 'Result', 'y': 'Frequency of Result'}
fig = px.bar(x=poss_results, y=frequencies, title=title, labels=labels)
fig.show()</code></pre><p>This program creates a model of a six-sided die, makes 1,000 rolls, and generates a histogram of the results:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!RoP0!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5743b989-c86a-4f68-842b-4332ff708040_1353x839.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!RoP0!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5743b989-c86a-4f68-842b-4332ff708040_1353x839.png 424w, https://substackcdn.com/image/fetch/$s_!RoP0!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5743b989-c86a-4f68-842b-4332ff708040_1353x839.png 848w, https://substackcdn.com/image/fetch/$s_!RoP0!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5743b989-c86a-4f68-842b-4332ff708040_1353x839.png 1272w, https://substackcdn.com/image/fetch/$s_!RoP0!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5743b989-c86a-4f68-842b-4332ff708040_1353x839.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!RoP0!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5743b989-c86a-4f68-842b-4332ff708040_1353x839.png" width="686" height="425.3909830007391" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/5743b989-c86a-4f68-842b-4332ff708040_1353x839.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:839,&quot;width&quot;:1353,&quot;resizeWidth&quot;:686,&quot;bytes&quot;:50520,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!RoP0!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5743b989-c86a-4f68-842b-4332ff708040_1353x839.png 424w, https://substackcdn.com/image/fetch/$s_!RoP0!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5743b989-c86a-4f68-842b-4332ff708040_1353x839.png 848w, https://substackcdn.com/image/fetch/$s_!RoP0!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5743b989-c86a-4f68-842b-4332ff708040_1353x839.png 1272w, https://substackcdn.com/image/fetch/$s_!RoP0!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5743b989-c86a-4f68-842b-4332ff708040_1353x839.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Using <code>fig.show()</code>, Plotly generates an HTML file that shows the plot in a browser tab.</figcaption></figure></div><p>The <code>fig.show()</code> method writes an HTML file to a temporary location, and opens that file in a browser. In the test function, we&#8217;ll replace <code>fig.show()</code> with a call to <code>fig.write_html()</code>, and then work with that HTML file directly.</p><h4>Testing die programs</h4><p>In a <a href="https://github.com/ehmatthes/mp_testing_pcc_3e/blob/3ad0e8c413f5578c4309fdbf44ea690095fe1938/tests/test_plotly_programs.py#L41">new test module</a>, <em>test_plotly_programs.py</em>, we&#8217;ll first copy the files we need to a temp directory:</p>
      <p>
          <a href="https://mostlypython.substack.com/p/testing-a-books-code-part-5-testing">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Deploying simple Streamlit apps]]></title><description><![CDATA[MP 81: It's straightforward, but there are some things to be aware of.]]></description><link>https://mostlypython.substack.com/p/deploying-simple-streamlit-apps</link><guid isPermaLink="false">https://mostlypython.substack.com/p/deploying-simple-streamlit-apps</guid><dc:creator><![CDATA[Eric Matthes]]></dc:creator><pubDate>Tue, 06 Feb 2024 17:31:00 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!YvYp!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc7d9674f-4dfe-482d-9ee3-61e4ac9d1cb7_1396x1056.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In a <a href="https://www.mostlypython.com/p/building-simple-dashboards-with-streamlit">recent post</a>, I showed how to make a simple dashboard using <a href="https://streamlit.io">Streamlit</a>. The example dashboard had several inputs that let you choose what kind of dice to use, how many dice to roll, and how many rolls to include in the simulation.</p><p>In this post we&#8217;ll see how to deploy a simple project like this to Streamlit&#8217;s <em>Community Cloud</em> hosting service. It&#8217;s a fairly straightforward process, but there are a couple privacy issues you might want to be aware of.</p><h3>What is Community Cloud?</h3><p>Streamlit refers to projects as <em>apps</em>, because once you have a working project it functions like an app. I&#8217;ve been talking about dashboards, but Streamlit is flexible enough that what you create doesn&#8217;t have to look like a dashboard.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!YvYp!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc7d9674f-4dfe-482d-9ee3-61e4ac9d1cb7_1396x1056.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!YvYp!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc7d9674f-4dfe-482d-9ee3-61e4ac9d1cb7_1396x1056.png 424w, https://substackcdn.com/image/fetch/$s_!YvYp!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc7d9674f-4dfe-482d-9ee3-61e4ac9d1cb7_1396x1056.png 848w, https://substackcdn.com/image/fetch/$s_!YvYp!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc7d9674f-4dfe-482d-9ee3-61e4ac9d1cb7_1396x1056.png 1272w, https://substackcdn.com/image/fetch/$s_!YvYp!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc7d9674f-4dfe-482d-9ee3-61e4ac9d1cb7_1396x1056.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!YvYp!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc7d9674f-4dfe-482d-9ee3-61e4ac9d1cb7_1396x1056.png" width="650" height="491.69054441260744" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/c7d9674f-4dfe-482d-9ee3-61e4ac9d1cb7_1396x1056.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1056,&quot;width&quot;:1396,&quot;resizeWidth&quot;:650,&quot;bytes&quot;:271073,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!YvYp!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc7d9674f-4dfe-482d-9ee3-61e4ac9d1cb7_1396x1056.png 424w, https://substackcdn.com/image/fetch/$s_!YvYp!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc7d9674f-4dfe-482d-9ee3-61e4ac9d1cb7_1396x1056.png 848w, https://substackcdn.com/image/fetch/$s_!YvYp!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc7d9674f-4dfe-482d-9ee3-61e4ac9d1cb7_1396x1056.png 1272w, https://substackcdn.com/image/fetch/$s_!YvYp!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc7d9674f-4dfe-482d-9ee3-61e4ac9d1cb7_1396x1056.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Streamlit&#8217;s <em>Community Cloud</em> has a <a href="https://streamlit.io/gallery?category=data-visualization">gallery</a> of representative projects. When you visit any of these projects, the code is one click away.</figcaption></figure></div><p>When you&#8217;ve built a functioning app with Streamlit, you&#8217;ll almost certainly want to deploy it somewhere so others can use it as well. Because Streamlit is an open source project that can be installed with pip, it can be deployed in the same way you deploy any Python code: on a physical server, on a VPS such as Digital Ocean or Linode, or on a managed Platform like Heroku.</p><p>If your app is hosted in a public repository on GitHub, you can deploy it for free to Streamlit&#8217;s <a href="https://share.streamlit.io">Community Cloud</a> service. There are a variety of reasons companies offer free hosting for public projects. For one thing, having a collection of open projects helps people learn how to use the underlying framework. The code for every project hosted on <em>Community Cloud</em> is one click away; if you see an app or feature you like, you can see exactly how its developers implemented the project using Streamlit.</p><p>You can make an account on <em>Community Cloud</em> using Google, GitHub, or through email-based authentication. If you don&#8217;t already have an account, I encourage you to read through the rest of this post before making one. Streamlit asks for more permissions than you probably want to grant, and there&#8217;s a way to avoid some of the permissions that isn&#8217;t very obvious.</p><h3>Connecting a GitHub repository</h3><p>I pushed the code for the <em>Rolling Dice</em> project to a GitHub <a href="https://github.com/ehmatthes/roller_demo">repository</a>, so it&#8217;s ready to deploy to <em>Community Cloud</em>. Make sure your project has a <a href="https://docs.streamlit.io/streamlit-community-cloud/deploy-your-app/app-dependencies">requirements</a> file, so Streamlit will know what packages it needs for deployment. To deploy a public project, go to <a href="https://share.streamlit.io">https://share.streamlit.io</a>.</p><p>I already have a <em>Community Cloud</em> account, but I disconnected my GitHub account in order to walk through the process again. Without a connected GitHub account, this is what the <em>share.streamlit.io</em> dashboard looks like:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!XjQi!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd84cd775-c802-4554-9783-673f9918c02b_1034x595.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!XjQi!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd84cd775-c802-4554-9783-673f9918c02b_1034x595.png 424w, https://substackcdn.com/image/fetch/$s_!XjQi!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd84cd775-c802-4554-9783-673f9918c02b_1034x595.png 848w, https://substackcdn.com/image/fetch/$s_!XjQi!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd84cd775-c802-4554-9783-673f9918c02b_1034x595.png 1272w, https://substackcdn.com/image/fetch/$s_!XjQi!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd84cd775-c802-4554-9783-673f9918c02b_1034x595.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!XjQi!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd84cd775-c802-4554-9783-673f9918c02b_1034x595.png" width="1034" height="595" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/d84cd775-c802-4554-9783-673f9918c02b_1034x595.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:595,&quot;width&quot;:1034,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:36949,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!XjQi!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd84cd775-c802-4554-9783-673f9918c02b_1034x595.png 424w, https://substackcdn.com/image/fetch/$s_!XjQi!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd84cd775-c802-4554-9783-673f9918c02b_1034x595.png 848w, https://substackcdn.com/image/fetch/$s_!XjQi!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd84cd775-c802-4554-9783-673f9918c02b_1034x595.png 1272w, https://substackcdn.com/image/fetch/$s_!XjQi!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd84cd775-c802-4554-9783-673f9918c02b_1034x595.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Streamlit&#8217;s <em>Community Cloud</em> dashboard.</figcaption></figure></div><p>Notice the caution symbol next to Settings. Streamlit really seems to want access to your GitHub repositories, and if you limit the access in any way you&#8217;ll see that symbol.</p><p>If you click <em>New app</em> and you haven&#8217;t already connected your GitHub account, you&#8217;ll see a button labeled <em>Connect to GitHub</em>:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!saEz!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F41bd4726-c993-4cd4-912b-98394387d9c4_1034x595.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!saEz!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F41bd4726-c993-4cd4-912b-98394387d9c4_1034x595.png 424w, https://substackcdn.com/image/fetch/$s_!saEz!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F41bd4726-c993-4cd4-912b-98394387d9c4_1034x595.png 848w, https://substackcdn.com/image/fetch/$s_!saEz!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F41bd4726-c993-4cd4-912b-98394387d9c4_1034x595.png 1272w, https://substackcdn.com/image/fetch/$s_!saEz!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F41bd4726-c993-4cd4-912b-98394387d9c4_1034x595.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!saEz!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F41bd4726-c993-4cd4-912b-98394387d9c4_1034x595.png" width="1034" height="595" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/41bd4726-c993-4cd4-912b-98394387d9c4_1034x595.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:595,&quot;width&quot;:1034,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:44899,&quot;alt&quot;:&quot;popup on community cloud dashboard warning about the need to connect to GitHub&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="popup on community cloud dashboard warning about the need to connect to GitHub" title="popup on community cloud dashboard warning about the need to connect to GitHub" srcset="https://substackcdn.com/image/fetch/$s_!saEz!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F41bd4726-c993-4cd4-912b-98394387d9c4_1034x595.png 424w, https://substackcdn.com/image/fetch/$s_!saEz!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F41bd4726-c993-4cd4-912b-98394387d9c4_1034x595.png 848w, https://substackcdn.com/image/fetch/$s_!saEz!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F41bd4726-c993-4cd4-912b-98394387d9c4_1034x595.png 1272w, https://substackcdn.com/image/fetch/$s_!saEz!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F41bd4726-c993-4cd4-912b-98394387d9c4_1034x595.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Community Cloud pulls code from GitHub repositories, so you have to connect your GitHub account.</figcaption></figure></div><p>There&#8217;s no way to avoid this connection, so click that button.</p><p>You&#8217;ll arrive at a GitHub page where you can authorize Streamlit to access your public repositories:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!vk4q!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd829adb4-9564-4a02-89e8-aabe225b6cf1_1279x953.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!vk4q!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd829adb4-9564-4a02-89e8-aabe225b6cf1_1279x953.png 424w, https://substackcdn.com/image/fetch/$s_!vk4q!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd829adb4-9564-4a02-89e8-aabe225b6cf1_1279x953.png 848w, https://substackcdn.com/image/fetch/$s_!vk4q!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd829adb4-9564-4a02-89e8-aabe225b6cf1_1279x953.png 1272w, https://substackcdn.com/image/fetch/$s_!vk4q!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd829adb4-9564-4a02-89e8-aabe225b6cf1_1279x953.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!vk4q!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd829adb4-9564-4a02-89e8-aabe225b6cf1_1279x953.png" width="1279" height="953" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/d829adb4-9564-4a02-89e8-aabe225b6cf1_1279x953.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:953,&quot;width&quot;:1279,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:103728,&quot;alt&quot;:&quot;GitHub permissions page, showing that Streamlit is asking for full read/write and admin permissions over all public repos&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="GitHub permissions page, showing that Streamlit is asking for full read/write and admin permissions over all public repos" title="GitHub permissions page, showing that Streamlit is asking for full read/write and admin permissions over all public repos" srcset="https://substackcdn.com/image/fetch/$s_!vk4q!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd829adb4-9564-4a02-89e8-aabe225b6cf1_1279x953.png 424w, https://substackcdn.com/image/fetch/$s_!vk4q!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd829adb4-9564-4a02-89e8-aabe225b6cf1_1279x953.png 848w, https://substackcdn.com/image/fetch/$s_!vk4q!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd829adb4-9564-4a02-89e8-aabe225b6cf1_1279x953.png 1272w, https://substackcdn.com/image/fetch/$s_!vk4q!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd829adb4-9564-4a02-89e8-aabe225b6cf1_1279x953.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Streamlit wants all the permissions on all the repositories, regardless of which features you plan to use.</figcaption></figure></div><p>I don&#8217;t particularly like the permissions that Streamlit asks for. They want full admin and read/write access to <em>all</em> your public repositories. I think this is because they offer an in-browser editor where you can work on your Streamlit projects. But they don&#8217;t give the option to opt in to that specific service; if you want to deploy a public project to <em>Community Cloud</em>, you have to grant all these permissions.</p><p>Public repositories are part of the open source supply chain, and asking for full access to all of a user&#8217;s repositories seems a little inappropriate in 2024. Streamlit says they don&#8217;t want to have to ask for permission more than once, but most developers I know are perfectly fine granting permissions on a per-repo, as-needed basis.</p><p>I believe you can grant these permissions, deploy your project, and then revoke permissions. You&#8217;ll lose access to some services through Streamlit until you grant permissions again, but your projects appear to continue running.</p><p><strong>If you see a request for access to your private repositories as well, you can bypass that request</strong> by going directly to the URL <a href="https://share.streamlit.io/deploy">https://share.streamlit.io/deploy</a>. I believe with a new account Streamlit tries to get access to private repositories. I bypassed that once, and it doesn&#8217;t seem to prompt for private access on subsequent deployments.</p><h3>Deploying your project</h3><p>After dealing with permissions, you&#8217;ll arrive at a page where you can tell Streamlit which repository you want to deploy from:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!e-76!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fccf08c80-00b5-463c-9c5b-0473cbea4695_1034x953.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!e-76!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fccf08c80-00b5-463c-9c5b-0473cbea4695_1034x953.png 424w, https://substackcdn.com/image/fetch/$s_!e-76!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fccf08c80-00b5-463c-9c5b-0473cbea4695_1034x953.png 848w, https://substackcdn.com/image/fetch/$s_!e-76!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fccf08c80-00b5-463c-9c5b-0473cbea4695_1034x953.png 1272w, https://substackcdn.com/image/fetch/$s_!e-76!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fccf08c80-00b5-463c-9c5b-0473cbea4695_1034x953.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!e-76!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fccf08c80-00b5-463c-9c5b-0473cbea4695_1034x953.png" width="1034" height="953" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ccf08c80-00b5-463c-9c5b-0473cbea4695_1034x953.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:953,&quot;width&quot;:1034,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:47762,&quot;alt&quot;:&quot;Streamlit form labeled \&quot;Deploy an app\&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Streamlit form labeled &quot;Deploy an app&quot;" title="Streamlit form labeled &quot;Deploy an app&quot;" srcset="https://substackcdn.com/image/fetch/$s_!e-76!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fccf08c80-00b5-463c-9c5b-0473cbea4695_1034x953.png 424w, https://substackcdn.com/image/fetch/$s_!e-76!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fccf08c80-00b5-463c-9c5b-0473cbea4695_1034x953.png 848w, https://substackcdn.com/image/fetch/$s_!e-76!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fccf08c80-00b5-463c-9c5b-0473cbea4695_1034x953.png 1272w, https://substackcdn.com/image/fetch/$s_!e-76!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fccf08c80-00b5-463c-9c5b-0473cbea4695_1034x953.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Deploying a project is really simple: provide the repository&#8217;s URL, specify the branch that should be deployed, and give the name of the main <em>.py</em> file that should be run.</figcaption></figure></div><p>You can fill this page out with your repository&#8217;s URL, the name of the branch you want to deploy from, and the name of the <em>.py</em> file that you&#8217;ve been calling with <code>streamlit run</code>. Streamlit will generate a unique URL for your project, and you&#8217;re free to enter a simpler name that hasn&#8217;t already been taken. For this project, I chose <a href="https://sl-roller.streamlit.app">sl-roller.streamlit.app</a>.</p><p>When you click <em>Deploy</em>, you&#8217;ll see a message that &#8220;Your app is in the oven.&#8221; In a moment, you should see a deployed version of your app that behaves just like the one you&#8217;ve been running locally. Here&#8217;s what the <em>Rolling Dice</em> demo looks like immediately after deployment:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!pXB-!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb6e8378-f537-4bcf-a902-f907e95b84c7_1279x953.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!pXB-!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb6e8378-f537-4bcf-a902-f907e95b84c7_1279x953.png 424w, https://substackcdn.com/image/fetch/$s_!pXB-!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb6e8378-f537-4bcf-a902-f907e95b84c7_1279x953.png 848w, https://substackcdn.com/image/fetch/$s_!pXB-!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb6e8378-f537-4bcf-a902-f907e95b84c7_1279x953.png 1272w, https://substackcdn.com/image/fetch/$s_!pXB-!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb6e8378-f537-4bcf-a902-f907e95b84c7_1279x953.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!pXB-!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb6e8378-f537-4bcf-a902-f907e95b84c7_1279x953.png" width="1279" height="953" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/db6e8378-f537-4bcf-a902-f907e95b84c7_1279x953.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:953,&quot;width&quot;:1279,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:210594,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!pXB-!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb6e8378-f537-4bcf-a902-f907e95b84c7_1279x953.png 424w, https://substackcdn.com/image/fetch/$s_!pXB-!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb6e8378-f537-4bcf-a902-f907e95b84c7_1279x953.png 848w, https://substackcdn.com/image/fetch/$s_!pXB-!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb6e8378-f537-4bcf-a902-f907e95b84c7_1279x953.png 1272w, https://substackcdn.com/image/fetch/$s_!pXB-!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb6e8378-f537-4bcf-a902-f907e95b84c7_1279x953.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">The live version of the <em>Rolling Dice</em> demo.</figcaption></figure></div><p>When you&#8217;re logged in to <em>Community Cloud</em>, you&#8217;ll see a sidebar that lets you manage your app. You can view the log, see how many people have visited your app, reboot it as needed, and more.</p><h3>A little more about private repos</h3><p>Streamlit really seems to want access to our private GitHub repositories. I bypassed the popup that demands access to private repositories as described above, but I still have a caution symbol next to my <em>Settings</em> link. Here&#8217;s what I see if I click on <em>Settings</em>:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!5BPA!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F051e7a52-1ba7-43c2-89d4-0008cd83a572_609x269.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!5BPA!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F051e7a52-1ba7-43c2-89d4-0008cd83a572_609x269.png 424w, https://substackcdn.com/image/fetch/$s_!5BPA!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F051e7a52-1ba7-43c2-89d4-0008cd83a572_609x269.png 848w, https://substackcdn.com/image/fetch/$s_!5BPA!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F051e7a52-1ba7-43c2-89d4-0008cd83a572_609x269.png 1272w, https://substackcdn.com/image/fetch/$s_!5BPA!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F051e7a52-1ba7-43c2-89d4-0008cd83a572_609x269.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!5BPA!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F051e7a52-1ba7-43c2-89d4-0008cd83a572_609x269.png" width="609" height="269" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/051e7a52-1ba7-43c2-89d4-0008cd83a572_609x269.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:269,&quot;width&quot;:609,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:24952,&quot;alt&quot;:&quot;Warning dialog with button labelled \&quot;Allow access\&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Warning dialog with button labelled &quot;Allow access&quot;" title="Warning dialog with button labelled &quot;Allow access&quot;" srcset="https://substackcdn.com/image/fetch/$s_!5BPA!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F051e7a52-1ba7-43c2-89d4-0008cd83a572_609x269.png 424w, https://substackcdn.com/image/fetch/$s_!5BPA!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F051e7a52-1ba7-43c2-89d4-0008cd83a572_609x269.png 848w, https://substackcdn.com/image/fetch/$s_!5BPA!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F051e7a52-1ba7-43c2-89d4-0008cd83a572_609x269.png 1272w, https://substackcdn.com/image/fetch/$s_!5BPA!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F051e7a52-1ba7-43c2-89d4-0008cd83a572_609x269.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Even if you&#8217;re not planning to deploy from a private repo, Streamlit continues to show a &#8220;Warning&#8221;. They really seem to want access to those private repos.</figcaption></figure></div><p>Streamlit&#8217;s justification for this warning is that they allow you to deploy <em>one</em> app from a private repository. But if you click <em>Allow access</em>, this is what you see:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!PxFj!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F99339ab6-1206-42c7-a1ed-683f7c0e13b7_526x339.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!PxFj!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F99339ab6-1206-42c7-a1ed-683f7c0e13b7_526x339.png 424w, https://substackcdn.com/image/fetch/$s_!PxFj!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F99339ab6-1206-42c7-a1ed-683f7c0e13b7_526x339.png 848w, https://substackcdn.com/image/fetch/$s_!PxFj!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F99339ab6-1206-42c7-a1ed-683f7c0e13b7_526x339.png 1272w, https://substackcdn.com/image/fetch/$s_!PxFj!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F99339ab6-1206-42c7-a1ed-683f7c0e13b7_526x339.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!PxFj!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F99339ab6-1206-42c7-a1ed-683f7c0e13b7_526x339.png" width="526" height="339" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/99339ab6-1206-42c7-a1ed-683f7c0e13b7_526x339.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:339,&quot;width&quot;:526,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:39452,&quot;alt&quot;:&quot;GitHub list of permissions Streamlit is requesting&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="GitHub list of permissions Streamlit is requesting" title="GitHub list of permissions Streamlit is requesting" srcset="https://substackcdn.com/image/fetch/$s_!PxFj!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F99339ab6-1206-42c7-a1ed-683f7c0e13b7_526x339.png 424w, https://substackcdn.com/image/fetch/$s_!PxFj!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F99339ab6-1206-42c7-a1ed-683f7c0e13b7_526x339.png 848w, https://substackcdn.com/image/fetch/$s_!PxFj!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F99339ab6-1206-42c7-a1ed-683f7c0e13b7_526x339.png 1272w, https://substackcdn.com/image/fetch/$s_!PxFj!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F99339ab6-1206-42c7-a1ed-683f7c0e13b7_526x339.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">They want all the permissions on all the private repos, too!</figcaption></figure></div><p>For the possibility of deploying from one private repository, Streamlit wants read and write access to <em>all</em> of your private repositories. In an <a href="https://github.com/streamlit/streamlit/issues/4344">issue</a> on the <code>streamlit</code> repository, staff have suggested that people can make a new GitHub account if they dislike Streamlit&#8217;s approach to handling permissions. This is not a very satisfying response, as it shifts all the responsibility for appropriate access onto developers.</p><p>I&#8217;m only using <em>Community Cloud</em> because it&#8217;s possible to bypass the demand for private repository access. If you decide to use it as well, I encourage you to keep that caution symbol next to your <em>Settings</em> link as well.</p><h3>Pushing changes</h3><p>When you connect a GitHub repository to a <em>Community Cloud</em> deployment, Streamlit sets a web hook on the repository. Any time you push changes to the branch that the deployment is pulling from, Streamlit gets a notification and automatically updates the deployed project.</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!cO7n!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F700ec5af-639f-48be-981d-770e3b35bfe4_792x190.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!cO7n!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F700ec5af-639f-48be-981d-770e3b35bfe4_792x190.png 424w, https://substackcdn.com/image/fetch/$s_!cO7n!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F700ec5af-639f-48be-981d-770e3b35bfe4_792x190.png 848w, https://substackcdn.com/image/fetch/$s_!cO7n!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F700ec5af-639f-48be-981d-770e3b35bfe4_792x190.png 1272w, https://substackcdn.com/image/fetch/$s_!cO7n!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F700ec5af-639f-48be-981d-770e3b35bfe4_792x190.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!cO7n!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F700ec5af-639f-48be-981d-770e3b35bfe4_792x190.png" width="792" height="190" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/700ec5af-639f-48be-981d-770e3b35bfe4_792x190.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:190,&quot;width&quot;:792,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:30479,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!cO7n!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F700ec5af-639f-48be-981d-770e3b35bfe4_792x190.png 424w, https://substackcdn.com/image/fetch/$s_!cO7n!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F700ec5af-639f-48be-981d-770e3b35bfe4_792x190.png 848w, https://substackcdn.com/image/fetch/$s_!cO7n!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F700ec5af-639f-48be-981d-770e3b35bfe4_792x190.png 1272w, https://substackcdn.com/image/fetch/$s_!cO7n!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F700ec5af-639f-48be-981d-770e3b35bfe4_792x190.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><figcaption class="image-caption">Streamlit sets a webhook that notifies them when the deployed project has been updated.</figcaption></figure></div><p>This means when you push updates to your repository&#8217;s main branch, or merge changes into the main branch, your deployment will pick up those same changes.<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-1" href="#footnote-1" target="_self">1</a></p><h3>Conclusions</h3><p>Streamlit is fairly easy to get started with, and if you&#8217;re already familiar with Git and GitHub it&#8217;s quite easy to deploy your public projects as well.</p><p>Be aware that projects hosted on <em>Community Cloud</em> have about 1GB of resources available, and they rate-limit requests to prevent abuse. I shared a project that got several requests a minute for a while, and the app handled it perfectly fine. I shared that same project to a larger audience, and everyone started getting 429 errors. If you have a project that&#8217;s going to see many concurrent users, <em>Community Cloud</em> is probably not an appropriate deployment solution.</p><p>I&#8217;ll follow this up with a couple more posts about using Streamlit over the next month or two. I&#8217;ll share a real-world dashboard I developed, and discuss a variety of issues involved with making performant deployments of your Streamlit apps. If you have questions or observations from your own experiences, please share them!</p><h3>Resources</h3><p>You can find the code files from this post in the&nbsp;<a href="https://github.com/ehmatthes/roller_demo">roller_demo</a>&nbsp;GitHub repository.</p><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-1" href="#footnote-anchor-1" class="footnote-number" contenteditable="false" target="_self">1</a><div class="footnote-content"><p>I&#8217;m using the word &#8220;main&#8221; loosely here. You can tell Streamlit to watch any branch on your respository. So you could have a <code>main</code> branch, a <code>dev</code> branch, and a <code>streamlit_cc</code> branch if you want. If you tell Streamlit to deploy from the <code>streamlit_cc</code> branch, that&#8217;s the only one it will pick up changes from.</p></div></div>]]></content:encoded></item><item><title><![CDATA[Testing a book's code, part 4: Testing Matplotlib data visualizations]]></title><description><![CDATA[MP 80: Modifying files and making assertions about images.]]></description><link>https://mostlypython.substack.com/p/testing-a-books-code-part-4-testing</link><guid isPermaLink="false">https://mostlypython.substack.com/p/testing-a-books-code-part-4-testing</guid><dc:creator><![CDATA[Eric Matthes]]></dc:creator><pubDate>Thu, 01 Feb 2024 17:30:29 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0116d71-d7da-42ba-ab8d-4e90f5e039ec_931x829.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><strong>Note:</strong> <em>This is the fourth post in a <a href="https://www.mostlypython.com/p/testing-a-books-code-696">series</a> about testing. This post, and the ones that follow will only be available to paid subscribers for the first 6 weeks. After that they will be available to everyone. Thank you to everyone who supports my ongoing work on Mostly Python.</em></p><div><hr></div><p>In the <a href="https://www.mostlypython.com/p/testing-a-books-code-part-3-testing">last post</a> we tested a game built with Pygame, and added a CLI argument to specify any version of Pygame we want. In this post we&#8217;ll test the output of data visualization programs that use <a href="https://matplotlib.org">Matplotlib</a>. In the end, we&#8217;ll be able to run tests against any version of Matplotlib as well.</p><p>Testing data visualization programs is interesting, because there&#8217;s a variety of output. There&#8217;s some terminal output that&#8217;s fairly straightforward to make assertions about, but the graphical output is much more interesting to test. Let&#8217;s dig in.</p><h3>Testing one Matplotlib program</h3><p>Here&#8217;s the <a href="https://github.com/ehmatthes/pcc_3e/blob/main/chapter_15/plotting_simple_line_graph/mpl_squares.py">first program</a> we want to test, called <em>mpl_squares.py</em>:</p><pre><code>import matplotlib.pyplot as plt

input_values = [1, 2, 3, 4, 5]
squares = [1, 4, 9, 16, 25]

plt.style.use('seaborn-v0_8')
fig, ax = plt.subplots()
ax.plot(input_values, squares, linewidth=3)

# Set chart title and label axes.
...

<strong>plt.show()</strong></code></pre><p>This code defines a small dataset, and then generates a line graph:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Hyft!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0778b947-da6d-463a-9ff0-278359191df4_804x618.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Hyft!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0778b947-da6d-463a-9ff0-278359191df4_804x618.png 424w, https://substackcdn.com/image/fetch/$s_!Hyft!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0778b947-da6d-463a-9ff0-278359191df4_804x618.png 848w, https://substackcdn.com/image/fetch/$s_!Hyft!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0778b947-da6d-463a-9ff0-278359191df4_804x618.png 1272w, https://substackcdn.com/image/fetch/$s_!Hyft!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0778b947-da6d-463a-9ff0-278359191df4_804x618.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Hyft!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0778b947-da6d-463a-9ff0-278359191df4_804x618.png" width="804" height="618" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/0778b947-da6d-463a-9ff0-278359191df4_804x618.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:618,&quot;width&quot;:804,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:40222,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!Hyft!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0778b947-da6d-463a-9ff0-278359191df4_804x618.png 424w, https://substackcdn.com/image/fetch/$s_!Hyft!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0778b947-da6d-463a-9ff0-278359191df4_804x618.png 848w, https://substackcdn.com/image/fetch/$s_!Hyft!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0778b947-da6d-463a-9ff0-278359191df4_804x618.png 1272w, https://substackcdn.com/image/fetch/$s_!Hyft!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0778b947-da6d-463a-9ff0-278359191df4_804x618.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">A simple plot as seen in Matplotlib&#8217;s plot viewer. The viewer is great for interactive work, but not very compatible with automated testing.</figcaption></figure></div><p>The call to <code>plt.show()</code> causes Matplotlib&#8217;s plot viewer to appear. This is great for introducing people to plotting libraries, but it&#8217;s problematic in an automated test environment. We don&#8217;t really want to call <code>plt.show()</code> in a test run.</p><h4>Rewriting the file before testing it</h4><p>We won&#8217;t test this file directly because we don&#8217;t want to call <code>plt.show()</code>, and we can&#8217;t change the files in the book&#8217;s repository. However, we can copy them to a different directory outside the repository and then do whatever we want with them. So let&#8217;s modify the program to write the plot as an image file. We can then make assertions about the freshly-generated image file.</p><p>We&#8217;ll start by making a new test file, called <em>test_matplotlib_programs.py</em>. Here&#8217;s the <a href="https://github.com/ehmatthes/mp_testing_pcc_3e/blob/001fe9fc2d3634a7ae5d1c1aef5fd2b15330dc35/tests/test_matplotlib_programs.py">first part</a> of the test for <em>mpl_squares.py</em>:</p>
      <p>
          <a href="https://mostlypython.substack.com/p/testing-a-books-code-part-4-testing">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Testing a book's code, part 3: Testing a game]]></title><description><![CDATA[MP 79: Testing code that might not seem testable at first.]]></description><link>https://mostlypython.substack.com/p/testing-a-books-code-part-3-testing</link><guid isPermaLink="false">https://mostlypython.substack.com/p/testing-a-books-code-part-3-testing</guid><dc:creator><![CDATA[Eric Matthes]]></dc:creator><pubDate>Thu, 25 Jan 2024 17:30:19 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!Xw9o!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F05ab0795-38c5-4488-b95a-9800b8b4fd8c_1520x1187.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><strong>Note:</strong> <em>This is the third post in a seven-part <a href="https://www.mostlypython.com/p/testing-a-books-code-696">series</a> about testing.</em></p><div><hr></div><p>In the <a href="https://www.mostlypython.com/p/testing-a-books-code-part-2-basic">last post</a> we focused on testing the basic programs in the first half of <em>Python Crash Course</em>. So far the test suite covers 60 programs, which only print output to the terminal. The tests run on macOS and Windows, and should run on most Linux systems as well. This first phase of work brought up some interesting testing concepts such as parametrization and fixtures, but the code that was being tested was pretty simple.</p><p>In this post we&#8217;ll make sure the test suite can be run with multiple versions of Python. After that, we&#8217;ll test the first project in the book, <em>Alien Invasion</em>. This is a 2d game project, and it&#8217;s probably not obvious at all how to test it. We&#8217;ll discuss those challenges, make a plan to address them, and write a test that gives us confidence that the game project works as it&#8217;s supposed to.</p><h3>Running tests on different versions of Python</h3><p><strong>Note:</strong> <em>I drafted this post last fall, before Python 3.12 was released. I&#8217;m leaving the version references as they were before 3.12 came out, because it shows how the test suite can be used during the pre-release period for new versions of Python.</em> </p><p>Most of my Python work at the moment uses Python 3.11.5, the most recent point release of the latest version of Python. The test suite runs in a virtual environment that was built with this version.</p><p>However, Python 3.12 is coming out soon, and the third release candidate (3.12.0rc3) was released about a week ago. I&#8217;d like to test the book&#8217;s code against this version, and each release candidate that comes out until 3.12.0 is officially released. This helps find any bugs in the pre-release versions of Python, and lets me address any incompatibilities in the existing codebase with newer versions of Python.</p><p>I use <a href="https://github.com/pyenv/pyenv">pyenv</a> to manage Python versions on my system. 3.12.0rc3 isn&#8217;t available quite yet through the Homebrew version of pyenv, so I&#8217;m going to use 3.12.0rc2 for this round of testing.</p><p>To make testing with different versions easier, I&#8217;m going to build a new virtual environment called <em>.venv_3120rc2</em>, and run the tests with that environment active. Here&#8217;s the terminal commands I&#8217;m using to switch between different Python versions, and verify that the commands are having the expected effect:</p><pre><code>(.venv)$ python --version
Python 3.11.5
(.venv)$ <strong>deactivate
</strong>$ <strong>pyenv local 3.12.0rc2
$ python --version
Python 3.12.0rc2</strong></code></pre><p>Now we can make a new virtual environment, and run the tests using that version of Python:</p><pre><code>$ <strong>python -m venv .venv_3120rc2</strong>                             
$ <strong>source .venv_3120rc2/bin/activate</strong>
(.venv_3120rc2)$ <strong>pip install pytest</strong>
(.venv_3120rc2)$ <strong>pytest -q</strong>
... [100%]
60 passed in 0.94s</code></pre><p>This shows that all the basic programs in the book should continue to run without issue under Python 3.12.</p><p>Note that this did not involve any changes to the test suite itself. The test suite uses the Python interpreter from the active virtual environment, so all we did was set up a second virtual environment. We can keep using the 3.12.0rc2 environment, or we can switch back to the 3.11.5 environment whenever we want.</p><h4>Making it clear which version was used for testing</h4><p>Dealing with multiple Python versions is not always straightforward. When you have multiple versions installed, it&#8217;s easy to end up accidentally using a different version than what you intended. This is especially true if everything works, and there&#8217;s no version-specific output.</p><p>Let&#8217;s add a short block that shows exactly which version was used to run the tests. We&#8217;ll do this in <em>tests/conftest.py</em>:</p><pre><code>...
@pytest.fixture(scope="session")
def python_cmd():
    """Return the path to the venv Python interpreter."""
    <strong>return utils.get_python_cmd()</strong>

<strong>def pytest_sessionfinish(session, exitstatus):
    """Custom cleanup work."""

    # Show which version of Python was used for tests.
    python_cmd = utils.get_python_cmd()
    cmd = f"{python_cmd} --version"
    output = utils.run_command(cmd)

    print(f"***** Tests were run with: {output}")</strong></code></pre><p>If you include a function called <code>pytest_sessionfinish()</code> in <em>conftest.py</em>, that function will be called automatically when the test session is over. We&#8217;d like to use the <code>python_cmd</code> fixture in this function, but fixtures are only available <em>during</em> a test session. By the time <code>pytest_sessionfinish()</code> is called, the test session is over. So, we need to pull the code for getting the value of <code>python_cmd</code> out into a utility function. This shortens the original <code>python_cmd()</code> fixture function, which effectively becomes a wrapper for the <a href="https://github.com/ehmatthes/mp_testing_pcc_3e/blob/d01283e2d5bc188e8d0b648dd3a3f9f419a84b44/tests/utils.py#L14">new utility function</a>.</p><p>In <code>pytest_sessionfinish()</code>, we call <code>python &#8212;-version</code>, where <code>python</code> is actually the full path to the interpeter that&#8217;s being used throughout the test session. We then print a line showing what the output of this call is. Normally you need to include the <code>-s</code> flag when calling pytest to see the output of <code>print()</code> calls. That won&#8217;t be necessary here, because the output of any <code>print()</code> calls in <code>pytest_sessionfinish()</code> are included in the overall test output by default.</p><p>When we run tests using <em>.venv</em>, we should see output reflecting Python 3.11. When we run tests using <em>.venv_3120rc2</em>, we should see output referencing Python 3.12:</p><pre><code>$ <strong>source .venv/bin/activate</strong>             
(.venv)$ <strong>pytest -q</strong>
... [100%]
***** Tests were run with: Python 3.11.5
60 passed in 0.90s
(.venv)$ <strong>deactivate</strong> 
$ <strong>source .venv_3120rc2/bin/activate</strong>
(.venv_3120rc2)$ <strong>pytest -q</strong>
... [100%]
***** Tests were run with: Python 3.12.0rc2
60 passed in 0.96s</code></pre><p>The output here shows one test run using Python 3.11.5, and a second run using Python 3.12.0rc2.</p><p>This actually caught an inconsistency for me while doing this writeup. I sometimes install pytest to my global Python environment, so I can run quick tests anywhere. But if you then install pytest to a virtual environment, it can be unclear at times which instance of pytest is being used. I ran pytest in an active 3.12.0rc2 virtual environment, but the output indicated Python 3.11 was being used. Even with the virtual environment active, pytest was still pointing to my system-level instance of pytest, because it was the first one listed in my path variable. I ended up uninstalling that system-level pytest, to avoid any future inconsistencies like this.</p><p>Running pytest without the <code>-q</code> flag usually shows which interpreter was used to start the test suite, but I always want to know that <code>python_cmd</code> is picking up the version I think it is.</p><h4>Why not use tox?</h4><p>The <a href="https://tox.wiki/en/4.12.1/">tox</a> library automates testing across different versions of Python, and different versions of third-party libraries as well. You might be wondering why I don&#8217;t use tox to manage versions instead of making individual virtual environments, which is similar to what tox does for you behind the scenes.</p><p>I haven&#8217;t used tox before, but I&#8217;m looking forward to using it when I have a need for it. I&#8217;m not using it for this test suite because I usually want to run tests against a specific version of Python, or a specific version of a library. I rarely want to run a full matrix of all versions of Python with all versions of each library. For my testing purposes, most of the time I want to pick a specific version of Python and maybe a specific version of a library. This is another way that testing for a book differs from testing traditional software projects.</p><h3>Testing Alien Invasion</h3><p>Testing video games is notoriously difficult. Most discussions of testing software focus on exercising code which has identifiable inputs we can test, and outputs we can make assertions against. The output of video games depends on a variety of inputs that often can&#8217;t be replicated fully. That&#8217;s why game companies pay people to play through their games in specific ways, and report on their experiences. It&#8217;s not nearly as fun as it sounds to young players.</p><p>Fortunately my testing needs are simple: I want to make sure the game runs on recent versions of Python and Pygame. The original game in the first edition of the book was function-based, and it would have been rather hard to write an automated test for the game. I restructured the game for the second edition of the book so that the entire game is represented by a class. This made the code a little more complicated at the start of the project, but resulted in a much cleaner overall structure for the finished game. It also made the game much more testable&#8212;you can import the main game file, create an instance of the game, and script the same kinds of actions an actual player might take.</p><p>A few years ago I wrote up a <a href="https://ehmatthes.github.io/pcc_2e/beyond_pcc/ai_player/">guide</a> for writing a script that would play the game automatically. It&#8217;s kind of fun to write a program that plays a game for you. Alien Invasion is a classic space shooter, so you can implement algorithms like &#8220;move the ship back and forth, firing continuously until the screen is cleared&#8221;. You can also write algorithms that target specific aliens, or algorithms that combine multiple strategies and include random fluctuations in behavior.</p><p>This background work made it fairly straightforward to figure out how to test Alien Invasion. The test function will call a script that automates the game play. The automation script will implement a simple strategy that clears one screen of aliens, and then exits. We&#8217;ll then make a few assertions about certain conditions that should be true at the end of Level 1. If those simple assertions pass, I&#8217;ll be confident that the game project works on the versions of Python and Pygame that were used for testing. I have no need to test more complex aspects of the game itself.</p><h4>Automating gameplay</h4><p>Here are the most important parts of <em>ai_tester.py</em>, the <a href="https://github.com/ehmatthes/mp_testing_pcc_3e/blob/f84f502f9040e1536cf5680e9fd29dd0e92424d7/tests/ai_tester.py">script</a> that automates game play for testing purposes:<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-1" href="#footnote-1" target="_self">1</a></p><pre><code><strong>class AITester:</strong>

    def __init__(self, ai_game):
        """Automatic player for Alien Invasion."""
        # Make a reference to the game object.
        <strong>self.ai_game = ai_game</strong>

    <strong>def run_game(self):</strong>
        """Replaces the original run_game(),
        so we can control the game.
        """
        # Start out in an active state.
        <strong>self.ai_game.game_active = True</strong>

        # Speed up the game for testing.
        <strong>self.ai_game.settings.speedup_scale = 50
        self.ai_game.settings.increase_speed()</strong>

        # Start the main loop for the game.
        while True:
            # Still call ai_game._check_events(),
            # so we can use keyboard to quit.
            self.ai_game._check_events()

            # Sweep the ship right and left continuously.
            ship = self.ai_game.ship
            screen_rect = self.ai_game.screen.get_rect()

            <strong>if not ship.moving_right and not ship.moving_left:</strong>
                # Ship hasn't started moving yet; move to the right.
                <strong>ship.moving_right = True</strong>
                ...

            <strong># Fire as often as possible.</strong>
<strong>            self.ai_game._fire_bullet()</strong>

            self.ai_game._update_screen()
            self.ai_game.clock.tick(60)

            <strong>if self.ai_game.stats.level &gt; 1:
                break</strong></code></pre><p>Notice that <code>AITester</code> does not inherit from <code>AlienInvasion</code>. Instead, it takes a reference to the game object and then defines its own <code>run_game()</code> method. That method takes the place of the original <code>run_game()</code> method, so we can control all actions in the game.</p><p>In this automated player, we start the game in an active state instead of waiting for the player to press a start button. We speed the game up by a factor of 50 for testing. If you speed it up too much, the aliens come down too fast to run through any actual game interactions.</p><p>We then start the ship moving and sweep it left and right continuously. As long as the game is active, we keep firing bullets at the aliens. We break out of the game&#8217;s main loop as soon as we&#8217;ve finished the first level.</p><h4>Testing the game</h4><p>Now we need to write a test function that calls <code>AITester</code>. We&#8217;ll do that in a <a href="https://github.com/ehmatthes/mp_testing_pcc_3e/blob/f84f502f9040e1536cf5680e9fd29dd0e92424d7/tests/test_alien_invasion.py">new test file</a>, called <em>test_alien_invasion.py</em>:</p><pre><code>from pathlib import Path
...

<strong>def test_ai_game():</strong>
    """Test basic functionality of the game."""

    # Add source path to sys.path, so we can import AlienInvasion.
    ai_path = Path(__file__).parents[1] / "chapter_14" / "scoring"
    <strong>sys.path.insert(0, str(ai_path))</strong>
    from alien_invasion import AlienInvasion
    from ai_tester import AITester
    
    # Create a game instance, and an AITester instance, and run game.
    <strong>os.chdir(ai_path)</strong>
    <strong>ai_game = AlienInvasion()
    ai_tester = AITester(ai_game)
    ai_tester.run_game()</strong>

    # Make assertions to ensure first level was played through.
    assert ai_game.stats.score == 3375
    assert ai_game.stats.level == 2</code></pre><p>This is a fairly short test function; most of the work for testing the game is already in the <code>AITester</code> class. Here we modify <code>sys.path</code> to include the directory where <em>alien_invasion.py</em> is located. There are many versions of the game project in the repository, to help readers who get off track in the game&#8217;s development. These are versions of the project as it stands at the end of each major section in the book. We only need to test the final version of the game; if that works, then all the other versions should work as well.<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-2" href="#footnote-2" target="_self">2</a></p><p>The main game file imports a number of other modules, which all depend on relative imports. So, we use <code>os.chdir()</code> to switch to the directory where <em>alien_invasion.py</em> is located. Since the game project doesn&#8217;t modify any files, we can safely run it from that directory.</p><p>Once in the <code>ai_path</code> directory, we can make an instance of <code>AlienInvasion</code>, and then make an instance of <code>AITester</code>. We call <code>run_game()</code>, and it automatically plays the game through one level.</p><p>When the first level is complete, we make two simple assertions: that the score is correct for having destroyed one screen&#8217;s worth of aliens, and that we&#8217;re at level 2. There are many more assertions we could consider, but that&#8217;s not really the point. The main point of this test is to show that the current code works for running the game, on the current version of Python and Pygame.</p><p>This test passes, and you can watch the AI player as it plays through the first level:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Xw9o!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F05ab0795-38c5-4488-b95a-9800b8b4fd8c_1520x1187.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Xw9o!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F05ab0795-38c5-4488-b95a-9800b8b4fd8c_1520x1187.png 424w, https://substackcdn.com/image/fetch/$s_!Xw9o!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F05ab0795-38c5-4488-b95a-9800b8b4fd8c_1520x1187.png 848w, https://substackcdn.com/image/fetch/$s_!Xw9o!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F05ab0795-38c5-4488-b95a-9800b8b4fd8c_1520x1187.png 1272w, https://substackcdn.com/image/fetch/$s_!Xw9o!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F05ab0795-38c5-4488-b95a-9800b8b4fd8c_1520x1187.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Xw9o!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F05ab0795-38c5-4488-b95a-9800b8b4fd8c_1520x1187.png" width="650" height="507.5892857142857" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/05ab0795-38c5-4488-b95a-9800b8b4fd8c_1520x1187.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1137,&quot;width&quot;:1456,&quot;resizeWidth&quot;:650,&quot;bytes&quot;:236842,&quot;alt&quot;:&quot;Terminal running pytest command, and game window open in front of terminal&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Terminal running pytest command, and game window open in front of terminal" title="Terminal running pytest command, and game window open in front of terminal" srcset="https://substackcdn.com/image/fetch/$s_!Xw9o!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F05ab0795-38c5-4488-b95a-9800b8b4fd8c_1520x1187.png 424w, https://substackcdn.com/image/fetch/$s_!Xw9o!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F05ab0795-38c5-4488-b95a-9800b8b4fd8c_1520x1187.png 848w, https://substackcdn.com/image/fetch/$s_!Xw9o!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F05ab0795-38c5-4488-b95a-9800b8b4fd8c_1520x1187.png 1272w, https://substackcdn.com/image/fetch/$s_!Xw9o!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F05ab0795-38c5-4488-b95a-9800b8b4fd8c_1520x1187.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">When the test suite runs, the automated player plays through the first level of the game. This happens at 50 times the speed of normal gameplay.</figcaption></figure></div><p>Interestingly, I slowed the game down in order to make a screenshot, and noticed the simple game play algorithm doesn&#8217;t actually clear the screen if it&#8217;s not sped up.</p><p>At full speed, the test finishes in about 6 seconds:</p><pre><code>(.venv)$ <strong>pytest tests/test_alien_invasion.py -q</strong>
. [100%]***** Tests were run with: Python 3.11.5
1 passed in 6.39s</code></pre><p>Six seconds is slow for a single test in most projects, but this is quite reasonable for what it does. It&#8217;s reassuring to see the game running for a few seconds, and I&#8217;m never going to use this in a CI environment. It&#8217;s also easy to exclude this test if I&#8217;m focusing on other tests.</p><h4>Testing different Pygame versions</h4><p>One of the main goals of this test suite is to be able to test different versions of a specific library. To support this, I&#8217;m going to add a CLI argument to the test suite, so I can run the following command:</p><pre><code>$ <strong>pytest tests/test_alien_invasion.py --pygame-version x.y.z</strong></code></pre><p>Here&#8217;s the new <a href="https://github.com/ehmatthes/mp_testing_pcc_3e/blob/87b1ecd98ee434e35a0d5cbb2223504fa025254f/tests/conftest.py#L11">function</a> in <em>conftest.py</em> that adds this CLI argument:</p><pre><code>def pytest_addoption(parser):
    parser.addoption(
        "--pygame-version", action="store",
        default=None,
        help="Pygame version to test"
    )</code></pre><p>The <code>pytest_addoption()</code> function adds new CLI options to pytest calls. The call to <code>parser.addoption()</code> defines <code>&#8212;-pygame-version</code> as an individual argument. Any value passed with this argument will be stored, and accessible throughout the test suite.</p><p>We&#8217;ll use this argument in a <a href="https://github.com/ehmatthes/mp_testing_pcc_3e/blob/87b1ecd98ee434e35a0d5cbb2223504fa025254f/tests/test_alien_invasion.py#L13">fixture function</a> in <em>test_alien_invasion.py</em>:</p><pre><code><strong>@pytest.fixture(scope="module", autouse=True)
def check_pygame_version(request, python_cmd):</strong>
    """Check if the correct version of Pygame is installed."""
    <strong>pygame_version = request.config.getoption("--pygame-version")
</strong>
    if pygame_version:
        print(f"\n*** Installing pygame {pygame_version}\n")

        cmd = f"{python_cmd} -m pip install pygame=={pygame_version}"
        output = utils.run_command(cmd)</code></pre><p>The code to check the version of Pygame needed for testing is placed in a fixture, for a couple reasons. First, this is setup work; it&#8217;s not really what we&#8217;re testing. That kind of work tends to belong in fixtures as opposed to test functions. But also, we&#8217;re going to do this same kind of thing when testing the other projects in the book. We&#8217;ll be using code similar to this later, and it will be easier to reuse this code if it&#8217;s implemented as a fixture from the start.</p><p>This fixture doesn&#8217;t need to return anything. When a fixture doesn&#8217;t have a return value, you can specify <code>autouse=True</code>, and the fixture will be called automatically for all tests in its scope. This fixture only applies to Pygame, so I&#8217;ve placed it in the <em>test_alien_invasion.py</em> file and given it the <code>module</code> scope.</p><p>You can check a custom pytest CLI argument by calling <code>request.config.getoption()</code>. If <code>&#8212;-pygame-version</code> was specified, we print a message that we&#8217;re installing a specific version, and then install that version to the active virtual environment.</p><p>Note that there&#8217;s no change needed to the actual test function, <code>test_ai_game()</code>. With an autouse fixture, the setup work is done automatically by the fixture.</p><p>We need to pass the <code>-s</code> flag in order to see the output of <code>print()</code> calls in fixtures and test functions. We can now test any version of Pygame we want. I&#8217;m usually interested in testing development releases, so I&#8217;ll specify a recent development release:</p><pre><code>(.venv)$ <strong>pytest tests/test_alien_invasion.py --pygame-version 2.5.0.dev2 -qs</strong>
*** Installing pygame 2.5.0.dev2
pygame 2.5.0.dev2 (SDL 2.26.4, Python 3.11.5)
Hello from the pygame community...
.
***** Tests were run with: Python 3.11.5
1 passed in 19.24s</code></pre><p>As the test suite runs, we can see that the requested version of Pygame is being installed. That information is verified in the output that Pygame generates when it&#8217;s loaded. We&#8217;ve modified the virtual environment in a way that will persist after the test runs, but we&#8217;ll address that in a moment. Note that this session takes longer than usual, because we&#8217;re installing a package and using it for the first time during the test run.</p><h4>The <code>check_library_version()</code> utility function</h4><p>We&#8217;re going to use this same approach for the data visualization and web app projects as well, so let&#8217;s move the code for checking library versions to a utility function. When doing small refactoring work like this, I like to write the function call I want to make, and then write the function itself.</p><p>Here&#8217;s the <a href="https://github.com/ehmatthes/mp_testing_pcc_3e/blob/b5f1f49c7d40a6cfc01377356bcf9658115bb420/tests/test_alien_invasion.py#L13">new version</a> of the fixture in test_alien_invasion.py:</p><pre><code>@pytest.fixture(scope="module", autouse=True)
def check_pygame_version(request, python_cmd):
    """Check if the correct version of Pygame is installed."""
    <strong>utils.check_library_version(request, python_cmd, "pygame")</strong></code></pre><p>All the code that was in this function is relevant to the task of checking the version of any library, so it all gets moved to the utility function. This one-line fixture function does still need to be in the <em>test_alien_invasion.py</em> module, however, because this is where we know which library to check for. Also, general utility functions can&#8217;t use fixtures as parameters. So, we have to keep the <code>request</code> and <code>python_cmd</code> fixtures here, and pass those values to the utility function.</p><p>Here&#8217;s the generalized <a href="https://github.com/ehmatthes/mp_testing_pcc_3e/blob/b5f1f49c7d40a6cfc01377356bcf9658115bb420/tests/utils.py#L23">utility function</a> for checking and installing specific versions of a library:</p><pre><code><strong>def check_library_version(request, python_cmd, lib_name):</strong>
    """Install a specific version of a library if needed."""
    lib_version = request.config.getoption(f"--{lib_name}-version")

    if lib_version:
        print(f"\n*** Installing {lib_name} {lib_version}\n")
        <strong>cmd = f"{python_cmd} -m pip install {lib_name}=={lib_version}"</strong>
        output = run_command(cmd)
        <strong>print(output)</strong>

    # Regardless of what version was requested,
    # show which version is being used.
    <strong>cmd = f"{python_cmd} -m pip freeze | grep {lib_name}"
    result = subprocess.run(cmd, capture_output=True,
            text=True, check=True, shell=True)</strong>
    output = result.stdout.strip()

    print(f"\n*** Running tests with {output}\n")</code></pre><p>This works for installing specific versions of Pygame, and it should work for the other libraries we&#8217;ll need to check as well. I&#8217;m printing the output of the <code>pip install</code> command, because it&#8217;s helpful to see that output while the test is running.</p><p>Issues with managing environments can be subtle, so regardless of which version was requested we display the actual version that&#8217;s being used for testing. We do this with the following command:</p><pre><code>$ <strong>python -m pip freeze | grep pygame</strong></code></pre><p>The <code>|</code> character is a pipe; we&#8217;re <em>piping</em> the output of <code>pip freeze</code> to <code>grep</code>, which shows any lines that include &#8220;pygame&#8221;. When you include a pipe in a command that&#8217;s executed through <code>subprocess.run()</code>, you have to include the argument <code>shell=True</code>. Otherwise the output of the first command goes to a different shell environment. Also, the command needs to be submitted as a single string, rather than a list of parts. This means we can&#8217;t use the <code>utils.run_command()</code> function. There&#8217;s no need to adapt <code>utils.run_command()</code> to this usage, we&#8217;ll just call <code>subprocess.run()</code> directly here.</p><p>Now the output is clear about what version of the library is being used for testing:</p><pre><code>(.venv)$ <strong>pytest tests/test_alien_invasion.py --pygame-version 2.4.0 -qs</strong>
*** Installing pygame 2.4.0
Collecting pygame==2.4.0
    ...
*** <strong>Running tests with pygame==2.4.0
pygame 2.4.0 (SDL 2.26.4, Python 3.11.5)</strong>
    ...
.
***** Tests were run with: Python 3.11.5
1 passed in 10.54s</code></pre><p>We get a message about what&#8217;s being installed, and confirmation about exactly which version is being used. There&#8217;s a bit of redundant output here because things are going well, but it&#8217;s really helpful to have all this information available when things don&#8217;t go as expected.</p><h4>Windows compatibility</h4><p>I do most of my work on macOS, and forget sometimes that I&#8217;m using tools that aren&#8217;t available on Windows. The previous version doesn&#8217;t work on Windows because of the pipe to <code>grep</code>.</p><p>We can fix that by getting all of the output from <code>pip freeze</code>, and then filtering for the output specific to the library we&#8217;re focusing on:</p><pre><code>def check_library_version(request, python_cmd, lib_name):
    """Install a specific version of a library if needed."""
    ...

    # Regardless of what version was requested,
    # show which version is being used.
    <strong>cmd = f"{python_cmd} -m pip freeze"
    output = run_command(cmd)
    lib_output = [
        line for line in output.splitlines()
        if lib_name in line
    ]</strong>

    print(f"\n*** Running tests with {lib_output}\n")</code></pre><p><a href="https://github.com/ehmatthes/mp_testing_pcc_3e/blob/582d9271695db70fe33376e18c3d85a21652871c/tests/utils.py#L23">This works</a> on both macOS and Windows, and I see no reason it won&#8217;t work on Linux as well.</p><h4>Specifying requirements</h4><p>At this point, we haven&#8217;t specified requirements for the test suite. The book&#8217;s repository doesn&#8217;t have a set of requirements, because the code isn&#8217;t usually run directly from the repository. It&#8217;s usually just used as a reference for the book.</p><p>Here are the requirements for the tests that have been written so far:</p><pre><code>(.venv)$ <strong>pip freeze &gt; requirements.txt</strong>
(.venv)$ <strong>cat requirements.txt</strong> 
iniconfig==2.0.0
packaging==23.2
pluggy==1.3.0
pygame==2.5.2
pytest==7.4.4</code></pre><p>There are just a small set of requirements, but pinning them makes it easier to run the tests on different systems. It will also make it possible to reset the test environment to its original state, after installing specific versions of libraries during individual test runs.</p><h4>Resetting the test environment</h4><p>There&#8217;s one problem with the way we&#8217;ve implemented support for <code>&#8212;-pygame-version</code>. There&#8217;s a mismatch between the <em>requirements.txt</em> file that pins the versions for default test runs, and what&#8217;s actually installed after specifying a different version of Pygame. For example, here&#8217;s what I see after doing a test run with Pygame 2.4.0:</p><pre><code>(.venv)$ <strong>cat requirements.txt| grep pygame</strong>
pygame==2.5.2
(.venv)$ <strong>pip freeze | grep pygame</strong>
pygame==2.4.0</code></pre><p>The requirements specify Pygame 2.5.2, but what&#8217;s actually installed is whatever version the last test run used.</p><p>A simple way to address this is to reinstall from <em>requirements.txt</em> at the end of every test run. This is a really quick and straightforward way to make sure the environment returns to a consistent state after each test session.</p><p>We can do this cleanup work in the <code>pytest_sessionfinish()</code> <a href="https://github.com/ehmatthes/mp_testing_pcc_3e/blob/635a359ce04baa635732626aac44e7f584780baf/tests/conftest.py#L29">function</a>, in <em>conftest.py</em>:</p><pre><code>def pytest_sessionfinish(session, exitstatus):
    """Custom cleanup work."""
    <strong>python_cmd = utils.get_python_cmd()</strong>

    # Reset any libraries that had a different version installed
    #   during the test run.
    print("\n\n--- Resetting test venv ---\n")

    <strong>req_txt_path = Path(__file__).parents[1] / "requirements.txt</strong>"
    req_txt_path = req_txt_path.as_posix()

    <strong>cmd = f"{python_cmd} -m pip install -r {req_txt_path}"</strong>
    output = utils.run_command(cmd)

    <strong>changed_lines = [
        line for line in output.split("\n")
        if "Requirement already satisfied" not in line
    ]</strong>
    
    <strong>if changed_lines:
        for line in changed_lines:
            print(line)
    else:
        print("  No packages were modified.")</strong>

    print("\n--- Finished resetting test venv ---\n")

    # Show which version of Python was used for tests.
    ...</code></pre><p>We get the value for <code>python_cmd</code> at the start of the function, because it&#8217;s used to reset the virtual environment and to show which version of Python was used. We get the path to <em>requirements.txt</em>, and then call <code>pip install</code>. We filter for any lines related to a package that needed to be reset, and print that output. If every requirement was already satisfied, we print an indication that nothing was modified during the test run:</p><pre><code>$ <strong>pytest tests/test_alien_invasion.py -q -s --pygame-version 2.4.0</strong>
*** Installing pygame 2.4.0
Collecting pygame==2.4.0
    ...
*** Running tests with ['pygame==2.4.0']
.
<strong>--- Resetting test venv ---</strong>
<strong>Collecting pygame==2.5.2
    ...
Successfully installed pygame-2.5.2
--- Finished resetting test venv ---</strong>
***** Tests were run with: Python 3.11.5
1 passed in 16.60s</code></pre><p>This test still takes about 6 seconds if you&#8217;re using a version of Pygame that&#8217;s already been used recently. If a new version was installed, the test takes about 16 seconds. That&#8217;s not surprising, and is still so much faster than testing different versions manually.</p><p>I&#8217;ll note that this does not currently work for me on Windows. I think pip isn&#8217;t cleaning up its temp directories when it installs a specific version of Pygame, and those directories are interfering with the final call to <code>pip install</code>. I&#8217;m going to see if this is an issue with all libraries, or just Pygame, and sort it out a little later.</p><p>The default output that pytest shows is really useful for most testing scenarios. This extra output is quite helpful when testing against particular versions. If I request a specific version for testing and see the message <em>No packages were modified</em>, I know something went wrong with the test run. This is much better than a test run that just silently passes when it shouldn&#8217;t.</p><h4>Running this test last</h4><p>There&#8217;s one last modification I&#8217;d like to make to this test. The Pygame window disappears by the time the test suite finishes, but I can&#8217;t find a way to make it disappear as soon as the test for Alien Invasion finishes. A workaround is to make sure this test runs last.</p><p>The <code>pytest-ordering</code> plugin lets you control some aspects of the order in which tests are run. My needs are simple; I want this one test module to run last. After installing <code>pytest-ordering</code>, this <a href="https://github.com/ehmatthes/mp_testing_pcc_3e/blob/8e302b9c896acfe9b9b136cb277206c3ded3872d/tests/test_alien_invasion.py#L15">one line</a> at the top of <em>test_alien_invasion.py</em> causes the module to be run last:</p><pre><code><strong>pytestmark = pytest.mark.run(order=-1)</strong>

@pytest.fixture(scope="module", autouse=True)
def check_pygame_version(request, python_cmd):
    ...</code></pre><p>Now when you run the entire test suite the Alien Invasion test runs last, and the Pygame window doesn&#8217;t hang around covering the terminal output.</p><h3>Conclusions</h3><p>Some projects can seem untestable, especially graphic-oriented projects. But if you can run your code, you can almost certainly test your code. Articulating the exact kind of tests you want to run, and what you want to get out of your test suite, can help determine how to approach building your test suite.</p><p>Standard tools like tox can be used to run test suites efficiently, in the ways most people need to test their software projects. If your needs don&#8217;t match the typical use cases of those tools, you can still manage your test environment to achieve the testing goals you originally set. This is part of why it&#8217;s good to articulate your goals before you start writing test code. Otherwise you can fall into the trap of writing whatever kinds of tests are most natural to write with the tools you have at hand, rather than the ones you actually need.</p><p>In testing, more information is often better than less information. It&#8217;s certainly worthwhile to have the ability to run tests with terse output, but you also probably want the option to get detailed information about a test run. Don&#8217;t be afraid to insert diagnostics into your test suite, that you can turn to when things aren&#8217;t behaving as you expect them to.</p><p>In the <a href="https://www.mostlypython.com/p/testing-a-books-code-part-4-testing">next post</a> we&#8217;ll start to test the data visualization projects. This will bring up a number of issues related to making assertions about output in the form of images and HTML files.</p><h3>Resources</h3><p>You can find the code files from this post in the&nbsp;<a href="https://github.com/ehmatthes/mp_testing_pcc_3e">mp_testing_pcc_3e</a>&nbsp;GitHub repository.</p><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-1" href="#footnote-anchor-1" class="footnote-number" contenteditable="false" target="_self">1</a><div class="footnote-content"><p>Note that the name of this file does <em>not</em> start with <em>test_</em>. We don&#8217;t want pytest to discover this file and try to run it. We want to call this file ourselves from a file that&#8217;s picked up by pytest.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-2" href="#footnote-anchor-2" class="footnote-number" contenteditable="false" target="_self">2</a><div class="footnote-content"><p>If any other versions don&#8217;t work for some reason, that would be an issue with my code, not a compatibility issue with Python or Pygame. Those kinds of errors would be straightforward to fix, as long as I know the final version of the game works.</p></div></div>]]></content:encoded></item><item><title><![CDATA[Building simple dashboards with Streamlit]]></title><description><![CDATA[MP 78: And not so simple ones as well!]]></description><link>https://mostlypython.substack.com/p/building-simple-dashboards-with-streamlit</link><guid isPermaLink="false">https://mostlypython.substack.com/p/building-simple-dashboards-with-streamlit</guid><dc:creator><![CDATA[Eric Matthes]]></dc:creator><pubDate>Tue, 23 Jan 2024 17:30:16 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F43a3b51c-80d0-43d5-9603-c74c55f5188e_1094x1113.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Building dashboards used to be primarily the realm of data specialists. But these days the tooling has advanced so much that if you know the basics of Python, you can probably put together a pretty functional dashboard with just a little code.</p><p>I&#8217;ve been hearing about <a href="https://streamlit.io">Streamlit</a> for a while now, and I&#8217;ve been looking for a project where it would be appropriate to use it. I recently found such a project, and Streamlit is just as impressive to work with as I&#8217;d heard. In this post I&#8217;ll go over just enough to help you get started building your own dashboards with Streamlit on your own system.</p><p>In a followup post, I&#8217;ll show how to quickly <a href="https://www.mostlypython.com/p/deploying-simple-streamlit-apps">deploy your dashboards</a> so others can use what you&#8217;ve created. I&#8217;ll also share the real-world project I&#8217;ve been using Streamlit to implement.</p><h3>What is Streamlit?</h3><p>In order to understand Streamlit, we need to be clear about what we mean by a dashboard. In the programming world, a dashboard is a way to view the most important data about a system. It&#8217;s usually a snapshot of a system where the data can be changing rapidly. Or, it can be a static dataset that you can tweak by adjusting the input values.</p><p>Streamlit is a tool for building dashboards using Python. You focus on the kind of data you want to present and what you want users to be able to do with it, and the library handles the presentation aspect of the dashboard. It&#8217;s an open source tool, and the company that developed it has a platform where you can host your open source projects. Because it&#8217;s open source, you can also self-host your own dashboards.</p><p>When using Streamlit, you don&#8217;t need to deal with HTML, CSS, or JavaScript at all. You just write <em>.py</em> files, and then run your main file with the <code>streamlit</code> command. Streamlit starts up a server, and shows your dashboard in a browser tab.</p><h3>Rolling Dice</h3><p>To explore Streamlit, let&#8217;s make an app that lets you roll some dice. Once we get it working, we&#8217;ll add a few features to see the kinds of things that Streamlit lets you do.</p><p>Let&#8217;s get started by installing Streamlit, and making a file to work in:</p><pre><code>$ <strong>python -m venv .venv</strong>
$ <strong>source .venv/bin/activate</strong>
(.venv)$ <strong>pip install streamlit</strong>
...
Successfully installed MarkupSafe-2.1.4...streamlit-1.30.0...
(.venv)$ <strong>touch roller.py</strong></code></pre><p>The main <code>streamlit</code> library includes a whole bunch of packages for working with data, including <code>pandas</code>, <code>numpy</code>, and a number of well-known plotting libraries.</p><p>Now let&#8217;s write some code in <em>roller.py</em> that simulates rolling a single 6-sided die:</p><pre><code>from random import randint
import streamlit as st

roll = randint(1, 6)

f"You rolled a {roll}."</code></pre><p>It&#8217;s conventional to import <code>streamlit</code> with the alias <code>st</code>. We use <code>randint()</code> to get a random integer between 1 and 6. Notice the last line is just a string; there&#8217;s no <code>print()</code> call, or anything else. When Streamlit finds a bare piece of data on a line, it presents that data on the dashboard if at all possible.</p><p>You run this program with the <code>streamlit run</code> command:</p><pre><code>(.venv)$ <strong>streamlit run roller.py</strong> 

      &#128075; Welcome to Streamlit!
      ...

  For better performance, install the Watchdog module:
  $ pip install watchdog</code></pre><p>The first time you run <code>streamlit</code>, it will show a welcome message and ask you for your email. There&#8217;s also some telemetry practices you should read about. Briefly, they want to send some usage data to their servers when the <code>streamlit</code> server runs. You can write a two-line configuration file that prevents this telemetry from running:</p><pre><code>[browser]
gatherUsageStats = false</code></pre><p>The welcome message tells you where to save this file. </p><p>They also recommend you install <code>watchdog</code>, a package that helps libraries watch for changes in your source code files. I&#8217;ve installed <code>watchdog</code> to this virtual environment.</p><p>In addition to showing the welcome message, <code>streamlit</code> opens a new browser tab:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!raQS!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcc403511-4c34-46d5-94a9-dd46bcd5435b_1240x470.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!raQS!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcc403511-4c34-46d5-94a9-dd46bcd5435b_1240x470.png 424w, https://substackcdn.com/image/fetch/$s_!raQS!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcc403511-4c34-46d5-94a9-dd46bcd5435b_1240x470.png 848w, https://substackcdn.com/image/fetch/$s_!raQS!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcc403511-4c34-46d5-94a9-dd46bcd5435b_1240x470.png 1272w, https://substackcdn.com/image/fetch/$s_!raQS!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcc403511-4c34-46d5-94a9-dd46bcd5435b_1240x470.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!raQS!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcc403511-4c34-46d5-94a9-dd46bcd5435b_1240x470.png" width="1240" height="470" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/cc403511-4c34-46d5-94a9-dd46bcd5435b_1240x470.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:470,&quot;width&quot;:1240,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:19329,&quot;alt&quot;:&quot;\&quot;You rolled a 6.\&quot; in a browser tab.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="&quot;You rolled a 6.&quot; in a browser tab." title="&quot;You rolled a 6.&quot; in a browser tab." srcset="https://substackcdn.com/image/fetch/$s_!raQS!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcc403511-4c34-46d5-94a9-dd46bcd5435b_1240x470.png 424w, https://substackcdn.com/image/fetch/$s_!raQS!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcc403511-4c34-46d5-94a9-dd46bcd5435b_1240x470.png 848w, https://substackcdn.com/image/fetch/$s_!raQS!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcc403511-4c34-46d5-94a9-dd46bcd5435b_1240x470.png 1272w, https://substackcdn.com/image/fetch/$s_!raQS!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcc403511-4c34-46d5-94a9-dd46bcd5435b_1240x470.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">The <code>streamlit run</code> command opens a new browser tab, with the dashboard displayed.</figcaption></figure></div><p>This is the start of our dashboard; it shows the message from the last line of <em>roller.py</em>. If you refresh the page, you&#8217;ll get a different number.</p><h4>The <code>st.write()</code> function</h4><p>Streamlit comes with a bunch of functions for writing data to the dashboard. The first to be aware of is <code>st.write()</code>. This function can write a wide range of data to the dashboard such as strings, markdown, LaTex expressions, DataFrames, Python objects, chart objects, and more.</p><p>This version of <em>roller.py</em> has the same output that we just saw:</p><pre><code>...
roll = randint(1, 6)

<strong>st.write(f"You rolled a {roll}.")</strong></code></pre><p>The first time you change your main <em>.py</em> file, you&#8217;ll see a number of options at the top of the dashboard:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!35Wn!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc7b5dfd1-2d55-4113-84d8-0b9948c9f08d_1240x470.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!35Wn!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc7b5dfd1-2d55-4113-84d8-0b9948c9f08d_1240x470.png 424w, https://substackcdn.com/image/fetch/$s_!35Wn!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc7b5dfd1-2d55-4113-84d8-0b9948c9f08d_1240x470.png 848w, https://substackcdn.com/image/fetch/$s_!35Wn!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc7b5dfd1-2d55-4113-84d8-0b9948c9f08d_1240x470.png 1272w, https://substackcdn.com/image/fetch/$s_!35Wn!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc7b5dfd1-2d55-4113-84d8-0b9948c9f08d_1240x470.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!35Wn!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc7b5dfd1-2d55-4113-84d8-0b9948c9f08d_1240x470.png" width="696" height="263.80645161290323" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/c7b5dfd1-2d55-4113-84d8-0b9948c9f08d_1240x470.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:470,&quot;width&quot;:1240,&quot;resizeWidth&quot;:696,&quot;bytes&quot;:27502,&quot;alt&quot;:&quot;options shown: Rerun, Always Rerun&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="options shown: Rerun, Always Rerun" title="options shown: Rerun, Always Rerun" srcset="https://substackcdn.com/image/fetch/$s_!35Wn!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc7b5dfd1-2d55-4113-84d8-0b9948c9f08d_1240x470.png 424w, https://substackcdn.com/image/fetch/$s_!35Wn!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc7b5dfd1-2d55-4113-84d8-0b9948c9f08d_1240x470.png 848w, https://substackcdn.com/image/fetch/$s_!35Wn!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc7b5dfd1-2d55-4113-84d8-0b9948c9f08d_1240x470.png 1272w, https://substackcdn.com/image/fetch/$s_!35Wn!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc7b5dfd1-2d55-4113-84d8-0b9948c9f08d_1240x470.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">After making changes to your dashboard, you&#8217;ll see some options about how to proceed.</figcaption></figure></div><p>Streamlit noticed that your source code changed, and it wants to know what to do. You can rerun the file manually, or you can have Streamlit <em>Always rerun</em> the file when it notices a change. I&#8217;ve found this option to be useful so far in my own work.</p><p>Streamlit&#8217;s documentation is really easy to navigate. For a good place to start, check out the documentation <a href="https://docs.streamlit.io/library/api-reference/write-magic/st.write">page</a> for <code>st.write()</code>.</p><h4>Input widgets: <code>st.button()</code></h4><p>It&#8217;s not very satisfying to have to reload the page in order to get a new roll. Let&#8217;s add a button that generates a new roll. Here&#8217;s how simple it is:</p><pre><code><strong>st.button("Roll")</strong>

roll = randint(1, 6)

st.write(f"You rolled a {roll}.")</code></pre><p>This one line adds a button to the dashboard, with the label &#8220;Roll&#8221;. Streamlit re-runs your entire file every time the user interacts with an input element. Without attaching any code to this button, clicking it causes a new number to be rolled:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!v4Bd!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fed451622-e57c-42e6-a298-cdcc4cf99f5a_1240x470.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!v4Bd!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fed451622-e57c-42e6-a298-cdcc4cf99f5a_1240x470.png 424w, https://substackcdn.com/image/fetch/$s_!v4Bd!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fed451622-e57c-42e6-a298-cdcc4cf99f5a_1240x470.png 848w, https://substackcdn.com/image/fetch/$s_!v4Bd!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fed451622-e57c-42e6-a298-cdcc4cf99f5a_1240x470.png 1272w, https://substackcdn.com/image/fetch/$s_!v4Bd!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fed451622-e57c-42e6-a298-cdcc4cf99f5a_1240x470.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!v4Bd!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fed451622-e57c-42e6-a298-cdcc4cf99f5a_1240x470.png" width="678" height="256.98387096774195" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ed451622-e57c-42e6-a298-cdcc4cf99f5a_1240x470.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:470,&quot;width&quot;:1240,&quot;resizeWidth&quot;:678,&quot;bytes&quot;:20918,&quot;alt&quot;:&quot;Roll button, and \&quot;You rolled a 4.\&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Roll button, and &quot;You rolled a 4.&quot;" title="Roll button, and &quot;You rolled a 4.&quot;" srcset="https://substackcdn.com/image/fetch/$s_!v4Bd!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fed451622-e57c-42e6-a298-cdcc4cf99f5a_1240x470.png 424w, https://substackcdn.com/image/fetch/$s_!v4Bd!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fed451622-e57c-42e6-a298-cdcc4cf99f5a_1240x470.png 848w, https://substackcdn.com/image/fetch/$s_!v4Bd!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fed451622-e57c-42e6-a298-cdcc4cf99f5a_1240x470.png 1272w, https://substackcdn.com/image/fetch/$s_!v4Bd!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fed451622-e57c-42e6-a298-cdcc4cf99f5a_1240x470.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">The entire script is re-run every time the user interacts with any input element. Clicking the <em>Roll</em> button generates a new random number.</figcaption></figure></div><p>There&#8217;s a lot more you can do with buttons; check out the <code>st.button()</code> documentation <a href="https://docs.streamlit.io/library/api-reference/widgets/st.button">page</a>.</p><h4>Input widgets: <code>st.radio()</code></h4><p>A dashboard with only one size die isn&#8217;t very useful, so let&#8217;s allow the user to choose what size die they&#8217;re rolling. We can do that with a radio select element:</p><pre><code><strong>side_options = [6, 10, 12, 20]
num_sides = st.radio("Number of sides:", side_options)</strong>

st.button("Roll")

<strong>roll = randint(1, num_sides)</strong>

st.write(f"You rolled a {roll}.")</code></pre><p>This is where it starts to get interesting; we&#8217;re allowing the user to make choices that affect the values that appear on the dashboard. The <code>side_options</code> list defines what size dice the user can choose from. The <code>st.radio()</code> function generates a radio input element, and when the user selects a value it&#8217;s assigned to <code>num_sides</code>.</p><p>The call to <code>randint()</code> now uses <code>num_sides</code> as its upper limit:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!yNY1!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbbbcbfe2-5841-4f6a-87ba-0efc016234cc_1240x587.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!yNY1!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbbbcbfe2-5841-4f6a-87ba-0efc016234cc_1240x587.png 424w, https://substackcdn.com/image/fetch/$s_!yNY1!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbbbcbfe2-5841-4f6a-87ba-0efc016234cc_1240x587.png 848w, https://substackcdn.com/image/fetch/$s_!yNY1!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbbbcbfe2-5841-4f6a-87ba-0efc016234cc_1240x587.png 1272w, https://substackcdn.com/image/fetch/$s_!yNY1!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbbbcbfe2-5841-4f6a-87ba-0efc016234cc_1240x587.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!yNY1!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbbbcbfe2-5841-4f6a-87ba-0efc016234cc_1240x587.png" width="694" height="328.5306451612903" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/bbbcbfe2-5841-4f6a-87ba-0efc016234cc_1240x587.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:587,&quot;width&quot;:1240,&quot;resizeWidth&quot;:694,&quot;bytes&quot;:31150,&quot;alt&quot;:&quot;Choices 6, 10, 12, 20 for \&quot;Number of sides:\&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Choices 6, 10, 12, 20 for &quot;Number of sides:&quot;" title="Choices 6, 10, 12, 20 for &quot;Number of sides:&quot;" srcset="https://substackcdn.com/image/fetch/$s_!yNY1!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbbbcbfe2-5841-4f6a-87ba-0efc016234cc_1240x587.png 424w, https://substackcdn.com/image/fetch/$s_!yNY1!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbbbcbfe2-5841-4f6a-87ba-0efc016234cc_1240x587.png 848w, https://substackcdn.com/image/fetch/$s_!yNY1!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbbbcbfe2-5841-4f6a-87ba-0efc016234cc_1240x587.png 1272w, https://substackcdn.com/image/fetch/$s_!yNY1!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbbbcbfe2-5841-4f6a-87ba-0efc016234cc_1240x587.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">The dashboard is getting more interactive; now you can choose what size die you&#8217;re rolling.</figcaption></figure></div><p>I wouldn&#8217;t say this looks like a dashboard yet, but it&#8217;s starting to look more interactive.</p><h4>Input widgets: <code>st.slider()</code></h4><p>In most situations where you want to roll dice, you don&#8217;t just roll a single die. Let&#8217;s allow the user to choose how many dice they want to roll:</p><pre><code># Input widgets
side_options = [6, 10, 12, 20]
num_sides = st.radio("Number of sides:", side_options)
<strong>num_dice = st.slider("Number of dice:", 1, 10, value=2)</strong>

st.button("Roll")

# Roll calculation
<strong>rolls = [randint(1, num_sides) for _ in range(num_dice)]
roll = sum(rolls)</strong>

# Output message
<strong>st.write("---")</strong>
<strong>st.subheader(roll)
st.write(str(rolls))</strong></code></pre><p>There&#8217;s a little more going on here, because we&#8217;ve got more dice to manage. But the streamlit-specific elements are fairly straightforward. We use <code>st.slider()</code> to let the user choose the number of dice to roll, from one to ten.</p><p>Rather than one call to <code>randint()</code>, we move the <code>randint()</code> call into a comprehension that generates the correct number of rolls. The <code>roll</code> variable now reflects the sum of these individual values.</p><p>For the output, we use the fact that Streamlit presents string values as markdown by default to place a divider between the input section of the dashboard, and the summary. We use the <code>st.subheader()</code> function to make the most important piece of information stand out, and we print a string representation of the full list of rolls:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!8MRm!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb431ca1f-7bc6-42b7-9648-c271c571b17c_1240x909.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!8MRm!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb431ca1f-7bc6-42b7-9648-c271c571b17c_1240x909.png 424w, https://substackcdn.com/image/fetch/$s_!8MRm!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb431ca1f-7bc6-42b7-9648-c271c571b17c_1240x909.png 848w, https://substackcdn.com/image/fetch/$s_!8MRm!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb431ca1f-7bc6-42b7-9648-c271c571b17c_1240x909.png 1272w, https://substackcdn.com/image/fetch/$s_!8MRm!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb431ca1f-7bc6-42b7-9648-c271c571b17c_1240x909.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!8MRm!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb431ca1f-7bc6-42b7-9648-c271c571b17c_1240x909.png" width="1240" height="909" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b431ca1f-7bc6-42b7-9648-c271c571b17c_1240x909.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:909,&quot;width&quot;:1240,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:40452,&quot;alt&quot;:&quot;slider labeled \&quot;Number of dice\&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="slider labeled &quot;Number of dice&quot;" title="slider labeled &quot;Number of dice&quot;" srcset="https://substackcdn.com/image/fetch/$s_!8MRm!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb431ca1f-7bc6-42b7-9648-c271c571b17c_1240x909.png 424w, https://substackcdn.com/image/fetch/$s_!8MRm!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb431ca1f-7bc6-42b7-9648-c271c571b17c_1240x909.png 848w, https://substackcdn.com/image/fetch/$s_!8MRm!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb431ca1f-7bc6-42b7-9648-c271c571b17c_1240x909.png 1272w, https://substackcdn.com/image/fetch/$s_!8MRm!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb431ca1f-7bc6-42b7-9648-c271c571b17c_1240x909.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Now you can roll up to ten dice at once.</figcaption></figure></div><p>If you call <code>st.write(rolls)</code> without converting <code>rolls</code> to a string, the same information will be presented but it will take up more vertical space.</p><h3>Including a chart</h3><p>What&#8217;s a dashboard without a chart? Let&#8217;s add a chart that shows the distribution of a large number of the kind of rolls the user is making. We&#8217;ll let them choose how many rolls to include in the simulation:</p><pre><code>from random import randint

<strong>import altair as alt
import pandas as pd</strong>
import streamlit as st


# Input widgets
side_options = [6, 10, 12, 20]
num_sides = st.radio("Number of sides:", side_options)
num_dice = st.slider("Number of dice:", 1, 10, value=2)
<strong>num_rolls_sim = st.slider("Number of rolls in simulation",
        1_000, 100_000, value=1_000, step=1_000)</strong>

st.button("Roll")

# Roll calculation
rolls = [randint(1, num_sides) for _ in range(num_dice)]
roll = sum(rolls)

<strong># Simulation rolls
sim_rolls = []
for _ in range(num_rolls_sim):
    sim_roll = sum(
        [randint(1, num_sides) for _ in range(num_dice)])
    sim_rolls.append(sim_roll)
df_sim = pd.DataFrame({"rolls": sim_rolls})

# Create histogram
chart = alt.Chart(df_sim).mark_bar().encode(
    alt.X("rolls", bin=True),
    y="count()",
)
chart.title = f"Simulation of {num_rolls_sim} rolls"</strong>

# Output
st.write("---")
st.subheader(roll)
st.write(str(rolls))

<strong>st.write("---")
st.altair_chart(chart)</strong></code></pre><p>You don&#8217;t need to follow this listing closely to get the main point. We import <code>pandas</code> to work with a DataFrame, and we import <a href="https://altair-viz.github.io/index.html">altair</a> to make a chart. We add a second slider that lets the user choose a number from 1,000 to 100,000, in increments of 1,000.</p><p>We make a list called <code>sim_rolls</code>, and generate as many rolls as the user requested. We then create a DataFrame from those rolls. This DataFrame, <code>df_sim</code>, is passed to <code>alt.Chart()</code> to create a simple histogram.</p><p>The final two lines create another section, and write the chart to the page:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!rMym!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2705fd5b-91e2-497f-90cc-1ffbf15c1139_1234x1444.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!rMym!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2705fd5b-91e2-497f-90cc-1ffbf15c1139_1234x1444.png 424w, https://substackcdn.com/image/fetch/$s_!rMym!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2705fd5b-91e2-497f-90cc-1ffbf15c1139_1234x1444.png 848w, https://substackcdn.com/image/fetch/$s_!rMym!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2705fd5b-91e2-497f-90cc-1ffbf15c1139_1234x1444.png 1272w, https://substackcdn.com/image/fetch/$s_!rMym!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2705fd5b-91e2-497f-90cc-1ffbf15c1139_1234x1444.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!rMym!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2705fd5b-91e2-497f-90cc-1ffbf15c1139_1234x1444.png" width="1234" height="1444" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/2705fd5b-91e2-497f-90cc-1ffbf15c1139_1234x1444.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1444,&quot;width&quot;:1234,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:74627,&quot;alt&quot;:&quot;histogram of dice rolls at bottom of dashboard&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="histogram of dice rolls at bottom of dashboard" title="histogram of dice rolls at bottom of dashboard" srcset="https://substackcdn.com/image/fetch/$s_!rMym!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2705fd5b-91e2-497f-90cc-1ffbf15c1139_1234x1444.png 424w, https://substackcdn.com/image/fetch/$s_!rMym!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2705fd5b-91e2-497f-90cc-1ffbf15c1139_1234x1444.png 848w, https://substackcdn.com/image/fetch/$s_!rMym!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2705fd5b-91e2-497f-90cc-1ffbf15c1139_1234x1444.png 1272w, https://substackcdn.com/image/fetch/$s_!rMym!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2705fd5b-91e2-497f-90cc-1ffbf15c1139_1234x1444.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">You can add a chart to your dashboard as easily as you can add any other element.</figcaption></figure></div><p>Okay, that&#8217;s an interesting number of elements to have on the dashboard. Let&#8217;s take a moment to reorganize them.</p><h3>Structuring the dashboard</h3><p>Streamlit offers a number of ways to organize your dashboard, without having to resort to writing HTML. For one thing, we can easily stick most of the input widgets in a sidebar:</p><pre><code>...
import streamlit as st

# Sidebar

# Input widgets.
side_options = [6, 10, 12, 20]
num_sides = <strong>st.sidebar.radio</strong>("Number of sides:", side_options)
num_dice = <strong>st.sidebar.slider</strong>("Number of dice:", 1, 10, value=2)
num_rolls_sim = <strong>st.sidebar.slider</strong>("Number of rolls in simulation",
        1_000, 100_000, value=1_000, step=1_000)

# Roll calculation.
...

# Create histogram.
...

# Main page
<strong>st.title("Rolling Dice")</strong>
<strong>st.button("Roll!")</strong>

st.write("---")
...</code></pre><p>By simply converting <code>st.radio()</code> to <code>st.sidebar.radio()</code>, widgets can be placed in a sidebar. This is an easy way to group some elements of your dashboard off to the side, and put more focus on the elements in the main part of the page.</p><p>I also added a title to the main part of the page, and moved the <em>Roll</em> button just below the title. These minor <a href="https://github.com/ehmatthes/mostly_python/blob/main/mp78_streamlit_basics/roller_sidebar.py">changes</a> make a much more presentable dashboard:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!eTYP!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F43a3b51c-80d0-43d5-9603-c74c55f5188e_1094x1113.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!eTYP!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F43a3b51c-80d0-43d5-9603-c74c55f5188e_1094x1113.png 424w, https://substackcdn.com/image/fetch/$s_!eTYP!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F43a3b51c-80d0-43d5-9603-c74c55f5188e_1094x1113.png 848w, https://substackcdn.com/image/fetch/$s_!eTYP!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F43a3b51c-80d0-43d5-9603-c74c55f5188e_1094x1113.png 1272w, https://substackcdn.com/image/fetch/$s_!eTYP!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F43a3b51c-80d0-43d5-9603-c74c55f5188e_1094x1113.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!eTYP!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F43a3b51c-80d0-43d5-9603-c74c55f5188e_1094x1113.png" width="1094" height="1113" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/43a3b51c-80d0-43d5-9603-c74c55f5188e_1094x1113.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1113,&quot;width&quot;:1094,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:74975,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!eTYP!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F43a3b51c-80d0-43d5-9603-c74c55f5188e_1094x1113.png 424w, https://substackcdn.com/image/fetch/$s_!eTYP!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F43a3b51c-80d0-43d5-9603-c74c55f5188e_1094x1113.png 848w, https://substackcdn.com/image/fetch/$s_!eTYP!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F43a3b51c-80d0-43d5-9603-c74c55f5188e_1094x1113.png 1272w, https://substackcdn.com/image/fetch/$s_!eTYP!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F43a3b51c-80d0-43d5-9603-c74c55f5188e_1094x1113.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Moving all the input widgets except the <em>Roll</em> button to the sidebar makes this page appear much more organized and easier to use.</figcaption></figure></div><p>That&#8217;s a pretty nice dashboard for about 40 lines of code!</p><h3>Conclusions</h3><p>Streamlit is even easier to get started with than I expected. By far, most of the complexity I&#8217;ve found centers around what I&#8217;m trying to do with the data; little complexity or difficulty has come from Streamlit itself. If you&#8217;re interested in building your own dashboard, take a look at the documentation of the elements used here, and see for yourself how clearly it&#8217;s laid out:</p><ul><li><p>Documentation <a href="https://docs.streamlit.io">home page</a>.</p></li><li><p><a href="https://docs.streamlit.io/library/api-reference/write-magic/st.write">st.write()</a></p></li><li><p><a href="https://docs.streamlit.io/library/api-reference/widgets/st.button">st.button()</a></p></li><li><p><a href="https://docs.streamlit.io/library/api-reference/widgets/st.radio">st.radio()</a></p></li><li><p><a href="https://docs.streamlit.io/library/api-reference/widgets/st.slider">st.slider()</a></p></li><li><p><a href="https://docs.streamlit.io/library/api-reference/charts/st.altair_chart">st.altair_chart()</a></p></li><li><p><a href="https://docs.streamlit.io/library/api-reference/text/st.title">st.title()</a></p></li><li><p><a href="https://docs.streamlit.io/library/api-reference/layout/st.sidebar">st.sidebar()</a></p></li></ul><p>There&#8217;s more to cover, so I&#8217;m going to write at least two followup posts. The <a href="https://www.mostlypython.com/p/deploying-simple-streamlit-apps">next one</a> will show how to deploy the dashboard we just made using Streamlit&#8217;s public hosting platform. After that, I&#8217;ll show an example of a real-world dashboard built with Streamlit.</p><h3>Resources</h3><p>You can find the code files from this post in the&nbsp;<a href="https://github.com/ehmatthes/mostly_python/tree/main/mp78_streamlit_basics">mostly_python</a>&nbsp;GitHub repository.</p>]]></content:encoded></item><item><title><![CDATA[Testing a book's code, part 2: Basic scripts]]></title><description><![CDATA[MP #77: Using parametrization to efficiently write a large batch of tests.]]></description><link>https://mostlypython.substack.com/p/testing-a-books-code-part-2-basic</link><guid isPermaLink="false">https://mostlypython.substack.com/p/testing-a-books-code-part-2-basic</guid><dc:creator><![CDATA[Eric Matthes]]></dc:creator><pubDate>Thu, 18 Jan 2024 17:30:10 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F619b8e3d-2484-4391-a328-34db1634db8c_1121x710.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><strong>Note:</strong> <em>This is the second post in a seven-part <a href="https://www.mostlypython.com/p/testing-a-books-code-696">series</a> about testing.</em></p><div><hr></div><p>In the <a href="https://www.mostlypython.com/p/testing-a-books-code">previous post</a>, I laid out an overall plan for testing all the code in the book. In this post, we&#8217;ll put that plan into action. We&#8217;ll make a fork of the book&#8217;s repository, and build a test suite in the new repository. We&#8217;ll do this in a way that allows us to pull in changes from the original repository, without any conflicts with the testing code.</p><p>We&#8217;ll also use parametrization to write a set of tests that cover all the basic scripts in the first half of the book. These tests will run each script that should be tested, and make sure the output is what we expect it to be.</p><p>You can follow along and run the code yourself, if you&#8217;re interested in doing so.</p><h3>Fork the original repository</h3><p>Making a private fork of a public repository is not as straightforward as it sounds. GitHub doesn&#8217;t automate the process. I believe that&#8217;s partially for technical reasons, and partially to encourage public forks. The best resource I found was <a href="https://gist.github.com/0xjac/85097472043b697ab57ba1b1c7530274">this gist</a>, and this <a href="https://github.blog/2022-04-25-the-friend-zone-friendly-forks-101/">official post</a> about forking.</p><p>I&#8217;m going to set up a public fork for this post, called <em>mp_testing_pcc_3e</em>. You can&#8217;t make a direct fork of your own repository; I ended up cloning the original <em>pcc_3e</em> repo, and pushing it to a <a href="https://github.com/ehmatthes/mp_testing_pcc_3e">new repo</a>. If you want to follow along, you can simply make a fork of the original <a href="https://github.com/ehmatthes/pcc_3e">pcc_3e</a> repository.</p><h3>Testing basic programs</h3><p>We&#8217;ll start by testing the basic programs in the book. These are programs that don&#8217;t use any third party libraries. They don&#8217;t write or modify any files, and they don&#8217;t accept user input. All we need to do is run them, and validate the output.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!7tGA!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff3903b5a-325a-4bec-952e-8ce46c29340b_935x591.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!7tGA!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff3903b5a-325a-4bec-952e-8ce46c29340b_935x591.png 424w, https://substackcdn.com/image/fetch/$s_!7tGA!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff3903b5a-325a-4bec-952e-8ce46c29340b_935x591.png 848w, https://substackcdn.com/image/fetch/$s_!7tGA!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff3903b5a-325a-4bec-952e-8ce46c29340b_935x591.png 1272w, https://substackcdn.com/image/fetch/$s_!7tGA!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff3903b5a-325a-4bec-952e-8ce46c29340b_935x591.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!7tGA!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff3903b5a-325a-4bec-952e-8ce46c29340b_935x591.png" width="638" height="403.2705882352941" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f3903b5a-325a-4bec-952e-8ce46c29340b_935x591.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:591,&quot;width&quot;:935,&quot;resizeWidth&quot;:638,&quot;bytes&quot;:96953,&quot;alt&quot;:&quot;list of program files from Chapter 4 of Python Crash Course&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="list of program files from Chapter 4 of Python Crash Course" title="list of program files from Chapter 4 of Python Crash Course" srcset="https://substackcdn.com/image/fetch/$s_!7tGA!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff3903b5a-325a-4bec-952e-8ce46c29340b_935x591.png 424w, https://substackcdn.com/image/fetch/$s_!7tGA!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff3903b5a-325a-4bec-952e-8ce46c29340b_935x591.png 848w, https://substackcdn.com/image/fetch/$s_!7tGA!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff3903b5a-325a-4bec-952e-8ce46c29340b_935x591.png 1272w, https://substackcdn.com/image/fetch/$s_!7tGA!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff3903b5a-325a-4bec-952e-8ce46c29340b_935x591.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">There are a lot of simple programs to test; this is just the <a href="https://github.com/ehmatthes/pcc_3e/tree/main/chapter_04">listing</a> from Chapter 4. Rather than writing a test for every program listed, we&#8217;ll write one test and then feed in each of the programs that need to be tested, along with the output we expect to see.</figcaption></figure></div><p>We&#8217;ll start by testing the first program in the book, <em>hello_world.py</em>. Just as a typical <em>Hello World</em> program demonstrates that your environment is set up correctly, this first test will show that our testing infrastructure is set up correctly and that a simple test can pass.<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-1" href="#footnote-1" target="_self">1</a></p><p>Make a virtual environment, activate it, and install <a href="https://docs.pytest.org/en/7.4.x/">pytest</a>:</p><pre><code>$ <strong>python -m venv .venv</strong>
$ <strong>source .venv/bin/activate</strong>
(.venv)$ <strong>pip install --upgrade pip</strong>
...
Successfully installed pip-23.3.2
(.venv)$ <strong>pip install pytest</strong>
...
Successfully installed ... pytest-7.4.4</code></pre><p>Make a <em>tests/</em> directory, and in that directory make a new file called <em>test_basic_programs.py</em>. Here&#8217;s the first test:</p><pre><code>import subprocess, sys
from pathlib import Path
from shlex import split

def test_basic_program():
    """Test a program that only prints output."""
    root_dir = Path(__file__).parents[1]
    path = root_dir / "chapter_01" / "hello_world.py"

    # Use the venv python.
    python_cmd = f"{sys.prefix}/bin/python"
    cmd = f"{python_cmd} {path}"

    # Run the command, and make assertions.
    cmd_parts = split(cmd)
    result = subprocess.run(cmd_parts,
        capture_output=True, text=True, check=True)
    output = result.stdout.strip()

    assert output == "Hello Python world!"</code></pre><p>This isn&#8217;t the friendliest first test to see if you&#8217;re new to testing, but it does address the issue of many testing tutorials being overly simplistic. You don&#8217;t have to understand everything you see in this test function. Feel free to skim the following explanation of the test function, and then focus on making sense of everything that follows.</p><p>The test function has a generic name, because I&#8217;m planning to use this one function to run tests for all the basic programs. To build a path to <em>hello_world.py</em> we first look at the location of the current file, and then get the second element of the <code>parents</code> attribute. This should be the root directly of the overall repository. We can then define the path to <em>hello_world.py</em>.</p><p>To run <em>hello_world.py</em> for testing purposes, we want to use the Python interpreter from the active virtual environment. This will allow us to define a number of virtual environments, each with a different version of Python, and use any one of those to run tests. The command shown here doesn&#8217;t work on Windows, but we&#8217;ll fix that shortly.</p><p>The command we want to test is this one:</p><pre><code>$ <strong>python hello_world.py</strong></code></pre><p>That command is built from the string <code>f&#8221;{python_cmd} {path}&#8221;</code>. We need to split this command into its parts before calling it with <code>subprocess.run()</code>. Finally, we assert that the output is what we expect it to be.</p><p>This test passes:</p><pre><code>(.venv)$ <strong>pytest tests/test_basic_programs.py -q</strong>
. [100%]
1 passed in 0.01s</code></pre><p>It&#8217;s really good to make sure your first test passes before writing more tests. This shows that the first test passes, but also that the testing infrastructure is set up correctly. Note that I&#8217;m using the <code>-q</code> flag here to save space; if you&#8217;re following along it&#8217;s more informative to omit that flag.</p><p>Also note that calling pytest without any arguments will fail at this point. If you try it, pytest will discover the example tests included in the book, and attempt to run them. We&#8217;ll address that issue in a moment.</p><h3>Parametrizing tests</h3><p>I know I want to run this same kind of test a bunch of times, for a bunch of different programs. If this first test runs using parametrization, we can add as many tests as we want, without modifying the actual test function.</p><p>Here&#8217;s the <a href="https://github.com/ehmatthes/mp_testing_pcc_3e/blob/9ef02c1a63e5ed99008aee6b083528a65b4ccebf/tests/test_basic_programs.py">first test</a>, parametrized:</p><pre><code>import subprocess, sys
...

<strong>import pytest</strong>

<strong>basic_programs = [
    ("chapter_01/hello_world.py", "Hello Python world!"),
]

@pytest.mark.parametrize(
    "file_path, expected_output", basic_programs)
def test_basic_program(file_path, expected_output):</strong>
    """Test a program that only prints output."""
    root_dir = Path(__file__).parents[1]
    <strong>path = root_dir / file_path</strong>

    # Use the venv python.
    ...

    # Run the command, and make assertions.
    ...

    <strong>assert output == expected_output</strong></code></pre><p>To parametrize tests we make a list that stores the data that should be passed to the test function. In this case, I&#8217;m calling that list <code>basic_programs</code>. Each entry is a tuple containing the path to the file that needs to be tested, and the output that&#8217;s expected when that file is run.</p><p>The <code>@pytest.mark.parametrize()</code> decorator feeds data into a test function. You provide a name (or names) for the data, and a source of the data. Here we&#8217;re passing in the list <code>basic_programs</code>. pytest will feed one item at a time from that list into the test function. Right now <code>basic_programs</code> consists of one tuple, containing a file path and an output string. The items in this tuple are unpacked to the two names we provided: <code>file_path</code>, and <code>expected_output</code>. In this example, <code>file_path</code> will be the path to <em>hello_world.py</em>, and <code>expected_output</code> will be the string <em>Hello Python world!</em></p><p>Inside the function, we only need to do two things: replace the hardcoded path to <em>hello_world.py</em> with the variable <code>file_path</code>, and replace the hardcoded output <em>&#8220;Hello Python world!&#8221;</em> with the variable <code>expected_output</code>. This test passes, with the same output as shown previously.</p><p>This can seem like a lot of complexity if you haven&#8217;t used parametrization before. But you&#8217;re about to see how much simpler it is to add more tests with this approach.</p><h3>Can a test fail?</h3><p>Before writing more tests, it&#8217;s a good idea to make sure your first test fails when you expect it to. I&#8217;m going to change the value of <code>expected_output</code>, and make sure the test fails:</p><pre><code>basic_programs = [
    ("chapter_01/hello_world.py", "<strong>Goodbye</strong> Python world!"),
]</code></pre><p>I changed <em>Hello</em> to <em>Goodbye</em>, ran the test, and it failed. That&#8217;s a good thing! Sometimes a test isn&#8217;t really running the way you think it is, and a change like this will still pass. If that happens, it&#8217;s much easier to troubleshoot now than later. </p><p>Make sure you undo the change that caused the failure, and run your test one more time to make sure it passes again. It&#8217;s so easy to introduce a bug that&#8217;s difficult to sort out if you don&#8217;t run these quick checks while building a test suite.<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-2" href="#footnote-2" target="_self">2</a></p><h3>Testing more basic programs</h3><p>Now we get to see the benefit of parametrization. We have a test that should work for any basic Python program that simply prints output, as long as we give it the path to the program and the expected output. Let&#8217;s start by <a href="https://github.com/ehmatthes/mp_testing_pcc_3e/blob/a113ceec230d218c2ee400234b7b04f0e4c82816/tests/test_basic_programs.py">adding</a> two more tests, and make sure they run (and pass):</p><pre><code>basic_programs = [
    # Chapter 1
    ('chapter_01/hello_world.py', 'Hello Python world!'),

    # Chapter 2
    <strong>('chapter_02/apostrophe.py', "One of...community."),
    ('chapter_02/comment.py', "Hello Python people!"),</strong>
]</code></pre><p>These three tests pass:</p><pre><code>$ <strong>pytest tests/test_basic_programs.py -q</strong>
... [100%]
3 passed in 0.03s</code></pre><p>That&#8217;s very satisfying to see; we&#8217;ve tripled the number of tests, without adding any new test functions!</p><h3>Testing all basic programs</h3><p>At this point, creating more tests is as simple as adding new entries to the list <code>basic_programs</code>. I opened a terminal, <code>cd</code>&#8217;d into each chapter directory, and ran the programs I wanted to test.</p><p>I use GPT almost every day now, and this was a perfect example of why. Many of the basic programs in the book generate multiple lines of code. For programs like this, I wanted the multiline output compressed to single lines. For example this output:</p><pre><code>Hello Python world!
Hello Python Crash Course world!</code></pre><p>should be converted to:</p><pre><code>Hello Python world!\nHello Python Crash Course world!</code></pre><p>For two lines this isn&#8217;t a big deal, but for many programs with 5-10 lines that gets tedious. That&#8217;s the kind of thing AI assistants are great at, so I gave GPT a prompt telling it I&#8217;d be feeding in multiple lines, and asked it to convert those lines to the equivalent single-line Python string. I could write a program to do this, but it was much easier to just feed the output into GPT. It didn&#8217;t take too long to have <a href="https://github.com/ehmatthes/mp_testing_pcc_3e/blob/eb46de9fbef8de9ddf623eafc3274bf0c6ac5a95/tests/test_basic_programs.py">tests for all</a> 47 basic programs in the first half of the book:</p><pre><code>(.venv)$ <strong>pytest tests/test_basic_programs.py -q</strong>
............................................... [100%]
47 passed in 0.49s</code></pre><p>We&#8217;ve only written one test function, but we&#8217;ve got 47 tests now! This is a pretty good start. Before writing more test functions, let&#8217;s pull anything out of the existing test function that&#8217;s going to be used in other tests.</p><h3>Getting <code>python_cmd</code> from a fixture</h3><p>Almost every test is going to use the value for <code>python_cmd</code> that&#8217;s set in <code>test_basic_program()</code>. Let&#8217;s pull that out into a fixture, so it can be used by any test that needs it.<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-3" href="#footnote-3" target="_self">3</a></p><p>You can place pytest configuration files in any directory in your test suite. Like a lot of things in the testing world this can be confusing at first, but it lets you structure an increasingly complex test suite in an organized way.</p><p>Make a file called <em>conftest.py</em>, in the <em>tests/</em> directory:</p><pre><code>import sys

import pytest

<strong>@pytest.fixture(scope="session")</strong>
def python_cmd():
    """Return path to the venv Python interpreter."""
    <strong>return f"{sys.prefix}/bin/python"</strong></code></pre><p>Fixtures are designed to make it easier to set up and share resources that are needed by more than one test function. Fixtures can be created once per session, module, class, or function.</p><p>Here we define a function called <code>python_cmd()</code>, that&#8217;s run once for the entire test session. The return value is the path to the Python interpreter for the current active virtual environment. Any test function can use this value by including the name <code>python_cmd</code> in its list of arguments.</p><p>Here&#8217;s what <code>test_basic_program()</code> should look like <a href="https://github.com/ehmatthes/mp_testing_pcc_3e/blob/main/tests/test_basic_programs.py">now</a>:</p><pre><code>@pytest.mark.parametrize(...)
def test_basic_program(<strong>python_cmd</strong>, file_path, expected_output):
    """Test a program that only prints output."""
    root_dir = Path(__file__).parents[1]
    path = root_dir / file_path

    # Run the command, and make assertions.
    <strong>cmd = f"{python_cmd} {path}"</strong>
    cmd_parts = split(cmd)
    ...</code></pre><p>The main change here is the addition of the <code>python_cmd</code> argument in the definition of <code>test_basic_program()</code>. When pytest finds an argument name that matches the name of a fixture function, it runs that function and passes the return value to the argument.</p><p>In this case pytest sees the <code>python_cmd</code> argument, and recognizes that there&#8217;s a fixture function in <em>conftest.py</em> with that same name. It runs that function and assigns the return value, which is the path to the virtual environment&#8217;s Python interpreter, to the parameter <code>python_cmd</code>. Note that since the fixture function has a <code>session</code> scope, pytest only runs the function once. It will pass the return value to any function that has <code>python_cmd</code> as one of its arguments, without running the fixture function again.</p><p>In the body of the function, we got rid of the code that defined <code>python_cmd</code>, and reorganized the other lines slightly. All the tests still pass.</p><h3>Pulling out some utils</h3><p>Let&#8217;s also start a <em>utils</em> file, to gather code that doesn&#8217;t need to be a fixture, but will be used by multiple tests.</p><p>Make a file in the <em>tests/</em> directory called <em>utils.py</em>. The first thing we&#8217;ll put there is a function to run a command:</p><pre><code>from shlex import split
import subprocess

def run_command(cmd):
    """Run a command, and return the output."""
    cmd_parts = split(cmd)
    result = subprocess.run(cmd_parts,
        capture_output=True, text=True, check=True)
    
    return result.stdout.strip()</code></pre><p>This function takes in a command, splits it into parts, and runs it by calling <code>subprocess.run()</code>. It returns a cleaned-up version of whatever was printed to stdout, which is where the output of <code>print()</code> calls is sent. This will make it easier to run a variety of programs , without having <code>subprocess.run()</code> calls in every test function.</p><p>Here are the <a href="https://github.com/ehmatthes/mp_testing_pcc_3e/blob/00df1d322690c9963ee8288d35c3acfeceb1bd28/tests/test_basic_programs.py">changes</a> to <em>test_basic_programs.py</em>:</p><pre><code>...
import pytest

<strong>import utils</strong>

basic_programs = [
    ...
]

@pytest.mark.parametrize(...)
def test_basic_program(...):
    """Test a program that only prints output."""
    root_dir = Path(__file__).parents[1]
    path = root_dir / file_path

    # Run the command, and make assertions.
    cmd = f"{python_cmd} {path}"
    <strong>output = utils.run_command(cmd)</strong>

    assert output == expected_output</code></pre><p>Here we&#8217;re importing the new <code>utils</code> module, and calling <code>utils.run_command()</code>. We&#8217;ve also removed the code that was replaced by the new utility function.</p><p>The tests all still pass, and we have a test function that&#8217;s much more readable than what we started with. More importantly, it will be easier to write more test functions that build on this one.</p><h3>Ignoring example tests</h3><p>I mentioned earlier that you can have multiple pytest configuration files, that control the behavior of different aspects of the test suite. We can simplify the command for running the test suite by ignoring the tests in the repository that are examples from the book.</p><p>In the root directory of the repository, make a new file called <em>conftest.py</em>. (Make sure you don&#8217;t overwrite the <em>conftest.py</em> file that&#8217;s already in the <em>tests/</em> directory.)</p><p>In this new file, add the following:</p><pre><code>"""Root pytest configuration file."""

# Ignore example tests from the book.
collect_ignore = [
    "chapter_07",
    "chapter_11",
    "solution_files",
]</code></pre><p>When you have multiple <em>conftest.py</em> files, it&#8217;s helpful to add a module-level docstring that makes it clear which one you&#8217;re working with.</p><p>When you run pytest it scans your entire repository, looking for modules and functions associated with tests. This is called <em>collecting</em> tests. If pytest finds a list called <code>collect_ignore</code> in any <em>conftest.py</em> file, it will avoid collecting tests from the specified directories.</p><p>With this configuration file in place, we no longer need to tell pytest where the tests we want to run are. We can just use the bare <code>pytest</code> command:</p><pre><code>(.venv)$ <strong>pytest -q</strong>
............................................... [100%]
47 passed in 0.58s</code></pre><p>This is much nicer for running tests, and also less prone to typos.</p><h3>Running on Windows</h3><p>It&#8217;s a good time to make these tests work on Windows, because the only thing that should be different is the code that builds <code>python_cmd</code>. We should be able to add some conditional code to the <code>python_cmd()</code> fixture that returns the correct path on all systems. This is another benefit of having fixtures and utility functions; as the test suite grows in complexity, that complexity doesn&#8217;t overwhelm the overall structure of the test suite.</p><p>I&#8217;m approaching this by starting a Windows VM, cloning the test repository, and running the tests as they&#8217;re currently written. I&#8217;m expecting them to fail, and that&#8217;s exactly what happens:</p><pre><code>(.venv)&gt; <strong>pytest -q</strong>
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
47 failed in 4.64s</code></pre><p>When you have a bunch of failures like this, it&#8217;s helpful to use the <code>-x</code> flag when calling pytest. This makes pytest stop after the first failed test, so you can focus on fixing one failure at a time. When most or all of the tests fail, fixing one failure often clears up most or all of the other failures.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!f4zc!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F619b8e3d-2484-4391-a328-34db1634db8c_1121x710.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!f4zc!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F619b8e3d-2484-4391-a328-34db1634db8c_1121x710.png 424w, https://substackcdn.com/image/fetch/$s_!f4zc!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F619b8e3d-2484-4391-a328-34db1634db8c_1121x710.png 848w, https://substackcdn.com/image/fetch/$s_!f4zc!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F619b8e3d-2484-4391-a328-34db1634db8c_1121x710.png 1272w, https://substackcdn.com/image/fetch/$s_!f4zc!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F619b8e3d-2484-4391-a328-34db1634db8c_1121x710.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!f4zc!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F619b8e3d-2484-4391-a328-34db1634db8c_1121x710.png" width="654" height="414.2194469223907" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/619b8e3d-2484-4391-a328-34db1634db8c_1121x710.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:710,&quot;width&quot;:1121,&quot;resizeWidth&quot;:654,&quot;bytes&quot;:97039,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!f4zc!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F619b8e3d-2484-4391-a328-34db1634db8c_1121x710.png 424w, https://substackcdn.com/image/fetch/$s_!f4zc!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F619b8e3d-2484-4391-a328-34db1634db8c_1121x710.png 848w, https://substackcdn.com/image/fetch/$s_!f4zc!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F619b8e3d-2484-4391-a328-34db1634db8c_1121x710.png 1272w, https://substackcdn.com/image/fetch/$s_!f4zc!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F619b8e3d-2484-4391-a328-34db1634db8c_1121x710.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">The output from running <code>pytest -qx</code> on Windows. The tests failed, as expected. If you&#8217;re not sure how to approach this, pasting this entire error message into GPT probably won&#8217;t fix everything, but it should point you in the right direction.</figcaption></figure></div><p>Looking through the failure output closely, which I won&#8217;t reproduce here, shows that the issue is coming from the <code>run_command()</code> function. But the root issue is actually in the <code>python_cmd()</code> fixture function.</p><p>Virtual environments are structured slightly differently on Windows than on macOS and Linux. Instead of <em>bin/python</em>, the path to the Python interpreter on Windows is <em>Scripts/python.exe</em>.</p><p>Here&#8217;s the update to <code>python_cmd()</code>, in <em>tests/conftest.py</em>:</p><pre><code>import sys
<strong>from pathlib import Path</strong>

import pytest

@pytest.fixture(scope="session")
def python_cmd():
    """Return the path to the venv Python interpreter."""
    <strong>if sys.platform == "win32":
        python_cmd = Path(sys.prefix) / "Scripts/python.exe"
    else:
        python_cmd = Path(sys.prefix) / "bin/python"

    return python_cmd.as_posix()</strong></code></pre><p>Strings don&#8217;t work well as paths across different OSes, which is why <a href="https://docs.python.org/3/library/pathlib.html">pathlib</a> was developed. The <code>as_posix()</code> method returns a string that can be interpreted correctly on all systems in the way we&#8217;re using it, particularly in <code>subprocess.run()</code> calls. Here we define <code>python_cmd</code> as a path to the interpreter on each OS, and then return the <code>as_posix()</code> version of the appropriate path.</p><p>There&#8217;s a similar change that needs to be made to <code>test_basic_program()</code> as well:</p><pre><code>@pytest.mark.parametrize(...)
def test_basic_program(...):
    ...

    # Run the command, and make assertions.
    <strong>cmd = f"{python_cmd} {path.as_posix()}"</strong>
    output = utils.run_command(cmd)

    assert output == expected_output</code></pre><p>When we write commands, we need to use the posix version of paths. We&#8217;ve already done that for <code>python_cmd</code>, but we need to do that for the path to the file we&#8217;re running as well.</p><p>With <a href="https://github.com/ehmatthes/demo_testing_pcc_3e/tree/4e99c15084c0a18cc483b3ad1ca3e6432218c50f/tests">these changes</a> all 47 tests still pass on macOS, and now they pass on Windows as well. I don&#8217;t have any reason to think they won&#8217;t work on Linux at this point, although I haven&#8217;t tested that yet. It&#8217;s also worth noting that GPT is really helpful for sorting out cross-OS compatibility issues. You can run your code on a different OS, give GPT the entire error message, and ask it for suggestions. It doesn&#8217;t fix everything, but it frequently points out a promising direction for what to focus on.</p><h3>Programs that need to be run from a specific directory</h3><p>Some of the basic programs need to be run from a specific directory. These are all programs that read from a file, and use relative imports.</p><p>I added a new list of programs to test along with the corresponding output, and made a second test function that&#8217;s almost identical to <code>test_basic_program()</code>:</p><pre><code><strong>import subprocess, sys, os</strong>
...

...
<strong># Programs that must be run from their parent directory.</strong>
<strong>chdir_programs = [
    ("chapter_10/.../file_reader.py", "3.14..79"),
    ...
]</strong>

@pytest.mark.parametrize("file_path, expected_output", basic_programs)
def test_basic_program(python_cmd, file_path, expected_output):
    """Test a program that only prints output."""
    ...

@pytest.mark.parametrize("file_path, expected_output", chdir_programs)
def test_chdir_program(python_cmd, file_path, expected_output):
    """Test a program that must be run from the parent directory."""
    root_dir = Path(__file__).parents[1]
    path = root_dir / file_path

    <strong># Change to the parent directory before running command.
    os.chdir(path.parent)</strong>

    # Run the command, and make assertions.
    cmd = f"{python_cmd} {path.as_posix()}"
    output = utils.run_command(cmd)

    assert output == expected_output</code></pre><p>I called the new list <code>chdir_programs</code>, because these are basic programs where we must change directories before running them.</p><p>The new function is called <code>test_chdir_program()</code>, and the only difference between it and <code>test_basic_program()</code> is the call to <code>os.chdir()</code>. We change to the directory containing the file that&#8217;s being tested before running it, so the relative file paths work correctly.</p><p>At <a href="https://github.com/ehmatthes/mp_testing_pcc_3e/blob/4264c85fba688efc094e8923ed901541d64affd1/tests/test_basic_programs.py">this point</a>, we have 60 passing tests:</p><pre><code>$ <strong>pytest -q</strong>
............................................................ [100%]
60 passed in 0.87s</code></pre><p>When I first wrote the test suite, I placed some programs that had import statements in this group. It turns out those work without calling <code>os.chdir()</code>, so I added those programs to the set of basic tests in this commit.</p><p>You might have noticed the significant overlap between these two test functions. It&#8217;s not unusual to have more repetitive code in test suites. I know I won&#8217;t be adding more test functions to this file, so I&#8217;m going to leave these two test functions as they are, without any further refactoring. I also know the codebase isn&#8217;t going to grow beyond what&#8217;s already in the book, so there&#8217;s no reason to think any more tests will be added to this file. Don&#8217;t refactor blindly; if you have clear reasons to think you&#8217;ve reached a point where a particular body of code is good enough, be willing to call it good enough and move on to other things.</p><h3>Conclusions</h3><p>There are a number of takeaways from this first phase of building out the test suite:</p><ul><li><p>Start by writing a single test, which helps to make sure your testing infrastructure is working. Make sure your first test passes, but also make sure it&#8217;s capable of failing.</p></li><li><p>Learn to parametrize your tests. It can seem complex if you haven&#8217;t done it before, but with a little work you can go from tens or hundreds of individual tests, to one or two tests that are used repeatedly.</p></li><li><p>Pull common setup tasks out into fixtures.</p></li><li><p>Pull common non-setup tasks out into utility functions.</p></li></ul><p>If you address some or all of these points, you&#8217;ll probably have a test suite that makes it easier to carry out ongoing development and maintenance work. You&#8217;ll also have a test suite that&#8217;s easier to understand, and easier to build on as your project evolves.</p><p>In the <a href="https://www.mostlypython.com/p/testing-a-books-code-part-3-testing">next post</a> we&#8217;ll make sure we can run the test suite using multiple versions of Python. We&#8217;ll also write tests for the first project, a 2d game called <em>Alien Invasion</em> that uses Pygame. Even if you&#8217;re not interested in games, it&#8217;s an interesting challenge to write automated tests for a project that seems to require user interactions. There are plenty of takeaways from this work that applies to a wide variety of projects as well.</p><h3>Resources</h3><p>You can find the code files from this post in the&nbsp;<a href="https://github.com/ehmatthes/mp_testing_pcc_3e">mp_testing_pcc_3e</a>&nbsp;GitHub repository.</p><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-1" href="#footnote-anchor-1" class="footnote-number" contenteditable="false" target="_self">1</a><div class="footnote-content"><p>A lot of people seem to think that <em>Hello World</em> programs are written just so everyone, even people brand new to programming, can understand them. Most of the time, they serve a much more significant purpose: they demonstrate that a programming language, and some relevant tools, are set up correctly on a system.</p><p>If you can run a <em>Hello World</em> program, you can probably run the programs you&#8217;re interested in. If you can&#8217;t get a <em>Hello World</em> program to run, you won&#8217;t have any luck with more complex programs until you sort out whatever issue is keeping <em>Hello World</em> from running.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-2" href="#footnote-anchor-2" class="footnote-number" contenteditable="false" target="_self">2</a><div class="footnote-content"><p>Brandon Rhodes gave an excellent keynote at PyTexas 2023 titled <a href="https://www.youtube.com/watch?v=APVNZfeOCI4">Walking the Line</a>. At one point he mentions the giddy but uneasy feeling we get when our tests start to pass consistently, while doing a bunch of refactoring work. Are we really that good, or are our tests not quite doing what we think they&#8217;re doing?</p><p>Making sure your tests can fail is a really important step in a number of different situations. If you haven&#8217;t seen a failing test in a while, consider introducing an intentional bug to make sure your tests can still fail, in the way you expect them to.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-3" href="#footnote-anchor-3" class="footnote-number" contenteditable="false" target="_self">3</a><div class="footnote-content"><p>If you haven&#8217;t heard this term before or you&#8217;re unclear about exactly what it is, a <em>fixture</em> is a resource that&#8217;s used by multiple test functions. It&#8217;s typically implemented as a function that returns a consistent resource or set of resources for every test function that needs it.</p></div></div>]]></content:encoded></item><item><title><![CDATA[Testing a book's code]]></title><description><![CDATA[MP 76: Testing code for a book is different than testing a standard programming project, but brings up many relevant issues.]]></description><link>https://mostlypython.substack.com/p/testing-a-books-code</link><guid isPermaLink="false">https://mostlypython.substack.com/p/testing-a-books-code</guid><dc:creator><![CDATA[Eric Matthes]]></dc:creator><pubDate>Thu, 11 Jan 2024 17:30:21 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd9f961fd-08f5-4534-a14e-3f5eaa1c153e_2001x1311.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><strong>Note:</strong> <em>This is the first post in a seven-part <a href="https://www.mostlypython.com/p/testing-a-books-code-696">series</a> about testing.</em></p><div><hr></div><p>Maintaining a technical book is more challenging than many people realize. If you want your book to stay relevant, a technical book is never really finished. You have to keep an eye on the code to make sure it hasn&#8217;t gone out of date. When that inevitably happens, you need a plan for communicating updates to readers, and a plan for how to approach new editions.</p><p>Just as in more traditional software projects, automated testing can (and should) play a significant role in making sure a book&#8217;s code stays up to date. In this series I&#8217;ll share my current approach to testing the code for <em>Python Crash Course</em>. My workflow has evolved significantly over the course of three editions, and almost ten years now. There&#8217;s a lot of overlap between testing traditional software projects and testing code for a book, but there are interesting differences as well.</p><p>This series will have obvious relevance to current authors, and people who are considering writing a technical book. But even if you have no interest in writing, there are many elements of this work that are relevant to testing in general. For example, this series will touch on all of the following:</p><ul><li><p>How to plan a testing suite that meets your actual needs, rather than one that simply exercises as much code as possible;</p></li><li><p>How to use parametrization to write a group of similar tests efficiently;</p></li><li><p>How to test visual output such as plots;</p></li><li><p>How to test programs that depend on user input;</p></li><li><p>How to test programs that depend on random values;</p></li><li><p>How to address cross-OS issues in testing;</p></li><li><p>How to run tests in parallel, to speed up your test suite;</p></li><li><p>How to use test artifacts to make debugging and ongoing development easier. </p></li></ul><p>While the context centers around testing code from a book, the variety of challenges that come up will address issues of relevance to people interested in testing a wide variety of projects.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!xGjE!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff1011d96-d5c7-4750-b546-b02359bfce29_451x595.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!xGjE!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff1011d96-d5c7-4750-b546-b02359bfce29_451x595.png 424w, https://substackcdn.com/image/fetch/$s_!xGjE!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff1011d96-d5c7-4750-b546-b02359bfce29_451x595.png 848w, https://substackcdn.com/image/fetch/$s_!xGjE!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff1011d96-d5c7-4750-b546-b02359bfce29_451x595.png 1272w, https://substackcdn.com/image/fetch/$s_!xGjE!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff1011d96-d5c7-4750-b546-b02359bfce29_451x595.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!xGjE!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff1011d96-d5c7-4750-b546-b02359bfce29_451x595.png" width="373" height="492.09534368070956" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f1011d96-d5c7-4750-b546-b02359bfce29_451x595.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:595,&quot;width&quot;:451,&quot;resizeWidth&quot;:373,&quot;bytes&quot;:353805,&quot;alt&quot;:&quot;Cover of Python Crash Course, third edition.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Cover of Python Crash Course, third edition." title="Cover of Python Crash Course, third edition." srcset="https://substackcdn.com/image/fetch/$s_!xGjE!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff1011d96-d5c7-4750-b546-b02359bfce29_451x595.png 424w, https://substackcdn.com/image/fetch/$s_!xGjE!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff1011d96-d5c7-4750-b546-b02359bfce29_451x595.png 848w, https://substackcdn.com/image/fetch/$s_!xGjE!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff1011d96-d5c7-4750-b546-b02359bfce29_451x595.png 1272w, https://substackcdn.com/image/fetch/$s_!xGjE!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff1011d96-d5c7-4750-b546-b02359bfce29_451x595.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption"><em>Python Crash Course</em> has sold enough copies that there&#8217;s always a significant number of people working through it at any given time. A comprehensive test suite helps identify issues with upcoming versions before readers find them.</figcaption></figure></div><h3>Series overview</h3><p>For some series on Mostly Python, I draft each post as the series evolves. For others, like this one, I&#8217;ve planned out all the posts ahead of time. This series will consist of seven parts:</p><p><strong>Part 1:</strong> How do you approach setting up a test suite? What kinds of things should you think about, and what decisions should you make, before writing any test code?</p><p><strong>Part 2:</strong> How do you test a large set of basic Python scripts? How can you use parametrization to do this kind of testing efficiently? (<a href="https://www.mostlypython.com/p/testing-a-books-code-part-2-basic">MP #77</a>)</p><p><strong>Part 3:</strong> How do you test a game project? In general, how do you test a project that has visual output, and depends on user interactions?</p><p><strong>Part 4:</strong> How do you test code that generates image-based data visualizations with Matplotlib? How do you validate static visual output?</p><p><strong>Part 5:</strong> How do you test code that generates HTML-based visualizations, when the HTML is slightly different on every test run?</p><p><strong>Part 6:</strong> How do you test a Django project, with a focus on external behaviors?</p><p><strong>Part 7:</strong> What are the takeaways from all this testing work?</p><p>If you&#8217;ve been thinking about testing but haven&#8217;t written a test suite of your own yet, I think you&#8217;ll find a number of things in here that are helpful to your own work. If you have started to write tests, I believe you&#8217;ll find some aspects that look familiar, and some ways of doing things that help you think differently about your own testing routines.</p><div><hr></div><h3>A good testing mindset</h3><p>I wrote in an <a href="https://mostlypython.substack.com/p/dont-start-with-unit-tests">earlier post</a> that people should consider writing integration tests before unit tests, for untested projects and projects that are under rapid development. Chris Neugebauer <a href="https://www.youtube.com/watch?v=1i5leCslA4g">gave a talk</a> at PyCon Australia in 2015 with a similar message. There&#8217;s a great quote from that talk that has stayed with me:</p><blockquote><p><em>If you have any code that can be run at all, there is a way to write tests for it.</em></p></blockquote><p>A book is full of code. That code can be run, so there must be a way to write tests for it.</p><h3>A brief history of testing <em>Python Crash Course</em></h3><p>When <em>Python Crash Course</em> first came out in 2015, I didn&#8217;t have any automated tests written for it. I posted all the code for the book online, and when readers pointed out inevitable mistakes I updated the repository, and made notes to fix those mistakes in the book the next time it went out for a new print run. With this approach, the book steadily got better over time.</p><p><em>Python Crash Course</em> is really two books in one. The first half of the book is an introduction to the basics of Python and the fundamentals of programming. The second half of the book is a walk-through of three different projects: a video game, a series of data visualizations, and a web app that goes all the way through deployment.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!ratn!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd9f961fd-08f5-4534-a14e-3f5eaa1c153e_2001x1311.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!ratn!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd9f961fd-08f5-4534-a14e-3f5eaa1c153e_2001x1311.png 424w, https://substackcdn.com/image/fetch/$s_!ratn!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd9f961fd-08f5-4534-a14e-3f5eaa1c153e_2001x1311.png 848w, https://substackcdn.com/image/fetch/$s_!ratn!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd9f961fd-08f5-4534-a14e-3f5eaa1c153e_2001x1311.png 1272w, https://substackcdn.com/image/fetch/$s_!ratn!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd9f961fd-08f5-4534-a14e-3f5eaa1c153e_2001x1311.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!ratn!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd9f961fd-08f5-4534-a14e-3f5eaa1c153e_2001x1311.png" width="632" height="414.0989010989011" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/d9f961fd-08f5-4534-a14e-3f5eaa1c153e_2001x1311.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:954,&quot;width&quot;:1456,&quot;resizeWidth&quot;:632,&quot;bytes&quot;:231628,&quot;alt&quot;:&quot;Brief TOC for Python Crash Course&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Brief TOC for Python Crash Course" title="Brief TOC for Python Crash Course" srcset="https://substackcdn.com/image/fetch/$s_!ratn!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd9f961fd-08f5-4534-a14e-3f5eaa1c153e_2001x1311.png 424w, https://substackcdn.com/image/fetch/$s_!ratn!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd9f961fd-08f5-4534-a14e-3f5eaa1c153e_2001x1311.png 848w, https://substackcdn.com/image/fetch/$s_!ratn!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd9f961fd-08f5-4534-a14e-3f5eaa1c153e_2001x1311.png 1272w, https://substackcdn.com/image/fetch/$s_!ratn!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd9f961fd-08f5-4534-a14e-3f5eaa1c153e_2001x1311.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">This is really two books in one: an introduction to the basics of Python, and then a series of projects. That&#8217;s an interesting variety of things to cover in one test suite.</figcaption></figure></div><p>This means the book&#8217;s longevity depends not only on how Python evolves, but also on the ongoing development of third party libraries for writing games, data visualizations, and web apps. Testing any one of these isn&#8217;t always straightforward; testing all of these in one comprehensive test suite is an interesting challenge.</p><p>Most of my early testing work for the book was manual. When a new version of Python was released, or a new version of any of the libraries the book depends on was released, I&#8217;d manually set up a virtual environment and run the code for each of the projects in the book. This worked, but it wasn&#8217;t particularly efficient. There were definitely a few times I didn&#8217;t do this work ahead of time, and just crossed my fingers that a new version of a library wouldn&#8217;t break the book&#8217;s code.</p><p>For the second edition of the book, which came out in 2019, I tried to write a proper test suite for the book. I wrote code that walked the entire repository for the book&#8217;s code, and tried to run all the files in the repository. That sort of worked, but it was a clunky mess of trying to exclude files that didn&#8217;t really need to be tested, or weren't testable in a straightforward way.</p><p>The third edition came out in January of 2023, and I&#8217;ve learned a lot more about testing since the previous edition came out. This time I laid out the goals I want to achieve with a test suite, and set out to build a set of tests that would do exactly that. Instead of walking through the repository and trying to test every file, I wrote a test suite that specifies exactly which files should be tested, and how. I&#8217;m really pleased with the result.</p><h3>An ideal test suite</h3><p>An ideal test suite would let me do the following:</p><p><strong>Run all the most significant programs in the book.</strong> We often hear about the ideal of 100% test coverage, but that&#8217;s just not necessary with a book. For example I have folders called <em><a href="https://github.com/ehmatthes/pcc_3e/tree/main/chapter_08/partial_programs">partial_programs/</a></em>, which show all the versions of each file as it&#8217;s being developed in the text of the book. If the final version of each of these programs works, I&#8217;m confident enough in all the partial versions.</p><p><strong>Run the test suite against different versions of Python, including release candidates.</strong> This is really important; I want to know that the code for the book will work on the latest version of Python. I want to test against release candidates of new versions, rather than waiting for the full public release of each new version.</p><p><strong>Run the test suite against any version of any library used in the book.</strong> This is critical as well. The book covers pytest, Pygame, Matplotlib, Plotly, Requests, and Django. All these libraries have different lifecycles, and I need an easy way to test the projects in the book against any version of these libraries, including release candidates.</p><p><strong>Drop into an active environment for a specific project.</strong> When maintaining the book, it gets quite tedious to set up an environment for a project in order to run it and look at certain aspects of that project. An ideal test suite will let me run the tests, and then drop into an active environment for any project in the book. This goes beyond testing; it&#8217;s testing that leaves artifacts behind that I can easily work with in a variety of ways. This is especially helpful when readers report possible issues, and the tests don&#8217;t necessarily cover the issue, but they do set up an environment where it would be easy to check out that issue.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!5VBi!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff08a8000-d664-4168-b367-357dd363f564_1127x604.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!5VBi!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff08a8000-d664-4168-b367-357dd363f564_1127x604.png 424w, https://substackcdn.com/image/fetch/$s_!5VBi!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff08a8000-d664-4168-b367-357dd363f564_1127x604.png 848w, https://substackcdn.com/image/fetch/$s_!5VBi!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff08a8000-d664-4168-b367-357dd363f564_1127x604.png 1272w, https://substackcdn.com/image/fetch/$s_!5VBi!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff08a8000-d664-4168-b367-357dd363f564_1127x604.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!5VBi!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff08a8000-d664-4168-b367-357dd363f564_1127x604.png" width="682" height="365.50842945874" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f08a8000-d664-4168-b367-357dd363f564_1127x604.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:604,&quot;width&quot;:1127,&quot;resizeWidth&quot;:682,&quot;bytes&quot;:63760,&quot;alt&quot;:&quot;Home page of the Learning Log project&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Home page of the Learning Log project" title="Home page of the Learning Log project" srcset="https://substackcdn.com/image/fetch/$s_!5VBi!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff08a8000-d664-4168-b367-357dd363f564_1127x604.png 424w, https://substackcdn.com/image/fetch/$s_!5VBi!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff08a8000-d664-4168-b367-357dd363f564_1127x604.png 848w, https://substackcdn.com/image/fetch/$s_!5VBi!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff08a8000-d664-4168-b367-357dd363f564_1127x604.png 1272w, https://substackcdn.com/image/fetch/$s_!5VBi!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff08a8000-d664-4168-b367-357dd363f564_1127x604.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption"><em>Learning Log</em>, the Django project that PCC readers build. Rather than just test this project, I want to be able to run the test and then drop into a working version of the project.</figcaption></figure></div><p><strong>Runs on macOS, Windows, and Linux.</strong> Readers use all these platforms, so I need to be able to test on each of these platforms as well.</p><h3>Ideal test commands</h3><p>It can be helpful to imagine the kinds of commands you&#8217;d like to be able to run if your ideal test suite already existed. These are the kinds of commands I&#8217;d like to be able to run for this project:</p><pre><code>$ <strong>pytest tests/</strong>
$ <strong>pytest tests/test_django_project.py</strong>
$ <strong>pytest tests/test_django_project.py --django-version 5.0a1</strong></code></pre><p>Having a sense of what kinds of commands we want to run points us toward the testing files we should start to write, and the kinds of features we should be looking to build into the test suite. Here I want an overall <em>tests/</em> directory, and I want individual test files for each of the major projects in the book. I also want to be able to include CLI arguments that let me specify exactly which version of a library to use for any given test run.</p><h3>The overall plan</h3><p>In most software projects the test suite lives alongside the project&#8217;s code, but that doesn&#8217;t quite work for this kind of testing. The main repository for the book is really for readers; trying to include a full author-focused testing suite in that repository is not ideal. I also don&#8217;t want to maintain these tests in public. They sometimes highlight issues that I recognize as minor, that might look like more significant breakage to people who don&#8217;t have the same perspective on the code and the purpose it serves.</p><p>When making an overall plan for the test suite, I also have to wrestle with one aspect of testing code for a book that&#8217;s different from testing standard projects. In a standard project, you&#8217;re somewhat free to change the code to make it more testable; this is one of the benefits of starting a test suite early on in a project. But a book&#8217;s code is &#8220;frozen&#8221;. It&#8217;s printed in physical books, and it&#8217;s developed with a higher priority on pedagogical goals than on performance or testability. So we need to come up with a way of testing that deals with all the code as it is. This isn&#8217;t entirely unique to testing code from a book. You may need to write tests for a project that includes code that you or your team don&#8217;t have direct control over.</p><p>Here&#8217;s the approach I came up with:</p><ul><li><p>Make a private fork of the book&#8217;s public repository.</p></li><li><p>Add any files I want to this repository, to support testing.</p></li><li><p>Don&#8217;t remove or modify any file that&#8217;s in the public repository. If I need to change something for testing purposes, make a copy of that resource and modify it during the test run.</p></li><li><p>When I make changes in the book&#8217;s code for new printings, pull those changes into this repository as well.</p></li><li><p>Never push anything from the test repository to the upstream public repository.</p></li></ul><p>For this series, I&#8217;m going to make a public version of the test repository, which I&#8217;ll then archive as a demo when the series is complete.</p><h3>Conclusions</h3><p>Writing a test suite for the code in a technical book presents a unique challenge, especially if the book covers a variety of different topics, using a number of third-party libraries. However, addressing these unique challenges brings up ideas that are useful to people testing a wide range of projects.</p><p>If you&#8217;re developing a test suite for a project of your own, take a moment to step back and ask yourself what your larger goals are for the test suite. Don&#8217;t fall into the trap of just writing a bunch of tests that exercise most of your code. What do you want to get out of your test runs? What do you want to be able to do immediately after a test runs? How do you want to use your tests as a part of your development workflow? Tests don&#8217;t have to be something that&#8217;s only dealt with <em>after</em> you write your code; they can be something that are useful while actively <em>developing</em> your code.</p><p>In the <a href="https://www.mostlypython.com/p/testing-a-books-code-part-2-basic">next post</a> we&#8217;ll write a set of tests that cover all the basic programs in the book. We&#8217;ll go through the process of writing the first test, making sure it can fail and then making sure it passes. We&#8217;ll use parametrization to quickly generate a large set of similar tests. We&#8217;ll also start to use fixtures to implement some features of the test suite that will be used throughout all the test modules.</p><p><strong>Note: </strong><em>If you have questions or thoughts to share about testing, please add a comment below, or feel free to reply to this email and I&#8217;ll be happy to respond.</em></p>]]></content:encoded></item><item><title><![CDATA[The value of streaks]]></title><description><![CDATA[MP 75: Streaks are quite different than New Year's resolutions.]]></description><link>https://mostlypython.substack.com/p/the-value-of-streaks</link><guid isPermaLink="false">https://mostlypython.substack.com/p/the-value-of-streaks</guid><dc:creator><![CDATA[Eric Matthes]]></dc:creator><pubDate>Thu, 04 Jan 2024 17:30:25 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F87514020-8576-4474-b647-3a056faf93f0_1638x960.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><strong>Note:</strong> <em>A new technical series will start next week. The first post introducing the series will be open to everyone. Also, if you&#8217;ve been hearing about Substack&#8217;s issues around content moderation and are interested in my take, I posted some thoughts <a href="https://www.mostlypython.com/p/substacks-nazi-problem">here</a>.</em></p><div><hr></div><p>I bought a rowing machine a couple years ago, to help stay in shape when I didn&#8217;t have enough time to get outside for a longer run. I rowed crew in college, and that background makes it easy to hop on and do a quick, effective workout.</p><p>This fall I tried Concept 2&#8217;s <a href="https://log.concept2.com/challenges/holiday">Holiday Challenge</a>, where you try to row 100 kilometers between Thanksgiving and Christmas. That&#8217;s about twice as much as I normally row. I just finished the challenge last week, and it had me thinking about what I&#8217;ve learned from attempting various streaks over the years.</p><p>Streaks are much more flexible and attainable than typical New Year&#8217;s resolutions. A resolution is supposed to be a permanent change, which is notoriously difficult to see through. A streak requires commitment, but it also has an end. If you choose the right kind of streak, the benefits can be much more attainable and significant.</p><h3>Rowing 100k</h3><p>Most of the time, I like to stay in shape by doing a number of different things: running, hiking, biking, paddling, and anything else I can do outdoors. But falls and winters in southeast Alaska are cold and rainy, and this winter is one of the wettest I&#8217;ve seen in 20 years here. It was a perfect fall to focus on indoor exercise.</p><p>When I row, I typically go for a specific time rather than a set distance. I&#8217;ll do 20 minutes at a faster pace, or 30-40 minutes at a slower pace. Before I started the 100k challenge I sketched out a rough schedule with a short, medium, and two longer rows each week. I did that for the first week, but then found I was really enjoying the longer rows. The core workout I settled on was a 10k row, which takes me just under 50 minutes at a steady pace.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!NxCG!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8d8ef585-4428-41de-8d81-196836efcaaf_1149x461.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!NxCG!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8d8ef585-4428-41de-8d81-196836efcaaf_1149x461.png 424w, https://substackcdn.com/image/fetch/$s_!NxCG!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8d8ef585-4428-41de-8d81-196836efcaaf_1149x461.png 848w, https://substackcdn.com/image/fetch/$s_!NxCG!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8d8ef585-4428-41de-8d81-196836efcaaf_1149x461.png 1272w, https://substackcdn.com/image/fetch/$s_!NxCG!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8d8ef585-4428-41de-8d81-196836efcaaf_1149x461.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!NxCG!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8d8ef585-4428-41de-8d81-196836efcaaf_1149x461.png" width="1149" height="461" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/8d8ef585-4428-41de-8d81-196836efcaaf_1149x461.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:461,&quot;width&quot;:1149,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:40413,&quot;alt&quot;:&quot;Chart showing a variety of workouts from ~5k through 15k&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Chart showing a variety of workouts from ~5k through 15k" title="Chart showing a variety of workouts from ~5k through 15k" srcset="https://substackcdn.com/image/fetch/$s_!NxCG!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8d8ef585-4428-41de-8d81-196836efcaaf_1149x461.png 424w, https://substackcdn.com/image/fetch/$s_!NxCG!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8d8ef585-4428-41de-8d81-196836efcaaf_1149x461.png 848w, https://substackcdn.com/image/fetch/$s_!NxCG!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8d8ef585-4428-41de-8d81-196836efcaaf_1149x461.png 1272w, https://substackcdn.com/image/fetch/$s_!NxCG!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8d8ef585-4428-41de-8d81-196836efcaaf_1149x461.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">To meet the 100k challenge, I did a few longer rows every week. Some people approach it by trying to row every day, but I really enjoy spacing out longer workouts.</figcaption></figure></div><p>Once I got into a routine, I enjoyed the longer rows so much I started pushing past that 10k mental limit. I did a 12k, and then a 15k row. I&#8217;ve always been much more of a distance athlete than a sprinter. I often find my pace around 30-40 minutes into a workout, and if I can make the time for it I really enjoy stretching that out to 60-90 minutes.</p><h5>Physical benefits</h5><p>Whenever I attempt a streak, there are some things I know I&#8217;ll get out of it if I&#8217;m successful. But I also know there will be some unexpected benefits as well, and I&#8217;m always curious to see what those end up being.</p><p>For this streak, I knew I&#8217;d get in better shape for rowing. That definitely happened, but I had no idea what directions it would take me. I&#8217;ve been surprised to find out how much I enjoy rowing longer distances. I rowed an 18k piece after the challenge was over, which worked out to just under 90 minutes. I chose that distance to see if I&#8217;m ready for a half-marathon, where you row the equivalent of a half-marathon in running (21,097 meters).</p><p>The 18k piece took more out of me than I expected, but I&#8217;m still planning to do the half marathon in the next week or so. We&#8217;re moving to North Carolina this summer, so much of our spring will be taken up with cleaning out from 20 years of living in one place, and sorting out the logistics of a cross-country move. If we weren&#8217;t taking that on, I&#8217;d keep going and prepare for a full marathon. I don&#8217;t want to spend too much of my life sitting on a rowing machine, but I&#8217;d love the satisfaction of rowing a marathon once.</p><p>I&#8217;ve also found that shorter pieces feel <em>much</em> easier than they used to. My results in 20-minute pieces are much better than they were before, because a 20-minute piece feels so much shorter and easier now. I can push myself harder for these shorter times, knowing it&#8217;s going to be over soon. I&#8217;m looking forward to testing myself on a variety of shorter pieces after finishing the half marathon.</p><h5>Mental benefits</h5><p>One of the things I really like about these longer rows is that they&#8217;re a perfect way to sit and watch the conference talks I&#8217;ve been wanting to see. For each long row, I set up a queue of talk videos. Throughout the month I watched about ten talks: all the <a href="https://www.youtube.com/playlist?list=PL2NFhrDSOxgX41jqYSi0HmO9Wsf6WDSmf">talks</a> I missed in person at DjangoCon US, the PyCon 2023 <a href="https://www.youtube.com/playlist?list=PL2Uw4_HvXqvY2zhJ9AMUa_Z6dtMGF3gtb">talks</a> I wanted to see, and a few classic talks I&#8217;ve had on my playlist for a long time. After most workouts I&#8217;d write a few notes about how to use what I learned from these talks in my current projects.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!F5PJ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F43135ca4-8a62-4470-aa23-0cbfa3f037e8_1299x833.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!F5PJ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F43135ca4-8a62-4470-aa23-0cbfa3f037e8_1299x833.png 424w, https://substackcdn.com/image/fetch/$s_!F5PJ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F43135ca4-8a62-4470-aa23-0cbfa3f037e8_1299x833.png 848w, https://substackcdn.com/image/fetch/$s_!F5PJ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F43135ca4-8a62-4470-aa23-0cbfa3f037e8_1299x833.png 1272w, https://substackcdn.com/image/fetch/$s_!F5PJ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F43135ca4-8a62-4470-aa23-0cbfa3f037e8_1299x833.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!F5PJ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F43135ca4-8a62-4470-aa23-0cbfa3f037e8_1299x833.png" width="612" height="392.4526558891455" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/43135ca4-8a62-4470-aa23-0cbfa3f037e8_1299x833.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:833,&quot;width&quot;:1299,&quot;resizeWidth&quot;:612,&quot;bytes&quot;:560820,&quot;alt&quot;:&quot;still from video, showing green commits and red commits. Sometimes we want the red commits.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="still from video, showing green commits and red commits. Sometimes we want the red commits." title="still from video, showing green commits and red commits. Sometimes we want the red commits." srcset="https://substackcdn.com/image/fetch/$s_!F5PJ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F43135ca4-8a62-4470-aa23-0cbfa3f037e8_1299x833.png 424w, https://substackcdn.com/image/fetch/$s_!F5PJ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F43135ca4-8a62-4470-aa23-0cbfa3f037e8_1299x833.png 848w, https://substackcdn.com/image/fetch/$s_!F5PJ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F43135ca4-8a62-4470-aa23-0cbfa3f037e8_1299x833.png 1272w, https://substackcdn.com/image/fetch/$s_!F5PJ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F43135ca4-8a62-4470-aa23-0cbfa3f037e8_1299x833.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Brandon Rhodes&#8217; PyTexas Keynote, <a href="https://www.youtube.com/watch?v=APVNZfeOCI4&amp;t=296s">Walking the Line</a>, was an excellent way to pass 40 minutes of rowing.</figcaption></figure></div><p>For the longer rows I found it helpful to queue up technical talks for most of the workout, followed by outdoor videos for the final stretches. I couldn&#8217;t really concentrate on a technical talk when I was running out of energy and trying to push myself at the end of a long piece. I started putting climbing videos at the end of these playlists, and it was much easier to push myself while watching others push themselves. For anyone interested in long-distance hiking, a short <a href="https://www.youtube.com/watch?v=hiVbB7Pf2lY">documentary</a> about the Pacific Crest Trail was really satisfying to watch during the end of a long row.</p><h5>Aside: Concept2 is an amazing company</h5><p>It&#8217;s worth sharing a little about the company that makes the rowing machines that almost all experienced rowers use. <a href="https://www.concept2.com">Concept 2</a> started as a small company that built rowing machines out of repurposed bicycle parts. Their machines have evolved steadily over the last 40+ years, but throughout this entire time they&#8217;ve focused on durability and backwards compatibility.</p><p>Concept 2 rowing machines (ergs, short for ergometers), are used by college and Olympic rowers every day for years on end. If you buy one for home use, it&#8217;s likely to last 20 years or more. And if it stops working, you can still buy parts for your 20-year-old machine. As a software developer who pays attention to backwards compatibility, this is an amazing thing to see from a company that focuses on athletic equipment.<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-1" href="#footnote-1" target="_self">1</a></p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!76LV!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F87514020-8576-4474-b647-3a056faf93f0_1638x960.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!76LV!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F87514020-8576-4474-b647-3a056faf93f0_1638x960.jpeg 424w, https://substackcdn.com/image/fetch/$s_!76LV!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F87514020-8576-4474-b647-3a056faf93f0_1638x960.jpeg 848w, https://substackcdn.com/image/fetch/$s_!76LV!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F87514020-8576-4474-b647-3a056faf93f0_1638x960.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!76LV!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F87514020-8576-4474-b647-3a056faf93f0_1638x960.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!76LV!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F87514020-8576-4474-b647-3a056faf93f0_1638x960.jpeg" width="680" height="398.3791208791209" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/87514020-8576-4474-b647-3a056faf93f0_1638x960.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:853,&quot;width&quot;:1456,&quot;resizeWidth&quot;:680,&quot;bytes&quot;:228643,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!76LV!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F87514020-8576-4474-b647-3a056faf93f0_1638x960.jpeg 424w, https://substackcdn.com/image/fetch/$s_!76LV!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F87514020-8576-4474-b647-3a056faf93f0_1638x960.jpeg 848w, https://substackcdn.com/image/fetch/$s_!76LV!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F87514020-8576-4474-b647-3a056faf93f0_1638x960.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!76LV!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F87514020-8576-4474-b647-3a056faf93f0_1638x960.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">A modern Concept 2 rowing machine. These new machines are incredibly smooth and reliable, but the basic design hasn&#8217;t changed in decades. They last millions of meters, and almost every part can be replaced.</figcaption></figure></div><h3>Other streaks</h3><h5>90 days of open source contributions</h5><p>A while back, I completed a streak of making at least one commit to an open source project each day, for 90 days. That was in the early days of GitHub, when many people were seeing their contributions laid out visually for the first time:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!UAuP!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F06ab992b-7eb3-4569-8f8f-f4d23fc934a3_1154x309.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!UAuP!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F06ab992b-7eb3-4569-8f8f-f4d23fc934a3_1154x309.png 424w, https://substackcdn.com/image/fetch/$s_!UAuP!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F06ab992b-7eb3-4569-8f8f-f4d23fc934a3_1154x309.png 848w, https://substackcdn.com/image/fetch/$s_!UAuP!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F06ab992b-7eb3-4569-8f8f-f4d23fc934a3_1154x309.png 1272w, https://substackcdn.com/image/fetch/$s_!UAuP!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F06ab992b-7eb3-4569-8f8f-f4d23fc934a3_1154x309.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!UAuP!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F06ab992b-7eb3-4569-8f8f-f4d23fc934a3_1154x309.png" width="1154" height="309" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/06ab992b-7eb3-4569-8f8f-f4d23fc934a3_1154x309.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:309,&quot;width&quot;:1154,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:42939,&quot;alt&quot;:&quot;GitHub contribution graph for 2013, showing intermittent contributions for much of the year, and consistent contributions for the last three months of the year&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="GitHub contribution graph for 2013, showing intermittent contributions for much of the year, and consistent contributions for the last three months of the year" title="GitHub contribution graph for 2013, showing intermittent contributions for much of the year, and consistent contributions for the last three months of the year" srcset="https://substackcdn.com/image/fetch/$s_!UAuP!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F06ab992b-7eb3-4569-8f8f-f4d23fc934a3_1154x309.png 424w, https://substackcdn.com/image/fetch/$s_!UAuP!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F06ab992b-7eb3-4569-8f8f-f4d23fc934a3_1154x309.png 848w, https://substackcdn.com/image/fetch/$s_!UAuP!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F06ab992b-7eb3-4569-8f8f-f4d23fc934a3_1154x309.png 1272w, https://substackcdn.com/image/fetch/$s_!UAuP!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F06ab992b-7eb3-4569-8f8f-f4d23fc934a3_1154x309.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">In 2013, I went 90 days making at least one open source contribution a day. That was a formative experience in my programming career.</figcaption></figure></div><p>That streak was interesting in that I didn&#8217;t set out to make it happen. I noticed one day that I had made at least one commit every day for the last 17 days. I decided to keep making daily contributions, without a specific end goal. I kept the streak alive as long as it was beneficial: if maintaining the streak helped me keep working when I was discouraged, or when the work wasn&#8217;t easy, I kept it going. I decided to end the streak when it started to make me not enjoy programming.</p><p>There were lots of takeaways from that period. Some people were trying year-long streaks at the time. Ninety days was enough to push myself professionally, but also to maintain a healthy work-life balance. It made me much more comfortable with Git workflows, such as the logistics of working on a variety of branches, and different ways of submitting PRs. This was also when I first started writing tests for my projects.<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-2" href="#footnote-2" target="_self">2</a></p><h5>A year of writing weekly posts</h5><p>I started this newsletter one year ago. One of my reasons for starting it was to commit to writing on a more consistent basis. I&#8217;ve managed to write at least one post a week for a full year, averaging about six posts each month. While it&#8217;s been a lot of work, I&#8217;ve also grown a lot from the experience:</p><p><strong>I can write more efficiently.</strong> I&#8217;ve been a writer for a long time now, so my overall writing process is pretty established. But one weakness I had for a long time was the need to write first drafts on paper. I just couldn&#8217;t put my thoughts together coherently when staring at a screen. I think that comes from doing most of my early writing on paper, and in journals. My first newsletter posts were drafted on paper, but I quickly transitioned to an almost entirely digital workflow. Now I tend to only draft on paper when I can&#8217;t seem to get into a flow with a screen in front of me.</p><p><strong>I&#8217;ve dug into a wider variety of topics than I otherwise would have.</strong> Many people have the impression that technical authors already know everything they&#8217;re writing about. That&#8217;s rarely true. Often in my programming work, I learn enough about any given topic to solve the problem I&#8217;m currently facing. But when I&#8217;m writing, I want to make sure I&#8217;ve explained each topic correctly and accurately. Writing pushes me into areas I wouldn&#8217;t otherwise explore. It also pushes me to not just make things work, but make sure I understand <em>how</em> they work.</p><p><strong>I&#8217;ve met a number of people I wouldn&#8217;t otherwise have met.</strong> My posts don&#8217;t tend to get a lot of comments, but people do write privately to share their reactions. Some of those exchanges have led to video calls and ongoing collaboration. I&#8217;m always deeply appreciative of the people that programming has brought me into contact with, all over the world. I recently joined a technical writers&#8217; group, and those conversations, both formal and informal, have been fantastic.</p><p>I really enjoy writing about programming, and the projects I&#8217;m working on. I don&#8217;t do it for the streak, but the long-term impacts of this kind of writing reminds me of what comes out of a streak.</p><h3>Conclusions</h3><p>I find streaks much more beneficial than resolutions. A resolution tends to be something that we&#8217;re supposed to change permanently, which can be really hard to do. A streak represents a commitment, but it has an end which you can keep in sight. When we finish, we get to reflect on how we&#8217;ve grown, and choose how to build on the experience.</p><p>After finishing a meaningful streak, what was difficult at the start becomes easy; what seemed impossible is now within reach. I&#8217;ve found this true with physical challenges like rowing 100k, and mental challenges such as keeping up an open source contribution streak.</p><p>If you&#8217;re considering taking on a streak, keep a couple things in mind. Make sure to define one that&#8217;s attainable, but will push you toward outcomes you can&#8217;t fully know ahead of time. Choose something you want to get better at: programming, exercise, music, reading, etc. Choose a frequency that fits into your life: every day, once a week, once a month. Finally, choose an end goal that feels like a stretch, but also feels attainable.</p><p>When you reach your end goal, be wary of continuing the streak. It can be hard to let go, but streaks can easily lead to burnout if you focus too much on the streak itself. Instead, consider what aspects of the streak were enjoyable and worth keeping in your life, and let go of the aspects that turned into obligations you started to dread.</p><p>I wish you a good start to 2024, and if you&#8217;ve got a resolution or a streak in mind, I hope it goes well. :)</p><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-1" href="#footnote-anchor-1" class="footnote-number" contenteditable="false" target="_self">1</a><div class="footnote-content"><p>If you&#8217;re curious to learn more about Concept 2&#8217;s history, see their <a href="https://40.concept2.com">40th anniversary page</a>. Their first machines used bicycle wheels with plastic cards attached to the spokes for resistance. This was called the Model A:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!D_q7!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb94d0bb2-45d9-41eb-9e32-83a62297f0f6_504x349.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!D_q7!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb94d0bb2-45d9-41eb-9e32-83a62297f0f6_504x349.jpeg 424w, https://substackcdn.com/image/fetch/$s_!D_q7!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb94d0bb2-45d9-41eb-9e32-83a62297f0f6_504x349.jpeg 848w, https://substackcdn.com/image/fetch/$s_!D_q7!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb94d0bb2-45d9-41eb-9e32-83a62297f0f6_504x349.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!D_q7!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb94d0bb2-45d9-41eb-9e32-83a62297f0f6_504x349.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!D_q7!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb94d0bb2-45d9-41eb-9e32-83a62297f0f6_504x349.jpeg" width="474" height="328.2261904761905" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b94d0bb2-45d9-41eb-9e32-83a62297f0f6_504x349.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:349,&quot;width&quot;:504,&quot;resizeWidth&quot;:474,&quot;bytes&quot;:19992,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!D_q7!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb94d0bb2-45d9-41eb-9e32-83a62297f0f6_504x349.jpeg 424w, https://substackcdn.com/image/fetch/$s_!D_q7!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb94d0bb2-45d9-41eb-9e32-83a62297f0f6_504x349.jpeg 848w, https://substackcdn.com/image/fetch/$s_!D_q7!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb94d0bb2-45d9-41eb-9e32-83a62297f0f6_504x349.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!D_q7!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb94d0bb2-45d9-41eb-9e32-83a62297f0f6_504x349.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">The Concept 2 Model A, released in 1981. It was basically a bicycle wheel with plastic cards attached to the spokes.</figcaption></figure></div><p>The first machine that looks a lot like the current one was the Model B, released in 1986. There have been just a few major redesigns; the Model C was introduced in 1993, and the Model D was introduced in 2003. There are a couple more variations, but most of the evolution took place across these four models. You can still <a href="https://shop.concept2.com/53-parts">buy parts</a> for all models, including the Model A. You can also buy retrofit kits to integrate the most important updates, such as the performance monitor, onto older machines. They truly don&#8217;t push people to replace machines that are still working.</p><p>Concept 2&#8217;s latest machines are called RowErgs, to differentiate from the bike and ski machines they also produce now. Their machines are also quite reasonably priced. They&#8217;re more expensive than the cheapest machines you can find, but they&#8217;re also much more affordable than the latest tablet-based machines. And there&#8217;s no subscriptions whatsoever.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-2" href="#footnote-anchor-2" class="footnote-number" contenteditable="false" target="_self">2</a><div class="footnote-content"><p>If you&#8217;re curious to read more, I wrote three posts about that streak on a Wordpress blog I kept at the time:</p><ul><li><p><a href="https://peak5390.wordpress.com/2013/10/09/what-ive-learned-from-my-first-30-day-github-streak/">What I&#8217;ve learned from my first 30-day GitHub streak</a></p></li><li><p><a href="https://peak5390.wordpress.com/2013/11/08/github-streak-days-31-60-what-ive-learned/">GitHub streak days 31-60: What I&#8217;ve learned</a></p></li><li><p><a href="https://peak5390.wordpress.com/2013/12/08/ninety-days-on-github/">Ninety Days on GitHub</a></p></li></ul></div></div>]]></content:encoded></item><item><title><![CDATA[When is it okay to use short variable names?]]></title><description><![CDATA[MP 74: Naming things is hard, but we can get better at it.]]></description><link>https://mostlypython.substack.com/p/when-is-it-okay-to-use-short-variable</link><guid isPermaLink="false">https://mostlypython.substack.com/p/when-is-it-okay-to-use-short-variable</guid><dc:creator><![CDATA[Eric Matthes]]></dc:creator><pubDate>Thu, 28 Dec 2023 17:30:31 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!m7cR!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F950bfeed-8607-4030-b5fe-7899c3736632_2552x1788.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I&#8217;ve been doing a lot of refactoring work lately. I expect new people to start contributing to the <a href="https://django-simple-deploy.readthedocs.io/en/latest/">project</a> I&#8217;m working on as it approaches its first stable release; with that in mind, one focus for this refactoring work is making the code more readable to people who aren&#8217;t already familiar with the overall codebase.</p><p>Recently I was working on a section of code that uses a number of list comprehensions. I rarely use single-letter variable names, and was surprised to find a number of places where single-letter names made the code more readable.<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-1" href="#footnote-1" target="_self">1</a></p><h3>Allowed modifications</h3><p>The code I&#8217;m working on modifies the user&#8217;s project so it&#8217;s ready for deployment to a remote host. Here&#8217;s the main form of the command that people run when using this project:</p><pre><code>$ <strong>python manage.py simple_deploy --platform &lt;</strong><em><strong>platform-name</strong></em><strong>&gt;</strong></code></pre><p>This command makes configuration changes to the user&#8217;s project based on the platform name they specify. Users can then run their platform&#8217;s <code>deploy</code> or <code>push</code> command, and they should have a working deployment of their project.</p><p>Ideally, the user should have a clean git status before running this code; all the changes made to their project should be contained in a single commit. This makes it easy to see what configuration changes were required for the target platform. It also allows the user to easily roll their project back to a clean state if they decide they don&#8217;t like any of the changes that were made.</p><p>When users run the <code>simple_deploy</code> command, it runs <code>git status</code> in the background before making any changes. If the output indicates the presence of uncommitted changes, the project exits with a message asking the user to commit their existing changes and then run the command again.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!m7cR!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F950bfeed-8607-4030-b5fe-7899c3736632_2552x1788.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!m7cR!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F950bfeed-8607-4030-b5fe-7899c3736632_2552x1788.png 424w, https://substackcdn.com/image/fetch/$s_!m7cR!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F950bfeed-8607-4030-b5fe-7899c3736632_2552x1788.png 848w, https://substackcdn.com/image/fetch/$s_!m7cR!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F950bfeed-8607-4030-b5fe-7899c3736632_2552x1788.png 1272w, https://substackcdn.com/image/fetch/$s_!m7cR!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F950bfeed-8607-4030-b5fe-7899c3736632_2552x1788.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!m7cR!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F950bfeed-8607-4030-b5fe-7899c3736632_2552x1788.png" width="646" height="452.55494505494505" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/950bfeed-8607-4030-b5fe-7899c3736632_2552x1788.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1020,&quot;width&quot;:1456,&quot;resizeWidth&quot;:646,&quot;bytes&quot;:609778,&quot;alt&quot;:&quot;Terminal window showing a run of `$ python manage.py simple_deploy`&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Terminal window showing a run of `$ python manage.py simple_deploy`" title="Terminal window showing a run of `$ python manage.py simple_deploy`" srcset="https://substackcdn.com/image/fetch/$s_!m7cR!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F950bfeed-8607-4030-b5fe-7899c3736632_2552x1788.png 424w, https://substackcdn.com/image/fetch/$s_!m7cR!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F950bfeed-8607-4030-b5fe-7899c3736632_2552x1788.png 848w, https://substackcdn.com/image/fetch/$s_!m7cR!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F950bfeed-8607-4030-b5fe-7899c3736632_2552x1788.png 1272w, https://substackcdn.com/image/fetch/$s_!m7cR!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F950bfeed-8607-4030-b5fe-7899c3736632_2552x1788.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Running <code>simple_deploy</code> with uncommitted changes generates an error message. You can override this behavior using the <code>&#8212;-ignore-unclean-git</code> flag.</figcaption></figure></div><p>However, there are some uncommitted changes that are acceptable, which shouldn&#8217;t block the code&#8217;s execution. For example, the user may have added <code>django-simple-deploy</code> to their project&#8217;s requirements. Or, they may have run the command once and fixed an issue that blocked configuration. Running the command creates a log directory, and that directory is added to <em>.gitignore</em>. We don&#8217;t want to block execution based on these kinds of changes.</p><h3>Examining changed files</h3><p>One of the simplest ways to check for uncommitted changes is to see which files have been changed. Here&#8217;s two lines of code from a function that checks whether it&#8217;s okay to proceed with modifying the user&#8217;s project:</p><pre><code>if any([path.name not in allowed_modifications for path in modified_paths]):
    return False</code></pre><p>This code looks for any file that&#8217;s been modified, that&#8217;s unrelated to a <code>simple_deploy</code> run. If any such files exist the function returns <code>False</code>, indicating it&#8217;s not okay to proceed.</p><h3>Significant and insignificant names</h3><p>Let&#8217;s look at just the list comprehension in this code:</p><pre><code>[<strong>path</strong>.name not in <strong>allowed_modifications</strong> for <strong>path</strong> in <strong>modified_paths</strong>]</code></pre><p>We&#8217;re thinking about how to name things, so let&#8217;s write down all the names used here:</p><pre><code>path
allowed_modifications
path
modified_paths</code></pre><p>Two of these names are defined outside the comprehension: <code>allowed_modifications</code>, and <code>modified_paths</code>. The other name, <code>path</code>, is only used inside the comprehension.<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-2" href="#footnote-2" target="_self">2</a></p><p>Here&#8217;s a version of the comprehension that de-emphasizes the name <code>path</code>:</p><pre><code>[<strong>p</strong>.name not in <strong>allowed_modifications</strong> for <strong>p</strong> in <strong>modified_paths</strong>]</code></pre><p>We don&#8217;t often use single-letter variable names, because they lack context. But in a comprehension, all the context is contained in a single line. Using the name <code>p</code> here emphasizes a few things:</p><ul><li><p>It&#8217;s the <code>name</code> attribute of the path that we&#8217;re focusing on;</p></li><li><p>We&#8217;re looking for names in <code>allowed_modifications</code>;</p></li><li><p>The paths we&#8217;re examining are coming from <code>modified_paths</code>.</p></li></ul><p>These are exactly the things that I want to call the reader&#8217;s attention to, if they&#8217;re unfamiliar with this codebase.</p><p>This is especially noticeable if we make the opposite kind of change, to a more verbose set of names:</p><pre><code>[<strong>modified_path</strong>.name not in <strong>allowed_modifications</strong> for <strong>modified_path</strong> in <strong>modified_paths</strong>]</code></pre><p>This is a common way to name things if we&#8217;re accustomed to using plural names for lists, and then using the singular version of that name in the opening line of a <code>for</code> loop:</p><pre><code>for <strong>modified_path</strong> in <strong>modified_paths</strong>:
    ...</code></pre><p>In the context of a full loop, where the first line is less busy than a comprehension, this naming approach works. That&#8217;s especially true if the block that follows has any degree of complexity.</p><h3>Coming back to <code>any()</code></h3><p>If it&#8217;s not clear what this code does, consider the emphasis shown in this version of the comprehension:</p><pre><code>[<strong>p.name not in allowed_modifications</strong> for p in modified_paths]</code></pre><p>The bold expression here will always evaluate to <code>True</code> or <code>False</code>. So we&#8217;ll end up with a list like this:</p><pre><code>[False, False, True, False]</code></pre><p>Wrapping <code>any()</code> around this list returns <code>True</code> if any of the values in the list are <code>True</code>, and <code>False</code> otherwise:</p><pre><code>&gt;&gt;&gt; <strong>any([False, False, True, False])</strong>
True</code></pre><p>The original code is a little hard to reason about out of context. If <em>any</em> of the modified files are not in the list of allowed modifications, the function returns <code>False</code>, indicating it&#8217;s not okay to proceed in configuring the user&#8217;s project. If none of the modified files are in that list, it will return <code>True</code> and we can proceed.</p><h3>Conclusions</h3><p>Naming things really is hard. I think when people say that, we often think about times where it was hard to come up with a descriptive name for an abstract concept we were working with. But many times there are smaller naming decisions that affect how readable our code is, especially to people who aren&#8217;t very familiar with the overall codebase.</p><p>When you&#8217;re writing a comprehension, consider using short names for the variables that only exist inside the comprehension. They should make sense to people reading your code, and draw attention to the more significant names that exist outside the comprehension itself. If you recognize other situations where a variable is only used in a single line, or in an otherwise isolated context, consider using short names there as well.</p><p>Don&#8217;t go overboard. For example, single-letter variable names in a standard <code>for</code> loop will probably make your code less readable.</p><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-1" href="#footnote-anchor-1" class="footnote-number" contenteditable="false" target="_self">1</a><div class="footnote-content"><p>The subtitle of this post is a play on an old programming joke that most readers have probably heard many times before. If you&#8217;re not familiar with this joke, here&#8217;s one variation:</p><blockquote><p><em>There are two hard things in programming: naming things, cache invalidation, and off-by-one errors.</em></p></blockquote><p>If you haven&#8217;t heard this before, I&#8217;m happy to be the first to share it with you. :)</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-2" href="#footnote-anchor-2" class="footnote-number" contenteditable="false" target="_self">2</a><div class="footnote-content"><p>Note that <code>name</code> in <code>path.name</code> is also used in the comprehension, but it&#8217;s not a variable name that we have control over. There&#8217;s no meaningful way to change the name of <code>name</code>.</p></div></div>]]></content:encoded></item><item><title><![CDATA[The joys of holiday coding]]></title><description><![CDATA[MP 73: If you're writing code today, you're not alone.]]></description><link>https://mostlypython.substack.com/p/the-joys-of-holiday-coding</link><guid isPermaLink="false">https://mostlypython.substack.com/p/the-joys-of-holiday-coding</guid><dc:creator><![CDATA[Eric Matthes]]></dc:creator><pubDate>Mon, 25 Dec 2023 17:30:30 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!xqGp!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcd056581-642b-4eda-8ccc-42dc2101088d_1112x835.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>My family is not religious, but we celebrate Christmas as an occasion to give gifts, recognize the transitions of mid-winter, and appreciate one another. It&#8217;s December 23 right now, and while I should put away my programming work to focus on holiday things, I keep going back to the project I&#8217;m working on. I don&#8217;t have a hard deadline for this project, and I don&#8217;t feel much pressure to get it done. I keep going back to it because interesting little problems keep coming up, and it&#8217;s hard to fully let go of it for a while.</p><p>This tension between wanting to let go of &#8220;work&#8221; and focus fully on holiday festivities has me thinking about all the projects I&#8217;ve done over the holidays in my life. Working on my own projects feels really good during the holidays, and brings up surprisingly strong feelings.</p><p>For most of my life, programming was not part of my daily work. I was a math and science teacher for a long time. Outside of a few school-related projects, I never got to write code during work hours. All my programming work had to be done in the early morning hours, late at night, or on weekends.</p><p>During those years, I did some of my best programming work over the holidays. A couple weeks of no school, the short days of winter, and fewer obligations meant I could curl up on the couch with a laptop and build out another side project. I remember one break when I wrote a C project to plot 3d functions, before plotting libraries were common. On another break I learned 3d modeling using <a href="https://openscad.org">OpenSCAD</a>. I wrote a Python program that generated OpenSCAD code, and made a Koch snowflake. I had it printed in stainless steel, and now that ornament hangs on our tree every year.</p><p>I got my first mechanical keyboard as a Christmas gift one year. Everyone in our family gets to play with their gifts on Christmas, so I sat alone for a couple hours and loudly coded a new website.<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-1" href="#footnote-1" target="_self">1</a> It was a tool for building math problems for my students based on what they were interested in, and I ended up using it throughout my last few years of teaching.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!xqGp!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcd056581-642b-4eda-8ccc-42dc2101088d_1112x835.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!xqGp!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcd056581-642b-4eda-8ccc-42dc2101088d_1112x835.png 424w, https://substackcdn.com/image/fetch/$s_!xqGp!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcd056581-642b-4eda-8ccc-42dc2101088d_1112x835.png 848w, https://substackcdn.com/image/fetch/$s_!xqGp!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcd056581-642b-4eda-8ccc-42dc2101088d_1112x835.png 1272w, https://substackcdn.com/image/fetch/$s_!xqGp!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcd056581-642b-4eda-8ccc-42dc2101088d_1112x835.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!xqGp!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcd056581-642b-4eda-8ccc-42dc2101088d_1112x835.png" width="658" height="494.091726618705" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/cd056581-642b-4eda-8ccc-42dc2101088d_1112x835.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:835,&quot;width&quot;:1112,&quot;resizeWidth&quot;:658,&quot;bytes&quot;:163318,&quot;alt&quot;:&quot;Website called Slice of Pi, showing a problem titled \&quot;Spirograph Predictions\&quot;. The problem has tags \&quot;fractions\&quot; and \&quot;spirograph\&quot;.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Website called Slice of Pi, showing a problem titled &quot;Spirograph Predictions&quot;. The problem has tags &quot;fractions&quot; and &quot;spirograph&quot;." title="Website called Slice of Pi, showing a problem titled &quot;Spirograph Predictions&quot;. The problem has tags &quot;fractions&quot; and &quot;spirograph&quot;." srcset="https://substackcdn.com/image/fetch/$s_!xqGp!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcd056581-642b-4eda-8ccc-42dc2101088d_1112x835.png 424w, https://substackcdn.com/image/fetch/$s_!xqGp!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcd056581-642b-4eda-8ccc-42dc2101088d_1112x835.png 848w, https://substackcdn.com/image/fetch/$s_!xqGp!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcd056581-642b-4eda-8ccc-42dc2101088d_1112x835.png 1272w, https://substackcdn.com/image/fetch/$s_!xqGp!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcd056581-642b-4eda-8ccc-42dc2101088d_1112x835.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Slice of Pi, a project that lets you write math problems and then add tags based on the mathematical content and the context. It was great for finding problems that related to students&#8217; interests, and matched their current level in math. I may revive this project in 2024.</figcaption></figure></div><p>These are sweet memories, and I sometimes justify this time by pointing out how much I learned during those work sessions. But I wasn&#8217;t just working; programming is really fun and satisfying when you get to work on whatever project you want to.</p><p>If you&#8217;re writing code over the holidays, please know that you&#8217;re not alone. There&#8217;s a lot of people out there writing code for the simple joy it brings. There&#8217;s also a lot of people writing code because they&#8217;re at a point in life where you grab every spare minute you can, to learn as much as you can and hopefully build a better life for yourself.</p><p>Whatever your reason for writing code today and in the days that follow, I hope it&#8217;s going well. And if you&#8217;re taking a break from coding over the holidays, I hope you find renewed joy in your work when you get back to it. :)</p><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-1" href="#footnote-anchor-1" class="footnote-number" contenteditable="false" target="_self">1</a><div class="footnote-content"><p>I learned about silent switches a short while later, and have since moved on from keyboards that annoy other family members.</p></div></div>]]></content:encoded></item><item><title><![CDATA[Empathetic testing]]></title><description><![CDATA[MP 72: What exactly are units, and how do we test them?]]></description><link>https://mostlypython.substack.com/p/empathetic-testing</link><guid isPermaLink="false">https://mostlypython.substack.com/p/empathetic-testing</guid><dc:creator><![CDATA[Eric Matthes]]></dc:creator><pubDate>Thu, 21 Dec 2023 17:30:35 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!4Fg5!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9dc0378d-46f6-4e30-9f41-77d7c4990d35_640x497.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><strong>Note: </strong><em>This post was inspired by a talk at DjangoCon US 2023, but this post is not specific to Django.</em></p><p>I went to DjangoCon US 2023 in Durham this fall, and really enjoyed the talks I was able to attend in person. But there were a bunch more I wanted to see, and now that the recordings are out I&#8217;ve been working my way through the <a href="https://www.youtube.com/watch?v=IhogJBzTyWc&amp;list=PL2NFhrDSOxgX41jqYSi0HmO9Wsf6WDSmf">playlist</a>.</p><p>One of the first talks I wanted to watch was <em><a href="https://www.youtube.com/watch?v=BH9BaJ3o628&amp;list=PL2NFhrDSOxgX41jqYSi0HmO9Wsf6WDSmf&amp;index=12">Empathetic testing</a></em>, by <a href="https://marcgibbons.com">Marc Gibbons</a>. It really resonated with my thoughts about testing, so I wanted to share some of his perspectives, and offer some reactions to the points he made.</p><h3>What is a unit, anyway?</h3><p>If you&#8217;ve dealt with testing at all, you almost certainly started with <em>unit</em> testing. Before reading further, take a moment and ask yourself what a <em>unit</em> is. Try to commit to a specific answer before reading on.</p><p>Despite what many people probably think, the term <em>unit</em> in <em>unit testing</em> doesn&#8217;t have a concrete definition that everyone agrees on. I want to keep this post short, so I&#8217;m just going to share my own working definition:</p><blockquote><p>A <em>unit</em> is a block of code that&#8217;s worth testing in isolation.</p></blockquote><p>Notice the phrasing here, <em>block of code</em>. I would guess that many people would define a unit as a <em>function</em>. But I&#8217;ve seen plenty of functions that are clearly larger than what I think of as a unit.<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-1" href="#footnote-1" target="_self">1</a></p><h3>Signs of a broken test suite</h3><p>I enjoy watching tech talks because of all the real-world stories people share to put their bigger points in context. In this talk, Marc tells a story of a really broken test suite. The context was software for a financial firm, and the example he focused on was a section of code that calculated the amortization table for paying off a loan.<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-2" href="#footnote-2" target="_self">2</a></p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!4Fg5!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9dc0378d-46f6-4e30-9f41-77d7c4990d35_640x497.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!4Fg5!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9dc0378d-46f6-4e30-9f41-77d7c4990d35_640x497.png 424w, https://substackcdn.com/image/fetch/$s_!4Fg5!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9dc0378d-46f6-4e30-9f41-77d7c4990d35_640x497.png 848w, https://substackcdn.com/image/fetch/$s_!4Fg5!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9dc0378d-46f6-4e30-9f41-77d7c4990d35_640x497.png 1272w, https://substackcdn.com/image/fetch/$s_!4Fg5!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9dc0378d-46f6-4e30-9f41-77d7c4990d35_640x497.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!4Fg5!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9dc0378d-46f6-4e30-9f41-77d7c4990d35_640x497.png" width="554" height="430.215625" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/9dc0378d-46f6-4e30-9f41-77d7c4990d35_640x497.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:497,&quot;width&quot;:640,&quot;resizeWidth&quot;:554,&quot;bytes&quot;:51289,&quot;alt&quot;:&quot;shows 59 payments of 290.00, and one payment of 289.44.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="shows 59 payments of 290.00, and one payment of 289.44." title="shows 59 payments of 290.00, and one payment of 289.44." srcset="https://substackcdn.com/image/fetch/$s_!4Fg5!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9dc0378d-46f6-4e30-9f41-77d7c4990d35_640x497.png 424w, https://substackcdn.com/image/fetch/$s_!4Fg5!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9dc0378d-46f6-4e30-9f41-77d7c4990d35_640x497.png 848w, https://substackcdn.com/image/fetch/$s_!4Fg5!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9dc0378d-46f6-4e30-9f41-77d7c4990d35_640x497.png 1272w, https://substackcdn.com/image/fetch/$s_!4Fg5!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9dc0378d-46f6-4e30-9f41-77d7c4990d35_640x497.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Sample amortization schedule for a 60-month loan. If you&#8217;re writing code to calculate payments like this, you probably want some thorough testing in place!</figcaption></figure></div><p>Marc&#8217;s team wanted to start using NumPy for some of their financial calculations. After some refactoring, the existing tests all passed. However, the site was broken for end users. They fixed the code, so the site worked again for end users. But now the tests failed. This is exactly the opposite of how tests should work! The test were <em>passing</em> when the site was <em>broken</em>, but <em>failed</em> when the site was <em>working</em>.</p><p>It&#8217;s important to note that this was not a terrible test suite. The test suite had served its purpose for some time before this refactoring work. An imperfect test suite that&#8217;s run regularly is much better than a perfect test suite that hasn&#8217;t been written yet.</p><h3>Avoid implementation bias</h3><p>When Marc was discussing the broken test suite, he described it like this:</p><blockquote><p><em>Our tests aren&#8217;t actually testing how things work; they&#8217;re testing the way the thing was built.</em></p></blockquote><p>While I&#8217;ve been aware of this kind of issue with tests, I hadn&#8217;t heard it named before. Marc&#8217;s larger point was to avoid <em>implementation bias</em>. A unit test should verify the <em>behavior</em> of a unit. It&#8217;s rarely important, or even valuable, to verify the <em>implementation</em> of a unit. Yet it&#8217;s easy to fall into that approach to writing tests sometimes if you&#8217;re not aware of the problems it can create.</p><p>The test suite he was working on broke because some of the tests were verifying the behavior of units, in a way that was overly dependent on the implementation of those units. A brittle test is technical debt. It&#8217;s helpful only as long as the current implementation is in place. A test written with implementation bias hinders refactoring, rather than supporting the evolution of the codebase.</p><h3>Empathetic testing</h3><p>Marc had an interesting take on how to address this kind of brittleness in tests. Rather than just directing people to test behaviors in a way that&#8217;s independent of the implementation, he suggested that people should approach testing from an empathetic perspective. He encouraged people to think of the future maintainers of the projects we work on, not just from the perspective of imagining them running our test suite, but from the perspective of realizing that they&#8217;ll probably be able to write better code than we&#8217;re currently writing.</p><p>Some people laugh at these kinds of suggestions. They might say things like, &#8220;It&#8217;s just code, there&#8217;s no feelings here!&#8221; But programming is full of feelings. We have to feel a bit of ego to write working code in the first place.</p><p>Every time we sit down at a keyboard, there&#8217;s some part of us that says, &#8220;<em><strong>I</strong></em> am the one who can solve this problem!&#8221; We need some pride and hubris in order to start working on a project. Marc&#8217;s point was that another voice should remind us, &#8220;Someone may well come along and improve this implementation at some point.&#8221; Our tests should server ourselves when we&#8217;re writing them, but they should also serve those future developers who see the problem even more clearly than we do. (And sometimes, we <em>are</em> that future contributor.)</p><h3>Conclusions</h3><p>I have a number of takeaways from watching this talk, and reflecting on the points that were brought up:</p><ul><li><p>Testing is almost always about verifying <em>behaviors</em>, not <em>implementations</em>.</p></li><li><p>We need to be thoughtful about the tests we write, even when writing small unit tests. We can ask ourselves, will this test <em>support</em> refactoring, or <em>hinder</em> refactoring?</p></li><li><p>Think clearly about the &#8220;API&#8221; of your smallest pieces of code. If a function&#8217;s external usage is consistent and stable, we can write unit tests that interact with the function in a stable and non-brittle way. This holds true even as the code inside the function gets refactored.</p></li></ul><p>Testing is not separate from programming. Writing, using, and understanding a test suite will help you understand your code better, and think more carefully about the overall structure of your project as well.</p><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-1" href="#footnote-anchor-1" class="footnote-number" contenteditable="false" target="_self">1</a><div class="footnote-content"><p>Interestingly, your working definition of a unit can help you decide how big your functions should be. If a block of code is worth testing, that block of code probably deserves to be in its own function. Similarly, if a block of code doesn&#8217;t need its own unit test, that block might not need to be in a function of its own.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-2" href="#footnote-anchor-2" class="footnote-number" contenteditable="false" target="_self">2</a><div class="footnote-content"><p>If you&#8217;re unfamiliar with this term, an <em>amortization table</em> is the list of payments someone needs to make in order to pay off a loan on time.</p><p>As a math teacher, this was one of my favorite things to teach students. Give them a loan amount, say $15,000 for a vehicle they want to buy. Give them an APR, say 6%. Finally, given them a term length, such as 60 months. Ask them how much they&#8217;ll need to pay each month in order to pay off the loan in 60 months.</p><p>You can do this with a spreadsheet. I love showing people how to do this, because it pulls back the curtain about how banks and other financial institutions come up with these kinds of numbers. Anyone who can analyze a loan like this is less likely to be taken advantage of financially. The software Marc was talking about automates these kinds of calculations.</p></div></div>]]></content:encoded></item><item><title><![CDATA[Django 5.0 is out!]]></title><description><![CDATA[MP 71: And upgrading is probably easier than you think.]]></description><link>https://mostlypython.substack.com/p/django-50-is-out</link><guid isPermaLink="false">https://mostlypython.substack.com/p/django-50-is-out</guid><dc:creator><![CDATA[Eric Matthes]]></dc:creator><pubDate>Thu, 14 Dec 2023 17:30:41 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!Y8ay!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F42a4ac3f-849b-4e87-b775-3f97ba0949e3_1590x935.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Django 5.0 was officially released last week. In this post, I&#8217;ll share a few things about this latest release:</p><ul><li><p>Where to go to learn about the newest features in Django;</p></li><li><p>How to make sense of Django&#8217;s version numbering system;</p></li><li><p>How to see if your project can easily be upgraded to 5.0;</p></li><li><p>How to deploy a Django project in just three steps.</p></li></ul><p>If you&#8217;re using Django and aren&#8217;t already familiar with its release cycle and upgrade process, read on for a little insight into how it works.</p><h3>What&#8217;s new in 5.0</h3><p>A lot has already been written about the new features in Django 5.0. Rather than repeating what others have written, I&#8217;ll simply point you to some of the resources that I&#8217;ve found most helpful:</p><h5>Official announcement</h5><p>The <a href="https://www.djangoproject.com/weblog/2023/dec/04/django-50-released/">official announcement</a> is always short and to the point. It mentions a few highlights, and includes links to relevant parts of the documentation.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Y8ay!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F42a4ac3f-849b-4e87-b775-3f97ba0949e3_1590x935.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Y8ay!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F42a4ac3f-849b-4e87-b775-3f97ba0949e3_1590x935.png 424w, https://substackcdn.com/image/fetch/$s_!Y8ay!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F42a4ac3f-849b-4e87-b775-3f97ba0949e3_1590x935.png 848w, https://substackcdn.com/image/fetch/$s_!Y8ay!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F42a4ac3f-849b-4e87-b775-3f97ba0949e3_1590x935.png 1272w, https://substackcdn.com/image/fetch/$s_!Y8ay!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F42a4ac3f-849b-4e87-b775-3f97ba0949e3_1590x935.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Y8ay!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F42a4ac3f-849b-4e87-b775-3f97ba0949e3_1590x935.png" width="660" height="388.02197802197804" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/42a4ac3f-849b-4e87-b775-3f97ba0949e3_1590x935.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:856,&quot;width&quot;:1456,&quot;resizeWidth&quot;:660,&quot;bytes&quot;:255292,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!Y8ay!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F42a4ac3f-849b-4e87-b775-3f97ba0949e3_1590x935.png 424w, https://substackcdn.com/image/fetch/$s_!Y8ay!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F42a4ac3f-849b-4e87-b775-3f97ba0949e3_1590x935.png 848w, https://substackcdn.com/image/fetch/$s_!Y8ay!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F42a4ac3f-849b-4e87-b775-3f97ba0949e3_1590x935.png 1272w, https://substackcdn.com/image/fetch/$s_!Y8ay!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F42a4ac3f-849b-4e87-b775-3f97ba0949e3_1590x935.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">The official <a href="https://www.djangoproject.com/weblog/2023/dec/04/django-50-released/">announcement</a> of the release of Django 5.0.</figcaption></figure></div><h5>Release notes</h5><p>If you want to keep up with Django development, it&#8217;s well worth your time to scroll through the entire set of <a href="https://docs.djangoproject.com/en/5.0/releases/5.0/">release notes</a>. I don&#8217;t encourage you to do a close reading of the entire document. However, becoming familiar with the structure of this document, and glancing through everything in it, will give you a good sense of how much active development Django sees in a single release cycle.</p><p>The most significant new features are mentioned in the official announcement, and in many blog posts. They&#8217;re also described in the first few sections of the release notes. But there&#8217;s a long list of minor features that are worth reading through, especially in areas of Django that are most relevant to your work. For example, if you do a lot of GIS work, you&#8217;d probably be interested in looking at the specific release notes for <a href="https://docs.djangoproject.com/en/5.0/releases/5.0/#django-contrib-gis">django.contrib.gis</a>.</p><p>There are also sections about <a href="https://docs.djangoproject.com/en/5.0/releases/5.0/#backwards-incompatible-changes-in-5-0">backwards incompatible changes</a>, <a href="https://docs.djangoproject.com/en/5.0/releases/5.0/#features-deprecated-in-5-0">deprecated</a> features, and features that were finally <a href="https://docs.djangoproject.com/en/5.0/releases/5.0/#features-removed-in-5-0">removed</a> in 5.0. If you want to make sure your Django projects can be upgraded reasonably efficiently, paying attention to these deprecation sections on an ongoing basis is important.</p><h5><em>New goodies in Django 5.0</em></h5><p>Marius Felisiak is one of the <a href="https://www.djangoproject.com/fundraising/#who-are-the-django-fellows">Django Fellows</a>. It&#8217;s his job to keep up with new developments in Django, so he&#8217;s in an excellent place to <a href="https://fly.io/django-beats/new-goodies-in-django-50/">write up</a> the new features that just came out.</p><h3>Understanding Django&#8217;s version numbering system</h3><p>Django uses a very consistent version numbering system, but unfortunately it&#8217;s a little misleading about what kind of changes people should expect to see in a new release.</p><p>I think people tend to associate <em>X.0</em> releases of any library with a bunch of backwards-incompatible, breaking changes. While Django 5.0 does have <em>some</em> backwards-incompatible changes, they aren&#8217;t particularly significant. Each release of Django has some breaking changes; the goal is to manage these carefully, so that people can steadily upgrade their projects over long periods of time. If you read each version&#8217;s release notes, you&#8217;ll have over a year of notice about any upcoming deprecations that are relevant to your work.</p><h5>Django&#8217;s numbering system</h5><p>Django makes three releases per major version number: <em>X.0</em>, <em>X.1</em>, and <em>X.2</em>. A new version comes out about every eight months.</p><p>For example, these are the most recent releases:</p><pre><code>4.0, 4.1, 4.2, 5.0</code></pre><p>And here are the next few versions we&#8217;ll see:</p><pre><code>5.1, 5.2, 6.0, 6.1, 6.2</code></pre><h5>LTS releases</h5><p>It&#8217;s helpful to focus on the <em>X.2</em> releases when trying to understand Django&#8217;s version numbering system. The <em>X.2</em> releases are considered LTS (long-term support) releases.</p><p>Here&#8217;s how this is described in the <a href="https://docs.djangoproject.com/en/4.2/releases/4.2/">4.2</a> release notes:</p><blockquote><p><em>Django 4.2 is designated as a <a href="https://docs.djangoproject.com/en/4.2/internals/release-process/#term-Long-term-support-release">long-term support release</a>. It will receive security updates for at least three years after its release.</em></p></blockquote><h5>Non-LTS releases</h5><p>The <em>X.0</em> and <em>X.1</em> releases receive active updates for eight months, and security updates for another eight months. This means Django 5.0 will receive active updates through August 2024, and security updates through April 2025.</p><h5>Visualizing support</h5><p>There&#8217;s a great visualization of the support lifetimes for each version in the <em>Supported Versions</em> section of Django&#8217;s <a href="https://www.djangoproject.com/download/">Download</a> page:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!OCtW!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1d6902f3-0cef-400d-89e0-ac50f980d245_1034x484.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!OCtW!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1d6902f3-0cef-400d-89e0-ac50f980d245_1034x484.png 424w, https://substackcdn.com/image/fetch/$s_!OCtW!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1d6902f3-0cef-400d-89e0-ac50f980d245_1034x484.png 848w, https://substackcdn.com/image/fetch/$s_!OCtW!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1d6902f3-0cef-400d-89e0-ac50f980d245_1034x484.png 1272w, https://substackcdn.com/image/fetch/$s_!OCtW!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1d6902f3-0cef-400d-89e0-ac50f980d245_1034x484.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!OCtW!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1d6902f3-0cef-400d-89e0-ac50f980d245_1034x484.png" width="670" height="313.6170212765957" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/1d6902f3-0cef-400d-89e0-ac50f980d245_1034x484.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:484,&quot;width&quot;:1034,&quot;resizeWidth&quot;:670,&quot;bytes&quot;:34276,&quot;alt&quot;:&quot;Chart showing the longer support lifetime of LTS releases, and the overlap between support periods for different versions.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Chart showing the longer support lifetime of LTS releases, and the overlap between support periods for different versions." title="Chart showing the longer support lifetime of LTS releases, and the overlap between support periods for different versions." srcset="https://substackcdn.com/image/fetch/$s_!OCtW!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1d6902f3-0cef-400d-89e0-ac50f980d245_1034x484.png 424w, https://substackcdn.com/image/fetch/$s_!OCtW!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1d6902f3-0cef-400d-89e0-ac50f980d245_1034x484.png 848w, https://substackcdn.com/image/fetch/$s_!OCtW!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1d6902f3-0cef-400d-89e0-ac50f980d245_1034x484.png 1272w, https://substackcdn.com/image/fetch/$s_!OCtW!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1d6902f3-0cef-400d-89e0-ac50f980d245_1034x484.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Django&#8217;s support cycle. LTS releases are supported for three years, and other releases are supported for 16 months.</figcaption></figure></div><p>In a <a href="https://fosstodon.org/@CodenameTim/111580669802909026">followup conversation</a> after this post was initially released, I was reminded that there&#8217;s a better version of this timeline:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!iHUf!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F992d665d-bce0-4a1b-a1fc-14100e4d736b_1342x958.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!iHUf!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F992d665d-bce0-4a1b-a1fc-14100e4d736b_1342x958.png 424w, https://substackcdn.com/image/fetch/$s_!iHUf!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F992d665d-bce0-4a1b-a1fc-14100e4d736b_1342x958.png 848w, https://substackcdn.com/image/fetch/$s_!iHUf!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F992d665d-bce0-4a1b-a1fc-14100e4d736b_1342x958.png 1272w, https://substackcdn.com/image/fetch/$s_!iHUf!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F992d665d-bce0-4a1b-a1fc-14100e4d736b_1342x958.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!iHUf!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F992d665d-bce0-4a1b-a1fc-14100e4d736b_1342x958.png" width="646" height="461.15350223546943" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/992d665d-bce0-4a1b-a1fc-14100e4d736b_1342x958.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:958,&quot;width&quot;:1342,&quot;resizeWidth&quot;:646,&quot;bytes&quot;:439127,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!iHUf!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F992d665d-bce0-4a1b-a1fc-14100e4d736b_1342x958.png 424w, https://substackcdn.com/image/fetch/$s_!iHUf!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F992d665d-bce0-4a1b-a1fc-14100e4d736b_1342x958.png 848w, https://substackcdn.com/image/fetch/$s_!iHUf!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F992d665d-bce0-4a1b-a1fc-14100e4d736b_1342x958.png 1272w, https://substackcdn.com/image/fetch/$s_!iHUf!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F992d665d-bce0-4a1b-a1fc-14100e4d736b_1342x958.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Another <a href="https://jefftriplett.com/django-release-cycle/">visualization</a> of the Django release cycle, which does a nicer job of showing which versions are still being supported.</figcaption></figure></div><p>This <a href="https://jefftriplett.com/django-release-cycle/">visualization</a> uses colors to indicate which state each release is currently in. Red indicates an older, unmaintained release. Green releases are receiving security updates. Yellow releases are receiving bug fixes and security updates, and blue versions are prereleases.</p><h5>Recommendations</h5><p>So which version should you use, and how should you think about upgrading? Django has built a well-deserved reputation for stability and reliability. If a feature makes it into a release, it&#8217;s almost certainly not going to cause you problems in production.</p><p>The old recommendation was to consider LTS releases unless you needed the features in a non-LTS release. That advice is outdated at this point. I would recommend the following:</p><ul><li><p>If you can commit to upgrading your project at least once a year, always upgrade to the most recent version that&#8217;s been released.</p></li><li><p>If you can&#8217;t commit to upgrading at least once a year, consider focusing on LTS releases.</p></li></ul><p>One advantage of staying on the latest release is that your upgrades are likely to be easier. Regardless of which strategy you adopt, you should always update your version to the latest point release. That is, if you&#8217;re using Django 5.0, you should upgrade to 5.0.1 as soon as it&#8217;s released. These minor point releases contain important security patches, and should never cause breaking changes.</p><p><strong>Note:</strong> <em>If you want to read all the details to inform your own upgrade policy, see the official documentation page for Django&#8217;s <a href="https://docs.djangoproject.com/en/4.2/internals/release-process/">release process</a>.</em></p><h3>Upgrading a project</h3><p>I have a <a href="https://github.com/ehmatthes/dsd_sample_blog_reqtxt/tree/0f42f3f6e46c14f40d7fe9ad5f9a9a2e23ee4036">project</a> that&#8217;s currently running on Django 4.1. This is a sample project, but I&#8217;m going to show how to update it to run on Django 5.0. If you have a project that isn&#8217;t overly complex, you can probably follow a similar upgrade process.</p><p>Upgrading to the latest version is often not as hard as people think. You can try to upgrade in an isolated environment, and get a quick sense of how easily your upgrade path will be. This is especially true if you have a reasonably effective set of tests for your project.</p><h5>BlogMaker Lite, on Django 4.1</h5><p>BlogMaker Lite is a sample project I made a while back when developing a separate project focused on deployment. This is a relatively simple Django project, but it&#8217;s not trivial. It has user accounts, and uses a third-party package for styling.</p><p>I haven&#8217;t touched this project in a while. Let&#8217;s clone it, start the development server, and make sure it still works:<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-1" href="#footnote-1" target="_self">1</a></p><pre><code>$ <strong>git clone https://github.com/ehmatthes/dsd_sample_blog_reqtxt.git</strong>
$ <strong>cd dsd_sample_blog_reqtxt</strong>
$ <strong>python -m venv b_env</strong>
$ <strong>source b_env/bin/activate</strong>
(b_env)$ <strong>pip install -r requirements.txt</strong>
(b_env)$ <strong>python manage.py migrate</strong>
(b_env)$ <strong>python manage.py runserver</strong></code></pre><p>Here&#8217;s the project&#8217;s home page:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Tc0y!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F796d5387-fded-4fb7-8971-4772f01520ea_1042x585.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Tc0y!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F796d5387-fded-4fb7-8971-4772f01520ea_1042x585.png 424w, https://substackcdn.com/image/fetch/$s_!Tc0y!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F796d5387-fded-4fb7-8971-4772f01520ea_1042x585.png 848w, https://substackcdn.com/image/fetch/$s_!Tc0y!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F796d5387-fded-4fb7-8971-4772f01520ea_1042x585.png 1272w, https://substackcdn.com/image/fetch/$s_!Tc0y!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F796d5387-fded-4fb7-8971-4772f01520ea_1042x585.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Tc0y!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F796d5387-fded-4fb7-8971-4772f01520ea_1042x585.png" width="662" height="371.6602687140115" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/796d5387-fded-4fb7-8971-4772f01520ea_1042x585.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:585,&quot;width&quot;:1042,&quot;resizeWidth&quot;:662,&quot;bytes&quot;:56603,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!Tc0y!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F796d5387-fded-4fb7-8971-4772f01520ea_1042x585.png 424w, https://substackcdn.com/image/fetch/$s_!Tc0y!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F796d5387-fded-4fb7-8971-4772f01520ea_1042x585.png 848w, https://substackcdn.com/image/fetch/$s_!Tc0y!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F796d5387-fded-4fb7-8971-4772f01520ea_1042x585.png 1272w, https://substackcdn.com/image/fetch/$s_!Tc0y!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F796d5387-fded-4fb7-8971-4772f01520ea_1042x585.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">BlogMaker Lite&#8217;s home page, running on a local development server.</figcaption></figure></div><p>The home page renders fine.</p><p>Let&#8217;s run the tests, and see if the project functions correctly. This project doesn&#8217;t have a standard Django test suite. I was focused on using this project to run repeated deployment attempts, so I was most interested in whether the project functioned correctly regardless of where and how it was deployed. It&#8217;s a simple set of calls to the project&#8217;s URLs using <code>requests</code>, followed by assertions about the contents of the response object. These days I&#8217;d probably use pytest for this, but I have no pressing need to change the current test script.</p><p>Here&#8217;s the test run:</p><pre><code>(b_env)$ <strong>python test_deployed_app_functionality.py \
    --url http://localhost:8000</strong>

Testing functionality of deployed app at http://localhost:8000/...

  Checking anonymous home page...
  Checking empty anonmyous all_blogs page...
  Checking empty anonmyous latest_posts page...
  ...</code></pre><p>The tests all pass, including the ones that make a new user, a new blog, and a couple blog posts. The last test fails, but that test checks that <code>DEBUG</code> is set to <code>False</code> for a deployed project. This isn&#8217;t a live deployment, so I was expecting that test to fail.</p><h5>BlogMaker Lite, on Django 5.0</h5><p>Now let&#8217;s upgrade Django to 5.0, and see if the tests still pass:</p><pre><code>(b_env)$ <strong>git checkout -b try_django_50</strong>
(b_env)$ <strong>pip install --upgrade django==5.0</strong>
Collecting django==5.0
  ...
Successfully installed asgiref-3.7.2 django-5.0
(b_env)$ <strong>python manage.py runserver</strong>
...
Django version 5.0, using settings 'blog.settings'
...</code></pre><p>I&#8217;m making a new branch for this work, so it&#8217;s easy to move forward with the upgrade or roll back to the 4.1 version of the project.</p><p>Installation of Django 5.0 was successful. I also restarted the development server, so it&#8217;s using the new version of Django to serve requests.</p><p>Now we can run the tests again. The test script has a flag, <code>&#8212;-flush-db</code>, that flushes the database before running the tests. This is useful when testing repeated deployments. I&#8217;ll use that here, because the tests look for only the data that&#8217;s created during a single run of the test suite:</p><pre><code>(b_env)$ <strong>python test_deployed_app_functionality.py \
    --url http://localhost:8000 --flush-db</strong>
Flushing db before running test...
  Flushed db.

Testing functionality of deployed app at http://localhost:8000/...

  Checking anonymous home page...
  Checking empty anonmyous all_blogs page...
  Checking empty anonmyous latest_posts page...
  ...</code></pre><p>The tests still pass, which means the basic functionality of this project works on Django 5.0.</p><h5>Updating requirements</h5><p>To conclude the upgrade process, we can now re-freeze the requirements:</p><pre><code>(b_env)$ <strong>pip freeze &gt; requirements.txt</strong>
(b_env)$ <strong>git commit -am "Upgraded to Django 5.0."</strong>
(b_env)$ <strong>git push origin try_django_50</strong></code></pre><p>I&#8217;ll follow this up by making a pull request and merging this change. The full <a href="https://github.com/ehmatthes/dsd_sample_blog_reqtxt/pull/2">PR</a> includes updates to all the project&#8217;s dependencies, as well as Django. I also added <code>.venv/</code> to the <code>.gitignore</code> file, so people using the more modern convention won&#8217;t accidentally commit their virtual environment.</p><p>Anyone who builds a new environment for the project will be using Django 5.0 from this point forward. If you already had a working version of the project, you could build a new virtual environment using the updated requirements, and you&#8217;d be using 5.0 as well.</p><h5>Try new versions of Django early!</h5><p>Many existing Django projects will run without errors on the newest version of Django. There are numerous stories about people taking really old Django projects and jumping straight to the latest version, without bothering to upgrade through all the intermediate versions. These upgrades often require <em>some</em> changes to the codebase, but not as many as people expect to see.</p><p>Many of the errors that people run into when upgrading to a new version are fairly minor. A quick skim of the release notes usually clarifies the changes that are needed in order to make the project run on the latest version. If you face a more significant issue during the upgrade process, the sooner you know about it the better. You&#8217;ll have more time to prepare a manageable upgrade plan.</p><p>Don&#8217;t put off trying new versions of Django. It&#8217;s often easier than you think it will be, and if it&#8217;s difficult for some reason you&#8217;ll want to know about that sooner rather than later.</p><h3>Bonus: Deploying BlogMaker Lite</h3><p>As a quick bonus, I&#8217;d like to show how quickly you can deploy many Django projects using <a href="https://django-simple-deploy.readthedocs.io/en/latest/">django-simple-deploy</a>, the deployment project I&#8217;ve been working on.</p><p>For example if you have an account on <a href="https://fly.io">Fly.io</a> and have their CLI installed, you can deploy BlogMaker Lite in just three steps. The first is installing django-simple-deploy:</p><pre><code>(b_env)$ <strong>pip install django-simple-deploy</strong>
Collecting django-simple-deploy
...
Successfully installed django-simple-deploy-0.6.1 toml-0.10.2</code></pre><p>Now we need to add simple_deploy to <code>INSTALLED_APPS</code>, in <em>settings.py</em>:</p><pre><code>INSTALLED_APPS = [
    # My apps.
    'blogs',
    'users',

    # Third party apps.
    'django_bootstrap5',
    <strong>'simple_deploy',</strong>

    # Default django apps.
    'django.contrib.admin',
    ...
]</code></pre><p>And finally, we make a call to <code>simple_deploy</code>, specifying the platform we want to deploy to:</p><pre><code>(b_env)$ <strong>python manage.py simple_deploy --platform fly_io --automate-all</strong>
Configuring project for deployment...
...</code></pre><p>The <code>&#8212;automate-all</code> flag tells <code>simple_deploy</code> to inspect the project, configure it for deployment to the specified platform, create the resources needed for deployment, commit changes, and call the platform&#8217;s <code>deploy</code> command. It runs migrations, and opens the project in a new browser tab:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Q5Hf!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fefe6832b-4889-4a28-a92e-3e2b3f6abbae_1042x585.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Q5Hf!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fefe6832b-4889-4a28-a92e-3e2b3f6abbae_1042x585.png 424w, https://substackcdn.com/image/fetch/$s_!Q5Hf!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fefe6832b-4889-4a28-a92e-3e2b3f6abbae_1042x585.png 848w, https://substackcdn.com/image/fetch/$s_!Q5Hf!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fefe6832b-4889-4a28-a92e-3e2b3f6abbae_1042x585.png 1272w, https://substackcdn.com/image/fetch/$s_!Q5Hf!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fefe6832b-4889-4a28-a92e-3e2b3f6abbae_1042x585.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Q5Hf!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fefe6832b-4889-4a28-a92e-3e2b3f6abbae_1042x585.png" width="668" height="375.02879078694815" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/efe6832b-4889-4a28-a92e-3e2b3f6abbae_1042x585.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:585,&quot;width&quot;:1042,&quot;resizeWidth&quot;:668,&quot;bytes&quot;:60534,&quot;alt&quot;:&quot;The same homepage image, showing a Fly.io URL in the address bar.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="The same homepage image, showing a Fly.io URL in the address bar." title="The same homepage image, showing a Fly.io URL in the address bar." srcset="https://substackcdn.com/image/fetch/$s_!Q5Hf!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fefe6832b-4889-4a28-a92e-3e2b3f6abbae_1042x585.png 424w, https://substackcdn.com/image/fetch/$s_!Q5Hf!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fefe6832b-4889-4a28-a92e-3e2b3f6abbae_1042x585.png 848w, https://substackcdn.com/image/fetch/$s_!Q5Hf!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fefe6832b-4889-4a28-a92e-3e2b3f6abbae_1042x585.png 1272w, https://substackcdn.com/image/fetch/$s_!Q5Hf!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fefe6832b-4889-4a28-a92e-3e2b3f6abbae_1042x585.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">BlogMaker Lite, deployed to Fly.io in just three steps using django-simple-deploy.</figcaption></figure></div><p>django-simple-deploy is currently in the pre-1.0 phase, so it won&#8217;t work in all situations. But most of the difficult issues have been sorted out, so bugs that come up now should be easier to address. You can choose from three currently supported platforms: <a href="https://fly.io">Fly.io</a>, <a href="https://platform.sh">Platform.sh</a>, and <a href="https://www.heroku.com">Heroku</a>. If you&#8217;re curious about this project, please give it a try. If you run into any difficulties, feel free to <a href="https://github.com/ehmatthes/django-simple-deploy/issues/new/choose">open an issue</a>.</p><p><strong>Note:</strong> <em>As with all deployment-focused work, please only try django-simple-deploy if you&#8217;re comfortable working with your platform&#8217;s dashboard, and ensuring all deployed resources have been destroyed in a timely manner.</em> </p><h3>Conclusions</h3><p>New versions of popular frameworks like Django are exciting, but they can also mean a pile of work dealing with upgrades. Fortunately, upgrading Django projects to the latest version has gotten much more straightforward over the years.</p><p>If you&#8217;ve put off trying the latest version because you think it will be a lot of work, try setting up a test environment and upgrading your project. It just might go quicker than you think, and you&#8217;ll make all subsequent upgrades easier as well.</p><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-1" href="#footnote-anchor-1" class="footnote-number" contenteditable="false" target="_self">1</a><div class="footnote-content"><p>I used to name my virtual environments <code>something_env/</code>, where <em>something</em> was an abbreviation of the overall project name. I have since adopted the convention of calling every virtual environment <code>.venv/</code>, like most Python developers these days.</p></div></div>]]></content:encoded></item><item><title><![CDATA[PyCon US talk proposals are open for one more week]]></title><description><![CDATA[MP 70: If you're considering speaking, please submit a proposal!]]></description><link>https://mostlypython.substack.com/p/pycon-us-talk-proposals-are-open</link><guid isPermaLink="false">https://mostlypython.substack.com/p/pycon-us-talk-proposals-are-open</guid><dc:creator><![CDATA[Eric Matthes]]></dc:creator><pubDate>Tue, 12 Dec 2023 17:30:39 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5b152916-0814-4d3a-94db-6a354ea57944_2048x1365.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>PyCon US will take place next May in Pittsburgh. The Call for Proposals (CFP) is open through next Monday, 12/18. Preparing a talk takes a lot of work, but submitting a proposal shouldn&#8217;t take too long. If you&#8217;ve considered submitting a proposal, you&#8217;ve got one week left!</p><p>If you&#8217;re not familiar with the proposal process, I&#8217;ll highlight some of the most important things to be aware of.</p><h3>Proposal deadline</h3><p>The deadline for submitting a proposal is 12/18, &#8220;Anywhere on Earth&#8221;. If you haven&#8217;t heard this phrase before, it&#8217;s a way of defining deadlines so that no one has to think about time zones. If it&#8217;s still 12/18 anywhere on Earth, you can still submit a talk proposal. Practically, that means you have until next Monday night to submit a proposal, no matter where you are in the world.</p><h3>Proposal resources</h3><p>There are a number of resources on the PyCon US site that are relevant to speakers. Here&#8217;s a rundown of the most important pages, and what kind of information you&#8217;ll find on each:</p><ul><li><p><strong><a href="https://us.pycon.org">PyCon US home page</a>:</strong> An overview of the conference.</p></li><li><p><strong><a href="https://us.pycon.org/2024/speaking/guidelines/">Proposal guidelines</a>:</strong> An overview of the kinds of proposals you can submit. Also includes a timeline for the proposal process.</p></li><li><p><strong><a href="https://us.pycon.org/2024/speaking/talks/">Proposing a talk</a>:</strong> Suggested topics, topics to avoid, and other advice about making a proposal.</p></li><li><p><strong><a href="https://us.pycon.org/2024/speaking/guidelines/pretalx/">Submitting a proposal</a>:</strong> Directions for actually submitting a talk proposal.</p></li></ul><h3>Kinds of talks</h3><p>There are several kinds of proposals you can submit:</p><ul><li><p><strong>Talks:</strong> Most talks are 30 minutes, although there are some 45-minute slots for important topics.</p></li><li><p><strong>Charlas:</strong> There&#8217;s a Spanish track at PyCon US called Charlas. If you speak Spanish, even if it&#8217;s not your first language, consider submitting a <a href="https://us.pycon.org/2024/speaking/charlas/">Charlas proposal</a>.</p></li><li><p><strong>Tutorials:</strong> You can <a href="https://us.pycon.org/2024/speaking/tutorials/">propose</a> a 3-hour interactive workshop for one of the days preceding the main conference. These are a tremendous amount of work, and are compensated.</p></li><li><p><strong>Posters:</strong> You can propose a <a href="https://us.pycon.org/2024/speaking/posters/">poster</a> that will be on display during part of the conference. There&#8217;s also a designated time for people to meet with you and discuss your work.</p></li></ul><h3>What to talk about</h3><p>PyCon is a wonderful event in no small part due to the balance that the talk committee aims for, and usually achieves. Their goal is to have a full range of topics in the talks that are chosen. There are technical talks at the beginner, intermediate, and expert levels. There are context-focused talks, where people share what they&#8217;re using Python for in their paid work, academic work, and volunteer work. There are numerous presentations about longstanding and newer open source projects.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Cm8u!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5b152916-0814-4d3a-94db-6a354ea57944_2048x1365.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Cm8u!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5b152916-0814-4d3a-94db-6a354ea57944_2048x1365.jpeg 424w, https://substackcdn.com/image/fetch/$s_!Cm8u!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5b152916-0814-4d3a-94db-6a354ea57944_2048x1365.jpeg 848w, https://substackcdn.com/image/fetch/$s_!Cm8u!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5b152916-0814-4d3a-94db-6a354ea57944_2048x1365.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!Cm8u!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5b152916-0814-4d3a-94db-6a354ea57944_2048x1365.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Cm8u!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5b152916-0814-4d3a-94db-6a354ea57944_2048x1365.jpeg" width="658" height="438.36538461538464" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/5b152916-0814-4d3a-94db-6a354ea57944_2048x1365.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:970,&quot;width&quot;:1456,&quot;resizeWidth&quot;:658,&quot;bytes&quot;:620146,&quot;alt&quot;:&quot;Me on stage at DjangoCon US 2022.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Me on stage at DjangoCon US 2022." title="Me on stage at DjangoCon US 2022." srcset="https://substackcdn.com/image/fetch/$s_!Cm8u!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5b152916-0814-4d3a-94db-6a354ea57944_2048x1365.jpeg 424w, https://substackcdn.com/image/fetch/$s_!Cm8u!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5b152916-0814-4d3a-94db-6a354ea57944_2048x1365.jpeg 848w, https://substackcdn.com/image/fetch/$s_!Cm8u!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5b152916-0814-4d3a-94db-6a354ea57944_2048x1365.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!Cm8u!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5b152916-0814-4d3a-94db-6a354ea57944_2048x1365.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">I spoke about django-simple-deploy at DjangoCon US 2022. It&#8217;s a lot of work to prepare for a talk, but it&#8217;s a really satisfying experience. Photo by <a href="https://www.bartpawlik.com">Bart Pawlik</a>.</figcaption></figure></div><p>In short, if you&#8217;re doing something you find interesting with Python, please consider submitting a proposal.</p><p>One thing I&#8217;d love to see discussed next year: How are people using AI assistants in real-world projects? There have been plenty of discussions about how AI assistants respond to specific prompts. But what is it like to do your daily professional work with whatever AI assistant you use? What is it good at? What do you avoid using it for? What changes are you seeing in it over time? Is their consensus on your team about how to use (and not use) AI assistants?</p><p>People are also tired of hearing about AI, so if you have a specific topic to discuss that has nothing to do with AI, please submit that as well!</p><h3>Drafting a proposal</h3><p>Once you have a topic in mind, you can draft a proposal. The PyCon website has a portal for submitting a proposal. It&#8217;s helpful to know the sections you&#8217;ll be asked to fill in before starting your proposal. Some of these are public, and some are kept confidential between you and the conference organizers.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!3N0A!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3042d0aa-95d2-491b-86ec-1807c895ddd2_1144x1093.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!3N0A!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3042d0aa-95d2-491b-86ec-1807c895ddd2_1144x1093.png 424w, https://substackcdn.com/image/fetch/$s_!3N0A!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3042d0aa-95d2-491b-86ec-1807c895ddd2_1144x1093.png 848w, https://substackcdn.com/image/fetch/$s_!3N0A!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3042d0aa-95d2-491b-86ec-1807c895ddd2_1144x1093.png 1272w, https://substackcdn.com/image/fetch/$s_!3N0A!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3042d0aa-95d2-491b-86ec-1807c895ddd2_1144x1093.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!3N0A!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3042d0aa-95d2-491b-86ec-1807c895ddd2_1144x1093.png" width="670" height="640.1311188811189" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/3042d0aa-95d2-491b-86ec-1807c895ddd2_1144x1093.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1093,&quot;width&quot;:1144,&quot;resizeWidth&quot;:670,&quot;bytes&quot;:392986,&quot;alt&quot;:&quot;Proposal submission page showing input fields for proposal title, session type, track, and description.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Proposal submission page showing input fields for proposal title, session type, track, and description." title="Proposal submission page showing input fields for proposal title, session type, track, and description." srcset="https://substackcdn.com/image/fetch/$s_!3N0A!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3042d0aa-95d2-491b-86ec-1807c895ddd2_1144x1093.png 424w, https://substackcdn.com/image/fetch/$s_!3N0A!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3042d0aa-95d2-491b-86ec-1807c895ddd2_1144x1093.png 848w, https://substackcdn.com/image/fetch/$s_!3N0A!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3042d0aa-95d2-491b-86ec-1807c895ddd2_1144x1093.png 1272w, https://substackcdn.com/image/fetch/$s_!3N0A!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3042d0aa-95d2-491b-86ec-1807c895ddd2_1144x1093.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">The first part of the proposal submission form. It&#8217;s a pretty straightforward interface, and you can save a draft and come back to it later. Make sure you do come back and actually submit your proposal!</figcaption></figure></div><p>If you want to submit a proposal, you&#8217;ll need to fill in the following sections:</p><ul><li><p><strong>Title:</strong> A one-line title for your proposal.</p></li><li><p><strong>Session Type:</strong> For example, a 30-minute talk.</p></li><li><p><strong>Track:</strong> This is basically the same thing as the Session Type.</p></li><li><p><strong>Description:</strong> A public description of your talk. It&#8217;s in Markdown, and you can use up to 300 words.</p></li><li><p><strong>Notes:</strong> Any additional information you might need to share with organizers. This is not public.</p></li><li><p><strong>Additional Speaker</strong>: You can co-present a talk if you want.</p></li><li><p><strong>Outline</strong>: Provide a rough outline of what you aim to present, and how long you&#8217;ll spend on each part of the talk.</p></li><li><p><strong>Category</strong>: There are about 30 categories to choose from. Some examples are <em>Applications of Python</em>, <em>Data Science</em>, <em>DevOps</em>, <em>Security</em>, etc.</p></li><li><p><strong>Audience:</strong> Choose from four main audience descriptions: <em>Just starting out</em>, <em>Some experience</em>, <em>Advanced experience</em>, <em>Community presentation</em>. This is meant to help attendees choose appropriate talks to go to.</p></li><li><p><strong>Link to Slides:</strong> You can provide a link to your slides ahead of time if you want to, and the audience can follow along during your talk. This link is not public until after your talk has been accepted.</p></li><li><p><strong>Have you previously given this talk</strong>? If so, the organizers would like a link to the relevant information.</p></li><li><p><strong>Recording release:</strong> This allows PyCon to record talks, and share them online after the conference is over.</p></li><li><p><strong>Resources:</strong> You can upload resources that you want attendees to have access to. I believe this is mostly used for tutorials.</p></li></ul><p>This might look like a lot, but it&#8217;s what the review committee needs in order to figure out which talks are well-thought out proposals, and which are just quick ideas that might not actually lead to a good talk. It also helps them put together a well-balanced talk schedule. The most important parts are the <strong>description</strong> and the <strong>outline</strong>; the rest are important things to know, but quick to fill in.</p><p><strong>Note:</strong> <em>Keep in mind that the first round of reviews are blind; reviewers will not see your name during the initial review process. This policy was put in place to keep the focus on the contents of the proposal, and avoid overemphasizing well-known speakers. Make sure you don&#8217;t include any personally identifying information in your proposal&#8217;s description.</em></p><h3>Don&#8217;t fear rejection!</h3><p>PyCon gets a lot of proposals, and the review committee needs to put together a balanced schedule of talks. You can submit an amazing proposal about how dictionaries work in Python, and if five other people submit similar proposals, most of those will have to be rejected.</p><p>There are many <em>really</em> good proposals that end up being rejected because of overlap like this. Many well-known and popular speakers have been quite open about how many rejections they&#8217;ve received; if your talk is rejected, please don&#8217;t feel too discouraged. You can always resubmit your proposal for a later conference.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!SIQS!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a947705-12f6-4820-9a64-ac9f164b9f7c_1207x516.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!SIQS!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a947705-12f6-4820-9a64-ac9f164b9f7c_1207x516.png 424w, https://substackcdn.com/image/fetch/$s_!SIQS!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a947705-12f6-4820-9a64-ac9f164b9f7c_1207x516.png 848w, https://substackcdn.com/image/fetch/$s_!SIQS!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a947705-12f6-4820-9a64-ac9f164b9f7c_1207x516.png 1272w, https://substackcdn.com/image/fetch/$s_!SIQS!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a947705-12f6-4820-9a64-ac9f164b9f7c_1207x516.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!SIQS!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a947705-12f6-4820-9a64-ac9f164b9f7c_1207x516.png" width="662" height="283.00911350455675" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/4a947705-12f6-4820-9a64-ac9f164b9f7c_1207x516.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:516,&quot;width&quot;:1207,&quot;resizeWidth&quot;:662,&quot;bytes&quot;:118596,&quot;alt&quot;:&quot;Timeline:  - October 25, 2023 - Call For Proposals Opens. - December 18, 2023 &#8212; Call For Proposals Closes (AoE) - February 5-6, 2024 &#8212; Notifications are sent to speakers - February 16, 2024 - Travel Grant applications close and final day for speakers to confirm attendance - February 28, 2024 &#8212; The schedule is posted here on the PyCon US website - May 15-23, 2024 &#8212; PyCon US 2024 in Pittsburgh, Pennsylvania - June 2024 &#8212; All recorded tutorials will be displayed on our Youtube channel.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Timeline:  - October 25, 2023 - Call For Proposals Opens. - December 18, 2023 &#8212; Call For Proposals Closes (AoE) - February 5-6, 2024 &#8212; Notifications are sent to speakers - February 16, 2024 - Travel Grant applications close and final day for speakers to confirm attendance - February 28, 2024 &#8212; The schedule is posted here on the PyCon US website - May 15-23, 2024 &#8212; PyCon US 2024 in Pittsburgh, Pennsylvania - June 2024 &#8212; All recorded tutorials will be displayed on our Youtube channel." title="Timeline:  - October 25, 2023 - Call For Proposals Opens. - December 18, 2023 &#8212; Call For Proposals Closes (AoE) - February 5-6, 2024 &#8212; Notifications are sent to speakers - February 16, 2024 - Travel Grant applications close and final day for speakers to confirm attendance - February 28, 2024 &#8212; The schedule is posted here on the PyCon US website - May 15-23, 2024 &#8212; PyCon US 2024 in Pittsburgh, Pennsylvania - June 2024 &#8212; All recorded tutorials will be displayed on our Youtube channel." srcset="https://substackcdn.com/image/fetch/$s_!SIQS!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a947705-12f6-4820-9a64-ac9f164b9f7c_1207x516.png 424w, https://substackcdn.com/image/fetch/$s_!SIQS!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a947705-12f6-4820-9a64-ac9f164b9f7c_1207x516.png 848w, https://substackcdn.com/image/fetch/$s_!SIQS!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a947705-12f6-4820-9a64-ac9f164b9f7c_1207x516.png 1272w, https://substackcdn.com/image/fetch/$s_!SIQS!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a947705-12f6-4820-9a64-ac9f164b9f7c_1207x516.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Decisions about proposals will be sent out the first week of February, 2024.</figcaption></figure></div><h3>Other thoughts</h3><p>There&#8217;s a few more things to keep in mind if you&#8217;re considering submitting a proposal:</p><h5>Be clear and specific</h5><p>The review committee is looking for reasons to accept your talk, or at least move it on to the second round of reviews. Give them all the information they need to make their decision, without writing up a full script for the talk.</p><h5>You can submit multiple proposals</h5><p>Because there&#8217;s a bit of luck around whether other people are submitting a talk on a similar subject, it&#8217;s perfectly acceptable to submit more than one proposal. However, it&#8217;s <em>much</em> better to submit one quality proposal than several lower-quality proposals.</p><h5>You can save a draft</h5><p>You can save your proposal as a draft. It&#8217;s actually good to fill in all the boxes, and come back a day or so later to look over your proposal before submitting it. Just remember to come back and follow through on submitting!</p><h5>Don&#8217;t submit an ad</h5><p>PyCon is a well-established conference, and no one wants to go see an ad for a for-profit project, or for the company somebody works for. If you want to share an interesting aspect of a commercial project, that <em>can</em> be appropriate. Just don&#8217;t submit a proposal that&#8217;s a thinly-veiled ad.</p><h5>You can share a preview link with someone</h5><p>It can be helpful to have someone else look over your proposal. While you can copy-paste parts of your proposal into an email, you can also share a link to a preview of your actual proposal.</p><h3>Conclusions</h3><p>If you&#8217;ve never submitted a proposal before, it can seem like a daunting process. But if you have a clear sense of what you&#8217;d like to present, it shouldn&#8217;t take too long to draft a proposal. It&#8217;s mostly a description of less than 300 words, and an outline showing you have a sense of how long you&#8217;ll spend on each major topic in your presentation.</p><p>I can&#8217;t promise a close review of proposals this close to the deadline, but if you want a quick check-in about an idea or a proposal, I&#8217;m happy to share my initial reactions. I&#8217;ve given one talk at DjangoCon, so I&#8217;ve been through this process. I&#8217;ve also had talks rejected, so I can commiserate if your proposal isn&#8217;t accepted. If you want some quick feedback, feel free to reply to this email with your thoughts.</p><p>If you submit a proposal, I wish you luck. :)</p>]]></content:encoded></item><item><title><![CDATA[A real-world off-by-one error]]></title><description><![CDATA[MP 69: Why do CLI apps use zero-indexing when presenting choices?]]></description><link>https://mostlypython.substack.com/p/a-real-world-off-by-one-error</link><guid isPermaLink="false">https://mostlypython.substack.com/p/a-real-world-off-by-one-error</guid><dc:creator><![CDATA[Eric Matthes]]></dc:creator><pubDate>Thu, 07 Dec 2023 17:30:24 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!2Avc!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc1a0af04-5d17-4cba-91c4-5f264ed3c1e2_800x438.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>You&#8217;ve probably heard the old joke about programming:</p><blockquote><p><em>The two most difficult things in programming are naming things, cache invalidation, and off-by-one errors.</em></p></blockquote><p>If you&#8217;ve never heard this joke before, I&#8217;m happy to be the first to share it with you. :)</p><p>Many people who are newer to programming think they&#8217;re the only ones who make &#8220;simple&#8221; mistakes like being off by one when using an index. But this joke has persisted for decades because people make &#8220;simple&#8221; mistakes like this throughout their careers in programming.</p><p>I recently made an off-by-one error in my own work, which led to a deeper understanding of how CLI apps are designed. I was never clear about why so many CLI apps use zero-indexing when presenting choices, until using a different approach led to a bug in my current project.</p><h3>Which app do you want to use?</h3><p>I&#8217;m currently working on a <a href="https://django-simple-deploy.readthedocs.io/en/latest/">project</a> to automate Django deployments. One challenge in trying to automate deployment centers around the possibility that the user may already have a number of pre-existing apps on the target platform. It&#8217;s <em>really</em> important that we don&#8217;t accidentally do anything with those apps.</p><p>During the deployment process, we need to identify which remote app to deploy against. To do this, we need to present the user with a list of choices:</p><pre><code>Looking for Fly.io app to deploy against...

*** Found multiple undeployed apps on Fly.io. ***
1: bold-silence-958
2: green-cherry-4581

<strong>Which app would you like to use?</strong> 1</code></pre><p>We tell the user that we&#8217;re looking for the right app to deploy to on the remote server, hosted at Fly.io. We show them all the apps that don&#8217;t already have an active deployment. We then let them choose which app to deploy to.</p><p>This dialog is user-facing, so I first implemented it as a list that starts at 1. We&#8217;re not writing code here, so I didn&#8217;t see any reason to start the list at 0.</p><h5>Confirming the user&#8217;s choice</h5><p>After the user makes their choice, we need to show the name of the app that was selected. We need to get a final confirmation that we should deploy to that app, because we can cause serious problems if we push to the wrong remote project. </p><p>Here&#8217;s a simplified version of the code I used to present this list of choices, and get the final confirmation that we&#8217;ve identified the correct app:</p><pre><code>msg = "\n*** Found multiple undeployed apps on Fly.io. ***"
<strong>for index, name in enumerate(app_names, start=1):</strong>
    <strong>msg += f"\n{index}: {name}"</strong>

msg += "\nWhich app would you like to use? "
selection = input(msg)

selected_name = app_names[int(selection)]

msg = f"You have selected {selected_name}. Is that correct?"
confirmed = get_confirmation(msg)

if confirmed:
    # Continue with deployment...</code></pre><p>Many people are familiar with the use of <code>enumerate()</code> to access the index value on each pass through a loop. We typically want this enumeration to start at the default value of 0, because sequence indexes start at 0. But you can tell <code>enumerate()</code> to use any starting value you want, using the <code>start</code> argument. Here we use <code>start=1</code> to present a list of choices starting at 1, rather than 0.</p><p>Can you spot the issue with this approach, as it&#8217;s implemented here?</p><h5>A buggy selection</h5><p>When I ran this code, it didn&#8217;t work quite right:</p><pre><code>Looking for Fly.io app to deploy against...

*** Found multiple undeployed apps on Fly.io. ***
<strong>1: bold-silence-958</strong>
2: green-cherry-4581

You can cancel this configuration work by entering q.
Which app would you like to use? <strong>1</strong>

<strong>You have selected green-cherry-4581.</strong> Is that correct? (yes|no)</code></pre><p>I selected the first choice, <code>bold-silence-958</code>. But the program picked out the name <code>green-cherry-4581</code>.</p><h5>What&#8217;s really happening</h5><p>The root of this issue is the fact that I&#8217;m using the value the user entered as the index when pulling the name from a list of app names. But the list of choices the user sees starts at one, and the indexes for my actual Python list starts at 0:</p><pre><code>msg = "\n*** Found multiple undeployed apps on Fly.io. ***"
for <strong>index</strong>, name in enumerate(app_names, <strong>start=1</strong>):
    msg += f"\n  {index}: {name}"

msg += "\nWhich app would you like to use? "
selection = input(msg)

<strong>selected_name = app_names[int(selection)]</strong></code></pre><p>When the list of names is presented, that <code>start=1</code> argument matches <code>bold-silence-958</code> with the index 1. But in the list <code>app_names</code>, index 1 refers to the second name in the list, <code>green-cherry-4581.</code> Whichever choice the user makes, we&#8217;ll always be one name away from the name they chose. And if they choose the last name shown, we&#8217;ll get an <code>IndexError</code>.</p><h5>Two solutions</h5><p>There are two solutions to this issue:</p><ul><li><p>We can present the list as it&#8217;s currently displayed, and subtract 1 from the selected index before pulling the name from the list.</p></li><li><p>Or, we can present the list using numbers that start at 0, matching the actual list indexes.</p></li></ul><p>I don&#8217;t have a strong reason to start numbering this list at 1. If this were a list of the best-selling apps in an app store, you&#8217;d probably want to start at 1. But these numbers are arbitrary; we&#8217;re just using numbers to avoid requiring the user to type in a name. I&#8217;d much rather have simple code where the numbers in the CLI match the numbers we&#8217;re working with, than deal with differing internal and external numbering systems. The code will be simpler, and people reading this block of code won&#8217;t have to think through potential off-by-one issues.</p><h5>The final implementation</h5><p>The final implementation is just as easy for the end user to work with, and easier for developers to think about as well:</p><pre><code>Looking for Fly.io app to deploy against...

*** Found multiple undeployed apps on Fly.io. ***
<strong>0: bold-silence-958
</strong>1: green-cherry-4581

You can cancel this configuration work by entering q.
Which app would you like to use? <strong>0</strong>

You have selected <strong>bold-silence-958</strong>. Is that correct? (yes|no)
<strong>y</strong></code></pre><h3>A third-party CLI</h3><p>As an example of an existing CLI that works like this, consider the CLI for Platform.sh. When you deploy a project to Platform.sh, you can open the project in a browser directly from the command line. The <code>platform url</code> command gives you a list of all the URLs associated with the deployed project, and asks you to choose one:</p><pre><code>$ <strong>platform url</strong>
Enter a number to open a URL
  [0] https://main-bvxea6i-cgxycamxvcfus.us-2.platformsh.site/
  [1] http://main-bvxea6i-cgxycamxvcfus.us-2.platformsh.site/
 &gt; <strong>0</strong></code></pre><p>This is presented as a zero-indexed list. I always thought CLIs were written this way because programmers just tend to think in terms of counting from 0. It never occurred to me that these selections were being used as the actual index for pulling a value from a sequence.</p><p>I haven&#8217;t seen the backend for their CLI, but I would guess that they&#8217;re using the input value directly to pull from their list of available URLs.</p><h3>A physical zero-indexed tool</h3><p>Most people have probably used zero-indexing before they started programming, without realizing it.</p><p>Think of a ruler for a moment. A ruler is a zero-indexed tool:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!2Avc!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc1a0af04-5d17-4cba-91c4-5f264ed3c1e2_800x438.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!2Avc!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc1a0af04-5d17-4cba-91c4-5f264ed3c1e2_800x438.jpeg 424w, https://substackcdn.com/image/fetch/$s_!2Avc!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc1a0af04-5d17-4cba-91c4-5f264ed3c1e2_800x438.jpeg 848w, https://substackcdn.com/image/fetch/$s_!2Avc!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc1a0af04-5d17-4cba-91c4-5f264ed3c1e2_800x438.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!2Avc!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc1a0af04-5d17-4cba-91c4-5f264ed3c1e2_800x438.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!2Avc!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc1a0af04-5d17-4cba-91c4-5f264ed3c1e2_800x438.jpeg" width="638" height="349.305" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/c1a0af04-5d17-4cba-91c4-5f264ed3c1e2_800x438.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:438,&quot;width&quot;:800,&quot;resizeWidth&quot;:638,&quot;bytes&quot;:98735,&quot;alt&quot;:&quot;rule showing the 0, 1, and 2 centimeter marks&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="rule showing the 0, 1, and 2 centimeter marks" title="rule showing the 0, 1, and 2 centimeter marks" srcset="https://substackcdn.com/image/fetch/$s_!2Avc!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc1a0af04-5d17-4cba-91c4-5f264ed3c1e2_800x438.jpeg 424w, https://substackcdn.com/image/fetch/$s_!2Avc!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc1a0af04-5d17-4cba-91c4-5f264ed3c1e2_800x438.jpeg 848w, https://substackcdn.com/image/fetch/$s_!2Avc!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc1a0af04-5d17-4cba-91c4-5f264ed3c1e2_800x438.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!2Avc!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc1a0af04-5d17-4cba-91c4-5f264ed3c1e2_800x438.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Most measuring tools, including rulers, start at 0. <a href="https://www.flickr.com/photos/26344495@N05/25943607203">Photo</a> from Flickr user <a href="https://www.flickr.com/people/26344495@N05/">Ivan Radic</a>, <a href="https://creativecommons.org/licenses/by/2.0/">CC BY 2.0</a>.</figcaption></figure></div><p>Objects can measure less than 1 centimeter, so a ruler starts at 0, not 1. It&#8217;s not just rulers that start at 0, either. Just about anything we measure starts at 0: length, angles, weights, and volumes, to name a few.</p><p>When I was a math teacher, I watched students make off-by-one style errors on a regular basis whenever we&#8217;d work with rulers or protractors. Zero-indexed systems are quite natural, and are more common than many people realize.</p><h3>Conclusions</h3><p>I like how addressing a small bug can sometimes lead to a deeper understanding of a concept or implementation pattern. I especially like it when the debugging work helps explain something I&#8217;ve seen often, but not paid much attention to before.</p><p>Off-by-one errors will always be with us, and they&#8217;ll keep coming up in surprising ways.</p><p></p><p></p>]]></content:encoded></item><item><title><![CDATA[Gift exchange]]></title><description><![CDATA[MP 68: A bit of practical work, and a bit of fun.]]></description><link>https://mostlypython.substack.com/p/gift-exchange</link><guid isPermaLink="false">https://mostlypython.substack.com/p/gift-exchange</guid><dc:creator><![CDATA[Eric Matthes]]></dc:creator><pubDate>Thu, 30 Nov 2023 17:30:27 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!wGGX!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe2d43e83-0a9e-4131-80a2-e6d8bcf96658_1084x372.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>My extended family, like many families I imagine, has people with all kinds of religious beliefs. One thing we all share, despite our vastly different beliefs, is the joy in giving gifts around this time of year.</p><p>We try to do a gift exchange most years. In past years someone would take pieces of paper, write everyone&#8217;s names on them, and pull them from a hat to see who each person would be paired up with. This lets everyone be more thoughtful about what they give. This year someone asked if I could write a program to match people up, without having anyone matched with a member of their immediate family.</p><p>This is the kind of task that&#8217;s often not worth automating, because you can easily spend more time writing the program than you actually save. But I had time one morning last week, and there&#8217;s a fun joke waiting on the other side of this work, so I said I&#8217;d be happy to give it a try.</p><h3>First pass</h3><p>This is the kind of program that&#8217;s been written thousands of times, so you could probably look it up and find a program someone else wrote and just run it against your own family&#8217;s names. But it&#8217;s also the kind of specific task that&#8217;s fun to play with without looking at other people&#8217;s work. I wanted to come up with something that works, share the results with our family, and then consider what more efficient solutions might look like.</p><p>First, we need a list of all the names of people participating in the gift exchange:</p><pre><code>names = [
    "Sherri",
    "Eric",
    "Terri",
    "Joel",
    "Joshua",
    "Catherine",
    "Jasmine",
    "Alexandra",
    "Jeremiah",
]</code></pre><p>I used <code>Faker</code> to generate this list of names, but inserted my own name into the list for a reason you&#8217;ll see in a moment.<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-1" href="#footnote-1" target="_self">1</a></p><h5>Matching constraints</h5><p>It might actually be worth writing a program to deal with a small matching problem like this, because matching people while meeting a set of constraints can be hard to do manually. The only constraint in our gift exchange is that partners shouldn&#8217;t be assigned to give gifts to each other.</p><p>Here are the partners in my (fake) family:</p><pre><code>partners = [
    ("Sherri", "Eric"),
    ("Terri", "Joel"),
    ("Joshua", "Catherine"),
    ("Jasmine", "Alexandra"),
]</code></pre><p>The constraint is implemented as a list of tuples. If any match we make includes one of these pairs, we need to reject that match.</p><h5>Failed attempts</h5><p>I always like to be honest about the background work I did before a writeup like this, because it&#8217;s easy to give the impression that I came up with a working solution on my first attempt. That&#8217;s rarely the case in my work, and the same is true for most of the programmers I know.</p><p>In my very first attempt I had it in my head that I needed to make pairs of people. I thought we needed to have an even number of participants for this to work. When I was looking at the first set of results I realized only half the people were giving gifts, and half were receiving gifts. That was a little facepalm moment, but it&#8217;s also something I love about programming. You can take an idea you have for a small problem, and try implementing it without having to think it through too deeply. The immediate feedback you get from trying something is invaluable, and almost always leads to a better understanding of the problem you&#8217;re trying to solve.</p><p>In my second attempt, I wrote a big nested loop. The outer loop kept running until a good set of matches was found, and the inner loop ran as long as there were still names that needed to be matched. That approach could work, but I find it hard to reason about nested <code>while</code> loops. I got a working solution by moving the body of the loops into functions, which greatly simplified my reasoning about the code.</p><h5>Verifying matches</h5><p>My main approach is to identify a list of givers and receivers. I should end up with nine sentences like this:</p><pre><code>Eric will give to Alexandra.</code></pre><p>This pairing would work, because it doesn&#8217;t violate the constraints defined in <code>partners</code>.</p><p>We need a function to check whether a match works or not:</p><pre><code><strong>def check_match(match):</strong>
    if match in partners:
        return False
    if tuple(reversed(match)) in partners:
        return False
    return True</code></pre><p>Even though we&#8217;re going to output sentences, the actual matches are going to be tuples like this:</p><pre><code>('Catherine', 'Joshua')</code></pre><p>In <code>check_match()</code>, we want to make sure the current match is not in the list <code>partners</code>. If it is, we return <code>False</code>. If the match is not in <code>partners</code>, we also need to check whether the reversed version of the tuple is in <code>partners</code>. We only return <code>True</code> if both of these checks pass.<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-2" href="#footnote-2" target="_self">2</a></p><p>When writing a function like this, it&#8217;s good to make a <a href="https://github.com/ehmatthes/mostly_python/blob/main/mp68_gift_exchange/gift_exchange_2_check_match.py#L20">quick check</a> that it works as expected:</p><pre><code>print(check_match(('Sherri', 'Eric')))
print(check_match(('Eric', 'Sherri')))
print(check_match(('Sherri', 'Terri')))</code></pre><p>The first two should fail because these matches are in the list <code>partners</code>. The third match should work.</p><pre><code>False
False
True</code></pre><p>The function seems to work, so we can move on. (If this were part of a longer-term project, I&#8217;d move this code to a test.)</p><h5>Matching everyone</h5><p>Now we need a function that takes the list of names, and tries to match everyone. I&#8217;m sure there are a number of ways of solving the problem of matching people for the purpose of giving gifts, and I&#8217;m sure some approaches are better than others. But I&#8217;m matching nine people for a family event, so I&#8217;m not too worried about finding the <em>best</em> solution, or a mathematically elegant solution. A lot of times in programming, you don&#8217;t need to find the best solution. You just need to find something that works, and you can then refine your solution as needed.</p><p>I&#8217;m going to try the following approach:</p><ul><li><p>Shuffle the list of names.</p></li><li><p>Have <strong>person A</strong> give to <strong>person B</strong>, <strong>person B</strong> give to <strong>person C</strong>, and so forth.</p></li><li><p>That should take care of everyone except the first person and the last person. The first person has only given a gift, and the last has only received a gift. So, the <strong>last person</strong> will give a gift to the <strong>first person</strong>.</p></li><li><p>If this results in a match that doesn&#8217;t work, we&#8217;ll start over with a new shuffling of the original list.</p></li></ul><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!wGGX!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe2d43e83-0a9e-4131-80a2-e6d8bcf96658_1084x372.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!wGGX!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe2d43e83-0a9e-4131-80a2-e6d8bcf96658_1084x372.png 424w, https://substackcdn.com/image/fetch/$s_!wGGX!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe2d43e83-0a9e-4131-80a2-e6d8bcf96658_1084x372.png 848w, https://substackcdn.com/image/fetch/$s_!wGGX!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe2d43e83-0a9e-4131-80a2-e6d8bcf96658_1084x372.png 1272w, https://substackcdn.com/image/fetch/$s_!wGGX!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe2d43e83-0a9e-4131-80a2-e6d8bcf96658_1084x372.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!wGGX!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe2d43e83-0a9e-4131-80a2-e6d8bcf96658_1084x372.png" width="630" height="216.19926199261994" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/e2d43e83-0a9e-4131-80a2-e6d8bcf96658_1084x372.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:372,&quot;width&quot;:1084,&quot;resizeWidth&quot;:630,&quot;bytes&quot;:40062,&quot;alt&quot;:&quot;four squares labeled a, b, c,... i. Arrow from a to b, b to c, and I to a.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="four squares labeled a, b, c,... i. Arrow from a to b, b to c, and I to a." title="four squares labeled a, b, c,... i. Arrow from a to b, b to c, and I to a." srcset="https://substackcdn.com/image/fetch/$s_!wGGX!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe2d43e83-0a9e-4131-80a2-e6d8bcf96658_1084x372.png 424w, https://substackcdn.com/image/fetch/$s_!wGGX!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe2d43e83-0a9e-4131-80a2-e6d8bcf96658_1084x372.png 848w, https://substackcdn.com/image/fetch/$s_!wGGX!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe2d43e83-0a9e-4131-80a2-e6d8bcf96658_1084x372.png 1272w, https://substackcdn.com/image/fetch/$s_!wGGX!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe2d43e83-0a9e-4131-80a2-e6d8bcf96658_1084x372.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><figcaption class="image-caption">After shuffling all the names, person A gives to person B, person B gives to person C, and so on. The last person in the shuffled list gives to the first person in the list.</figcaption></figure></div><h5>Attempting a set of matches</h5><p>Now we can write a function that attempts to make a full set of matches. The <a href="https://github.com/ehmatthes/mostly_python/blob/main/mp68_gift_exchange/gift_exchange_3_works.py#L29">function</a> will be given a copy of the original list of names, so it&#8217;s free to modify its copy of <code>names</code>:</p><pre><code><strong>def attempt_match(names):</strong>
    <strong>random.shuffle(names)</strong>
    matches = []

    # Keep track of first giver.
    <strong>giver = names.pop()
    first_giver = giver</strong>

    while names:
        # Receiver is next name in list.
        receiver = names.pop()
        match = (giver, receiver)

        # Bail if it's a bad match.
        if not check_match(match):
            return False

        # Store match.
        matches.append(match)

        # Current receiver becomes next giver.
        giver = receiver

    # The last receiver gives to the first giver.
    match = (receiver, first_giver)
    if not check_match(match):
        return False

    # If we're still here, all matches are good.
    return matches</code></pre><p>The comments in here should help make sense of this code. We first shuffle the names, so everyone is in a random order. We then make an empty list to store matches in.</p><p>We need to keep track of the first person assigned to give a gift, because they&#8217;ll need to be matched with the last person in the list when we&#8217;re all done.<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-3" href="#footnote-3" target="_self">3</a> So before starting the loop, we call <code>pop()</code> to pull a name from the list. This is the first person assigned to <code>giver</code>, but we also assign that name to <code>first_giver</code>.</p><p>With the first giver identified, we start looping through the remaining names. We pull the next name, and assign that name to <code>receiver</code>. This allows us to make the first match, of the form <code>(giver, receiver)</code>. We check this match, and return <code>False</code> if it fails the call to <code>check_match()</code>. If it passes that check, we append the tuple to <code>matches</code>. Before moving on to the next name, we assign the current receiver to <code>giver</code>, making them the next person to give a gift.<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-4" href="#footnote-4" target="_self">4</a></p><p>The loop ends when there are no names left. At that point, the first giver and the final receiver need to be paired up. We check that match, and if it passes we can return the set of matches.</p><h5>Finding a working set of matches</h5><p>Now we&#8217;re ready to <a href="https://github.com/ehmatthes/mostly_python/blob/main/mp68_gift_exchange/gift_exchange_3_works.py#L66">use</a> these functions, and hopefully get a working set of matches.</p><pre><code>num_attempts = 0
<strong>while num_attempts &lt; 10:</strong>
    num_attempts += 1

    <strong>matches = attempt_match(names[:])</strong>
    if matches:
        # Print summary, and exit.
        print(num_attempts)
        summarize_matches(matches)
        sys.exit()

# Failed to find a good match.
print("Couldn't make a good set of matches.")</code></pre><p>I&#8217;m not sure how hard it is to find a working set of matches, so I&#8217;m setting a limit of 10 attempts for now. On each pass through the main loop, we call <code>attempt_match()</code>. That function will either return <code>False</code>, or a good set of matches. If we have a good set of matches, we print a summary of the matches and exit. I&#8217;m always curious how many attempts it takes, so I&#8217;m also printing that number.</p><p>If the loop ends without making a good match, we display a clear message to that effect.</p><p>Here&#8217;s the <code>summarize_matches()</code> function:</p><pre><code><strong>def summarize_matches(matches):</strong>
    for giver, receiver in matches:
        print(f"{giver} will give to {receiver}.")</code></pre><p>This works:</p><pre><code>5
Jasmine will give to Joshua.
Joshua will give to Alexandra.
Alexandra will give to Joel.
Joel will give to Jeremiah.
Jeremiah will give to Eric.
Eric will give to Catherine.
Catherine will give to Sherri.
Sherri will give to Terri.</code></pre><p>In this run, it took 5 attempts to find a good set of matches. Running the program a few times, I saw it generate a good set of matches in 1 attempt, and I saw it take up to 7 attempts.</p><h5>Sharing with family</h5><p>As soon as this worked, I copied the output text and sent it to our family&#8217;s group text. I was quite intentional about sending the very first set of matches that met our family&#8217;s constraints. I&#8217;ll admit a little temptation to look over the matches, and see if I think they&#8217;re &#8220;good&#8221;. But that feels pretty bad, and even for a small thing like a gift exchange you really want to keep the trust your family has in you to be impartial.</p><h5>A bit of lightness&#8230;</h5><p>There&#8217;s a great joke to be had here, and I wonder if anyone reading has been thinking of it. I rewrote the function <code>summarize_matches()</code> slightly:</p><pre><code>def summarize_matches(matches):
    for giver, receiver in matches:
        <strong>print(f"{giver} will give to Eric.")</strong></code></pre><p>This generates some very agreeable output:</p><pre><code>Jeremiah will give to Eric.
Jasmine will give to Eric.
Sherri will give to Eric.
Catherine will give to Eric.
Terri will give to Eric.
Alexandra will give to Eric.
Joel will give to Eric.</code></pre><p>I wrote a message to the family group text saying there was an issue with the initial output, and pasted this in as the corrected output. My immediate family groaned and sent facepalm emojis, but I quickly got more appropriately amused responses from my extended family.</p><h3>Improvements</h3><p>Before reading about other implementations, I found a significantly better approach just by writing this post. A lot of times I find that a bit of writing lets me see things more clearly. Sometimes that comes from writing a post, sometimes it&#8217;s writing to a colleague, and sometimes it comes from writing documentation.</p><p>As I was writing out the explanation for <code>attempt_match()</code>, I realized I could work directly with list indexes, rather than using intermediate variables such as <code>giver</code> and <code>receiver</code>. This <a href="https://github.com/ehmatthes/mostly_python/blob/main/mp68_gift_exchange/gift_exchange_4_indexes.py#L29">version</a> requires much less code:</p><pre><code>def attempt_match(names):
    random.shuffle(names)

    # Match each person with the next, then match the last
    #   person with the first.
    <strong>matches = [(names[i], names[i+1]) for i in range(len(names)-1)]
    matches.append((names[-1], names[0]))</strong>

    # Check all matches.
    <strong>if all([check_match(match) for match in matches]):</strong>
        return matches
    return False</code></pre><p>What we&#8217;re really doing in the earlier approach, after shuffling <code>names</code>, is matching each item in <code>names</code> with the item that follows. That is, we&#8217;re matching <code>names[i]</code> with <code>names[i+1]</code>. This doesn&#8217;t work for the last name in the list; we instead match that name with the first name in the list.</p><p>The code above uses a comprehension to do most of this. This approach lets us make all the matches in just two lines of code.<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-5" href="#footnote-5" target="_self">5</a></p><p>With the whole set of matches made, we can use another comprehension to check all the matches in one line of code. Consider this comprehension:</p><pre><code>[check_match(match) for match in matches]</code></pre><p>This calls <code>check_match()</code> for every match in the list <code>matches</code>. It generates a list of boolean values, one for each match in the set of matches that were just generated. The <code>all()</code> <a href="https://docs.python.org/3/library/functions.html#all">function</a> returns <code>True</code> if all values in the list are <code>True</code>. If they are, we return the list <code>matches</code>. If any of these are <code>False</code>, there&#8217;s a bad match and we return <code>False</code> to indicate a failed attempt at making an acceptable set of matches.</p><h3>Conclusions</h3><p>This was an interesting little exercise. I was going to do a bit more reading about mathematically elegant solutions to this problem, but the final version of <code>attempt_match()</code> is clean enough to satisfy my curiosity about this kind of problem for now.</p><p>If I were writing a version of this program to serve a high-traffic site, I&#8217;d take the time to read up on mathematically efficient models of this kind of problem. For example if I were implementing the matching algorithm for a game lobby, I&#8217;d want to use the most efficient approach.</p><p>Have you written any fun little programs to make your family life a little better? If so, please share your story! :)</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://mostlypython.substack.com/p/gift-exchange/comments&quot;,&quot;text&quot;:&quot;Leave a comment&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://mostlypython.substack.com/p/gift-exchange/comments"><span>Leave a comment</span></a></p><h3>Resources</h3><p>You can find the code files from this post in the&nbsp;<a href="https://github.com/ehmatthes/mostly_python/tree/main/mp68_gift_exchange">mostly_python</a>&nbsp;GitHub repository.</p><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-1" href="#footnote-anchor-1" class="footnote-number" contenteditable="false" target="_self">1</a><div class="footnote-content"><p>If you haven&#8217;t used <code>Faker</code> before, it&#8217;s great for generating small and large amounts of sample data. After running <code>pip install faker</code>, here&#8217;s the code for generating this list of names:</p><pre><code>&gt;&gt;&gt; <strong>from faker import Faker</strong>
&gt;&gt;&gt; <strong>fake = Faker()</strong>
&gt;&gt;&gt; <strong>names = [fake.first_name() for _ in range(9)]</strong>
&gt;&gt;&gt; <strong>names</strong>
['Sherri', 'Peter', 'Terri', ..., 'Jeremiah']</code></pre><p>This list was generated using the default <code>en_US</code> locale; you can generate names from a wide range of locales. You can also generate much more than just names. See the <a href="https://faker.readthedocs.io/en/master/#">documentation</a> for more.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-2" href="#footnote-anchor-2" class="footnote-number" contenteditable="false" target="_self">2</a><div class="footnote-content"><p>Note that <code>reversed(match)</code> returns an iterator, not a tuple:</p><pre><code>&gt;&gt;&gt; <strong>match = ('Sherri', 'Eric')</strong>
&gt;&gt;&gt; <strong>rev_match = reversed(match)</strong>
&gt;&gt;&gt; <strong>rev_match</strong>
&lt;reversed object at 0x104655bd0&gt;
&gt;&gt;&gt; <strong>tuple(rev_match)</strong>
('Eric', 'Sherri')</code></pre><p>Wrapping the iterator in a call to <code>tuple()</code> returns a tuple that we can use in our comparisons.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-3" href="#footnote-anchor-3" class="footnote-number" contenteditable="false" target="_self">3</a><div class="footnote-content"><p>Writing this sentence was the moment when I realized there was a <em>much</em> simpler way to implement this! But this is also why I like working with Python; you can reason things out, develop solutions that make sense, and then develop more concise solutions. You don&#8217;t have to come up with an elegant solution right away.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-4" href="#footnote-anchor-4" class="footnote-number" contenteditable="false" target="_self">4</a><div class="footnote-content"><p>Revisiting this code again after publication, I realized the bold line here is not used:</p><pre><code>def attempt_match(names):
    ...
    while names:
        ...
        <strong># Current receiver becomes next giver.
        giver = receiver
    
</strong>    # The last receiver gives to the first giver.
    match = (receiver, first_giver)
    if not check_match(match):
        return False

    # If we're still here, all matches are good.
    return matches</code></pre><p>Originally I used the following code every time I defined a <code>match</code>:</p><pre><code>match = (giver, receiver)</code></pre><p>In order to use this exact code, I had to assign the right names to <code>giver</code> and <code>receiver</code>. I later realized I could just make the final match using the final value of <code>receiver</code>, and <code>first_giver</code>.</p><p>I&#8217;m leaving the incorrect code in the post, because one of the main points here is that exploratory code is often messy, including lines that aren&#8217;t actually needed. The final version of the program does not use these lines.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-5" href="#footnote-anchor-5" class="footnote-number" contenteditable="false" target="_self">5</a><div class="footnote-content"><p>If it&#8217;s hard to make sense of the comprehension as written, see if the multiline version is more clear:</p><pre><code><code>    matches = [
        (names[i], names[i + 1])
        for i in range(len(names) - 1)
    ]
    matches.append((names[-1], names[0]))</code></code></pre><p>The first line of the expression generates a tuple of the form <code>(giver, receiver)</code>. The second line generates the index values to loop over. We want to use the indices from 0 through one less than the length of the list. For more about comprehensions, see <a href="https://www.mostlypython.com/p/python-lists-a-closer-look-part-3">MP #5</a>.</p><p>Also, on my final review of this post, I realized you can make all the matches inside the comprehension:</p><pre><code>    matches = [
        (names[i], names[i + 1])
        for i in range(-1, len(names) - 1)
    ]</code></pre><p>By starting the range at <code>-1</code>, the first iteration of the loop in the comprehension matches the last person with the first person.</p></div></div>]]></content:encoded></item><item><title><![CDATA[Mostly Python's referral program is now active]]></title><description><![CDATA[Hi everyone, As some of you may know, Substack began implementing a referral program earlier this summer. I didn&#8217;t enable the program right away, because I wanted to see how it worked before doing so. I&#8217;m happy to help people share my work, but I don&#8217;t ever want to come across as spammy or overly self-promotional.]]></description><link>https://mostlypython.substack.com/p/mostly-pythons-referral-program-is</link><guid isPermaLink="false">https://mostlypython.substack.com/p/mostly-pythons-referral-program-is</guid><dc:creator><![CDATA[Eric Matthes]]></dc:creator><pubDate>Wed, 29 Nov 2023 02:30:29 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F4ac4a977-0380-4396-8bbc-5215a0dbbb9b_500x500.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Hi everyone,</p><p>As some of you may know, Substack began implementing a referral program earlier this summer. I didn&#8217;t enable the program right away, because I wanted to see how it worked before doing so. I&#8217;m happy to help people share my work, but I don&#8217;t ever want to come across as spammy or overly self-promotional.</p><p>At this point there&#8217;s already a lot of meaningful content on <em>Mostly Python</em>, and you might want to share what&#8217;s here with other programmers you know. The referral program is pretty simple, so I enabled it today. From this point forward if you share the overall newsletter, or any individual post, you&#8217;ll get a few perks:</p><ul><li><p>If you refer 3 people who end up subscribing, you&#8217;ll get a month of paid access.</p></li><li><p>If you refer 5 people, you&#8217;ll get 3 months of paid access.</p></li><li><p>If you refer 25 people, you&#8217;ll get a year of paid access.</p></li></ul><p>If you&#8217;re already a paying subscriber, your subscription will be extended by the appropriate amount.</p><p>You can click any <em>Share</em> or <em>Refer a friend</em> link you see, and your referral will count toward these complimentary subscriptions. If you&#8217;ve referred a few people and don&#8217;t see a message about a complimentary subscription, please reply to any email you receive from <em>Mostly Python</em>, and I&#8217;ll help you get it sorted.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://mostlypython.substack.com/?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share Mostly Python&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://mostlypython.substack.com/?utm_source=substack&utm_medium=email&utm_content=share&action=share"><span>Share Mostly Python</span></a></p><p>I really enjoy writing about my work with Python, and what I&#8217;ve learned about the language and the community. I don&#8217;t like gamification and turning growth into a competition. If you know someone who would benefit from this newsletter, please share a post or the newsletter itself. If you have questions about the overall Substack referral program, see their <a href="https://support.substack.com/hc/en-us/articles/16142857300372">FAQ</a>.</p><p>Thank you for helping get the word out about <em>Mostly Python</em>!</p><p>Thanks,</p><p>Eric</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!0ib-!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb3073a6e-89b4-4ff5-a8d3-fbea4e3e9d14_200x200.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!0ib-!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb3073a6e-89b4-4ff5-a8d3-fbea4e3e9d14_200x200.png 424w, https://substackcdn.com/image/fetch/$s_!0ib-!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb3073a6e-89b4-4ff5-a8d3-fbea4e3e9d14_200x200.png 848w, https://substackcdn.com/image/fetch/$s_!0ib-!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb3073a6e-89b4-4ff5-a8d3-fbea4e3e9d14_200x200.png 1272w, https://substackcdn.com/image/fetch/$s_!0ib-!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb3073a6e-89b4-4ff5-a8d3-fbea4e3e9d14_200x200.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!0ib-!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb3073a6e-89b4-4ff5-a8d3-fbea4e3e9d14_200x200.png" width="200" height="200" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b3073a6e-89b4-4ff5-a8d3-fbea4e3e9d14_200x200.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:200,&quot;width&quot;:200,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:21857,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!0ib-!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb3073a6e-89b4-4ff5-a8d3-fbea4e3e9d14_200x200.png 424w, https://substackcdn.com/image/fetch/$s_!0ib-!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb3073a6e-89b4-4ff5-a8d3-fbea4e3e9d14_200x200.png 848w, https://substackcdn.com/image/fetch/$s_!0ib-!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb3073a6e-89b4-4ff5-a8d3-fbea4e3e9d14_200x200.png 1272w, https://substackcdn.com/image/fetch/$s_!0ib-!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb3073a6e-89b4-4ff5-a8d3-fbea4e3e9d14_200x200.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div>]]></content:encoded></item></channel></rss>