Congratulations!

[Valid RSS] This is a valid RSS feed.

Recommendations

This feed is valid, but interoperability with the widest range of feed readers could be improved by implementing the following recommendations.

Source: https://www.nedbatchelder.com/blog/rss.xml

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <?xml-stylesheet type="text/xsl" href="https://nedbatchelder.com/rssfull2html.xslt" media="screen" ?>
  3.  
  4. <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns="http://purl.org/rss/1.0/">
  5. <channel rdf:about="https://nedbatchelder.com/blog">
  6. <title>Ned Batchelder's blog</title>
  7. <link>https://nedbatchelder.com/blog</link>
  8. <description>Ned Batchelder's personal blog.</description>
  9. <dc:language>en-US</dc:language>
  10. <image rdf:resource="https://nedbatchelder.com/pix/rss-banner.gif"/>
  11. <items>
  12. <rdf:Seq>
  13. <rdf:li resource="https://nedbatchelder.com/blog/202510/side_project_advice.html"/><rdf:li resource="https://nedbatchelder.com/blog/202510/natural_cubics_circular_simplex.html"/><rdf:li resource="https://nedbatchelder.com/blog/202509/hobby_hilbert_simplex.html"/><rdf:li resource="https://nedbatchelder.com/blog/202509/testing_is_better_than_dsa.html"/><rdf:li resource="https://nedbatchelder.com/blog/202508/finding_unneeded_pragmas.html"/><rdf:li resource="https://nedbatchelder.com/blog/202508/starting_with_pytests_parametrize.html"/><rdf:li resource="https://nedbatchelder.com/blog/202507/coveragepy_regex_pragmas.html"/><rdf:li resource="https://nedbatchelder.com/blog/202507/coverage_7100_patch.html"/><rdf:li resource="https://nedbatchelder.com/blog/202507/2048_iterators_and_iterables.html"/><rdf:li resource="https://nedbatchelder.com/blog/202506/math_factoid_of_the_day_63.html"/>
  14. </rdf:Seq>
  15. </items>
  16. </channel>
  17. <image rdf:about="https://nedbatchelder.com/pix/rss-banner.gif">
  18. <title>Ned Batchelder's blog</title>
  19. <link>https://nedbatchelder.com/blog</link>
  20. <url>https://nedbatchelder.com/pix/rss-banner.gif</url>
  21. </image>
  22. <item rdf:about="https://nedbatchelder.com/blog/202510/side_project_advice.html">
  23. <title>Side project advice</title>
  24. <link>https://nedbatchelder.com/blog/202510/side_project_advice.html</link>
  25. <dc:date>2025-10-30T06:23:13-04:00</dc:date>
  26. <dc:creator>Ned Batchelder</dc:creator>
  27. <description><![CDATA[<p>Last night was a <a rel="external noopener" href="https://about.bostonpython.com/">Boston Python project night</a> where I
  28. had a good conversation with a few people that was mostly guided by questions
  29. from a nice guy named Mark.</p><h1 id="h_how_to_write_nice_code_in_research">How to write nice code in research<a class="headerlink" aria-label="Link to this header" href="#h_how_to_write_nice_code_in_research"></a></h1><p>Mark works in research and made the classic observation that research code is
  30. often messy, and asked about how to make it nicer.</p><p>I pointed out that for software engineers, the code is the product. For
  31. research, the results are the product, so there&#8217;s a reason the code can be and
  32. often is messier.  It&#8217;s important to keep the goal in mind. I mentioned it might
  33. not be worth it to add type annotations, detailed docstrings, or whatever else
  34. would make the code &#8220;nice&#8221;.</p><p>But the more you can make &#8220;nice&#8221; a habit, the less work it will be to do it
  35. as a matter of course. Even in a result-driven research environment, you&#8217;ll be
  36. able to write code the way you want, or at least push back a little bit. Code
  37. usually lives longer than people expect, so the nicer you can make it,
  38. the better it will be.</p><h1 id="h_side_projects">Side projects<a class="headerlink" aria-label="Link to this header" href="#h_side_projects"></a></h1><p>Side projects are a good opportunity to work differently. If work means messy
  39. code, your side project could be pristine. If work is very strict, your side
  40. project can be thrown together just for fun.  You get to set the goals.</p><p>And different side projects can be different. I develop
  41. <a href="https://coverage.readthedocs.io" rel="external noopener">coverage.py</a> very differently
  42. than <a href="https://nedbatchelder.com/blog/202510/natural_cubics_circular_simplex.html">fun math art
  43. projects</a>. Coverage.py has an extensive test suite run on many versions of
  44. Python (including nightly builds of the tip of main).  The math art projects
  45. usually have no tests at all.</p><p>Side projects are a great place to decide how you want to code and to
  46. practice that style.  Later you can bring those skills and learnings back to a
  47. work environment.</p><h1 id="h_forgive_yourself">Forgive yourself<a class="headerlink" aria-label="Link to this header" href="#h_forgive_yourself"></a></h1><p>Mark said one of his difficulties with side projects is perfectionism. He&#8217;ll
  48. come back to a project and find he wants to rewrite the whole thing.</p><p>My advice is: forgive yourself.  It&#8217;s OK to rewrite the whole thing. It&#8217;s OK
  49. to not rewrite the whole thing. It&#8217;s OK to ignore it for months at a time.  It&#8217;s
  50. OK to stop in the middle of a project and never come back to it. It&#8217;s OK to
  51. obsess about &#8220;irrelevant&#8221; details.</p><p>The great thing about a side project is that you are the only person who
  52. decides what and how it should be.</p><h1 id="h_how_to_stay_motivated">How to stay motivated<a class="headerlink" aria-label="Link to this header" href="#h_how_to_stay_motivated"></a></h1><p>But how to stay motivated on side projects? For me, it&#8217;s very motivating that
  53. many people use and get value from coverage.py. It&#8217;s a service to the community
  54. that I find rewarding.  Other side projects will have other motivations: a
  55. chance to learn new things, flex different muscles, stretch myself in new
  56. ways.</p><p>Find a reason that motivates you, and structure your side projects to lean
  57. into that reason. Don&#8217;t forget to forgive yourself if it doesn&#8217;t work out the
  58. way you planned or if you change your mind.</p><h1 id="h_how_to_write_something_people_will_use">How to write something people will use<a class="headerlink" aria-label="Link to this header" href="#h_how_to_write_something_people_will_use"></a></h1><p>Sure, it&#8217;s great to have a project that many people use, but how do you find
  59. a project that will end up like that?  The best way is to write something that
  60. you find useful. Then talk about it with people.  You never know what will catch
  61. on.</p><p>I mentioned my <a href="https://pypi.org/project/cogapp/" rel="external noopener">cog</a> project,
  62. which I first wrote in 2004 for one reason, but which is now being used by other
  63. people (including me) for different purposes.  It
  64. <a href="https://nedbatchelder.com/blog/202201/cog_resurgence.html">took years to catch on</a>.</p><p>Of course there&#8217;s no guarantee something like that will happen: it most
  65. likely won&#8217;t.  But I don&#8217;t know of a better way to make something people will
  66. use than to start by making something that <em>you</em> will use.</p><h1 id="h_other_topics">Other topics<a class="headerlink" aria-label="Link to this header" href="#h_other_topics"></a></h1><p>The discussion wasn&#8217;t as linear as this. We touched on other things along the
  67. way: unit tests vs system tests, obligations to support old versions of
  68. software, how to navigate huge code bases. There were probably other tangents
  69. that I&#8217;ve forgotten.</p><p>Project nights are almost never just about projects: they are about
  70. connecting with people in lots of different ways. This discussion felt like a
  71. good connection.  I hope the ideas of choosing your own paths and forgiving
  72. yourself hit home.</p>
  73. ]]></description>
  74. </item>
  75. <item rdf:about="https://nedbatchelder.com/blog/202510/natural_cubics_circular_simplex.html">
  76. <title>Natural cubics, circular Simplex</title>
  77. <link>https://nedbatchelder.com/blog/202510/natural_cubics_circular_simplex.html</link>
  78. <dc:date>2025-10-21T07:14:23-04:00</dc:date>
  79. <dc:creator>Ned Batchelder</dc:creator>
  80. <description><![CDATA[<p>This post continues where <a href="https://nedbatchelder.com/blog/202509/hobby_hilbert_simplex.html">Hobby Hilbert Simplex</a> left
  81. off.  If you haven&#8217;t read it yet, start there. It explains the basics of Hobby
  82. curves, Hilbert sorting and Simplex noise that I&#8217;m using.</p><h1 id="h_animation">Animation<a class="headerlink" aria-label="Link to this header" href="#h_animation"></a></h1><p>To animate one of our drawings, instead of considering 40 lines, we&#8217;ll think
  83. about 140 lines.  The first frame of the animation will draw lines 1 through 40,
  84. the second draws lines 2 through 41, and so on until the 100th frame is lines
  85. 100 through 140:</p><div class="figurep"><figure><picture><img src="https://nedbatchelder.com/pix/fluidity/linear_hobby.gif" alt="Swoopy lines flowing across the image, but with occasional jumps" width="600" height="600" class="hairline"></picture></figure></div><p>I&#8217;ve used a single Hilbert sorter for all of the frames to remove some
  86. jumping, but the Hobby curves still hop around. Also the animation doesn&#8217;t loop
  87. smoothly, so there&#8217;s a giant jump from frame 100 back to frame 1.</p><h1 id="h_natural_cubics">Natural cubics<a class="headerlink" aria-label="Link to this header" href="#h_natural_cubics"></a></h1><p>Hobby curves look nice, but have this unfortunate discontinuity where a small
  88. change in a point can lead to a radical change in the curve. There&#8217;s another way
  89. to compute curves through points automatically, called natural cubic curves.
  90. These curves don&#8217;t jump around the way Hobby curves can.</p><p>Jake Low&#8217;s <a rel="external noopener" href="https://www.jakelow.com/blog/hobby-curves">page about Hobby curves</a> has interactive
  91. examples of natural cubic curves which you should try.  Natural cubics don&#8217;t
  92. look as nice to our eyes as Hobby curves. Below is a comparison.  Each row has
  93. the same points, with Hobby curves on the left and natural cubic curves on the
  94. right:</p><div class="figurep"><figure><picture><source type="image/webp" srcset="https://nedbatchelder.com/iv/webp/pix/fluidity/hobby_vs_cubic.png.webp"><img src="https://nedbatchelder.com/pix/fluidity/hobby_vs_cubic.png" alt="On the right are nice blobby shapes, on the left are the same points but connected with sometimes pointy awkward curves" width="1200" height="1800" class="hairline"></picture></figure></div><p>The &#8220;natural&#8221; cubics actually have a quite unnatural appearance. But in an
  95. animation, those quirks could be a good trade-off for smooth transitions. Here&#8217;s
  96. an animation with the same points as our first one, but with natural cubic
  97. curves:</p><div class="figurep"><figure><picture><img src="https://nedbatchelder.com/pix/fluidity/linear_cubic.gif" alt="A flowing animation with pointier curves, only one jump at the end" width="600" height="600" class="hairline"></picture></figure></div><p>Now the motion is smooth except for the jump from frame 100 back to frame 1.
  98. Let&#8217;s do something about that.</p><h1 id="h_circular_simplex">Circular Simplex<a class="headerlink" aria-label="Link to this header" href="#h_circular_simplex"></a></h1><p>So far, we&#8217;ve been choosing points by sampling the simplex noise in small steps along
  99. a horizontal line: use a fixed u value, then take tiny steps along the v axis.
  100. That gave us our x coordinates, and a similar line with a different u value gave
  101. us the y coordinates. The ending point will be completely unrelated to the
  102. starting point.  To make a seamlessly looping animation, we need our x,y values
  103. to cycle seamlessly, returning to where they started.</p><p>We can make our x,y coordinates loop by choosing u,v values in a circle.
  104. Because the u,v values return to their starting point in the continuous simplex
  105. noise, the x,y coordinates will return as well. We use two circles: one for the
  106. x coordinates and another for the y.  The circles are far from each other to
  107. keep x and y independent of each other. The size of the circle is determined by
  108. the distance we want for each step and how many steps we want in the loop.</p><p>Here are three point paths created two ways, with linear sampling on the
  109. right and circular sampling on the left. Because simplex provides values between
  110. -1 and 1, the points wander within a square:</p><div class="figurep"><figure><picture><source type="image/webp" srcset="https://nedbatchelder.com/iv/webp/pix/fluidity/point_trails.png.webp"><img src="https://nedbatchelder.com/pix/fluidity/point_trails.png" alt="On the right, three trails of points that don't form a closed loop. On the left, three closed loops but still with interesting random shapes" width="1200" height="600" class="hairline"></picture></figure></div><p>It can get a bit confusing at this point: these traces are not the curves we
  111. are drawing.  They are the paths of the control points for successive curves. We
  112. draw curves through corresponding sets of points to get our animation. The first
  113. curve connects the first red/green/blue points, the second curve connects the
  114. second set, and so on.</p><p>Using circular sampling of the simplex noise, we can make animations that
  115. loop perfectly:</p><div class="figurep"><figure><picture><img src="https://nedbatchelder.com/pix/fluidity/circular_5.gif" alt="A smoothly looping animation" width="600" height="600" class="hairline"></picture></figure></div><div class="figurep"><figure><picture><img src="https://nedbatchelder.com/pix/fluidity/circular_9.gif" alt="A smoothly looping animation" width="600" height="600" class="hairline"></picture></figure></div><div class="figurep"><figure><picture><img src="https://nedbatchelder.com/pix/fluidity/circular_25.gif" alt="A smoothly looping animation" width="600" height="600" class="hairline"></picture></figure></div><h1 id="h_colophon">Colophon<a class="headerlink" aria-label="Link to this header" href="#h_colophon"></a></h1><p>If you are interested, the code is available on GitHub at
  116. <a href="https://github.com/nedbat/fluidity" rel="external noopener">nedbat/fluidity</a>.</p>
  117. ]]></description>
  118. </item>
  119. <item rdf:about="https://nedbatchelder.com/blog/202509/hobby_hilbert_simplex.html">
  120. <title>Hobby Hilbert Simplex</title>
  121. <link>https://nedbatchelder.com/blog/202509/hobby_hilbert_simplex.html</link>
  122. <dc:date>2025-09-26T08:14:04-04:00</dc:date>
  123. <dc:creator>Ned Batchelder</dc:creator>
  124. <description><![CDATA[<p>I saw a generative art piece I liked and wanted to learn how it was made.
  125. Starting with the artist&#8217;s Kotlin code, I dug into three new algorithms, hacked
  126. together some Python code, experimented with alternatives, and learned a lot.
  127. Now I can explain it to you.</p><p>It all started with this post by
  128. <a href="https://genart.social/@hamoid/115125620138280715" rel="external noopener">aBe on Mastodon</a>:</p><blockquote class="mastodon-post" lang="en" cite="https://genart.social/@hamoid/115125620138280715" data-source="fediverse">
  129.  <p>I love how these lines separate and reunite. And the fact that I can express this idea in 3 or 4 lines of code.</p><p>For me they&#8217;re lives represented by closed paths that end where they started, spending part of the journey together, separating while we go in different directions and maybe reconnecting again in the future.</p><p><a href="https://genart.social/tags/CreativeCoding" rel="nofollow noopener" class="mention hashtag" target="_blank">#<span>CreativeCoding</span></a> <a href="https://genart.social/tags/algorithmicart" rel="nofollow noopener" class="mention hashtag" target="_blank">#<span>algorithmicart</span></a>  <a href="https://genart.social/tags/proceduralArt" rel="nofollow noopener" class="mention hashtag" target="_blank">#<span>proceduralArt</span></a> <a href="https://genart.social/tags/OPENRNDR" rel="nofollow noopener" class="mention hashtag" target="_blank">#<span>OPENRNDR</span></a> <a href="https://genart.social/tags/Kotlin" rel="nofollow noopener" class="mention hashtag" target="_blank">#<span>Kotlin</span></a></p>
  130.  <figure><figure><img src="https://media.hachyderm.io/cache/media_attachments/files/115/125/620/285/265/947/small/5a73d40e6a4a81c1.png" alt="80 wobbly black hobby curves with low opacity. In some places the curves travel together, but sometimes they split in 2 or 3 groups and later reunite. Due to the low opacity, depending on how many curves overlap the result is brighter or darker." width="480" height="480"></figure></figure>
  131.  <footer>
  132.     — aBe (@hamoid@genart.social) <a href="https://genart.social/@hamoid/115125620138280715" rel="external noopener"><time datetime="2025-08-31T21:59:13.000Z">8/31/2025, 5:59:13 PM</time></a>
  133.  </footer>
  134. </blockquote><p>The drawing is made by choosing 10 random points, drawing a curve through
  135. those points, then slightly scooching the points and drawing another curve.
  136. There are 40 curves, each slightly different than the last.  Occasionally
  137. the next curve makes a jump, which is why they separate and reunite.</p><p>Eventually I made something similar:</p><div class="figurep"><figure><picture><source type="image/webp" srcset="https://nedbatchelder.com/iv/webp/pix/fluidity/repro_139.png.webp"><img src="https://nedbatchelder.com/pix/fluidity/repro_139.png" alt="An image similar to the one from Mastodon, with smoky sinuous curves" width="600" height="600" class="hairline"></picture></figure></div><p>Along the way I had to learn about three techniques I got from the Kotlin
  138. code: Hobby curves, Hilbert sorting, and simplex noise.</p><p>Each of these algorithms tries to do something &#8220;natural&#8221; automatically, so
  139. that we can generate art that looks nice without any manual steps.</p><h1 id="h_hobby_curves">Hobby curves<a class="headerlink" aria-label="Link to this header" href="#h_hobby_curves"></a></h1><p>To draw swoopy curves through our random points, we use an algorithm
  140. developed by John Hobby as part of Donald Knuth&#8217;s Metafont type design system.
  141. Jake Low has a <a rel="external noopener" href="https://www.jakelow.com/blog/hobby-curves">great interactive page for playing with Hobby
  142. curves</a>, you should try it.</p><p>Here are three examples of Hobby curves through ten random points:</p><div class="figurep"><figure><picture><source type="image/webp" srcset="https://nedbatchelder.com/iv/webp/pix/fluidity/hobby_unsorted.png.webp"><img src="https://nedbatchelder.com/pix/fluidity/hobby_unsorted.png" alt="Red random points connected by green lines then with a curve through all ten." width="600" height="200" class="hairline"></picture></figure></div><p>The curves are nice, but kind of a scribble, because we&#8217;re joining points
  143. together in the order we generated them (shown by the green lines).  If you
  144. asked a person to connect random points, they wouldn&#8217;t jump back and forth
  145. across the canvas like this.  They would find a nearby point to use next,
  146. producing a more natural tour of the set.</p><p>We&#8217;re generating everything automatically, so we can&#8217;t manually intervene
  147. to choose a natural order for the points.  Instead we use Hilbert sorting.</p><h1 id="h_hilbert_sorting">Hilbert sorting<a class="headerlink" aria-label="Link to this header" href="#h_hilbert_sorting"></a></h1><p>The Hilbert space-filling fractal visits every square in a 2D grid.
  148. <a rel="external noopener" href="https://doc.cgal.org/latest/Spatial_sorting/index.html">Hilbert sorting</a> uses a Hilbert fractal traversing
  149. the canvas, and sorts the points by when their square is visited by the fractal.
  150. This gives a tour of the points that corresponds more closely to what people
  151. expect.  Points that are close together in space are likely (but not guaranteed)
  152. to be close in the ordering.</p><p>If we sort the points using Hilbert sorting, we get much nicer curves. Here
  153. are the same points as last time:</p><div class="figurep"><figure><picture><source type="image/webp" srcset="https://nedbatchelder.com/iv/webp/pix/fluidity/hobby_sorted.png.webp"><img src="https://nedbatchelder.com/pix/fluidity/hobby_sorted.png" alt="The same three examples of ten points, but the curves make more sense now" width="600" height="200" class="hairline"></picture></figure></div><p>Here are pairs of the same points, unsorted and sorted side-by-side:</p><div class="figurep"><figure><picture><source type="image/webp" srcset="https://nedbatchelder.com/iv/webp/pix/fluidity/hilbert_compared.png.webp"><img src="https://nedbatchelder.com/pix/fluidity/hilbert_compared.png" alt="Comparing the scribbles and the nice curves" width="400" height="800" class="hairline"></picture></figure></div><p>If you compare closely, the points in each pair are the same, but the sorted
  154. points are connected in a better order, producing nicer curves.</p><h1 id="h_simplex_noise">Simplex noise<a class="headerlink" aria-label="Link to this header" href="#h_simplex_noise"></a></h1><p>Choosing random points would be easy to do with a random number generator,
  155. but we want the points to move in interesting graceful ways.  To do that, we use
  156. simplex noise. This is a 2D function (let&#8217;s call the inputs u and v) that
  157. produces a value from -1 to 1.  The important thing is the function is
  158. continuous: if you sample it at two (u,v) coordinates that are close together,
  159. the results will be close together.  But it&#8217;s also random: the continuous curves
  160. you get are wavy in unpredictable ways.  Think of the simplex noise function as
  161. a smooth hilly landscape.</p><p>To get an (x,y) point for our drawing, we choose a (u,v) coordinate to
  162. produce an x value and a completely different (u,v) coordinate for the y.  To
  163. get the next (x,y) point, we keep the u values the same and change the v values by
  164. just a tiny bit.  That makes the (x,y) points move smoothly but interestingly.</p><p>Here are the trails of four points taking 50 steps using this scheme:</p><div class="figurep"><figure><picture><source type="image/webp" srcset="https://nedbatchelder.com/iv/webp/pix/fluidity/point_motion.png.webp"><img src="https://nedbatchelder.com/pix/fluidity/point_motion.png" alt="Four trails of red dots showing how the randomness creates unpredictable but interesting paths" width="400" height="400" class="hairline"></picture></figure></div><p>If we use seven points taking five steps, and draw curves through the seven
  165. points at each step, we get examples like this:</p><div class="figurep"><figure><picture><source type="image/webp" srcset="https://nedbatchelder.com/iv/webp/pix/fluidity/small_runs.png.webp"><img src="https://nedbatchelder.com/pix/fluidity/small_runs.png" alt="Drawing curves through the points, widely spaced to show the construction" width="600" height="300" class="hairline"></picture></figure></div><p>I&#8217;ve left the points visible, and given them large steps so the lines are
  166. very widely spaced to show the motion.  Taking out the points and drawing more
  167. lines with smaller steps gives us this:</p><div class="figurep"><figure><picture><source type="image/webp" srcset="https://nedbatchelder.com/iv/webp/pix/fluidity/large_runs.png.webp"><img src="https://nedbatchelder.com/pix/fluidity/large_runs.png" alt="More lines to move toward the look we want" width="600" height="300" class="hairline"></picture></figure></div><p>With 40 lines drawn wider with some transparency, we start to see the smoky
  168. fluidity:</p><div class="figurep"><figure><picture><source type="image/webp" srcset="https://nedbatchelder.com/iv/webp/pix/fluidity/larger_runs.png.webp"><img src="https://nedbatchelder.com/pix/fluidity/larger_runs.png" alt="Now we're getting the original effect" width="600" height="300" class="hairline"></picture></figure></div><h1 id="h_jumps">Jumps<a class="headerlink" aria-label="Link to this header" href="#h_jumps"></a></h1><p>In his Mastodon post, aBe commented on the separating of the lines as one of
  169. the things he liked about this. But why do they do that?  If we are moving the
  170. points in small increments, why do the curves sometimes make large jumps?</p><p>The first reason is because of Hobby curves.  They do a great job drawing a
  171. curve through a set of points as a person might.  But a downside of the
  172. algorithm is sometimes changing a point a small amount makes the entire curve
  173. take a different route. If you play around with the interactive examples on
  174. <a rel="external noopener" href="https://www.jakelow.com/blog/hobby-curves">Jake Low&#8217;s page</a> you will see the curve can unexpectedly
  175. take a different shape.</p><p>As we inch our points along, sometimes the Hobby curve jumps.</p><p>The second reason is due to Hilbert sorting.  Each of our lines is sorted
  176. independently of how the previous line was sorted.  If a point&#8217;s small motion
  177. moves it into a different grid square, it can change the sorting order, which
  178. changes the Hobby curve even more.</p><p>If we sort the first line, and then keep that order of points for all the
  179. lines, the result has fewer jumps, but the Hobby curves still act
  180. unpredictably:</p><div class="figurep"><figure><picture><source type="image/webp" srcset="https://nedbatchelder.com/iv/webp/pix/fluidity/first_line_runs.png.webp"><img src="https://nedbatchelder.com/pix/fluidity/first_line_runs.png" alt="The same two sets of points as the last figure. Fewer jumps, but still with some discontinuities" width="600" height="300" class="hairline"></picture></figure></div><h1 id="h_colophon">Colophon<a class="headerlink" aria-label="Link to this header" href="#h_colophon"></a></h1><p>This was all done with Python, using other people&#8217;s implementations of the
  181. hard parts:
  182. <a href="https://github.com/ltrujello/Hobby_Curve_Algorithm/blob/main/python/hobby.py" rel="external noopener">hobby.py</a>,
  183. <a href="https://pypi.org/project/hilbertcurve/" rel="external noopener">hilbertcurve</a>, and
  184. <a href="https://pypi.org/project/super-simplex/" rel="external noopener">super-simplex</a>.  My code
  185. is on GitHub
  186. (<a href="https://github.com/nedbat/fluidity" rel="external noopener">nedbat/fluidity</a>), but it&#8217;s a
  187. mess.  Think of it as a woodworking studio with half-finished pieces and wood
  188. chips strewn everywhere.</p><p>A lot of the learning and experimentation was in
  189. <a href="https://github.com/nedbat/fluidity/blob/main/play.ipynb" rel="external noopener">my Jupyter
  190. notebook</a>.  Part of the process for work like this is playing around with
  191. different values of tweakable parameters and seeds for the random numbers to get
  192. the effect you want, either artistic or pedagogical. The notebook shows some of
  193. the thumbnail galleries I used to pick the examples to show.</p><p>I went on to play with animations, which led to other learnings, but those
  194. will have to wait for another blog post.
  195. <b>Update:</b> I animated these in <a href="https://nedbatchelder.com/blog/202510/natural_cubics_circular_simplex.html">Natural cubics, circular Simplex</a>.
  196. </p>
  197. ]]></description>
  198. </item>
  199. <item rdf:about="https://nedbatchelder.com/blog/202509/testing_is_better_than_dsa.html">
  200. <title>Testing is better than DSA</title>
  201. <link>https://nedbatchelder.com/blog/202509/testing_is_better_than_dsa.html</link>
  202. <dc:date>2025-09-22T12:04:08-04:00</dc:date>
  203. <dc:creator>Ned Batchelder</dc:creator>
  204. <description><![CDATA[<p>I see new learners asking about &#8220;DSA&#8221; a lot.  Data Structures and Algorithms
  205. are of course important: considered broadly, they are the two ingredients that
  206. make up all programs.  But in my opinion, &#8220;DSA&#8221; as an abstract field of study
  207. is over-emphasized.</p><p>I understand why people focus on DSA: it&#8217;s a concrete thing to learn about,
  208. there are web sites devoted to testing you on it, and most importantly, because
  209. job interviews often involve DSA coding questions.</p><p>Before I get to other opinions, let me make clear that anything you can do to
  210. help you get a job is a good thing to do.  If grinding
  211. <a rel="external noopener" href="https://leetcode.com/">leetcode</a> will land you a position, then do it.</p><p>But I hope companies hiring entry-level engineers aren&#8217;t asking them to
  212. reverse linked lists or balance trees.  Asking about techniques that can be
  213. memorized ahead of time won&#8217;t tell them anything about how well you can work.
  214. The stated purpose of those interviews is to see how well you can figure out
  215. solutions, in which case memorization will defeat the point.</p><p>The thing new learners don&#8217;t understand about DSA is that actual software
  216. engineering almost never involves implementing the kinds of algorithms that
  217. &#8220;DSA&#8221; teaches you.  Sure, it can be helpful to work through some of these
  218. puzzles and see how they are solved, but writing real code just doesn&#8217;t involve
  219. writing that kind of code.</p><p>Here is what I think in-the-trenches software engineers should know about
  220. data structures and algorithms:</p><ul>
  221.  
  222. <li>Data structures are ways to organize data. Learn some of the basics: linked
  223. list, array, hash table, tree.  By &#8220;learn&#8221; I mean understand what it does
  224. and why you might want to use one.</li>
  225.  
  226. <li>Different data structures can be used to organize the same data in different
  227. ways.  Learn some of the trade-offs between structures that are similar.</li>
  228.  
  229. <li>Algorithms are ways of manipulating data.  I don&#8217;t mean named algorithms
  230. like Quicksort, but algorithms as any chunk of code that works on data and
  231. does something with it.</li>
  232.  
  233. <li>How you organize data affects what algorithms you can use to work with the
  234. data.  Some data structures will be slow for some operations where another
  235. structure will be fast.</li>
  236.  
  237. <li>Algorithms have a &#8220;time complexity&#8221; (Big O): <a rel="external noopener" href="/text/bigo.html">how the code
  238. slows as the data grows</a>.  Get a sense of what this means.</li>
  239.  
  240. <li>Python has a number of built-in data structures.  Learn how they work, and
  241. the time complexity of their operations.</li>
  242.  
  243. <li>Learn how to think about your code to understand its time complexity.</li>
  244.  
  245. <li>Read a little about more esoteric things like <a rel="external noopener" href="https://systemdesign.one/bloom-filters-explained/">Bloom
  246. filters</a>, so you can find them later in the unlikely case you need them.</li>
  247.  
  248. </ul><p>Here are some things you don&#8217;t need to learn:</p><ul>
  249.  
  250. <li>The details of a dozen different sorting algorithms.  Look at two to see
  251. different ways of approaching the same problem, then move on.</li>
  252.  
  253. <li>The names of &#8220;important&#8221; algorithms.  Those have all been implemented for
  254. you.</li>
  255.  
  256. <li>The answers to all N problems on some quiz web site.  You won&#8217;t be asked
  257. these exact questions, and they won&#8217;t come up in your real work.  Again: try a
  258. few to get a feel for how some algorithms work.  The exact answers are not what
  259. you need.</li>
  260.  
  261. </ul><p>Of course some engineers need to implement hash tables, or sorting algorithms
  262. or whatever.  We love those engineers: they write libraries we can use off the
  263. shelf so we don&#8217;t have to implement them ourselves.</p><p>There have been times when I implemented something that felt like An
  264. Algorithm (for example, <a href="https://nedbatchelder.com/blog/201707/finding_fuzzy_floats.html">Finding fuzzy floats</a>), but it was
  265. more about considering another perspective on my data, looking at the time
  266. complexity, and moving operations around to avoid quadratic behavior. It wasn&#8217;t
  267. opening a textbook to find the famous algorithm that would solve my problem.</p><p>Again: if it will help you get a job, deep-study DSA. But don&#8217;t be
  268. disappointed when you don&#8217;t use it on the job.</p><p>If you want to prepare yourself for a career, and also stand out in job
  269. interviews, learn how to write tests:</p><ul>
  270.  
  271. <li>This will be a skill you use constantly. Real-world software means writing
  272. tests much more than school teaches you to.</li>
  273.  
  274. <li>In a job search, testing experience will stand out more than DSA depth.  It
  275. shows you&#8217;ve thought about what it takes to write high-quality software instead
  276. of just academic exercises.</li>
  277.  
  278. <li>It&#8217;s not obvious how to test code well. It&#8217;s a puzzle and a problem to
  279. solve.  If you like figuring out solutions to tricky questions, focus on how to
  280. write code so that it can be tested, and how to test it.</li>
  281.  
  282. <li>Testing not only gives you more confidence in your code, it helps you write
  283. better code in the first place.</li>
  284.  
  285. <li>Testing applies everywhere, from tiny bits of code to entire architectures,
  286. assisting you in design and implementation at all scales.</li>
  287.  
  288. <li>If pursued diligently, testing is an engineering discipline in its own
  289. right, with a fascinating array of tools and techniques.</li>
  290.  
  291. </ul><p>Less DSA, more testing.</p>
  292. ]]></description>
  293. </item>
  294. <item rdf:about="https://nedbatchelder.com/blog/202508/finding_unneeded_pragmas.html">
  295. <title>Finding unneeded pragmas</title>
  296. <link>https://nedbatchelder.com/blog/202508/finding_unneeded_pragmas.html</link>
  297. <dc:date>2025-08-24T17:28:12-04:00</dc:date>
  298. <dc:creator>Ned Batchelder</dc:creator>
  299. <description><![CDATA[<p>To answer a <a rel="external noopener" href="https://github.com/nedbat/coveragepy/issues/251">long-standing coverage.py feature request</a>, I
  300. threw together an experiment: a tool to identify lines that have been excluded
  301. from coverage, but which were actually executed.</p><p>The program is a standalone file in the coverage.py repo. It is unsupported.
  302. I&#8217;d like people to try it to see what they think of the idea. Later we can
  303. decide what to do with it.</p><p>To try it: copy <a rel="external noopener" href="https://github.com/nedbat/coveragepy/blob/master/lab/warn_executed.py">warn_executed.py</a> from
  304. GitHub.  Create a .toml file that looks something like this:</p><blockquote class="code"><pre class="toml"><div class="source"><span class="c1">#&#xA0;Regexes&#xA0;that&#xA0;identify&#xA0;excluded&#xA0;lines:</span>
  305. <br><span class="n">warn-executed</span><span class="w">&#xA0;</span><span class="o">=</span><span class="w">&#xA0;</span><span class="p">[</span>
  306. <br><span class="w">&#xA0;&#xA0;&#xA0;&#xA0;</span><span class="s2">&quot;pragma:&#xA0;no&#xA0;cover&quot;</span><span class="p">,</span>
  307. <br><span class="w">&#xA0;&#xA0;&#xA0;&#xA0;</span><span class="s2">&quot;raise&#xA0;AssertionError&quot;</span><span class="p">,</span>
  308. <br><span class="w">&#xA0;&#xA0;&#xA0;&#xA0;</span><span class="s2">&quot;pragma:&#xA0;cant&#xA0;happen&quot;</span><span class="p">,</span>
  309. <br><span class="w">&#xA0;&#xA0;&#xA0;&#xA0;</span><span class="s2">&quot;pragma:&#xA0;never&#xA0;called&quot;</span><span class="p">,</span>
  310. <br><span class="w">&#xA0;&#xA0;&#xA0;&#xA0;</span><span class="p">]</span>
  311. <br>
  312. <br><span class="c1">#&#xA0;Regexes&#xA0;that&#xA0;identify&#xA0;partial&#xA0;branch&#xA0;lines:</span>
  313. <br><span class="n">warn-not-partial</span><span class="w">&#xA0;</span><span class="o">=</span><span class="w">&#xA0;</span><span class="p">[</span>
  314. <br><span class="w">&#xA0;&#xA0;&#xA0;&#xA0;</span><span class="s2">&quot;pragma:&#xA0;no&#xA0;branch&quot;</span><span class="p">,</span>
  315. <br><span class="w">&#xA0;&#xA0;&#xA0;&#xA0;</span><span class="p">]</span>
  316. <br></div>
  317. </pre></blockquote><p>These are exclusion regexes that you&#8217;ve used in your coverage runs.  The
  318. program will print out any line identified by a pattern and that ran during your
  319. tests.  It might be that you don&#8217;t need to exclude the line, because it ran.</p><p>In this file, none of your coverage settings or the default regexes are
  320. assumed: you need to explicitly specify all the patterns you want flagged.</p><p>Run the program with Python 3.11 or higher, giving the name of the coverage
  321. data file and the name of your new TOML configuration file. It will print the
  322. lines that might not need excluding:</p><blockquote class="code"><pre class="shell"><div class="source">$<span class="w">&#xA0;</span>python3.12<span class="w">&#xA0;</span>warn_executed.py<span class="w">&#xA0;</span>.coverage<span class="w">&#xA0;</span><span>warn.toml</span>
  323. <br></div>
  324. </pre></blockquote><p>The reason for a new list of patterns instead of just reading the existing
  325. coverage settings is that some exclusions are &#8220;don&#8217;t care&#8221; rather than &#8220;this
  326. will never happen.&#8221; For example, I exclude &#8220;def __repr__&#8221; because some
  327. __repr__&#8217;s are just to make my debugging easier. I don&#8217;t care if the test suite
  328. runs them or not. It might run them, so I don&#8217;t want it to be a warning that
  329. they actually ran.</p><p>This tool is not perfect. For example, I exclude &#8220;if TYPE_CHECKING:&#8221; because
  330. I want that entire clause excluded.  But the if-line itself is actually run.  If
  331. I include that pattern in the warn-executed list, it will flag all of those
  332. lines.  Maybe I&#8217;m forgetting a way to do this: it would be good to have a way to
  333. exclude the body of the if clause while understanding that the if-line itself is
  334. executed.</p><p>Give <a rel="external noopener" href="https://github.com/nedbat/coveragepy/blob/master/lab/warn_executed.py">warn_executed.py</a> a try and comment on
  335. <a rel="external noopener" href="https://github.com/nedbat/coveragepy/issues/251">the issue</a> about what you think of it.</p>
  336. ]]></description>
  337. </item>
  338. <item rdf:about="https://nedbatchelder.com/blog/202508/starting_with_pytests_parametrize.html">
  339. <title>Starting with pytest’s parametrize</title>
  340. <link>https://nedbatchelder.com/blog/202508/starting_with_pytests_parametrize.html</link>
  341. <dc:date>2025-08-13T06:14:46-04:00</dc:date>
  342. <dc:creator>Ned Batchelder</dc:creator>
  343. <description><![CDATA[<p>Writing tests can be difficult and repetitive.  Pytest has a feature called
  344. parametrize that can make it reduce duplication, but it can be hard to
  345. understand if you are new to the testing world. It&#8217;s not as complicated as it
  346. seems.</p><p>Let&#8217;s say you have a function called <code>add_nums()</code> that adds up a list of
  347. numbers, and you want to write tests for it.  Your tests might look like
  348. this:</p><blockquote class="code"><pre class="python"><div class="source"><span class="k">def</span><span class="w">&#xA0;</span><span class="nf">test_123</span><span class="p">():</span>
  349. <br>&#xA0;&#xA0;&#xA0;&#xA0;<span class="k">assert</span>&#xA0;<span class="n">add_nums</span><span class="p">([</span><span class="mi">1</span><span class="p">,</span>&#xA0;<span class="mi">2</span><span class="p">,</span>&#xA0;<span class="mi">3</span><span class="p">])</span>&#xA0;<span class="o">==</span>&#xA0;<span class="mi">6</span>
  350. <br>
  351. <br><span class="k">def</span><span class="w">&#xA0;</span><span class="nf">test_negatives</span><span class="p">():</span>
  352. <br>&#xA0;&#xA0;&#xA0;&#xA0;<span class="k">assert</span>&#xA0;<span class="n">add_nums</span><span class="p">([</span><span class="mi">1</span><span class="p">,</span>&#xA0;<span class="mi">2</span><span class="p">,</span>&#xA0;<span class="o">-</span><span class="mi">3</span><span class="p">])</span>&#xA0;<span class="o">==</span>&#xA0;<span class="mi">0</span>
  353. <br>
  354. <br><span class="k">def</span><span class="w">&#xA0;</span><span class="nf">test_empty</span><span class="p">():</span>
  355. <br>&#xA0;&#xA0;&#xA0;&#xA0;<span class="k">assert</span>&#xA0;<span class="n">add_nums</span><span class="p">([])</span>&#xA0;<span class="o">==</span>&#xA0;<span class="mi">0</span>
  356. <br></div>
  357. </pre></blockquote><p>This is great: you&#8217;ve tested some behaviors of your <code>add_nums()</code>
  358. function.  But it&#8217;s getting tedious to write out more test cases.  The names of the
  359. function have to be different from each other, and they don&#8217;t mean anything, so
  360. it&#8217;s extra work for no benefit.  The test functions all have the same structure,
  361. so you&#8217;re repeating uninteresting details. You want to add more cases but it
  362. feels like there&#8217;s friction that you want to avoid.</p><p>If we look at these functions, they are very similar.  In any software, when
  363. we have functions that are similar in structure, but differ in some details, we
  364. can refactor them to be one function with parameters for the differences. We can
  365. do the same for our test functions.</p><p>Here the functions all have the same structure: call <code>add_nums()</code> and
  366. assert what the return value should be.  The differences are the list we pass to
  367. <code>add_nums()</code> and the value we expect it to return.  So we can turn those
  368. into two parameters in our refactored function:</p><blockquote class="code"><pre class="python"><div class="source"><span class="k">def</span><span class="w">&#xA0;</span><span class="nf">test_add_nums</span><span class="p">(</span><span class="n">nums</span><span class="p">,</span>&#xA0;<span class="n">expected_total</span><span class="p">):</span>
  369. <br>&#xA0;&#xA0;&#xA0;&#xA0;<span class="k">assert</span>&#xA0;<span class="n">add_nums</span><span class="p">(</span><span class="n">nums</span><span class="p">)</span>&#xA0;<span class="o">==</span>&#xA0;<span class="n">expected_total</span>
  370. <br></div>
  371. </pre></blockquote><p>Unfortunately, tests aren&#8217;t run like regular functions.  We write the test
  372. functions, but we don&#8217;t call them ourselves.  That&#8217;s the reason the names of the
  373. test functions don&#8217;t matter.  The test runner (pytest) finds functions named
  374. <code>test_*</code> and calls them for us.  When they have no parameters, pytest can
  375. call them directly.  But now that our test function has two parameters, we have
  376. to give pytest instructions about how to call it.</p><p>To do that, we use the <code>@pytest.mark.parametrize</code> decorator. Using it
  377. looks like this:</p><blockquote class="code"><pre class="python"><div class="source"><span class="kn">import</span><span class="w">&#xA0;</span><span class="nn">pytest</span>
  378. <br>
  379. <br><span class="nd">@pytest</span><span class="o">.</span><span class="n">mark</span><span class="o">.</span><span class="n">parametrize</span><span class="p">(</span>
  380. <br>&#xA0;&#xA0;&#xA0;&#xA0;<span class="s2">&quot;nums,&#xA0;expected_total&quot;</span><span class="p">,</span>
  381. <br>&#xA0;&#xA0;&#xA0;&#xA0;<span class="p">[</span>
  382. <br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<span class="p">([</span><span class="mi">1</span><span class="p">,</span>&#xA0;<span class="mi">2</span><span class="p">,</span>&#xA0;<span class="mi">3</span><span class="p">],</span>&#xA0;<span class="mi">6</span><span class="p">),</span>
  383. <br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<span class="p">([</span><span class="mi">1</span><span class="p">,</span>&#xA0;<span class="mi">2</span><span class="p">,</span>&#xA0;<span class="o">-</span><span class="mi">3</span><span class="p">],</span>&#xA0;<span class="mi">0</span><span class="p">),</span>
  384. <br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<span class="p">([],</span>&#xA0;<span class="mi">0</span><span class="p">),</span>
  385. <br>&#xA0;&#xA0;&#xA0;&#xA0;<span class="p">]</span>
  386. <br><span class="p">)</span>
  387. <br><span class="k">def</span><span class="w">&#xA0;</span><span class="nf">test_add_nums</span><span class="p">(</span><span class="n">nums</span><span class="p">,</span>&#xA0;<span class="n">expected_total</span><span class="p">):</span>
  388. <br>&#xA0;&#xA0;&#xA0;&#xA0;<span class="k">assert</span>&#xA0;<span class="n">add_nums</span><span class="p">(</span><span class="n">nums</span><span class="p">)</span>&#xA0;<span class="o">==</span>&#xA0;<span class="n">expected_total</span>
  389. <br></div>
  390. </pre></blockquote><p>There&#8217;s a lot going on here, so let&#8217;s take it step by step.</p><p>If you haven&#8217;t seen a decorator before, it starts with <code>@</code> and is like a
  391. prologue to a function definition. It can affect how the function is defined or
  392. provide information about the function.</p><p>The parametrize decorator is itself a function call that takes two arguments.
  393. The first is a string (&#8220;nums, expected_total&#8221;) that names the two arguments to
  394. the test function.  Here the decorator is instructing pytest, &#8220;when you call
  395. <code>test_add_nums</code>, you will need to provide values for its <code>nums and</code>
  396. <code>expected_total parameters</code>.&#8221;</p><p>The second argument to <code>parametrize</code> is a list of the values to supply
  397. as the arguments.  Each element of the list will become one call to our test
  398. function.  In this example, the list has three tuples, so pytest will call our
  399. test function three times.  Since we have two parameters to provide, each
  400. element of the list is a tuple of two values.</p><p>The first tuple is <code>([1, 2, 3], 6)</code>, so the first time pytest calls
  401. test_add_nums, it will call it as test_add_nums([1, 2, 3], 6).  All together,
  402. pytest will call us three times, like this:</p><blockquote class="code"><pre class="python"><div class="source"><span class="n">test_add_nums</span><span class="p">([</span><span class="mi">1</span><span class="p">,</span>&#xA0;<span class="mi">2</span><span class="p">,</span>&#xA0;<span class="mi">3</span><span class="p">],</span>&#xA0;<span class="mi">6</span><span class="p">)</span>
  403. <br><span class="n">test_add_nums</span><span class="p">([</span><span class="mi">1</span><span class="p">,</span>&#xA0;<span class="mi">2</span><span class="p">,</span>&#xA0;<span class="o">-</span><span class="mi">3</span><span class="p">],</span>&#xA0;<span class="mi">0</span><span class="p">)</span>
  404. <br><span class="n">test_add_nums</span><span class="p">([],</span>&#xA0;<span class="mi">0</span><span class="p">)</span>
  405. <br></div>
  406. </pre></blockquote><p>This will all happen automatically.  With our original test functions, when
  407. we ran pytest, it showed the results as three passing tests because we had three
  408. separate test functions.  Now even though we only have one function, it still
  409. shows as three passing tests!  Each set of values is considered a separate test
  410. that can pass or fail independently.  This is the main advantage of using
  411. parametrize instead of writing three separate assert lines in the body of a
  412. simple test function.</p><p>What have we gained?</p><ul>
  413.  
  414. <li>We don&#8217;t have to write three separate functions with different names.</li>
  415.  
  416. <li>We don&#8217;t have to repeat the same details in each function (<code>assert</code>,
  417. <code>add_nums()</code>, <code>==</code>).</li>
  418.  
  419. <li>The differences between the tests (the actual data) are written succinctly
  420. all in one place.</li>
  421.  
  422. <li>Adding another test case is as simple as adding another line of data to the
  423. decorator.</li>
  424.  
  425. </ul>
  426. ]]></description>
  427. </item>
  428. <item rdf:about="https://nedbatchelder.com/blog/202507/coveragepy_regex_pragmas.html">
  429. <title>Coverage.py regex pragmas</title>
  430. <link>https://nedbatchelder.com/blog/202507/coveragepy_regex_pragmas.html</link>
  431. <dc:date>2025-07-28T12:04:12-04:00</dc:date>
  432. <dc:creator>Ned Batchelder</dc:creator>
  433. <description><![CDATA[<p><a rel="external noopener" href="https://coverage.readthedocs.io/">Coverage.py</a> lets you indicate code to exclude from
  434. measurement by adding comments to your Python files. But coverage implements
  435. them differently than other similar tools. Rather than having fixed syntax for
  436. these comments, they are defined using regexes that you can change or add to.
  437. This has been surprisingly powerful.</p><p>The basic behavior: coverage finds lines in your source files that match the
  438. regexes.  These lines are excluded from measurement, that is, it&#8217;s OK if they
  439. aren&#8217;t executed.  If a matched line is part of a multi-line statement the
  440. whole multi-line statement is excluded.  If a matched line introduces a block of
  441. code the entire block is excluded.</p><p>At first, these regexes were just to make it easier to implement the basic
  442. &#8220;here&#8217;s the comment you use&#8221; behavior for pragma comments.  But it also enabled
  443. pragma-less exclusions.  You could decide (for example) that you didn&#8217;t care to
  444. test any <code>__repr__</code> methods.  By adding <code>def __repr__</code> as an exclusion
  445. regex, all of those methods were automatically excluded from coverage
  446. measurement without having to add a comment to each one. Very nice.</p><p>Not only did this let people add custom exclusions in their projects, but
  447. it enabled third-party plugins that could configure regexes in other interesting
  448. ways:</p><ul>
  449.  
  450. <li><a href="https://pypi.org/project/covdefaults/" rel="external noopener">covdefaults</a> adds a
  451. bunch of default exclusions, and also platform- and version-specific comment
  452. syntaxes.</li>
  453.  
  454. <li><a href="https://pypi.org/project/coverage-conditional-plugin/" rel="external noopener">coverage-conditional-plugin</a>
  455. gives you a way to create comment syntaxes for entire files, for whether other
  456. packages are installed, and so on.</li>
  457.  
  458. </ul><p>Then about a year ago, <a rel="external noopener" href="https://github.com/nedbat/coveragepy/pull/1807">Daniel Diniz contributed a
  459. change</a> that amped up the power: regexes could match multi-line patterns.
  460. This sounds like not that large a change, but it enabled much more powerful
  461. exclusions.  As a sign, it made it possible to support <a rel="external noopener" href="https://coverage.readthedocs.io/en/latest/changes.html#version-7-6-0-2024-07-11">four
  462. different feature requests</a>.</p><p>To make it work, Daniel changed the matching code.  Originally, it was a loop
  463. over the lines in the source file, checking each line for a match against the
  464. regexes.  The new code uses the entire source file as the target string, and
  465. loops over the matches against that text.  Each match is converted into a set of
  466. line numbers and added to the results.</p><p>The power comes from being able to use one pattern to match many lines. For
  467. example, one of the four feature requests was <a rel="external noopener" href="https://github.com/nedbat/coveragepy/issues/118">how to exclude an
  468. entire file</a>.  With configurable multi-line regex patterns, you can do this
  469. yourself:</p><blockquote class="code"><pre>\A(?s:.*# pragma: exclude file.*)\Z<br></pre></blockquote><p>With this regex, if you put the comment &#8220;# pragma: exclude file&#8221; in your
  470. source file, the entire file will be excluded.  The <code>\A</code> and <code>\Z</code>
  471. match the start and end of the target text, which remember is the entire file.
  472. The <code>(?s:...)</code> means the <a rel="external noopener" href="https://docs.python.org/3/library/re.html#re.S">s/DOTALL</a> flag is in
  473. effect, so <code>.</code> can match newlines.  This pattern matches the entire source
  474. file if the desired pragma is somewhere in the file.</p><p>Another requested feature was <a rel="external noopener" href="https://github.com/nedbat/coveragepy/issues/1803">excluding code between two
  475. lines</a>.  We can use &#8220;# no cover: start&#8221; and &#8220;# no cover: end&#8221; as delimiters
  476. with this regex:</p><blockquote class="code"><pre># no cover: start(?s:.*?)# no cover: stop<br></pre></blockquote><p>Here <code>(?s:.*?)</code> means any number of any character at all, but as few as
  477. possible.  A star in regexes means as many as possible, but star-question-mark
  478. means as few as possible.  We need the minimal match so that we don&#8217;t match from
  479. the start of one pair of comments all the way through to the end of a different
  480. pair of comments.</p><p>This regex approach is powerful, but is still fairly shallow.  For example,
  481. either of these two examples would get the wrong lines if you had a string
  482. literal with the pragma text in it.  There isn&#8217;t a regex that skips easily over
  483. string literals.</p><p>This kind of difficulty hit home when I added a new default pattern to
  484. exclude empty placeholder methods like this:</p><blockquote class="code"><pre class="python"><div class="source"><span class="k">def</span><span class="w">&#xA0;</span><span class="nf">not_yet</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>&#xA0;<span class="o">...</span>
  485. <br>
  486. <br><span class="k">def</span><span class="w">&#xA0;</span><span class="nf">also_not_this</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
  487. <br>&#xA0;&#xA0;&#xA0;&#xA0;<span class="o">...</span>
  488. <br>
  489. <br><span class="k">async</span>&#xA0;<span class="k">def</span><span class="w">&#xA0;</span><span class="nf">definitely_not_this</span><span class="p">(</span>
  490. <br>&#xA0;&#xA0;&#xA0;&#xA0;<span class="bp">self</span><span class="p">,</span>
  491. <br>&#xA0;&#xA0;&#xA0;&#xA0;<span class="n">arg1</span><span class="p">,</span>
  492. <br><span class="p">):</span>
  493. <br>&#xA0;&#xA0;&#xA0;&#xA0;<span class="o">...</span>
  494. <br></div>
  495. </pre></blockquote><p>We can&#8217;t just match three dots, because ellipses can be used in other places
  496. than empty function bodies. We need to be more delicate. I ended up with:</p><blockquote class="code"><pre>^\s*(((async )?def .*?)?\)(\s*-&gt;.*?)?:\s*)?\.\.\.\s*(#|$)<br></pre></blockquote><p>This craziness ensures the ellipsis is part of an (async) def, that the
  497. ellipsis appears first in the body (but no docstring allowed, doh!), allows for
  498. a comment on the line, and so on.  And even with a pattern this complex, it
  499. would incorrectly match this contrived line:</p><blockquote class="code"><pre class="python"><div class="source"><span class="k">def</span><span class="w">&#xA0;</span><span class="nf">f</span><span class="p">():</span>&#xA0;<span class="nb">print</span><span class="p">(</span><span class="s2">&quot;(well):&#xA0;...&#xA0;#2&#xA0;false&#xA0;positive!&quot;</span><span class="p">)</span>
  500. <br></div>
  501. </pre></blockquote><p>So regexes aren&#8217;t perfect, but they&#8217;re a pretty good balance: flexible and
  502. powerful, and will work great on real code even if we can invent weird edge
  503. cases where they fail.</p><p>What started as a simple implementation expediency has turned into a powerful
  504. configuration option that has done more than I would have thought.</p>
  505. ]]></description>
  506. </item>
  507. <item rdf:about="https://nedbatchelder.com/blog/202507/coverage_7100_patch.html">
  508. <title>Coverage 7.10.0: patch</title>
  509. <link>https://nedbatchelder.com/blog/202507/coverage_7100_patch.html</link>
  510. <dc:date>2025-07-24T19:03:27-04:00</dc:date>
  511. <dc:creator>Ned Batchelder</dc:creator>
  512. <description><![CDATA[<p>Years ago I greeted a friend returning from vacation and asked how it had
  513. been. She answered, &#8220;It was good, I got a lot done!&#8221; I understand that feeling.
  514. I just had a long vacation myself, and used the time to clean up some old issues
  515. and add some new features in <a rel="external noopener" href="https://pypi.org/project/coverage/">coverage.py v7.10</a>.</p><p>The major new feature is a configuration option,
  516. <a rel="external noopener" href="https://coverage.readthedocs.io/en/latest/config.html#run-patch"><code>[run] patch</code></a>.  With it, you specify named
  517. patches that coverage can use to monkey-patch some behavior that gets in the way
  518. of coverage measurement.</p><p>The first is <code>subprocess</code>.  Coverage works great when you start your
  519. program with coverage measurement, but has long had the problem of how to also
  520. measure the coverage of sub-processes that your program created.  The existing
  521. solution had been a complicated two-step process of creating obscure .pth files
  522. and setting environment variables.  Whole projects appeared on PyPI to handle
  523. this for you.</p><p>Now, <code>patch = subprocess</code> will do this for you automatically, and clean
  524. itself up when the program ends.  It handles sub-processes created by the
  525. <a rel="external noopener" href="https://docs.python.org/3/library/subprocess.html#module-subprocess">subprocess</a> module, the
  526. <a rel="external noopener" href="https://docs.python.org/3/library/os.html#os.system">os.system()</a> function, and any of the
  527. <a rel="external noopener" href="https://docs.python.org/3/library/os.html#os.execl">execv</a> or <a rel="external noopener" href="https://docs.python.org/3/library/os.html#os.spawnl">spawnv</a> families of
  528. functions.</p><p>This alone has spurred <a rel="external noopener" href="https://bsky.app/profile/did:plc:yj4vzsbzzkpswr7x5yagzhhx/post/3luqfffiiqk27">one user to exclaim</a>,</p><blockquote><div><p>The latest release of Coverage feels like a Christmas present!
  529. The native support for Python subprocesses is so good!</p></div></blockquote><p>Another patch is <code>_exit</code>.  This patches
  530. <a rel="external noopener" href="https://docs.python.org/3/library/os.html#os._exit">os._exit()</a> so that coverage saves its data before
  531. exiting. The os._exit() function is an immediate and abrupt termination of the
  532. program, skipping all kinds of registered clean up code.  This patch makes it
  533. possible to collect coverage data from programs that end this way.</p><p>The third patch is <code>execv</code>.  The <a rel="external noopener" href="https://docs.python.org/3/library/os.html#os.execl">execv</a> functions
  534. end the current program and replace it with a new program in the same process.
  535. The <code>execv</code> patch arranges for coverage to save its data before the
  536. current program is ended.</p><p>Now that these patches are available, it seems silly that it&#8217;s taken so long.
  537. They (mostly) weren&#8217;t difficult. I guess it took looking at the old issues,
  538. realizing the friction they caused, and thinking up a new way to let users
  539. control the patching.  Monkey-patching is a bit invasive, so I&#8217;ve never wanted
  540. to do it implicitly.  The patch option gives the user an explicit way to request
  541. what they need without having to get into the dirty details themselves.</p><p>Another process-oriented feature was contributed by Arkady Gilinsky: with
  542. <code>--save-signal=USR1</code> you can specify a user signal that coverage will
  543. attend to.  When you send the signal to your running coverage process, it will
  544. save the collected data to disk.  This gives a way to measure coverage in a
  545. long-running process without having to end the process.</p><p>There were some other fixes and features along the way, like better HTML
  546. coloring of multi-line statements, and more default exclusions
  547. (<code>if TYPE_CHECKING:</code> and <code>...</code>).</p><p>It feels good to finally address some of these pain points.  I also closed
  548. some stale issues and pull requests.  There is more to do, always more to do,
  549. but this feels like a real step forward.  Give <a rel="external noopener" href="https://coverage.readthedocs.io/en/7.10.0/changes.html#version-7-10-0-2025-07-24">coverage
  550. 7.10.0</a> a try and let me know how it works for you.</p>
  551. ]]></description>
  552. </item>
  553. <item rdf:about="https://nedbatchelder.com/blog/202507/2048_iterators_and_iterables.html">
  554. <title>2048: iterators and iterables</title>
  555. <link>https://nedbatchelder.com/blog/202507/2048_iterators_and_iterables.html</link>
  556. <dc:date>2025-07-15T06:52:29-04:00</dc:date>
  557. <dc:creator>Ned Batchelder</dc:creator>
  558. <description><![CDATA[<p>I wrote a <a rel="external noopener" href="https://github.com/nedbat/odds/blob/master/2048/2048.py">low-tech terminal-based version</a> of the
  559. classic <a rel="external noopener" href="https://play2048.co/">2048 game</a> and had some interesting difficulties
  560. with iterators along the way.</p><p>2048 has a 4<span class="times">×</span>4 grid with sliding tiles.  Because the tiles can slide
  561. left or right and up or down, sometimes we want to loop over the rows and
  562. columns from 0 to 3, and sometimes from 3 to 0.  My first attempt looked like
  563. this:</p><blockquote class="code"><pre class="python"><div class="source"><span class="n">N</span>&#xA0;<span class="o">=</span>&#xA0;<span class="mi">4</span>
  564. <br><span class="k">if</span>&#xA0;<span class="n">sliding_right</span><span class="p">:</span>
  565. <br>&#xA0;&#xA0;&#xA0;&#xA0;<span class="n">cols</span>&#xA0;<span class="o">=</span>&#xA0;<span class="nb">range</span><span class="p">(</span><span class="n">N</span><span class="o">-</span><span class="mi">1</span><span class="p">,</span>&#xA0;<span class="o">-</span><span class="mi">1</span><span class="p">,</span>&#xA0;<span class="o">-</span><span class="mi">1</span><span class="p">)</span>&#xA0;&#xA0;&#xA0;<span class="c1">#&#xA0;3&#xA0;2&#xA0;1&#xA0;0</span>
  566. <br><span class="k">else</span><span class="p">:</span>
  567. <br>&#xA0;&#xA0;&#xA0;&#xA0;<span class="n">cols</span>&#xA0;<span class="o">=</span>&#xA0;<span class="nb">range</span><span class="p">(</span><span class="n">N</span><span class="p">)</span>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<span class="c1">#&#xA0;0&#xA0;1&#xA0;2&#xA0;3</span>
  568. <br>
  569. <br><span class="k">if</span>&#xA0;<span class="n">sliding_down</span><span class="p">:</span>
  570. <br>&#xA0;&#xA0;&#xA0;&#xA0;<span class="n">rows</span>&#xA0;<span class="o">=</span>&#xA0;<span class="nb">range</span><span class="p">(</span><span class="n">N</span><span class="o">-</span><span class="mi">1</span><span class="p">,</span>&#xA0;<span class="o">-</span><span class="mi">1</span><span class="p">,</span>&#xA0;<span class="o">-</span><span class="mi">1</span><span class="p">)</span>&#xA0;&#xA0;&#xA0;<span class="c1">#&#xA0;3&#xA0;2&#xA0;1&#xA0;0</span>
  571. <br><span class="k">else</span><span class="p">:</span>
  572. <br>&#xA0;&#xA0;&#xA0;&#xA0;<span class="n">rows</span>&#xA0;<span class="o">=</span>&#xA0;<span class="nb">range</span><span class="p">(</span><span class="n">N</span><span class="p">)</span>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<span class="c1">#&#xA0;0&#xA0;1&#xA0;2&#xA0;3</span>
  573. <br>
  574. <br><span class="k">for</span>&#xA0;<span class="n">row</span>&#xA0;<span class="ow">in</span>&#xA0;<span class="n">rows</span><span class="p">:</span>
  575. <br>&#xA0;&#xA0;&#xA0;&#xA0;<span class="k">for</span>&#xA0;<span class="n">col</span>&#xA0;<span class="ow">in</span>&#xA0;<span class="n">cols</span><span class="p">:</span>
  576. <br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<span class="o">...</span>
  577. <br></div>
  578. </pre></blockquote><p>This worked, but those counting-down ranges are ugly. Let&#8217;s make it
  579. nicer:</p><blockquote class="code"><pre class="python"><div class="source"><span class="n">cols</span>&#xA0;<span class="o">=</span>&#xA0;<span class="nb">range</span><span class="p">(</span><span class="n">N</span><span class="p">)</span>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<span class="c1">#&#xA0;0&#xA0;1&#xA0;2&#xA0;3</span>
  580. <br><span class="k">if</span>&#xA0;<span class="n">sliding_right</span><span class="p">:</span>
  581. <br>&#xA0;&#xA0;&#xA0;&#xA0;<span class="n">cols</span>&#xA0;<span class="o">=</span>&#xA0;<span class="nb">reversed</span><span class="p">(</span><span class="n">cols</span><span class="p">)</span>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<span class="c1">#&#xA0;3&#xA0;2&#xA0;1&#xA0;0</span>
  582. <br>
  583. <br><span class="n">rows</span>&#xA0;<span class="o">=</span>&#xA0;<span class="nb">range</span><span class="p">(</span><span class="n">N</span><span class="p">)</span>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<span class="c1">#&#xA0;0&#xA0;1&#xA0;2&#xA0;3</span>
  584. <br><span class="k">if</span>&#xA0;<span class="n">sliding_down</span><span class="p">:</span>
  585. <br>&#xA0;&#xA0;&#xA0;&#xA0;<span class="n">rows</span>&#xA0;<span class="o">=</span>&#xA0;<span class="nb">reversed</span><span class="p">(</span><span class="n">rows</span><span class="p">)</span>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<span class="c1">#&#xA0;3&#xA0;2&#xA0;1&#xA0;0</span>
  586. <br>
  587. <br><span class="k">for</span>&#xA0;<span class="n">row</span>&#xA0;<span class="ow">in</span>&#xA0;<span class="n">rows</span><span class="p">:</span>
  588. <br>&#xA0;&#xA0;&#xA0;&#xA0;<span class="k">for</span>&#xA0;<span class="n">col</span>&#xA0;<span class="ow">in</span>&#xA0;<span class="n">cols</span><span class="p">:</span>
  589. <br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<span class="o">...</span>
  590. <br></div>
  591. </pre></blockquote><p>Looks cleaner, but it doesn&#8217;t work!  Can you see why? It took me a bit of
  592. debugging to see the light.</p><p><code>range()</code> produces an iterable: something that can be iterated over.
  593. Similar but different is that <code>reversed()</code> produces an iterator: something
  594. that is already iterating.  Some iterables (like ranges) can be used more than
  595. once, creating a new iterator each time.  But once an iterator like
  596. <code>reversed()</code> has been consumed, it is done.  Iterating it again will
  597. produce no values.</p><p>If &#8220;iterable&#8221; vs &#8220;iterator&#8221; is already confusing here&#8217;s a quick definition:
  598. an iterable is something that can be iterated, that can produce values in a
  599. particular order.  An iterator tracks the state of an iteration in progress. An
  600. analogy: the pages of a book are iterable; a bookmark is an iterator.  The
  601. English hints at it: an iter-able is able to be iterated at some point, an
  602. iterator is actively iterating.</p><p>The outer loop of my double loop was iterating only once over the rows, so
  603. the row iteration was fine whether it was going forward or backward.  But the
  604. columns were being iterated again for each row.  If the columns were going
  605. forward, they were a range, a reusable iterable, and everything worked fine.</p><p>But if the columns were meant to go backward, they were a one-use-only
  606. iterator made by <code>reversed()</code>.  The first row would get all the columns,
  607. but the other rows would try to iterate using a fully consumed iterator and get
  608. nothing.</p><p>The simple fix was to use <code>list()</code> to turn my iterator into a reusable
  609. iterable:</p><blockquote class="code"><pre class="python"><div class="source"><span class="n">cols</span>&#xA0;<span class="o">=</span>&#xA0;<span class="nb">list</span><span class="p">(</span><span class="nb">reversed</span><span class="p">(</span><span class="n">cols</span><span class="p">))</span>
  610. <br></div>
  611. </pre></blockquote><p>The code was slightly less nice, but it worked.  An even better fix
  612. was to change my doubly nested loop into a single loop:</p><blockquote class="code"><pre class="python"><div class="source"><span class="k">for</span>&#xA0;<span class="n">row</span><span class="p">,</span>&#xA0;<span class="n">col</span>&#xA0;<span class="ow">in</span>&#xA0;<span class="n">itertools</span><span class="o">.</span><span class="n">product</span><span class="p">(</span><span class="n">rows</span><span class="p">,</span>&#xA0;<span class="n">cols</span><span class="p">):</span>
  613. <br></div>
  614. </pre></blockquote><p>That also takes care of the original iterator/iterable problem, so I can get
  615. rid of that first fix:</p><blockquote class="code"><pre class="python"><div class="source"><span class="n">cols</span>&#xA0;<span class="o">=</span>&#xA0;<span class="nb">range</span><span class="p">(</span><span class="n">N</span><span class="p">)</span>
  616. <br><span class="k">if</span>&#xA0;<span class="n">sliding_right</span><span class="p">:</span>
  617. <br>&#xA0;&#xA0;&#xA0;&#xA0;<span class="n">cols</span>&#xA0;<span class="o">=</span>&#xA0;<span class="nb">reversed</span><span class="p">(</span><span class="n">cols</span><span class="p">)</span>
  618. <br>
  619. <br><span class="n">rows</span>&#xA0;<span class="o">=</span>&#xA0;<span class="nb">range</span><span class="p">(</span><span class="n">N</span><span class="p">)</span>
  620. <br><span class="k">if</span>&#xA0;<span class="n">sliding_down</span><span class="p">:</span>
  621. <br>&#xA0;&#xA0;&#xA0;&#xA0;<span class="n">rows</span>&#xA0;<span class="o">=</span>&#xA0;<span class="nb">reversed</span><span class="p">(</span><span class="n">rows</span><span class="p">)</span>
  622. <br>
  623. <br><span class="k">for</span>&#xA0;<span class="n">row</span><span class="p">,</span>&#xA0;<span class="n">col</span>&#xA0;<span class="ow">in</span>&#xA0;<span class="n">itertools</span><span class="o">.</span><span class="n">product</span><span class="p">(</span><span class="n">rows</span><span class="p">,</span>&#xA0;<span class="n">cols</span><span class="p">):</span>
  624. <br>&#xA0;&#xA0;&#xA0;&#xA0;<span class="o">...</span>
  625. <br></div>
  626. </pre></blockquote><p>Once I had this working, I wondered why <code>product()</code> solved the
  627. iterator/iterable problem.  The <a rel="external noopener" href="https://docs.python.org/3/library/itertools.html#itertools.product">docs have a sample Python
  628. implementation</a> that shows why: internally, <code>product()</code> is doing just
  629. what my <code>list()</code> call did: it makes an explicit iterable from each of the
  630. iterables it was passed, then picks values from them to make the pairs. This
  631. lets <code>product()</code> accept iterators (like my reversed range) rather than
  632. forcing the caller to always pass iterables.</p><p>If your head is spinning from all this iterable / iterator / iteration talk,
  633. I don&#8217;t blame you.  Just now I said, &#8220;it makes an explicit iterable from each of
  634. the iterables it was passed.&#8221; How does that make sense?  Well, an iterator is an
  635. iterable.  So <code>product()</code> can take either a reusable iterable (like a range
  636. or a list) or it can take a use-once iterator (like a reversed range).  Either
  637. way, it populates its own reusable iterables internally.</p><p>Python&#8217;s iteration features are powerful but sometimes require careful
  638. thinking to get right.  Don&#8217;t overlook the tools in itertools, and mind your
  639. iterators and iterables!</p><p class="bulletsep">•    •    •</p><p>Some more notes:</p><p>1: Another way to reverse a range: you can slice them!</p><blockquote class="code"><pre class="python"><div class="source"><span class="o">&gt;&gt;&gt;</span>&#xA0;<span class="nb">range</span><span class="p">(</span><span class="mi">4</span><span class="p">)</span>
  640. <br><span class="nb">range</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span>&#xA0;<span class="mi">4</span><span class="p">)</span>
  641. <br><span class="o">&gt;&gt;&gt;</span>&#xA0;<span class="nb">range</span><span class="p">(</span><span class="mi">4</span><span class="p">)[::</span><span class="o">-</span><span class="mi">1</span><span class="p">]</span>
  642. <br><span class="nb">range</span><span class="p">(</span><span class="mi">3</span><span class="p">,</span>&#xA0;<span class="o">-</span><span class="mi">1</span><span class="p">,</span>&#xA0;<span class="o">-</span><span class="mi">1</span><span class="p">)</span>
  643. <br><span class="o">&gt;&gt;&gt;</span>&#xA0;<span class="nb">reversed</span><span class="p">(</span><span class="nb">range</span><span class="p">(</span><span class="mi">4</span><span class="p">))</span>
  644. <br><span class="o">&lt;</span><span class="n">range_iterator</span>&#xA0;<span class="nb">object</span>&#xA0;<span class="n">at</span>&#xA0;<span class="mh">0x10307cba0</span><span class="o">&gt;</span>
  645. <br></div>
  646. </pre></blockquote><p>It didn&#8217;t occur to me to reverse-slice the range, since <code>reversed</code> is
  647. right there, but the slice gives you a new reusable range object while reversing
  648. the range gives you a use-once iterator.</p><p>2: Why did <code>product()</code> explicitly store the values it would need but
  649. <code>reversed</code> did not? Two reasons: first, <code>reversed()</code> depends on the
  650. <code>__reversed__</code> dunder method, so it&#8217;s up to the original object to decide
  651. how to implement it. Ranges know how to produce their values in backward order,
  652. so they don&#8217;t need to store them all.  Second, <code>product()</code> is going to need
  653. to use the values from each iterable many times and can&#8217;t depend on the
  654. iterables being reusable.</p>
  655. ]]></description>
  656. </item>
  657. <item rdf:about="https://nedbatchelder.com/blog/202506/math_factoid_of_the_day_63.html">
  658. <title>Math factoid of the day: 63</title>
  659. <link>https://nedbatchelder.com/blog/202506/math_factoid_of_the_day_63.html</link>
  660. <dc:date>2025-06-16T00:00:00-04:00</dc:date>
  661. <dc:creator>Ned Batchelder</dc:creator>
  662. <description><![CDATA[<p>63 is a <a rel="external noopener" href="https://en.wikipedia.org/wiki/Centered_octahedral_number">centered octahedral number</a>. That means if you
  663. build an approximation of an octahedron with cubes, one size of octahedron will
  664. have 63 cubes.</p><p>In the late 1700&#8217;s <a rel="external noopener" href="https://en.wikipedia.org/wiki/Ren%C3%A9_Just_Ha%C3%BCy">René Just Haüy</a> developed a theory
  665. about how crystals formed: successive layers of fundamental primitives in
  666. orderly arrangements.  One of those arrangements was stacking cubes together to
  667. make an octahedron.</p><p>Start with one cube:</p><div class="figurep"><img src="https://nedbatchelder.com/code/diagrams/hauy/0.svg" alt="Just one lonely cube"></div><p>Add six more cubes around it, one on each face. Now we have seven:</p><div class="figurep"><img src="https://nedbatchelder.com/code/diagrams/hauy/1.svg" alt="Seven cubes as a crude octahedron"></div><p>Add another layer, adding a cube to touch each visible cube, making 25:</p><div class="figurep"><img src="https://nedbatchelder.com/code/diagrams/hauy/2.svg" alt="25 cubes arranged like an octahedron five cubes wide"></div><p>One more layer and we have a total of 63:</p><div class="figurep"><img src="https://nedbatchelder.com/code/diagrams/hauy/3.svg" alt="63 cubes arranged like an octahedron seven cubes wide"></div><p>The remaining numbers in <a href="https://oeis.org/A001845" rel="external noopener">the sequence</a>
  668. less than 10,000 are 129, 231, 377, 575, 833, 1159, 1561, 2047, 2625, 3303,
  669. 4089, 4991, 6017, 7175, 8473, 9919.</p><p>63 also shows up in the <a rel="external noopener" href="https://en.wikipedia.org/wiki/Delannoy_number">Delannoy numbers</a>: the
  670. number of ways to traverse a grid from the lower left corner to upper right
  671. using only steps north, east, or northeast.  Here are the 63 ways of moving on a
  672. 3<span class="times">×</span>3 grid:</p><div class="figurep"><img src="https://nedbatchelder.com/code/diagrams/delannoy3.svg" alt="63 different ways to traverse a 3x3 grid"></div><p>(Diagram from <a href="https://en.wikipedia.org/wiki/File:Delannoy3x3.svg" rel="external noopener">Wikipedia</a>)</p><p>In fact, the number of cubes in a Haüy octahedron with N layers is the same
  673. as the number of Delannoy steps on a 3<span class="times">×</span>N grid!</p><p>Since the two ideas are both geometric and fairly simple, I would love to
  674. find a geometric explanation for the correspondence.  The octahedron is
  675. three-dimensional, and the Delannoy grids have that tantalizing 3 in them.  It
  676. seems like there should be a way to convert Haüy coordinates to Delannoy
  677. coordinates to show how they relate.  But I haven&#8217;t found one...</p><p class="bulletsep">•    •    •</p><p>Colophon: I made the octahedron diagrams by asking Claude to write a
  678. <a href="https://nedbatchelder.com/code/diagrams/hauy/hauy_oct.py">Python program</a> to do it.
  679. It wasn&#8217;t a fast process because it took pushing and prodding to get the
  680. diagrams to come out the way I liked.  But Claude was very competent, and I
  681. could think about the results rather than about projections or color spaces.  I
  682. could dip into it for 10 minutes at a time over a number of days without having
  683. to somehow reconstruct a mental context.</p><p>This kind of casual hobby programming is perfect for AI assistance.  I don&#8217;t
  684. need the code to be perfect or even good, I just want the diagrams to be nice.
  685. I don&#8217;t have the focus time to learn how to write the program, so I can leave it
  686. to an imperfect assistant.</p>
  687. ]]></description>
  688. </item>
  689. </rdf:RDF>
  690.  

If you would like to create a banner that links to this page (i.e. this validation result), do the following:

  1. Download the "valid RSS" banner.

  2. Upload the image to your own server. (This step is important. Please do not link directly to the image on this server.)

  3. Add this HTML to your page (change the image src attribute if necessary):

If you would like to create a text link instead, here is the URL you can use:

http://www.feedvalidator.org/check.cgi?url=https%3A//www.nedbatchelder.com/blog/rss.xml

Copyright © 2002-9 Sam Ruby, Mark Pilgrim, Joseph Walton, and Phil Ringnalda