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/202506/math_factoid_of_the_day_63.html"/><rdf:li resource="https://nedbatchelder.com/blog/202506/digital_equipment_corporation_no_more.html"/><rdf:li resource="https://nedbatchelder.com/blog/202505/pycon_summer_camp.html"/><rdf:li resource="https://nedbatchelder.com/blog/202505/filtering_github_actions_by_changed_files.html"/><rdf:li resource="https://nedbatchelder.com/blog/202504/regex_affordances.html"/><rdf:li resource="https://nedbatchelder.com/blog/202504/find_the_bear.html"/><rdf:li resource="https://nedbatchelder.com/blog/202504/nedflix.html"/><rdf:li resource="https://nedbatchelder.com/blog/202503/human_sorting_improved.html"/><rdf:li resource="https://nedbatchelder.com/blog/202503/horseless_intelligence.html"/><rdf:li resource="https://nedbatchelder.com/blog/202503/faster_branch_coverage_measurement.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/202506/math_factoid_of_the_day_63.html">
  23. <title>Math factoid of the day: 63</title>
  24. <link>https://nedbatchelder.com/blog/202506/math_factoid_of_the_day_63.html</link>
  25. <dc:date>2025-06-16T00:00:00-04:00</dc:date>
  26. <dc:creator>Ned Batchelder</dc:creator>
  27. <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
  28. build an approximation of an octahedron with cubes, one size of octahedron will
  29. 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
  30. about how crystals formed: successive layers of fundamental primitives in
  31. orderly arrangements.  One of those arrangements was stacking cubes together to
  32. 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>
  33. less than 10,000 are 129, 231, 377, 575, 833, 1159, 1561, 2047, 2625, 3303,
  34. 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
  35. number of ways to traverse a grid from the lower left corner to upper right
  36. using only steps north, east, or northeast.  Here are the 63 ways of moving on a
  37. 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
  38. 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
  39. find a geometric explanation for the correspondence.  The octahedron is
  40. three-dimensional, and the Delannoy grids have that tantalizing 3 in them.  It
  41. seems like there should be a way to convert Haüy coordinates to Delannoy
  42. 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
  43. <a href="https://nedbatchelder.com/code/diagrams/hauy/hauy_oct.py">Python program</a> to do it.
  44. It wasn&#8217;t a fast process because it took pushing and prodding to get the
  45. diagrams to come out the way I liked.  But Claude was very competent, and I
  46. could think about the results rather than about projections or color spaces.  I
  47. could dip into it for 10 minutes at a time over a number of days without having
  48. to somehow reconstruct a mental context.</p><p>This kind of casual hobby programming is perfect for AI assistance.  I don&#8217;t
  49. need the code to be perfect or even good, I just want the diagrams to be nice.
  50. I don&#8217;t have the focus time to learn how to write the program, so I can leave it
  51. to an imperfect assistant.</p>
  52. ]]></description>
  53. </item>
  54. <item rdf:about="https://nedbatchelder.com/blog/202506/digital_equipment_corporation_no_more.html">
  55. <title>Digital Equipment Corporation no more</title>
  56. <link>https://nedbatchelder.com/blog/202506/digital_equipment_corporation_no_more.html</link>
  57. <dc:date>2025-06-09T09:43:53-04:00</dc:date>
  58. <dc:creator>Ned Batchelder</dc:creator>
  59. <description><![CDATA[<p>Today is the 39-year anniversary of my first day working for
  60. <a rel="external noopener" href="https://en.wikipedia.org/wiki/Digital_Equipment_Corporation">Digital Equipment Corporation</a>.  It was my first real job in
  61. the tech world, two years out of college.  <a href="https://nedbatchelder.com/blog/200606/digital_equipment_corporation.html">I wrote
  62. about it 19 years ago</a>, but it&#8217;s on my mind again.</p><p>More and more, I find that people have never heard of Digital (as we called
  63. it) or DEC (as they preferred we didn&#8217;t call it but everyone did).  It&#8217;s
  64. something I&#8217;ve had to get used to.  I try to relate a story from that time, and
  65. I find that even experienced engineers with deep knowledge of technologies don&#8217;t
  66. know of the company.</p><p>I mention this not in a crabby &#8220;kids these days&#8221; kind of way.  It does
  67. surprise me, but I&#8217;m taking it as a learning opportunity.  If there&#8217;s a lesson
  68. to learn, it is this:</p><blockquote><div><p>This too shall pass.</p></div></blockquote><p>I am now working for Netflix, and one of the great things about it is that
  69. everyone has heard of Netflix.  I can mention my job to anyone and they are
  70. impressed in some way.  Techies know it as one of the FAANG companies, and
  71. &#8220;civilians&#8221; know it for the entertainment it produces and delivers.</p><p>When I joined Digital in 1986, at least among tech people, it was similar.
  72. Everyone knew about Digital and what they had done: the creation of the
  73. minicomputer, the genesis of Unix and C, the ubiquitous VT100.  Many foundations
  74. of the software world flowed directly and famously from Digital.</p><p>These days Digital isn&#8217;t quite yet a footnote to history, but it is more and
  75. more unknown even among the most tech-involved. And the tech world carries
  76. on!</p><p>My small team at Netflix has a number of young engineers, less than two years
  77. out of college, and even an intern still in college.  I&#8217;m sure they felt
  78. incredibly excited to join a company as well-known and influential as Netflix.
  79. In 39 years when they tell a story from the early days of their career will they
  80. start with, &#8220;Have you heard of Netflix?&#8221; and have to adjust to the blank stares
  81. they get in return?</p><p>This too shall pass.</p>
  82. ]]></description>
  83. </item>
  84. <item rdf:about="https://nedbatchelder.com/blog/202505/pycon_summer_camp.html">
  85. <title>PyCon summer camp</title>
  86. <link>https://nedbatchelder.com/blog/202505/pycon_summer_camp.html</link>
  87. <dc:date>2025-05-15T07:05:20-04:00</dc:date>
  88. <dc:creator>Ned Batchelder</dc:creator>
  89. <description><![CDATA[<p>I&#8217;m headed to PyCon today, and I&#8217;m reminded about how it feels like summer
  90. camp, in mostly good ways, but also in a tricky way.</p><p>You take some time off from your &#8220;real&#8221; life, you go somewhere else, you hang
  91. out with old friends and meet some new friends.  You do different things than in
  92. your real life, some are playful, some take real work.  These are all good ways
  93. it&#8217;s like summer camp.</p><p>Here&#8217;s the tricky thing to watch out for: like summer camp, you can make
  94. connections to people or projects that are intense and feel like they could last
  95. forever.  You make friends at summer camp, or even have semi-romantic crushes on
  96. people. You promise to stay in touch, you think it&#8217;s the &#8220;real thing.&#8221;  When you
  97. get home, you write an email or two, maybe a phone call, but it fades away.  The
  98. excitement of the summer is overtaken by your autumnal real life again.</p><p>PyCon can be the same way, either with people or projects.  Not a romance,
  99. but the exciting feeling that you want to keep doing the project you started at
  100. PyCon, or be a member of some community you hung out with for those days.  You
  101. want to keep talking about that exciting thing with that person.  These are
  102. great feelings, but it&#8217;s easy to emotionally over-commit to those efforts and
  103. then have it fade away once PyCon is over.</p><p>How do you know what projects are just crushes, and which are permanent
  104. relationships?  Maybe it doesn&#8217;t matter, and we should just get excited about
  105. things.</p><p>I know I started at least one effort last year that I thought would be done
  106. in a few months, but has since stalled.  Now I am headed back to PyCon.  Will I
  107. become attached to yet more things this time?  Is that bad? Should I temper my
  108. enthusiasm, or is it fine to light a few fires and accept that some will peter
  109. out?</p>
  110. ]]></description>
  111. </item>
  112. <item rdf:about="https://nedbatchelder.com/blog/202505/filtering_github_actions_by_changed_files.html">
  113. <title>Filtering GitHub actions by changed files</title>
  114. <link>https://nedbatchelder.com/blog/202505/filtering_github_actions_by_changed_files.html</link>
  115. <dc:date>2025-05-04T07:00:06-04:00</dc:date>
  116. <dc:creator>Ned Batchelder</dc:creator>
  117. <description><![CDATA[<p>Coverage.py has a large test suite that runs in many environments, which can
  118. take a while.  But some changes don&#8217;t require running the test suite at all.
  119. I&#8217;ve changed the actions to detect when they need to run based on what files
  120. have changed, but there were some twists and turns along the way.</p><p>The <a rel="external noopener" href="https://github.com/dorny/paths-filter">dorny/paths-filter</a> action can check which files
  121. have changed for pull requests or branches.  I <a rel="external noopener" href="https://github.com/nedbat/coveragepy/commit/62891c81d2c7f175ff14f0d492b5695f507780e8#diff-2dcad576de77b3d5156ec1976a4f88236339c48a6000beda4885b0fe8e3ba006">added it to
  122. my tests action</a> like this:</p><blockquote class="code"><pre class="yaml"><div class="source"><span class="nt">jobs</span><span class="p">:</span>
  123. <br>
  124. <br><span class="w">&#xA0;&#xA0;</span><span class="nt">changed</span><span class="p">:</span>
  125. <br><span class="w">&#xA0;&#xA0;&#xA0;&#xA0;</span><span class="nt">name</span><span class="p">:</span><span class="w">&#xA0;</span><span class="s">&quot;Check</span><span class="nv">&#xA0;</span><span class="s">what</span><span class="nv">&#xA0;</span><span class="s">files</span><span class="nv">&#xA0;</span><span class="s">changed&quot;</span>
  126. <br><span class="w">&#xA0;&#xA0;&#xA0;&#xA0;</span><span class="nt">outputs</span><span class="p">:</span>
  127. <br><span class="w">&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;</span><span class="nt">python</span><span class="p">:</span><span class="w">&#xA0;</span><span class="l l-Scalar l-Scalar-Plain">${{&#xA0;steps.filter.outputs.python&#xA0;}}</span>
  128. <br><span class="w">&#xA0;&#xA0;&#xA0;&#xA0;</span><span class="nt">steps</span><span class="p">:</span>
  129. <br><span class="w">&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;</span><span class="p p-Indicator">-</span><span class="w">&#xA0;</span><span class="nt">name</span><span class="p">:</span><span class="w">&#xA0;</span><span class="s">&quot;Check</span><span class="nv">&#xA0;</span><span class="s">out</span><span class="nv">&#xA0;</span><span class="s">the</span><span class="nv">&#xA0;</span><span class="s">repo&quot;</span>
  130. <br><span class="w">&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;</span><span class="nt">uses</span><span class="p">:</span><span class="w">&#xA0;</span><span class="l l-Scalar l-Scalar-Plain">actions/checkout</span>
  131. <br>
  132. <br><span class="w">&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;</span><span class="p p-Indicator">-</span><span class="w">&#xA0;</span><span class="nt">name</span><span class="p">:</span><span class="w">&#xA0;</span><span class="s">&quot;Examine</span><span class="nv">&#xA0;</span><span class="s">changed</span><span class="nv">&#xA0;</span><span class="s">files&quot;</span>
  133. <br><span class="w">&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;</span><span class="nt">uses</span><span class="p">:</span><span class="w">&#xA0;</span><span class="l l-Scalar l-Scalar-Plain">dorny/paths-filter</span>
  134. <br><span class="w">&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;</span><span class="nt">id</span><span class="p">:</span><span class="w">&#xA0;</span><span class="l l-Scalar l-Scalar-Plain">filter</span>
  135. <br><span class="w">&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;</span><span class="nt">with</span><span class="p">:</span>
  136. <br><span class="w">&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;</span><span class="nt">filters</span><span class="p">:</span><span class="w">&#xA0;</span><span class="p p-Indicator">|</span>
  137. <br><span class="w">&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;</span><span class="no">python:</span>
  138. <br><span class="w">&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;</span><span class="no">-&#xA0;&quot;**.py&quot;</span>
  139. <br>
  140. <br><span class="w">&#xA0;&#xA0;</span><span class="nt">tests</span><span class="p">:</span>
  141. <br><span class="w">&#xA0;&#xA0;&#xA0;&#xA0;</span><span class="c1">#&#xA0;Don&#39;t&#xA0;run&#xA0;tests&#xA0;if&#xA0;the&#xA0;branch&#xA0;name&#xA0;includes&#xA0;&quot;-notests&quot;.</span>
  142. <br><span class="w">&#xA0;&#xA0;&#xA0;&#xA0;</span><span class="c1">#&#xA0;Only&#xA0;run&#xA0;tests&#xA0;if&#xA0;Python&#xA0;files&#xA0;changed.</span>
  143. <br><span class="w">&#xA0;&#xA0;&#xA0;&#xA0;</span><span class="nt">needs</span><span class="p">:</span><span class="w">&#xA0;</span><span class="l l-Scalar l-Scalar-Plain">changed</span>
  144. <br><span class="w">&#xA0;&#xA0;&#xA0;&#xA0;</span><span class="nt">if</span><span class="p">:</span><span class="w">&#xA0;</span><span class="l l-Scalar l-Scalar-Plain">${{&#xA0;!contains(github.ref,&#xA0;&#39;-notests&#39;)&#xA0;&amp;&amp;&#xA0;needs.changed.outputs.python&#xA0;==&#xA0;&#39;true&#39;&#xA0;}}</span>
  145. <br></div>
  146. </pre></blockquote><p>The &#8220;changed&#8221; jobs checks what files have changed, then the &#8220;tests&#8221; job
  147. examines its output to decide whether to run at all.</p><p>It&#8217;s a little awkward having an output for the &#8220;changed&#8221; job as an
  148. intermediary, but this did what I wanted: if any .py file changed, run the
  149. tests, otherwise don&#8217;t run them.  I left in an old condition: if the branch name
  150. includes &#8220;-notests&#8221;, then don&#8217;t run the tests.</p><p>This worked, but I realized I needed to run the tests on other conditions
  151. also.  What if no Python file changed, but the GitHub action file itself had
  152. changed?  So I <a rel="external noopener" href="https://github.com/nedbat/coveragepy/commit/9cb9bdc6847115c1a357ec1cb95c697e49a434ae#diff-2dcad576de77b3d5156ec1976a4f88236339c48a6000beda4885b0fe8e3ba006">added that as a condition</a>. The
  153. if-expression was getting long, so I made it a multi-line string:</p><blockquote class="code"><pre class="yaml"><div class="source"><span class="nt">jobs</span><span class="p">:</span>
  154. <br>
  155. <br><span class="w">&#xA0;&#xA0;</span><span class="nt">changed</span><span class="p">:</span>
  156. <br><span class="w">&#xA0;&#xA0;&#xA0;&#xA0;</span><span class="nt">name</span><span class="p">:</span><span class="w">&#xA0;</span><span class="s">&quot;Check</span><span class="nv">&#xA0;</span><span class="s">what</span><span class="nv">&#xA0;</span><span class="s">files</span><span class="nv">&#xA0;</span><span class="s">changed&quot;</span>
  157. <br><span class="w">&#xA0;&#xA0;&#xA0;&#xA0;</span><span class="nt">outputs</span><span class="p">:</span>
  158. <br><span class="w">&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;</span><span class="nt">python</span><span class="p">:</span><span class="w">&#xA0;</span><span class="l l-Scalar l-Scalar-Plain">${{&#xA0;steps.filter.outputs.python&#xA0;}}</span>
  159. <br><span class="w">&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;</span><span class="nt">workflow</span><span class="p">:</span><span class="w">&#xA0;</span><span class="l l-Scalar l-Scalar-Plain">${{&#xA0;steps.filter.outputs.workflow&#xA0;}}</span>
  160. <br><span class="w">&#xA0;&#xA0;&#xA0;&#xA0;</span><span class="nt">steps</span><span class="p">:</span>
  161. <br><span class="w">&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;</span><span class="p p-Indicator">-</span><span class="w">&#xA0;</span><span class="nt">name</span><span class="p">:</span><span class="w">&#xA0;</span><span class="s">&quot;Check</span><span class="nv">&#xA0;</span><span class="s">out</span><span class="nv">&#xA0;</span><span class="s">the</span><span class="nv">&#xA0;</span><span class="s">repo&quot;</span>
  162. <br><span class="w">&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;</span><span class="nt">uses</span><span class="p">:</span><span class="w">&#xA0;</span><span class="l l-Scalar l-Scalar-Plain">actions/checkout</span>
  163. <br>
  164. <br><span class="w">&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;</span><span class="p p-Indicator">-</span><span class="w">&#xA0;</span><span class="nt">name</span><span class="p">:</span><span class="w">&#xA0;</span><span class="s">&quot;Examine</span><span class="nv">&#xA0;</span><span class="s">changed</span><span class="nv">&#xA0;</span><span class="s">files&quot;</span>
  165. <br><span class="w">&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;</span><span class="nt">uses</span><span class="p">:</span><span class="w">&#xA0;</span><span class="l l-Scalar l-Scalar-Plain">dorny/paths-filter</span>
  166. <br><span class="w">&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;</span><span class="nt">id</span><span class="p">:</span><span class="w">&#xA0;</span><span class="l l-Scalar l-Scalar-Plain">filter</span>
  167. <br><span class="w">&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;</span><span class="nt">with</span><span class="p">:</span>
  168. <br><span class="w">&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;</span><span class="nt">filters</span><span class="p">:</span><span class="w">&#xA0;</span><span class="p p-Indicator">|</span>
  169. <br><span class="w">&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;</span><span class="no">python:</span>
  170. <br><span class="w">&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;</span><span class="no">-&#xA0;&quot;**.py&quot;</span>
  171. <br><span class="w">&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;</span><span class="no">workflow:</span>
  172. <br><span class="w">&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;</span><span class="no">-&#xA0;&quot;.github/workflows/testsuite.yml&quot;</span>
  173. <br>
  174. <br><span class="w">&#xA0;&#xA0;</span><span class="nt">tests</span><span class="p">:</span>
  175. <br><span class="w">&#xA0;&#xA0;&#xA0;&#xA0;</span><span class="c1">#&#xA0;Don&#39;t&#xA0;run&#xA0;tests&#xA0;if&#xA0;the&#xA0;branch&#xA0;name&#xA0;includes&#xA0;&quot;-notests&quot;.</span>
  176. <br><span class="w">&#xA0;&#xA0;&#xA0;&#xA0;</span><span class="c1">#&#xA0;Only&#xA0;run&#xA0;tests&#xA0;if&#xA0;Python&#xA0;files&#xA0;or&#xA0;this&#xA0;workflow&#xA0;changed.</span>
  177. <br><span class="w">&#xA0;&#xA0;&#xA0;&#xA0;</span><span class="nt">needs</span><span class="p">:</span><span class="w">&#xA0;</span><span class="l l-Scalar l-Scalar-Plain">changed</span>
  178. <br><span class="w">&#xA0;&#xA0;&#xA0;&#xA0;</span><span class="nt">if</span><span class="p">:</span><span class="w">&#xA0;</span><span class="p p-Indicator">|</span>
  179. <br><span class="w">&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;</span><span class="no">${{</span>
  180. <br><span class="w">&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;</span><span class="no">!contains(github.ref,&#xA0;&#39;-notests&#39;)</span>
  181. <br><span class="w">&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;</span><span class="no">&amp;&amp;&#xA0;(</span>
  182. <br><span class="w">&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;</span><span class="no">needs.changed.outputs.python&#xA0;==&#xA0;&#39;true&#39;</span>
  183. <br><span class="w">&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;</span><span class="no">||&#xA0;needs.changed.outputs.workflow&#xA0;==&#xA0;&#39;true&#39;</span>
  184. <br><span class="w">&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;</span><span class="no">)</span>
  185. <br><span class="w">&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;</span><span class="no">}}</span>
  186. <br></div>
  187. </pre></blockquote><p>This seemed to work, but it has a bug that I will get to in a bit.</p><p>Thinking about it more, I realized there are other files that could affect
  188. the test results: requirements files, test output files, and the tox.ini.
  189. Rather than add them as three more conditions, I <a rel="external noopener" href="https://github.com/nedbat/coveragepy/commit/18f2240406d18417eac3497df6b2d184c719a7cc#diff-2dcad576de77b3d5156ec1976a4f88236339c48a6000beda4885b0fe8e3ba006">combined
  190. them all into one</a>:</p><blockquote class="code"><pre class="yaml"><div class="source"><span class="nt">jobs</span><span class="p">:</span>
  191. <br>
  192. <br><span class="w">&#xA0;&#xA0;</span><span class="nt">changed</span><span class="p">:</span>
  193. <br><span class="w">&#xA0;&#xA0;&#xA0;&#xA0;</span><span class="nt">name</span><span class="p">:</span><span class="w">&#xA0;</span><span class="s">&quot;Check</span><span class="nv">&#xA0;</span><span class="s">what</span><span class="nv">&#xA0;</span><span class="s">files</span><span class="nv">&#xA0;</span><span class="s">changed&quot;</span>
  194. <br><span class="w">&#xA0;&#xA0;&#xA0;&#xA0;</span><span class="nt">outputs</span><span class="p">:</span>
  195. <br><span class="w">&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;</span><span class="nt">run_tests</span><span class="p">:</span><span class="w">&#xA0;</span><span class="l l-Scalar l-Scalar-Plain">${{&#xA0;steps.filter.outputs.run_tests&#xA0;}}</span>
  196. <br><span class="w">&#xA0;&#xA0;&#xA0;&#xA0;</span><span class="nt">steps</span><span class="p">:</span>
  197. <br><span class="w">&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;</span><span class="p p-Indicator">-</span><span class="w">&#xA0;</span><span class="nt">name</span><span class="p">:</span><span class="w">&#xA0;</span><span class="s">&quot;Check</span><span class="nv">&#xA0;</span><span class="s">out</span><span class="nv">&#xA0;</span><span class="s">the</span><span class="nv">&#xA0;</span><span class="s">repo&quot;</span>
  198. <br><span class="w">&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;</span><span class="nt">uses</span><span class="p">:</span><span class="w">&#xA0;</span><span class="l l-Scalar l-Scalar-Plain">actions/checkout</span>
  199. <br>
  200. <br><span class="w">&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;</span><span class="p p-Indicator">-</span><span class="w">&#xA0;</span><span class="nt">name</span><span class="p">:</span><span class="w">&#xA0;</span><span class="s">&quot;Examine</span><span class="nv">&#xA0;</span><span class="s">changed</span><span class="nv">&#xA0;</span><span class="s">files&quot;</span>
  201. <br><span class="w">&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;</span><span class="nt">uses</span><span class="p">:</span><span class="w">&#xA0;</span><span class="l l-Scalar l-Scalar-Plain">dorny/paths-filter</span>
  202. <br><span class="w">&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;</span><span class="nt">id</span><span class="p">:</span><span class="w">&#xA0;</span><span class="l l-Scalar l-Scalar-Plain">filter</span>
  203. <br><span class="w">&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;</span><span class="nt">with</span><span class="p">:</span>
  204. <br><span class="w">&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;</span><span class="nt">filters</span><span class="p">:</span><span class="w">&#xA0;</span><span class="p p-Indicator">|</span>
  205. <br><span class="w">&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;</span><span class="no">run_tests:</span>
  206. <br><span class="w">&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;</span><span class="no">-&#xA0;&quot;**.py&quot;</span>
  207. <br><span class="w">&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;</span><span class="no">-&#xA0;&quot;.github/workflows/testsuite.yml&quot;</span>
  208. <br><span class="w">&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;</span><span class="no">-&#xA0;&quot;tox.ini&quot;</span>
  209. <br><span class="w">&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;</span><span class="no">-&#xA0;&quot;requirements/*.pip&quot;</span>
  210. <br><span class="w">&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;</span><span class="no">-&#xA0;&quot;tests/gold/**&quot;</span>
  211. <br>
  212. <br><span class="w">&#xA0;&#xA0;</span><span class="nt">tests</span><span class="p">:</span>
  213. <br><span class="w">&#xA0;&#xA0;&#xA0;&#xA0;</span><span class="c1">#&#xA0;Don&#39;t&#xA0;run&#xA0;tests&#xA0;if&#xA0;the&#xA0;branch&#xA0;name&#xA0;includes&#xA0;&quot;-notests&quot;.</span>
  214. <br><span class="w">&#xA0;&#xA0;&#xA0;&#xA0;</span><span class="c1">#&#xA0;Only&#xA0;run&#xA0;tests&#xA0;if&#xA0;files&#xA0;that&#xA0;affect&#xA0;tests&#xA0;have&#xA0;changed.</span>
  215. <br><span class="w">&#xA0;&#xA0;&#xA0;&#xA0;</span><span class="nt">needs</span><span class="p">:</span><span class="w">&#xA0;</span><span class="l l-Scalar l-Scalar-Plain">changed</span>
  216. <br><span class="w">&#xA0;&#xA0;&#xA0;&#xA0;</span><span class="nt">if</span><span class="p">:</span><span class="w">&#xA0;</span><span class="p p-Indicator">|</span>
  217. <br><span class="w">&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;</span><span class="no">${{</span>
  218. <br><span class="w">&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;</span><span class="no">needs.changed.outputs.run_tests&#xA0;==&#xA0;&#39;true&#39;</span>
  219. <br><span class="w">&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;</span><span class="no">&amp;&amp;&#xA0;!contains(github.ref,&#xA0;&#39;-notests&#39;)</span>
  220. <br><span class="w">&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;</span><span class="no">}}</span>
  221. <br></div>
  222. </pre></blockquote><p>BTW: these commits also update the quality checks workflow which has other
  223. kinds of mix-and-match conditions to deal with that you might be interested
  224. in.</p><p>All seemed good! Then I made a commit that only changed my Makefile, and the
  225. tests ran! Why!? The Makefile isn&#8217;t one of the checked files.  The paths-filter
  226. action helpfully includes debug output that showed that only the Makefile was
  227. considered changed, and that the &#8220;run_test&#8221; output was false.</p><p>I took a guess that GitHub actions don&#8217;t like expressions with newlines in
  228. them.  Using the trusty <a href="https://yaml-multiline.info/" rel="external noopener">YAML multi-line
  229. string cheat sheet</a>, I tried changing from the literal block style (with a
  230. pipe) to the folded style (with a greater-than):</p><blockquote class="code"><pre class="yaml"><div class="source"><span class="nt">if</span><span class="p">:</span><span class="w">&#xA0;</span><span class="p p-Indicator">&gt;</span>
  231. <br><span class="w">&#xA0;&#xA0;</span><span class="no">${{</span>
  232. <br><span class="w">&#xA0;&#xA0;&#xA0;&#xA0;</span><span class="no">needs.changed.outputs.run_tests&#xA0;==&#xA0;&#39;true&#39;</span>
  233. <br><span class="w">&#xA0;&#xA0;&#xA0;&#xA0;</span><span class="no">&amp;&amp;&#xA0;!contains(github.ref,&#xA0;&#39;-notests&#39;)</span>
  234. <br><span class="w">&#xA0;&#xA0;</span><span class="no">}}</span>
  235. <br></div>
  236. </pre></blockquote><p>The literal form includes all newlines, the folded style turns newlines into
  237. spaces.  To check that I had it right, I tried parsing the YAML files: to my
  238. surprise, both forms included all the newlines, there was no difference at all.
  239. It turns out that YAML &#8220;helpfully&#8221; notices changes in indentation, and includes
  240. newlines for indented lines.  My expression is nicely indented, so it has
  241. newlines no matter what syntax I use.</p><p>The <a rel="external noopener" href="https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/evaluate-expressions-in-workflows-and-actions">GitHub actions docs</a> don&#8217;t mention it, but it
  242. seems that newlines do break expression evaluation.  Sigh. My expressions are
  243. not as long now as they had gotten during this exploration, so I
  244. <a rel="external noopener" href="https://github.com/nedbat/coveragepy/commit/902ef7a111a0fb5b48828f4502b01db1e9e002aa#diff-2dcad576de77b3d5156ec1976a4f88236339c48a6000beda4885b0fe8e3ba006">changed them all back to one line</a>, and now it all works
  245. as I wanted.</p><p>There are some other things I&#8217;d like to tweak: when the tests are skipped,
  246. the final status is &#8220;success&#8221;, but I&#8217;m wondering if there&#8217;s a way to make it
  247. &#8220;skipped&#8221;.  I&#8217;m also torn about whether every change to master should run all
  248. the workflows or if they should also filter based on the changed files.
  249. Currently they are filtered.</p><p>Continuous integration and GitHub workflows are great, but they always seem
  250. to involve this kind of fiddling in environments that are difficult to debug.
  251. Maybe I&#8217;ve saved you some grief.</p>
  252. ]]></description>
  253. </item>
  254. <item rdf:about="https://nedbatchelder.com/blog/202504/regex_affordances.html">
  255. <title>Regex affordances</title>
  256. <link>https://nedbatchelder.com/blog/202504/regex_affordances.html</link>
  257. <dc:date>2025-04-18T11:19:46-04:00</dc:date>
  258. <dc:creator>Ned Batchelder</dc:creator>
  259. <description><![CDATA[<p>Python regexes have a number of features that bring new power to text
  260. manipulation.  I&#8217;m not talking about fancy matching features like negative
  261. look-behinds, but ways you can construct and use regexes.  As a demonstration,
  262. I&#8217;ll show you some real code from a real project.</p><p>Coverage.py will expand environment variables in values read from its
  263. configuration files.  It does this with a function called
  264. <code>substitute_variables</code>:</p><blockquote class="code"><pre class="python"><div class="source"><span class="k">def</span><span class="w">&#xA0;</span><span class="nf">substitute_variables</span><span class="p">(</span>
  265. <br>&#xA0;&#xA0;&#xA0;&#xA0;<span class="n">text</span><span class="p">:</span>&#xA0;<span class="nb">str</span><span class="p">,</span>
  266. <br>&#xA0;&#xA0;&#xA0;&#xA0;<span class="n">variables</span><span class="p">:</span>&#xA0;<span class="nb">dict</span><span class="p">[</span><span class="nb">str</span><span class="p">,</span>&#xA0;<span class="nb">str</span><span class="p">],</span>
  267. <br><span class="p">)</span>&#xA0;<span class="o">-&gt;</span>&#xA0;<span class="nb">str</span><span class="p">:</span>
  268. <br><span class="w">&#xA0;&#xA0;&#xA0;&#xA0;</span><span class="sd">&quot;&quot;&quot;</span>
  269. <br><span class="sd">&#xA0;&#xA0;&#xA0;&#xA0;Substitute&#xA0;``${VAR}``&#xA0;variables&#xA0;in&#xA0;`text`.</span>
  270. <br>
  271. <br><span class="sd">&#xA0;&#xA0;&#xA0;&#xA0;Variables&#xA0;in&#xA0;the&#xA0;text&#xA0;can&#xA0;take&#xA0;a&#xA0;number&#xA0;of</span>
  272. <br><span class="sd">&#xA0;&#xA0;&#xA0;&#xA0;shell-inspired&#xA0;forms::</span>
  273. <br>
  274. <br><span class="sd">&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;$VAR</span>
  275. <br><span class="sd">&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;${VAR}</span>
  276. <br><span class="sd">&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;${VAR?}&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;strict:&#xA0;an&#xA0;error&#xA0;if&#xA0;no&#xA0;VAR.</span>
  277. <br><span class="sd">&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;${VAR-miss}&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;defaulted:&#xA0;&quot;miss&quot;&#xA0;if&#xA0;no&#xA0;VAR.</span>
  278. <br><span class="sd">&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;$$&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;just&#xA0;a&#xA0;dollar&#xA0;sign.</span>
  279. <br>
  280. <br><span class="sd">&#xA0;&#xA0;&#xA0;&#xA0;`variables`&#xA0;is&#xA0;a&#xA0;dictionary&#xA0;of&#xA0;variable&#xA0;values.</span>
  281. <br>
  282. <br><span class="sd">&#xA0;&#xA0;&#xA0;&#xA0;Returns&#xA0;the&#xA0;resulting&#xA0;text&#xA0;with&#xA0;values&#xA0;substituted.</span>
  283. <br>
  284. <br><span class="sd">&#xA0;&#xA0;&#xA0;&#xA0;&quot;&quot;&quot;</span>
  285. <br></div>
  286. </pre></blockquote><p>Call it with a string and a dictionary, and it makes the substitutions:</p><blockquote class="code"><pre class="python"><div class="source"><span class="o">&gt;&gt;&gt;</span>&#xA0;<span class="n">substitute_variables</span><span class="p">(</span>
  287. <br><span class="o">...</span>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<span class="n">text</span><span class="o">=</span><span class="s2">&quot;Look:&#xA0;$FOO&#xA0;${BAR-default}&#xA0;$$&quot;</span><span class="p">,</span>
  288. <br><span class="o">...</span>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<span class="n">variables</span><span class="o">=</span><span class="p">{</span><span class="s1">&#39;FOO&#39;</span><span class="p">:</span>&#xA0;<span class="s1">&#39;Xyzzy&#39;</span><span class="p">},</span>
  289. <br><span class="o">...</span>&#xA0;<span class="p">)</span>
  290. <br>
  291. <br><span class="s1">&#39;Look:&#xA0;Xyzzy&#xA0;default&#xA0;$&#39;</span>
  292. <br></div>
  293. </pre></blockquote><p>We use a regex to pick apart the text:</p><blockquote class="code"><pre class="python"><div class="source"><span class="n">dollar_pattern</span>&#xA0;<span class="o">=</span>&#xA0;<span class="sa">r</span><span class="s2">&quot;&quot;&quot;(?x)&#xA0;&#xA0;&#xA0;#&#xA0;Verbose&#xA0;regex&#xA0;syntax</span>
  294. <br><span class="s2">&#xA0;&#xA0;&#xA0;&#xA0;\$&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;#&#xA0;A&#xA0;dollar&#xA0;sign,</span>
  295. <br><span class="s2">&#xA0;&#xA0;&#xA0;&#xA0;(?:&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;#&#xA0;then</span>
  296. <br><span class="s2">&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;(?P&lt;dollar&gt;&#xA0;\$&#xA0;)&#xA0;|&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;#&#xA0;a&#xA0;dollar&#xA0;sign,&#xA0;or</span>
  297. <br><span class="s2">&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;(?P&lt;word1&gt;&#xA0;\w+&#xA0;)&#xA0;|&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;#&#xA0;a&#xA0;plain&#xA0;word,&#xA0;or</span>
  298. <br><span class="s2">&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;\{&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;#&#xA0;a&#xA0;{-wrapped</span>
  299. <br><span class="s2">&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;(?P&lt;word2&gt;&#xA0;\w+&#xA0;)&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;#&#xA0;word,</span>
  300. <br><span class="s2">&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;(?:&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;#&#xA0;either</span>
  301. <br><span class="s2">&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;(?P&lt;strict&gt;&#xA0;\?&#xA0;)&#xA0;|&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;#&#xA0;strict&#xA0;or</span>
  302. <br><span class="s2">&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;-(?P&lt;defval&gt;&#xA0;[^}]*&#xA0;)&#xA0;&#xA0;&#xA0;&#xA0;#&#xA0;defaulted</span>
  303. <br><span class="s2">&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;)?&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;#&#xA0;maybe</span>
  304. <br><span class="s2">&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;}</span>
  305. <br><span class="s2">&#xA0;&#xA0;&#xA0;&#xA0;)</span>
  306. <br><span class="s2">&#xA0;&#xA0;&#xA0;&#xA0;&quot;&quot;&quot;</span>
  307. <br></div>
  308. </pre></blockquote><p>This isn&#8217;t a super-fancy regex: it doesn&#8217;t use advanced pattern matching.
  309. But there are some useful regex features at work here:</p><ul>
  310.  
  311. <li>The <code>(?x)</code> flag at the beginning turns on &#8220;verbose&#8221; regex syntax. In
  312. this mode, all white space is ignored so the regex can be multi-line and we can
  313. indent to help see the structure, and comments are allowed at the ends of
  314. lines.</li>
  315.  
  316. <li>Named groups like <code>(?P&lt;word1&gt; … )</code> are used to capture parts of
  317. the text that we can retrieve later by name.</li>
  318.  
  319. <li>There are also two groups used to get the precedence of operators right, but
  320. we don&#8217;t want to capture those values separately, so I use the non-capturing
  321. group syntax for them: <code>(?: … )</code>.  In this code, we only ever access groups
  322. by name, so I could have left them as regular capturing groups, but I think it&#8217;s
  323. clearer to indicate up-front that we won&#8217;t be using them.</li>
  324.  
  325. </ul><p>The verbose syntax in particular makes it easier to understand the regex.
  326. Compare to what it would look like in one line:</p><blockquote class="code"><pre class="python"><div class="source"><span class="sa">r</span><span class="s2">&quot;\$(?:(?P&lt;dollar&gt;\$)|(?P&lt;word1&gt;\w+)|\{(?P&lt;word2&gt;\w+)(?:(?P&lt;strict&gt;\?)|-(?P&lt;defval&gt;[^}]*))?})&quot;</span>
  327. <br></div>
  328. </pre></blockquote><p>Once we have the regex, we can use <code>re.sub()</code> to replace the variables
  329. with their values:</p><blockquote class="code"><pre class="python"><div class="source"><span class="n">re</span><span class="o">.</span><span class="n">sub</span><span class="p">(</span><span class="n">dollar_pattern</span><span class="p">,</span>&#xA0;<span class="n">dollar_replace</span><span class="p">,</span>&#xA0;<span class="n">text</span><span class="p">)</span>
  330. <br></div>
  331. </pre></blockquote><p>But we&#8217;re going to use another power feature of Python regexes:
  332. <code>dollar_replace</code> here isn&#8217;t a string, it&#8217;s a function! Each fragment the
  333. regex matches will be passed as a match object to our <code>dollar_replace</code>
  334. function.  It returns a string which re.sub() uses as the replacement in the
  335. text:</p><blockquote class="code"><pre class="python"><div class="source"><span class="k">def</span><span class="w">&#xA0;</span><span class="nf">dollar_replace</span><span class="p">(</span><span class="n">match</span><span class="p">:</span>&#xA0;<span class="n">re</span><span class="o">.</span><span class="n">Match</span><span class="p">[</span><span class="nb">str</span><span class="p">])</span>&#xA0;<span class="o">-&gt;</span>&#xA0;<span class="nb">str</span><span class="p">:</span>
  336. <br><span class="w">&#xA0;&#xA0;&#xA0;&#xA0;</span><span class="sd">&quot;&quot;&quot;Called&#xA0;for&#xA0;each&#xA0;$replacement.&quot;&quot;&quot;</span>
  337. <br>&#xA0;&#xA0;&#xA0;&#xA0;<span class="c1">#&#xA0;Get&#xA0;the&#xA0;one&#xA0;group&#xA0;that&#xA0;matched.</span>
  338. <br>&#xA0;&#xA0;&#xA0;&#xA0;<span class="n">groups</span>&#xA0;<span class="o">=</span>&#xA0;<span class="n">match</span><span class="o">.</span><span class="n">group</span><span class="p">(</span><span class="s1">&#39;dollar&#39;</span><span class="p">,</span>&#xA0;<span class="s1">&#39;word1&#39;</span><span class="p">,</span>&#xA0;<span class="s1">&#39;word2&#39;</span><span class="p">)</span>
  339. <br>&#xA0;&#xA0;&#xA0;&#xA0;<span class="n">word</span>&#xA0;<span class="o">=</span>&#xA0;<span class="nb">next</span><span class="p">(</span><span class="n">g</span>&#xA0;<span class="k">for</span>&#xA0;<span class="n">g</span>&#xA0;<span class="ow">in</span>&#xA0;<span class="n">groups</span>&#xA0;<span class="k">if</span>&#xA0;<span class="n">g</span><span class="p">)</span>
  340. <br>
  341. <br>&#xA0;&#xA0;&#xA0;&#xA0;<span class="k">if</span>&#xA0;<span class="n">word</span>&#xA0;<span class="o">==</span>&#xA0;<span class="s2">&quot;$&quot;</span><span class="p">:</span>
  342. <br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<span class="k">return</span>&#xA0;<span class="s2">&quot;$&quot;</span>
  343. <br>&#xA0;&#xA0;&#xA0;&#xA0;<span class="k">elif</span>&#xA0;<span class="n">word</span>&#xA0;<span class="ow">in</span>&#xA0;<span class="n">variables</span><span class="p">:</span>
  344. <br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<span class="k">return</span>&#xA0;<span class="n">variables</span><span class="p">[</span><span class="n">word</span><span class="p">]</span>
  345. <br>&#xA0;&#xA0;&#xA0;&#xA0;<span class="k">elif</span>&#xA0;<span class="n">match</span><span class="p">[</span><span class="s2">&quot;strict&quot;</span><span class="p">]:</span>
  346. <br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<span class="n">msg</span>&#xA0;<span class="o">=</span>&#xA0;<span class="sa">f</span><span class="s2">&quot;Variable&#xA0;</span><span class="si">{</span><span class="n">word</span><span class="si">}</span><span class="s2">&#xA0;is&#xA0;undefined:&#xA0;</span><span class="si">{</span><span class="n">text</span><span class="si">!r}</span><span class="s2">&quot;</span>
  347. <br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<span class="k">raise</span>&#xA0;<span class="ne">NameError</span><span class="p">(</span><span class="n">msg</span><span class="p">)</span>
  348. <br>&#xA0;&#xA0;&#xA0;&#xA0;<span class="k">else</span><span class="p">:</span>
  349. <br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<span class="k">return</span>&#xA0;<span class="n">match</span><span class="p">[</span><span class="s2">&quot;defval&quot;</span><span class="p">]</span>
  350. <br></div>
  351. </pre></blockquote><p>First we use <code>match.group()</code>.  Called with a number of names, it returns
  352. a tuple of what those named groups matched. They could be the matched text, or
  353. None if the group didn&#8217;t match anything.</p><p>The way our regex is written only one of those three groups will match, so
  354. the tuple will have one string and two None&#8217;s.  To get the matched string, we
  355. use <code>next()</code> to find it. If the built-in <code>any()</code> returned the first
  356. true thing it found this code could be simpler, but it doesn&#8217;t so we have to do
  357. it this way.</p><p>Now we can check the value to decide on the replacement:</p><ul>
  358. <li>If the match was a dollar sign, we return a dollar sign.</li>
  359.  
  360. <li>If the word is one of our defined variables, we return the value of the
  361. variable.</li>
  362.  
  363. <li>Since the word isn&#8217;t a defined variable, we check if the &#8220;strict&#8221; marker was
  364. found, and if so, raise an exception.</li>
  365.  
  366. <li>Otherwise we return the default value provided.</li>
  367.  
  368. </ul><p>The final piece of the implementation is to use <code>re.sub()</code> and return
  369. the result:</p><blockquote class="code"><pre class="python"><div class="source"><span class="k">return</span>&#xA0;<span class="n">re</span><span class="o">.</span><span class="n">sub</span><span class="p">(</span><span class="n">dollar_pattern</span><span class="p">,</span>&#xA0;<span class="n">dollar_replace</span><span class="p">,</span>&#xA0;<span class="n">text</span><span class="p">)</span>
  370. <br></div>
  371. </pre></blockquote><p>Regexes are often criticized for being too opaque and esoteric.  But done
  372. right, they can be very powerful and don&#8217;t have to be a burden.  What we&#8217;ve done
  373. here is used simple pattern matching paired with useful API features to
  374. compactly write a useful transformation.</p><p>BTW, if you are interested, the <a rel="external noopener" href="https://github.com/nedbat/coveragepy/blob/master/coverage/misc.py#L228-L276">real code is in
  375. coverage.py</a>.</p>
  376. ]]></description>
  377. </item>
  378. <item rdf:about="https://nedbatchelder.com/blog/202504/find_the_bear.html">
  379. <title>Find the bear</title>
  380. <link>https://nedbatchelder.com/blog/202504/find_the_bear.html</link>
  381. <dc:date>2025-04-06T13:46:19-04:00</dc:date>
  382. <dc:creator>Ned Batchelder</dc:creator>
  383. <description><![CDATA[<p>A parenting story from almost 30 years ago.</p><p>My wife told me about something her dad did when she was young: in the car,
  384. knowing they were approaching an exit on the highway, he&#8217;d say to himself, but
  385. loud enough for his daughters in the back to hear, &#8220;If only I could find exit
  386. 10...&#8221; The girls would look out the window and soon spot the very sign he
  387. needed! &#8220;There it is Dad, we found it!&#8221;  I liked it, it was clever and
  388. sweet.</p><p>When my son <a rel="external noopener" href="https://www.imdb.com/name/nm6026174/">Max</a> was six or so, we were headed into
  389. Boston to visit the big FAO Schwarz toy store that used to be on Boylston St.
  390. They had a large bronze statue of a teddy bear on the corner in front of the
  391. store.  It must have been 10 or 12 feet tall. I wanted to try my father-in-law&#8217;s
  392. technique with it.</p><p>Max had always been observant, competent and confident.  The kind of kid who
  393. could quickly tell you if a piece was missing from a Lego kit.   I figured he&#8217;d
  394. be the perfect target for this.</p><p>We got off the T (the subway if you aren&#8217;t from Boston), and had to walk a
  395. bit.  When we were a half block from the store, I could clearly see the bear
  396. up ahead. I said, &#8220;If only I could find that bear statue...&#8221;</p><p>Max responded, &#8220;Oh Dad, I knew you didn&#8217;t know where it was!&#8221;</p><p class="bulletsep">•    •    •</p><p>The store closed in 2004 and the bear was removed. I thought it was gone for
  397. good.  But on a walk a few weeks ago, I happened upon it outside the Tufts
  398. Children&#8217;s Hospital.</p><p>Now I definitely know where it is:</p><div class="figurep"><figure><picture><source type="image/webp" srcset="https://nedbatchelder.com/iv/webp/pix/fao-schwarz-bear.jpg.webp"><img src="https://nedbatchelder.com/pix/fao-schwarz-bear.jpg" alt="The bronze bear statue" width="550" height="760"></picture></figure></div>
  399. ]]></description>
  400. </item>
  401. <item rdf:about="https://nedbatchelder.com/blog/202504/nedflix.html">
  402. <title>Nedflix</title>
  403. <link>https://nedbatchelder.com/blog/202504/nedflix.html</link>
  404. <dc:date>2025-04-03T18:19:39-04:00</dc:date>
  405. <dc:creator>Ned Batchelder</dc:creator>
  406. <description><![CDATA[<p>Well, Anthropic and I were not a good fit, though
  407. <a href="https://nedbatchelder.com/blog/202407/anthropic.html">as predicted</a> it was an experience.  I&#8217;ve
  408. started a new job on the Python language team at Netflix.  It feels like a much
  409. better match in a number of ways.</p>
  410. ]]></description>
  411. </item>
  412. <item rdf:about="https://nedbatchelder.com/blog/202503/human_sorting_improved.html">
  413. <title>Human sorting improved</title>
  414. <link>https://nedbatchelder.com/blog/202503/human_sorting_improved.html</link>
  415. <dc:date>2025-03-29T12:59:35-04:00</dc:date>
  416. <dc:creator>Ned Batchelder</dc:creator>
  417. <description><![CDATA[<p>When sorting strings, you&#8217;d often like the order to make sense to a person.
  418. That means numbers need to be treated numerically even if they are in a larger
  419. string.</p><p>For example, sorting Python versions with the default sort() would give
  420. you:</p><blockquote class="code"><pre>Python 3.10<br>Python 3.11<br>Python 3.9<br></pre></blockquote><p>when you want it to be:</p><blockquote class="code"><pre>Python 3.9<br>Python 3.10<br>Python 3.11<br></pre></blockquote><p>I wrote about this long ago (<a href="https://nedbatchelder.com/blog/200712/human_sorting.html">Human sorting</a>), but have
  421. continued to tweak the code and needed to <a rel="external noopener" href="https://github.com/nedbat/watchgha/commit/cfcd48ac3f24f5b76aa02caa695af13e37f38bcf">add it to a
  422. project</a> recently.  Here&#8217;s the latest:</p><blockquote class="code"><pre class="python"><div class="source"><span class="kn">import</span><span class="w">&#xA0;</span><span class="nn">re</span>
  423. <br>
  424. <br><span class="k">def</span><span class="w">&#xA0;</span><span class="nf">human_key</span><span class="p">(</span><span class="n">s</span><span class="p">:</span>&#xA0;<span class="nb">str</span><span class="p">)</span>&#xA0;<span class="o">-&gt;</span>&#xA0;<span class="nb">tuple</span><span class="p">[</span><span class="nb">list</span><span class="p">[</span><span class="nb">str</span>&#xA0;<span class="o">|</span>&#xA0;<span class="nb">int</span><span class="p">],</span>&#xA0;<span class="nb">str</span><span class="p">]:</span>
  425. <br><span class="w">&#xA0;&#xA0;&#xA0;&#xA0;</span><span class="sd">&quot;&quot;&quot;Turn&#xA0;a&#xA0;string&#xA0;into&#xA0;a&#xA0;sortable&#xA0;value&#xA0;that&#xA0;works&#xA0;how&#xA0;humans&#xA0;expect.</span>
  426. <br>
  427. <br><span class="sd">&#xA0;&#xA0;&#xA0;&#xA0;&quot;z23A&quot;&#xA0;-&gt;&#xA0;([&quot;z&quot;,&#xA0;23,&#xA0;&quot;a&quot;],&#xA0;&quot;z23A&quot;)</span>
  428. <br>
  429. <br><span class="sd">&#xA0;&#xA0;&#xA0;&#xA0;The&#xA0;original&#xA0;string&#xA0;is&#xA0;appended&#xA0;as&#xA0;a&#xA0;last&#xA0;value&#xA0;to&#xA0;ensure&#xA0;the</span>
  430. <br><span class="sd">&#xA0;&#xA0;&#xA0;&#xA0;key&#xA0;is&#xA0;unique&#xA0;enough&#xA0;so&#xA0;that&#xA0;&quot;x1y&quot;&#xA0;and&#xA0;&quot;x001y&quot;&#xA0;can&#xA0;be&#xA0;distinguished.</span>
  431. <br>
  432. <br><span class="sd">&#xA0;&#xA0;&#xA0;&#xA0;&quot;&quot;&quot;</span>
  433. <br>&#xA0;&#xA0;&#xA0;&#xA0;<span class="k">def</span><span class="w">&#xA0;</span><span class="nf">try_int</span><span class="p">(</span><span class="n">s</span><span class="p">:</span>&#xA0;<span class="nb">str</span><span class="p">)</span>&#xA0;<span class="o">-&gt;</span>&#xA0;<span class="nb">str</span>&#xA0;<span class="o">|</span>&#xA0;<span class="nb">int</span><span class="p">:</span>
  434. <br><span class="w">&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;</span><span class="sd">&quot;&quot;&quot;If&#xA0;`s`&#xA0;is&#xA0;a&#xA0;number,&#xA0;return&#xA0;an&#xA0;int,&#xA0;else&#xA0;`s`&#xA0;unchanged.&quot;&quot;&quot;</span>
  435. <br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<span class="k">try</span><span class="p">:</span>
  436. <br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<span class="k">return</span>&#xA0;<span class="nb">int</span><span class="p">(</span><span class="n">s</span><span class="p">)</span>
  437. <br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<span class="k">except</span>&#xA0;<span class="ne">ValueError</span><span class="p">:</span>
  438. <br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<span class="k">return</span>&#xA0;<span class="n">s</span>
  439. <br>
  440. <br>&#xA0;&#xA0;&#xA0;&#xA0;<span class="k">return</span>&#xA0;<span class="p">([</span><span class="n">try_int</span><span class="p">(</span><span class="n">c</span><span class="p">)</span>&#xA0;<span class="k">for</span>&#xA0;<span class="n">c</span>&#xA0;<span class="ow">in</span>&#xA0;<span class="n">re</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="sa">r</span><span class="s2">&quot;(\d+)&quot;</span><span class="p">,</span>&#xA0;<span class="n">s</span><span class="o">.</span><span class="n">casefold</span><span class="p">())],</span>&#xA0;<span class="n">s</span><span class="p">)</span>
  441. <br>
  442. <br><span class="k">def</span><span class="w">&#xA0;</span><span class="nf">human_sort</span><span class="p">(</span><span class="n">strings</span><span class="p">:</span>&#xA0;<span class="nb">list</span><span class="p">[</span><span class="nb">str</span><span class="p">])</span>&#xA0;<span class="o">-&gt;</span>&#xA0;<span class="kc">None</span><span class="p">:</span>
  443. <br><span class="w">&#xA0;&#xA0;&#xA0;&#xA0;</span><span class="sd">&quot;&quot;&quot;Sort&#xA0;a&#xA0;list&#xA0;of&#xA0;strings&#xA0;how&#xA0;humans&#xA0;expect.&quot;&quot;&quot;</span>
  444. <br>&#xA0;&#xA0;&#xA0;&#xA0;<span class="n">strings</span><span class="o">.</span><span class="n">sort</span><span class="p">(</span><span class="n">key</span><span class="o">=</span><span class="n">human_key</span><span class="p">)</span>
  445. <br></div>
  446. </pre></blockquote><p>The central idea here is to turn a string like <code>"Python 3.9"</code> into the
  447. key <code>["Python ", 3, ".", 9]</code> so that numeric components will be sorted by
  448. their numeric value. The re.split() function gives us interleaved words and
  449. numbers, and try_int() turns the numbers into actual numbers, giving us sortable
  450. key lists.</p><p>There are two improvements from the original:</p><ul>
  451.  
  452. <li>The sort is made case-insensitive by using casefold() to lower-case the
  453. string.</li>
  454.  
  455. <li>The key returned is now a two-element tuple: the first element is the list
  456. of intermixed strings and integers that gives us the ordering we want.  The
  457. second element is the original string unchanged to ensure that unique strings
  458. will always result in distinct keys.  Without it, <code>"x1y"</code> and
  459. <code>"x001Y"</code> would both produce the same key.  This solves a
  460. <a rel="external noopener" href="https://github.com/nedbat/coveragepy/issues/1709">problem that actually happened</a> when sorting the items of
  461. a dictionary.
  462.  
  463. <blockquote class="code"><pre class="python"><div class="source"><span class="c1">#&#xA0;Without&#xA0;the&#xA0;tuple:&#xA0;different&#xA0;strings,&#xA0;same&#xA0;key!!</span>
  464. <br><span class="n">human_key</span><span class="p">(</span><span class="s2">&quot;x1y&quot;</span><span class="p">)</span>&#xA0;<span class="o">-&gt;</span>&#xA0;<span class="p">[</span><span class="s2">&quot;x&quot;</span><span class="p">,</span>&#xA0;<span class="mi">1</span><span class="p">,</span>&#xA0;<span class="s2">&quot;y&quot;</span><span class="p">]</span>
  465. <br><span class="n">human_key</span><span class="p">(</span><span class="s2">&quot;x001Y&quot;</span><span class="p">)</span>&#xA0;<span class="o">-&gt;</span>&#xA0;<span class="p">[</span><span class="s2">&quot;x&quot;</span><span class="p">,</span>&#xA0;<span class="mi">1</span><span class="p">,</span>&#xA0;<span class="s2">&quot;y&quot;</span><span class="p">]</span>
  466. <br>
  467. <br><span class="c1">#&#xA0;With&#xA0;the&#xA0;tuple:&#xA0;different&#xA0;strings,&#xA0;different&#xA0;keys.</span>
  468. <br><span class="n">human_key</span><span class="p">(</span><span class="s2">&quot;x1y&quot;</span><span class="p">)</span>&#xA0;<span class="o">-&gt;</span>&#xA0;<span class="p">([</span><span class="s2">&quot;x&quot;</span><span class="p">,</span>&#xA0;<span class="mi">1</span><span class="p">,</span>&#xA0;<span class="s2">&quot;y&quot;</span><span class="p">],</span>&#xA0;<span class="s2">&quot;x1y&quot;</span><span class="p">)</span>
  469. <br><span class="n">human_key</span><span class="p">(</span><span class="s2">&quot;x001Y&quot;</span><span class="p">)</span>&#xA0;<span class="o">-&gt;</span>&#xA0;<span class="p">([</span><span class="s2">&quot;x&quot;</span><span class="p">,</span>&#xA0;<span class="mi">1</span><span class="p">,</span>&#xA0;<span class="s2">&quot;y&quot;</span><span class="p">],</span>&#xA0;<span class="s2">&quot;x001Y&quot;</span><span class="p">)</span>
  470. <br></div>
  471. </pre></blockquote>
  472.  
  473. </li>
  474.  
  475. </ul><p>If you are interested, there are many different ways to split the string into
  476. the word/number mix.  The <a rel="external noopener" href="https://nedbatchelder.com/blog/200712/human_sorting.html#comments">comments on the old post</a>
  477. have many alternatives, and there are certainly more.</p><p>This still makes some assumptions about what is wanted, and doesn&#8217;t cover all
  478. possible options (floats? negative/positive? full file paths?).  For those, you
  479. probably want the full-featured <a rel="external noopener" href="https://pypi.org/project/natsort/">natsort</a> (natural sort)
  480. package.</p>
  481. ]]></description>
  482. </item>
  483. <item rdf:about="https://nedbatchelder.com/blog/202503/horseless_intelligence.html">
  484. <title>Horseless intelligence</title>
  485. <link>https://nedbatchelder.com/blog/202503/horseless_intelligence.html</link>
  486. <dc:date>2025-03-17T13:56:03-04:00</dc:date>
  487. <dc:creator>Ned Batchelder</dc:creator>
  488. <description><![CDATA[<p>AI is everywhere these days, and everyone has opinions and thoughts. These
  489. are some of mine.</p><p>Full disclosure: for a time I worked for Anthropic, the makers of
  490. <a href="https://claude.ai" rel="external noopener">Claude.ai</a>.  I no longer do, and nothing in
  491. this post (or elsewhere on this site) is their opinion or is proprietary to
  492. them.</p><h1 id="h_how_to_use_ai">How to use AI<a class="headerlink" aria-label="Link to this header" href="#h_how_to_use_ai"></a></h1><p>My advice about using AI is simple: use AI as an assistant, not an expert,
  493. and use it judiciously.  Some people will object, &#8220;but AI can be wrong!&#8221;  Yes,
  494. and so can the internet in general, but no one now recommends avoiding online
  495. resources because they can be wrong.  They recommend taking it all with a grain
  496. of salt and being careful.  That&#8217;s what you should do with AI help as well.</p><p>We are all learning how to use AI well.  Prompt engineering is a new
  497. discipline.  It surprises me that large language models (LLMs) give better
  498. answers if you include phrases like &#8220;think step-by-step&#8221; or &#8220;check your answer
  499. before you reply&#8221; in your prompt, but they do improve the result.  LLMs are not
  500. search engines, but like search engines, you have to approach them as unique
  501. tools that will do better if you know how to ask the right questions.</p><p>If you approach AI thinking that it will hallucinate and be wrong, and then
  502. discard it as soon as it does, you are falling victim to
  503. <a rel="external noopener" href="https://en.wikipedia.org/wiki/Confirmation_bias">confirmation bias</a>.  Yes, AI will be wrong sometimes.  That
  504. doesn&#8217;t mean it is useless.  It means you have to use it carefully.</p><p>I&#8217;ve used AI to help me write code when I didn&#8217;t know how to get started
  505. because it needed more research than I could afford at the moment.  The AI
  506. didn&#8217;t produce finished code, but it got me going in the right direction, and
  507. iterating with it got me to working code.</p><p>One thing it seemed to do well was to write more tests given a few examples
  508. to start from.  Your workflow probably has steps where AI can help you. It&#8217;s
  509. not a magic bullet, it&#8217;s a tool that you have to learn how to use.</p><h1 id="h_the_future_of_coding">The future of coding<a class="headerlink" aria-label="Link to this header" href="#h_the_future_of_coding"></a></h1><p>In beginner-coding spaces like <a rel="external noopener" href="https://discord.gg/python">Python Discord</a>,
  510. anxious learners ask if there is any point in learning to code, since won&#8217;t AI
  511. take all the jobs soon anyway?</p><p><a rel="external noopener" href="https://simonwillison.net/">Simon Willison</a> seems to be our best guide to the
  512. head-spinning pace of AI development these days (if you can keep up with the
  513. head-spinning pace of his blog!) I like what he <a rel="external noopener" href="https://news.ycombinator.com/item?id=43166157">said
  514. recently</a> about how AI will affect new programmers:</p><blockquote><div>
  515.  
  516.    <p>There has never been a better time to learn to code — the
  517.    learning curve is being shaved down by these new LLM-based tools, and the
  518.    amount of value people with programming literacy can produce is going up by
  519.    an order of magnitude.</p>
  520.  
  521.    <p>People who know both coding and LLMs will be a whole lot more attractive
  522.    to hire to build software than people who just know LLMs for many years to
  523.    come.</p>
  524.  
  525. </div></blockquote><p>Simon has also emphasized in his writing what I have found: AI lets me write
  526. code that I wouldn&#8217;t have undertaken without its help.  It doesn&#8217;t produce the
  527. finished code, but it&#8217;s a helpful pair-programming assistant.</p><h1 id="h_can_llms_think">Can LLMs think?<a class="headerlink" aria-label="Link to this header" href="#h_can_llms_think"></a></h1><p>Another objection I see often: &#8220;but LLMs can&#8217;t think, they just predict the
  528. next word!&#8221; I&#8217;m not sure we have a consensus understanding of what &#8220;think&#8221; means
  529. in this context.  Airplanes don&#8217;t fly in the same way that birds do.
  530. Automobiles don&#8217;t run in the same way that horses do.  The important thing is
  531. that they accomplish many of the same tasks.</p><p>OK, so AI doesn&#8217;t think the same way that people do.  I&#8217;m fine with that.
  532. What&#8217;s important to me is that it can do some work for me, work that could also
  533. be done by people thinking.  Cars (&#8220;horseless carriages&#8221;) do work that used to
  534. be done by horses running.  No one now complains that cars work differently than
  535. horses.</p><p>If &#8220;just predict the next word&#8221; is an accurate description of what LLMs are
  536. doing, it&#8217;s a demonstration of how surprisingly powerful predicting the next
  537. word can be.</p><h1 id="h_harms">Harms<a class="headerlink" aria-label="Link to this header" href="#h_harms"></a></h1><p>I am concerned about the harms that AI can cause.  Some people and
  538. organizations are focused on Asimov-style harms (will society collapse, will
  539. millions die?) and I am glad they are.  But I&#8217;m more concerned with
  540. Dickens-style harms: people losing jobs not because AI can do their work, but
  541. because people in charge will think AI can do other people&#8217;s work.  Harms due to
  542. people misunderstanding what AI does and doesn&#8217;t do well and misusing it.</p><p>I don&#8217;t see easy solutions to these problems.  To go back to the car analogy:
  543. we&#8217;ve been a car society for about 120 years.  For most of that time we&#8217;ve been
  544. leaning more and more towards cars.  We are still trying to find the right
  545. balance, the right way to reduce the harm they cause while keeping the benefits
  546. they give us.</p><p>AI will be similar.  The technology is not going to go away. We will not turn
  547. our back on it and put it back into the bottle.  We&#8217;ll continue to work on
  548. improving how it works and how we work with it.  There will be good and bad.
  549. The balance will depend on how well we collectively use it and educate each
  550. other, and how well we pay attention to what is happening.</p><h1 id="h_future">Future<a class="headerlink" aria-label="Link to this header" href="#h_future"></a></h1><p>The pro-AI hype in the industry now is at a fever pitch, it&#8217;s completely
  551. overblown.  But the anti-AI crowd also seems to be railing against it without a
  552. clear understanding of the current capabilities or the useful approaches.</p><p>I&#8217;m going to be using AI more, and learning where it works well and where it
  553. doesn&#8217;t.</p>
  554. ]]></description>
  555. </item>
  556. <item rdf:about="https://nedbatchelder.com/blog/202503/faster_branch_coverage_measurement.html">
  557. <title>Faster branch coverage measurement</title>
  558. <link>https://nedbatchelder.com/blog/202503/faster_branch_coverage_measurement.html</link>
  559. <dc:date>2025-03-09T16:07:02-04:00</dc:date>
  560. <dc:creator>Ned Batchelder</dc:creator>
  561. <description><![CDATA[<p>After nearly two years, I think this is finally ready: coverage.py can use
  562. <a rel="external noopener" href="https://docs.python.org/3/library/sys.monitoring.html">sys.monitoring</a> to more efficiently measure branch
  563. coverage.</p><p>I would love for people to try it, but it&#8217;s a little involved at the
  564. moment:</p><ul>
  565.  
  566. <li>You need to have your own build of Python from the main branch on GitHub,
  567. because the CPython side of the work landed after 3.14 alpha 5.  Alpha 6 is
  568. supposed to arrive within a week, so that will make it easier.</li>
  569.  
  570. <li>I haven&#8217;t released a version of coverage.py to PyPI with this code yet, so
  571. you also need to install coverage from GitHub:</li>
  572.  
  573. <blockquote class="code"><pre class="shell"><div class="source">%<span class="w">&#xA0;</span>python3<span class="w">&#xA0;</span>-m<span class="w">&#xA0;</span>pip<span class="w">&#xA0;</span>install<span class="w">&#xA0;</span><span>git+https://github.com/nedbat/coveragepy</span>
  574. <br></div>
  575. </pre></blockquote>
  576.  
  577. </ul><p>Once you have both of those things, set the environment variable
  578. <code>COVERAGE_CORE=sysmon</code> and run coverage as you usually do.  If all goes
  579. well, it should be faster.  Please let me know!</p><p>Feedback is welcome in <a rel="external noopener" href="https://github.com/nedbat/coveragepy/issues">GitHub issues</a> or in the
  580. <a rel="external noopener" href="https://discord.gg/RBw567RyWc">#coverage-py channel</a> in the Python Discord server.</p><p>This has been a long journey, starting when <a rel="external noopener" href="https://discuss.python.org/t/pep-669-low-impact-monitoring-for-cpython/13018/33">I first
  581. commented on PEP 669</a> that underpins this work.  Mark Shannon and I have had
  582. many back and forths about the behavior of sys.monitoring, finally landing on
  583. something that would work for us both.</p><p>For the curious: traditionally coverage.py relied on
  584. <a rel="external noopener" href="https://docs.python.org/3/library/sys.html#sys.settrace">sys.settrace</a>. Python calls my recording function for
  585. every line of Python executed.  It&#8217;s simple and effective, but inefficient.
  586. After I&#8217;ve been told a line was executed once, I don&#8217;t need to be told again,
  587. but settrace keeps calling my function.  The new
  588. <a rel="external noopener" href="https://docs.python.org/3/library/sys.monitoring.html">sys.monitoring</a> that arrived in Python 3.12 lets me
  589. disable an event once it&#8217;s fired, so after the first ping there&#8217;s no overhead to
  590. running that same code multiple times.</p><p>It took a while to iron out the event behavior that lets us measure branches
  591. as well as lines, but Python 3.14.0 after alpha 5 has it, so we&#8217;re finally able
  592. to announce coverage.py support for people to try out.</p>
  593. ]]></description>
  594. </item>
  595. </rdf:RDF>
  596.  

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