<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Shreya Boyane</title>
    <description>The latest articles on DEV Community by Shreya Boyane (@sonicab).</description>
    <link>https://dev.to/sonicab</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3623165%2F8c72c8b9-1f9d-47b9-90e9-dfa7abfb7623.png</url>
      <title>DEV Community: Shreya Boyane</title>
      <link>https://dev.to/sonicab</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sonicab"/>
    <language>en</language>
    <item>
      <title>huggingface_hub v2.3.0 week: how I fixed a Windows crash in hf cache verify</title>
      <dc:creator>Shreya Boyane</dc:creator>
      <pubDate>Thu, 29 Oct 2026 01:16:21 +0000</pubDate>
      <link>https://dev.to/sonicab/huggingfacehub-v230-week-how-i-fixed-a-windows-crash-in-hf-cache-verify-44e6</link>
      <guid>https://dev.to/sonicab/huggingfacehub-v230-week-how-i-fixed-a-windows-crash-in-hf-cache-verify-44e6</guid>
      <description>&lt;p&gt;&lt;a href="https://github.com/huggingface/huggingface_hub/releases/tag/v2.3.0" rel="noopener noreferrer"&gt;huggingface_hub v2.3.0&lt;/a&gt; came out on October 22, 2026, and its headline number is a good one: a recursive &lt;code&gt;HfFileSystem.find()&lt;/code&gt; over &lt;code&gt;allenai/objaverse&lt;/code&gt;, about 800k files, went from 776 s to 19.7 s. Lower in the notes is a smaller fix, &lt;a href="https://github.com/huggingface/huggingface_hub/pull/5198" rel="noopener noreferrer"&gt;#5198&lt;/a&gt;, for a &lt;code&gt;UnicodeDecodeError&lt;/code&gt; under the C locale that broke checkpoint saving in TRL.&lt;/p&gt;

&lt;p&gt;That same week I hit the mirror image of that bug. In #5198, text coming into the library failed to decode. In my case, text going out of the &lt;code&gt;hf&lt;/code&gt; CLI failed to encode, and it took down &lt;code&gt;hf cache verify&lt;/code&gt; on my Windows 11 machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;p&gt;On Windows, Python writes to a terminal in UTF-8 but to a pipe, a file or a CI log in the old ANSI code page, cp1252. &lt;code&gt;hf cache verify&lt;/code&gt; began its failure report with a ❌, cp1252 has no ❌, and the command crashed at the exact moment it had bad news to report. The fix prints &lt;code&gt;[FAIL]&lt;/code&gt; when the stream cannot encode the emoji. It was merged on October 27.&lt;/p&gt;

&lt;h2&gt;
  
  
  The concept, animated
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F49m3qrkzhrocnbefv00v.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F49m3qrkzhrocnbefv00v.gif" alt="Animated figure. The print path as labelled blocks: hf cache verify, the out.text call with the ❌ marker, sys.stdout with its .encoding engraved (utf-8 in a console, cp1252 when piped), and the destination. The ❌ token reaches a console; piped on Windows it drops into a UnicodeEncodeError tray under sys.stdout; after #5255 an _ascii_safe(&amp;quot;❌&amp;quot;, &amp;quot;[FAIL]&amp;quot;) block in front of stdout swaps it for [FAIL] and the report prints." width="800" height="602"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 1. The blocks are the real call chain in &lt;code&gt;cli/cache.py&lt;/code&gt; and &lt;code&gt;cli/_output.py&lt;/code&gt;. The token is the first character the failure report prints. The three states: a console, a pipe on Windows before the fix, and a pipe after it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Python decides &lt;code&gt;sys.stdout.encoding&lt;/code&gt; once, when the program starts, and on Windows it decides differently depending on where the output goes. Into a console: UTF-8, which can encode every character. Into a pipe, a redirected file or a CI step: the ANSI code page, which on my machine is cp1252, a 1990s table of about 250 characters with no ❌ in it. The engraved &lt;code&gt;.encoding&lt;/code&gt; on the &lt;code&gt;sys.stdout&lt;/code&gt; block in figure 1 flips between the two.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;hf cache verify&lt;/code&gt; checks that the files in your local Hugging Face cache match the checksums on the Hub, and the first thing it printed when a file was bad was &lt;code&gt;❌ Checksum verification failed for the following file(s):&lt;/code&gt;. In a console, the token passes. In a pipe, &lt;code&gt;sys.stdout&lt;/code&gt; cannot encode it: &lt;code&gt;UnicodeEncodeError&lt;/code&gt;, position 0. The list of bad files, the one thing you asked for, never comes out, and the command exits with &lt;code&gt;Error: Invalid value.&lt;/code&gt; That label is the CLI's name for any &lt;code&gt;ValueError&lt;/code&gt;, and &lt;code&gt;UnicodeEncodeError&lt;/code&gt; happens to be one, so an output bug looked like bad input.&lt;/p&gt;

&lt;p&gt;One line reproduces it with no Hugging Face code at all: &lt;code&gt;PYTHONIOENCODING=cp1252 python -c "print('❌')"&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I ran into it
&lt;/h2&gt;

&lt;p&gt;I use the &lt;code&gt;hf&lt;/code&gt; CLI every day on Windows 11, and that week I was exploring it from source (&lt;code&gt;main&lt;/code&gt;, which already reported 2.4.0.dev0, on Python 3.11). I wanted the output of &lt;code&gt;hf cache verify&lt;/code&gt; saved to a file, as a record of which cached files had gone bad. To get a bad file, I made one: I downloaded &lt;code&gt;config.json&lt;/code&gt; from &lt;code&gt;julien-c/dummy-unknown&lt;/code&gt;, edited the cached copy so its checksum would no longer match, and ran the check through a Git Bash pipe.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ hf cache verify julien-c/dummy-unknown --cache-dir ./e2e_cache | cat
Error: Invalid value. 'charmap' codec can't encode character '❌' in position 0: character maps to &amp;lt;undefined&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;charmap&lt;/code&gt; is how Python names the codec behind cp1252, the character was ❌ at position 0, and the pipe was the last clue. A terminal and a pipe get different encodings, so output that looks fine while you develop can crash in a log file.&lt;/p&gt;

&lt;p&gt;Then I searched the repo and found &lt;code&gt;_ascii_safe&lt;/code&gt; in &lt;code&gt;cli/_output.py&lt;/code&gt;. It came from &lt;a href="https://github.com/huggingface/huggingface_hub/pull/4610" rel="noopener noreferrer"&gt;#4610&lt;/a&gt;, which swapped &lt;code&gt;✓&lt;/code&gt; for &lt;code&gt;[OK]&lt;/code&gt; when stdout cannot encode it. That PR also listed the places it had not fixed yet, and &lt;code&gt;❌&lt;/code&gt; in &lt;code&gt;cache verify&lt;/code&gt; was on the list. Before writing code, I checked that no open or closed issue or PR covered it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix, in one idea
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv9scipmmax3tdpnnozig.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv9scipmmax3tdpnnozig.png" alt="Still from the third state: the _ascii_safe block sits in front of sys.stdout, the token at the pipe reads [FAIL], and the output line reads &amp;quot;[FAIL] Checksum verification failed for the following file(s): ...&amp;quot;." width="800" height="601"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 2. After #5255 the command asks &lt;code&gt;_ascii_safe&lt;/code&gt; whether stdout can encode ❌ before printing. When it cannot, the plain-text &lt;code&gt;[FAIL]&lt;/code&gt; goes out instead and the report prints.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Ask before you print. &lt;code&gt;_ascii_safe(char, fallback)&lt;/code&gt; tries to encode the character with the current stdout encoding and returns the fallback if that fails. The answer is cached, because the encoding does not change during a run. The fix calls it once, &lt;code&gt;_ascii_safe("❌", "[FAIL]")&lt;/code&gt;, and uses the result in all three failure messages: checksum mismatches, missing files (&lt;code&gt;--fail-on-missing-files&lt;/code&gt;) and extra files (&lt;code&gt;--fail-on-extra-files&lt;/code&gt;). I picked &lt;code&gt;[FAIL]&lt;/code&gt; to match the existing &lt;code&gt;[OK]&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The same pipe now prints the report, and the exit code is still 1:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ hf cache verify julien-c/dummy-unknown --cache-dir ./e2e_cache | cat
[FAIL] Checksum verification failed for the following file(s):
  - config.json: expected d9e9f15bc825e4b2c9249e9578f884bbcb5e3684 (git-sha1), got 8d7314a1cf98df9734b0ab8aa85ffda51006c369
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I wrote a regression test before touching &lt;code&gt;cache.py&lt;/code&gt;. Click's &lt;code&gt;CliRunner(charset="cp1252")&lt;/code&gt; gives the command a cp1252 stdout on any OS, so the bug shows up without a Windows box. On &lt;code&gt;main&lt;/code&gt; the test failed with the real &lt;code&gt;UnicodeEncodeError&lt;/code&gt;. After the change, &lt;code&gt;pytest tests/test_cli.py -k verify&lt;/code&gt; gave 4 passed, and the five CLI test files gave 476 passed and 5 skipped.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this means if you run hf on Windows
&lt;/h2&gt;

&lt;p&gt;If you send &lt;code&gt;hf cache verify&lt;/code&gt; output anywhere other than a console on Windows (a file, a pipe, a CI log), you now get the list of bad files instead of &lt;code&gt;Error: Invalid value.&lt;/code&gt; That is the case where the report matters most, because nobody is watching the terminal. A failed check still exits with 1, so scripts behave the same, and a UTF-8 stdout still shows ❌.&lt;/p&gt;

&lt;p&gt;The fix landed on &lt;code&gt;main&lt;/code&gt; after v2.3.0 was released, so v2.3.0 does not have it. Until it reaches a release, setting &lt;code&gt;PYTHONUTF8=1&lt;/code&gt; makes Python use UTF-8 for stdout and avoids the crash.&lt;/p&gt;

&lt;p&gt;I kept the PR to &lt;code&gt;cache verify&lt;/code&gt; on purpose. The other gaps from #4610 are untouched, and strings that come from the Hub, such as an emoji in a repo description printed in a table, still fail on a cp1252 stdout. That is the one I want to look at next, and the open question is whether to fall back per glyph or once, centrally.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who got it over the line
&lt;/h2&gt;

&lt;p&gt;CONTRIBUTING.md asks for an issue before code changes, so on October 25 I opened &lt;a href="https://github.com/huggingface/huggingface_hub/issues/5254" rel="noopener noreferrer"&gt;#5254&lt;/a&gt; and then &lt;a href="https://github.com/huggingface/huggingface_hub/pull/5255" rel="noopener noreferrer"&gt;#5255&lt;/a&gt; with &lt;code&gt;Fixes #5254&lt;/code&gt;. The PR body led with the real Windows repro and the before and after output, because a maintainer had asked on another issue whether a bug had been seen on a real cache.&lt;/p&gt;

&lt;p&gt;Less than four hours later, community reviewer &lt;a href="https://github.com/Ayushdevo" rel="noopener noreferrer"&gt;Ayushdevo&lt;/a&gt; ran the cp1252 regression on Python 3.12 and checked that all three failure paths used the same marker and still exited with a failure. I had only run 3.11, so that was a check I could not give myself. Thank you, Ayushdevo.&lt;/p&gt;

&lt;p&gt;Maintainer &lt;a href="https://github.com/hanouticelina" rel="noopener noreferrer"&gt;hanouticelina&lt;/a&gt; approved it with "Thanks for fixing!" and merged it on October 27, a little over two days after I opened it. Before merging, hanouticelina applied review suggestions that took my regression test out, so what landed is one file: 5 lines added and 4 removed in &lt;code&gt;cli/cache.py&lt;/code&gt;. The test had already done its job by then, failing on &lt;code&gt;main&lt;/code&gt; and passing after the fix. Thank you for the quick review.&lt;/p&gt;

&lt;p&gt;Thanks also to &lt;a href="https://github.com/Wauplin" rel="noopener noreferrer"&gt;Wauplin&lt;/a&gt;, who wrote #4610 and left that list of known gaps behind. It predicted that each remaining gap would be a one-line &lt;code&gt;_ascii_safe()&lt;/code&gt; call, and mine was.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I took away
&lt;/h2&gt;

&lt;p&gt;Before I write a fix in someone else's codebase, I now read the last PR that touched the same problem. Here that was #4610. It gave me the helper, the fallback pattern (&lt;code&gt;✓&lt;/code&gt; became &lt;code&gt;[OK]&lt;/code&gt;, so &lt;code&gt;❌&lt;/code&gt; became &lt;code&gt;[FAIL]&lt;/code&gt;), and my exact bug on a list of known gaps. A vague "Windows encoding problem" became a small change that followed the maintainers' own plan, with a real repro at the top of the PR.&lt;/p&gt;

&lt;p&gt;I'm Shreya Boyane, an ML engineer with an MS in AI from WPI and a background as a Lead DevOps Engineer, so a crash that only shows up in a pipe or a CI log felt like familiar ground. I'm open to ML engineering roles. You can find me on GitHub as &lt;a href="https://github.com/Sonica-B" rel="noopener noreferrer"&gt;Sonica-B&lt;/a&gt;, at &lt;a href="https://shreyaboyane.dev" rel="noopener noreferrer"&gt;shreyaboyane.dev&lt;/a&gt;, and on &lt;a href="https://www.linkedin.com/in/shreya-boyane/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>python</category>
      <category>huggingface</category>
      <category>windows</category>
      <category>opensource</category>
    </item>
    <item>
      <title>supervision 0.31 sees depth. ComparisonAnnotator now sees grayscale</title>
      <dc:creator>Shreya Boyane</dc:creator>
      <pubDate>Thu, 29 Oct 2026 01:16:10 +0000</pubDate>
      <link>https://dev.to/sonicab/supervision-031-sees-depth-comparisonannotator-now-sees-grayscale-4532</link>
      <guid>https://dev.to/sonicab/supervision-031-sees-depth-comparisonannotator-now-sees-grayscale-4532</guid>
      <description>&lt;p&gt;&lt;a href="https://github.com/roboflow/supervision/releases/tag/0.31.0" rel="noopener noreferrer"&gt;supervision 0.31.0&lt;/a&gt; shipped on October 16, and its release notes open with "supervision now sees depth." A depth map is a single-channel image. So is a thermal frame, and so is an MRI slice like the ones in my TumorLytix project. The next two patch releases, 0.31.1 on October 20 and 0.31.2 on October 24, taught the mask, halo, crop and icon annotators to draw on grayscale scenes.&lt;/p&gt;

&lt;p&gt;On October 25 I opened a pull request for one annotator that still crashed on those frames. It was merged on October 26. This post is the short version of that side quest, from the first &lt;code&gt;ValueError&lt;/code&gt; to a test pattern I plan to reuse.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;p&gt;A colour is three numbers. A pixel in a grayscale image holds one number. &lt;code&gt;ComparisonAnnotator&lt;/code&gt; tried to put the three numbers into every pixel under a mask, and NumPy refused. The rest of the library already had a rule for this case, keep the blue value, and a helper that applies it. The fix routes the annotator's three colours through that helper and adds eight tests that check grayscale output against the BGR output, channel for channel.&lt;/p&gt;

&lt;h2&gt;
  
  
  The concept, animated
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkkjrcm10u8p9fe34vanb.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkkjrcm10u8p9fe34vanb.gif" alt="Animated figure. Two three-by-three pixel grids: scene (H, W, 3) BGR with three slots per pixel and scene (H, W) grayscale with one slot per pixel, each with a mask outline over four pixels. Three chips, b, g and r, from color_overlap.as_bgr() drop into a masked pixel: the three slots take them; the single slot cannot and the ValueError text appears; after #2803 a _color_for_canvas(color, scene) block between them lets only b through." width="800" height="602"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 1. The grids are the colour layer NumPy allocates with the scene's shape, the outline is the boolean mask the annotator writes under, and the chips are the three values of one colour in OpenCV's B, G, R order. The three states: a BGR scene, a grayscale scene before the fix, and after it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ComparisonAnnotator&lt;/code&gt; highlights where two sets of detections on the same frame differ: one colour for regions only the first set covers, one for regions only the second covers, and one for the overlap. Its docstring suggests it for comparing two models, or a ground truth with a prediction.&lt;/p&gt;

&lt;p&gt;Under the hood it builds an empty colour layer shaped like the scene and writes each colour into the pixels under its mask. On a BGR frame every pixel has three slots, and a colour's three values fit. On a one-channel frame every pixel has one slot. NumPy cannot place three values into it, and the first colour written is the overlap, so that was the line that failed. In my repro two 41 by 41 boxes overlapped in a 21 by 21 square, which is where the 441 in the message comes from.&lt;/p&gt;

&lt;p&gt;The same thing happened for &lt;code&gt;(H, W)&lt;/code&gt; arrays, &lt;code&gt;(H, W, 1)&lt;/code&gt; arrays and a Pillow &lt;code&gt;"L"&lt;/code&gt; image, with &lt;code&gt;opencv-python&lt;/code&gt; installed and with the NumPy fallback that supervision uses when OpenCV is absent.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I got there
&lt;/h2&gt;

&lt;p&gt;I was exploring supervision's annotators with one question: would they draw on the grayscale frames that medical and thermal pipelines produce? I wrote a probe script that runs every concrete annotator on four kinds of scene: &lt;code&gt;(H, W)&lt;/code&gt; arrays, &lt;code&gt;(H, W, 1)&lt;/code&gt; arrays, BGRA images and float images.&lt;/p&gt;

&lt;p&gt;On &lt;code&gt;develop&lt;/code&gt; at commit b651cca, most of the grayscale work had already landed. &lt;code&gt;MaskAnnotator&lt;/code&gt;, &lt;code&gt;HaloAnnotator&lt;/code&gt;, &lt;code&gt;PolygonAnnotator&lt;/code&gt;, &lt;code&gt;ColorAnnotator&lt;/code&gt;, &lt;code&gt;CropAnnotator&lt;/code&gt; and &lt;code&gt;IconAnnotator&lt;/code&gt; all handled single-channel scenes. &lt;code&gt;ComparisonAnnotator&lt;/code&gt; raised a &lt;code&gt;ValueError&lt;/code&gt; on all three single-channel inputs.&lt;/p&gt;

&lt;p&gt;Before writing any code, I checked that nobody else was on it. A search for "ComparisonAnnotator" found only merged PRs, none about grayscale, and no open PR or issue. Mohammad Hijjawi, who wrote the grayscale fixes in #2755 and #2788, had listed the annotators that still failed and left them for separate PRs: &lt;code&gt;BackgroundOverlayAnnotator&lt;/code&gt;, &lt;code&gt;HeatMapAnnotator&lt;/code&gt; and &lt;code&gt;RichLabelAnnotator&lt;/code&gt;. &lt;code&gt;ComparisonAnnotator&lt;/code&gt; was on neither list, so I took it and left those three alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix, in one idea
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa4ayyt0h6on2c2qw38ia.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa4ayyt0h6on2c2qw38ia.png" alt="Still from the third state: the _color_for_canvas block sits between the colour and the grids, the b chip sits in the masked grayscale pixel's single slot, and the g and r chips wait, dimmed, at the block." width="800" height="601"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 2. After #2803 every colour passes through &lt;code&gt;_color_for_canvas&lt;/code&gt;. On a three-channel scene the tuple goes through unchanged. On a single-channel scene only the blue value reaches the pixel.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The repo already had the answer in a helper. &lt;code&gt;_color_for_canvas&lt;/code&gt;, added in #2755, keeps only the first value of the BGR tuple when the canvas is grayscale. That value is the blue component, and it is what &lt;code&gt;cv2.rectangle&lt;/code&gt; and &lt;code&gt;cv2.fillPoly&lt;/code&gt; write on single-channel images. The fix routes all three of the annotator's colours through it. On a three-channel scene the helper returns the tuple unchanged, so BGR frames take exactly the path they took before. The change to &lt;code&gt;core.py&lt;/code&gt; is 8 lines added and 4 removed, plus a docstring update and a changelog line.&lt;/p&gt;

&lt;p&gt;Why blue instead of a luminance mix, which would look more natural? Because the rest of the repo already uses blue, so a frame annotated with &lt;code&gt;BoxAnnotator&lt;/code&gt;, &lt;code&gt;MaskAnnotator&lt;/code&gt; and &lt;code&gt;ComparisonAnnotator&lt;/code&gt; gets one gray value per colour. The convention has a cost, and I put it in the PR description: the default red and green both have a blue component of 0, so on a grayscale frame both "only" regions come out the same gray. If you need to tell them apart, pass colours with different blue components. Changing the convention itself would be a decision for the whole library.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing layouts against each other
&lt;/h2&gt;

&lt;p&gt;Hard-coded pixel values would tie the test to today's colours and blend math. So the new test class annotates a BGR scene and its grayscale copy with the same detections, then asserts that the grayscale result equals the first channel of the BGR result. That one assertion runs for box, mask and oriented-box detections on &lt;code&gt;(H, W)&lt;/code&gt; and &lt;code&gt;(H, W, 1)&lt;/code&gt; scenes, which makes six cases. A seventh checks the legend labels the same way. An eighth checks that a Pillow &lt;code&gt;"L"&lt;/code&gt; image comes back as an &lt;code&gt;"L"&lt;/code&gt; image with the expected pixels.&lt;/p&gt;

&lt;p&gt;All 8 fail on &lt;code&gt;develop&lt;/code&gt; and pass with the change, on the NumPy fallback and on &lt;code&gt;opencv-python&lt;/code&gt; 5.0.0. I ran both backends because OpenCV is optional now and CI tests both. On the fallback, the full suite went from 5,931 passed to 5,939 passed. One unrelated test timed out on my laptop, on both branches.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the fix changed for users
&lt;/h2&gt;

&lt;p&gt;If your frames come from an MRI scanner, a thermal camera or a depth sensor, you can now compare two sets of detections on them without converting to BGR first. &lt;code&gt;ComparisonAnnotator&lt;/code&gt; accepts &lt;code&gt;(H, W)&lt;/code&gt; arrays, &lt;code&gt;(H, W, 1)&lt;/code&gt; arrays and Pillow &lt;code&gt;"L"&lt;/code&gt; images, and it hands back the same layout it received. Its gray values match the boxes and masks that other annotators draw on the same frame. BGR users see no change. The fix is on &lt;code&gt;develop&lt;/code&gt;, with a changelog entry under Unreleased.&lt;/p&gt;

&lt;p&gt;BGRA frames are still an open question. They need a maintainer decision: strip the alpha channel and reattach it in the input decorator, or reject BGRA outright. I plan to raise it as an issue.&lt;/p&gt;

&lt;h2&gt;
  
  
  Thank you, Borda and the supervision maintainers
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/Borda" rel="noopener noreferrer"&gt;Borda&lt;/a&gt; (Jirka Borovec) approved and merged #2803 on October 26, the day after I opened it. Thank you. For a first-time contributor the path was clear: a CLA bot, a maintainer approving the first CI runs, and an &lt;code&gt;AGENTS.md&lt;/code&gt; that asks for a changelog line linking the PR number. That last rule meant one small follow-up commit once #2803 existed.&lt;/p&gt;

&lt;p&gt;Thanks also to the maintainers who shipped six supervision releases between October 4 and October 24, and to Mohammad Hijjawi, whose #2755 gave me the helper and whose list of remaining failures told me which fixes were already spoken for.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I took away
&lt;/h2&gt;

&lt;p&gt;When a library accepts several image layouts, test the layouts against each other. The grayscale result should equal one channel of the BGR result, and that single assertion stays true for any colour, opacity or geometry. A hard-coded pixel test covers one case and breaks the day a default colour changes.&lt;/p&gt;

&lt;p&gt;The parity test also makes review cheap. A reviewer can read one assertion and know what all eight cases prove, which matters when a maintainer is reading dozens of outside PRs a month. It is the first test I will write the next time I touch an image pipeline, including my own.&lt;/p&gt;

&lt;p&gt;I'm open to ML engineering roles, computer vision included. You can find my work at &lt;a href="https://shreyaboyane.dev" rel="noopener noreferrer"&gt;shreyaboyane.dev&lt;/a&gt;, on GitHub as &lt;a href="https://github.com/Sonica-B" rel="noopener noreferrer"&gt;Sonica-B&lt;/a&gt;, and on &lt;a href="https://www.linkedin.com/in/shreya-boyane/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>python</category>
      <category>computervision</category>
      <category>opencv</category>
      <category>opensource</category>
    </item>
    <item>
      <title>One extra field turned an A2A error into a TypeError. Fixed in a2a-sdk 1.3.0</title>
      <dc:creator>Shreya Boyane</dc:creator>
      <pubDate>Thu, 29 Oct 2026 01:15:53 +0000</pubDate>
      <link>https://dev.to/sonicab/one-extra-field-turned-an-a2a-error-into-a-typeerror-fixed-in-a2a-sdk-130-3pb0</link>
      <guid>https://dev.to/sonicab/one-extra-field-turned-an-a2a-error-into-a-typeerror-fixed-in-a2a-sdk-130-3pb0</guid>
      <description>&lt;p&gt;On October 27, 2026, SAP announced general availability of &lt;a href="https://architecture.learning.sap.com/news/2026/10/27/a2a-agent-gateway-interoperability" rel="noopener noreferrer"&gt;inbound Agent2Agent (A2A) interoperability&lt;/a&gt;, so third-party agents can now invoke SAP Agents through its Agent Gateway. Every launch like this means more A2A clients calling agents their authors do not control. Two days before SAP's announcement, I had opened a pull request for that exact situation in the A2A Python SDK. An agent could send a valid A2A error, and the calling code would receive a &lt;code&gt;TypeError&lt;/code&gt;. My fix shipped the same day as SAP's news, in &lt;a href="https://github.com/a2aproject/a2a-python/releases/tag/v1.3.0" rel="noopener noreferrer"&gt;a2a-sdk 1.3.0&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;A2A is an open protocol that lets AI agents discover each other and exchange messages and tasks over JSON-RPC, REST or gRPC. It &lt;a href="https://a2a-protocol.org/latest/blog/2026/08/27/a-new-chapter-for-a2a-joining-the-agentic-ai-foundation/" rel="noopener noreferrer"&gt;joined the Agentic AI Foundation&lt;/a&gt; in August 2026, and &lt;code&gt;a2a-sdk&lt;/code&gt; is its official Python SDK.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;p&gt;When an agent sent back an error, the SDK handed the reply to a third-party library before looking at it itself. That library is strict about which fields an error may carry, so one extra field, the kind the A2A spec says to ignore, made it throw a raw &lt;code&gt;TypeError&lt;/code&gt;. The SDK's own translation of error codes into named exceptions never got a turn. Version 1.3.0 reads the reply itself first, at one place, and the library only builds requests now.&lt;/p&gt;

&lt;h2&gt;
  
  
  The concept, animated
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy80m8zj8recczhaz8cxb.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy80m8zj8recczhaz8cxb.gif" alt="Animated figure. The reply's path through JsonRpcTransport as labelled blocks: agent reply, the json-rpc package's JSONRPC20Response(**body), the SDK's _create_jsonrpc_error(), your code with its except TaskNotFoundError branch, and three bins for TaskNotFoundError, A2AClientError and result. A token labelled {&amp;quot;code&amp;quot;: -32001, &amp;quot;reason&amp;quot;: &amp;quot;x&amp;quot;} drops into a raw TypeError tray at the json-rpc block. In 1.3.0 that block lifts out, _check_jsonrpc_response() takes its place, and the token reaches the TaskNotFoundError bin; a body that is a list reaches A2AClientError." width="800" height="602"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 1. The blocks are the real call chain in &lt;code&gt;client/transports/jsonrpc.py&lt;/code&gt;, in order. The token is the reply body. The three states: an error object with one extra field before 1.3.0, the same reply in 1.3.0, and a body that is not an object in 1.3.0.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A client talks to agents it does not control, so odd replies are normal. The SDK's promise about them is clear: a failure reaches your code either as &lt;code&gt;A2AClientError&lt;/code&gt; or as a typed A2A error such as &lt;code&gt;TaskNotFoundError&lt;/code&gt;. The &lt;a href="https://a2a-protocol.org/latest/specification/" rel="noopener noreferrer"&gt;A2A spec&lt;/a&gt; (section 5.7) adds that implementations should ignore fields they do not recognise.&lt;/p&gt;

&lt;p&gt;Now follow the token in the first state. It carries the error code &lt;code&gt;-32001&lt;/code&gt;, which means &lt;code&gt;TaskNotFoundError&lt;/code&gt;, plus one extra field, &lt;code&gt;reason&lt;/code&gt;, that the spec says to ignore. The first block it reaches is &lt;code&gt;JSONRPC20Response(**body)&lt;/code&gt; from the &lt;code&gt;json-rpc&lt;/code&gt; package. It validates the envelope before the SDK sees anything, and it builds the error with a constructor that accepts only &lt;code&gt;code&lt;/code&gt;, &lt;code&gt;message&lt;/code&gt; and &lt;code&gt;data&lt;/code&gt;. Any other member is, to that library, a programming mistake. So the token drops into the tray labelled raw &lt;code&gt;TypeError&lt;/code&gt;: &lt;code&gt;JSONRPCError.__init__() got an unexpected keyword argument 'reason'&lt;/code&gt;. The SDK's &lt;code&gt;_create_jsonrpc_error()&lt;/code&gt; block never receives it, and your &lt;code&gt;except TaskNotFoundError:&lt;/code&gt; branch never runs.&lt;/p&gt;

&lt;p&gt;The library was correct by its own rules. It simply ran first, so its rules won and the SDK's contract lost.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I found seven of these
&lt;/h2&gt;

&lt;p&gt;I was exploring the client with one question: what reaches my code when an agent answers wrong? No issue existed, so I played the misbehaving agent myself, wrote seven bad response bodies by hand, and fed them to the client through a mock transport. All seven came back as raw Python exceptions.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What the agent sent&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;a list, or a string (say, from a proxy)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;TypeError&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;A2AClientError&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;neither &lt;code&gt;result&lt;/code&gt; nor &lt;code&gt;error&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ValueError&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;A2AClientError&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;"error": "boom"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;TypeError&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;A2AClientError&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;error object with an extra member&lt;/td&gt;
&lt;td&gt;&lt;code&gt;TypeError&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;TaskNotFoundError&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;error object without &lt;code&gt;message&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ValueError&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;TaskNotFoundError&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SSE &lt;code&gt;data:&lt;/code&gt; that is not JSON&lt;/td&gt;
&lt;td&gt;&lt;code&gt;JSONDecodeError&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;A2AClientError&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;event: error&lt;/code&gt; with an extra member&lt;/td&gt;
&lt;td&gt;&lt;code&gt;TypeError&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;TaskNotFoundError&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Following one reply through the code explained the whole table. Nine request methods handed the body straight to the &lt;code&gt;json-rpc&lt;/code&gt; validator, and streaming used the same validator on each event. The SDK's own mapping from error codes to exceptions even had a fallback for a missing &lt;code&gt;message&lt;/code&gt;, but it could never execute, because the library failed first. Streaming had a second twist: the JSON parse for each event ran in a place where its exception escaped the handler that was supposed to catch it, so a broken &lt;code&gt;data:&lt;/code&gt; line leaked a &lt;code&gt;JSONDecodeError&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix, in one idea
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe33w2nkex8ghtsufmznw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe33w2nkex8ghtsufmznw.png" alt="Still from the third state: the json-rpc block is lifted out and faded, the SDK's _check_jsonrpc_response() block stands in its place, and a token labelled &amp;quot;a list, not an object&amp;quot; sits in the A2AClientError bin." width="800" height="601"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 2. In 1.3.0 the &lt;code&gt;json-rpc&lt;/code&gt; validator is out of the chain. The SDK reads the envelope itself at one choke point, so an error object becomes the typed A2A error whatever extra fields it carries, and a body that is not even an object becomes &lt;code&gt;A2AClientError&lt;/code&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Check the wire envelope yourself, once, and hand libraries only what they can handle. Every reply already passes through one sending function or the streaming loop, so I put one check there. It asks three plain questions in order: is the body an object; if it has an &lt;code&gt;error&lt;/code&gt;, is that an object, and if so, map it to the A2A exception by its code; otherwise, is there a &lt;code&gt;result&lt;/code&gt;. Anything that fails those questions becomes &lt;code&gt;A2AClientError&lt;/code&gt; with a message that says what was wrong. The streaming loop turns a &lt;code&gt;JSONDecodeError&lt;/code&gt; into &lt;code&gt;A2AClientError&lt;/code&gt; the same way. A falsy &lt;code&gt;error&lt;/code&gt; next to a &lt;code&gt;result&lt;/code&gt; still counts as success, as before. The client no longer builds &lt;code&gt;JSONRPC20Response&lt;/code&gt; objects at all; requests still use &lt;code&gt;JSONRPC20Request&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Wrapping each of the nine calls in its own try/except would have turned &lt;code&gt;TaskNotFoundError&lt;/code&gt; into a generic error and copied the same code nine times. One choke point keeps the typed exceptions.&lt;/p&gt;

&lt;p&gt;The tests live in a new class, &lt;code&gt;TestMalformedJsonRpcResponses&lt;/code&gt;, with 11 cases. Ten fail on &lt;code&gt;main&lt;/code&gt; with the exceptions in the table and pass with the change. The eleventh guards that &lt;code&gt;delete_task_push_notification_config&lt;/code&gt; still raises on an error response. The existing tests pass unchanged. I ran everything on Windows 11 with Python 3.10.19: &lt;code&gt;lint.sh&lt;/code&gt; passed, the transport tests gave 51 passed, and the wider run gave 2213 passed at 93% total coverage. Three failures and four setup errors fail the same way on &lt;code&gt;main&lt;/code&gt;, and two integration suites do not run on Windows, so I left those to Linux CI. All 17 checks passed there, and the coverage bot showed &lt;code&gt;client/transports/jsonrpc.py&lt;/code&gt; going from 92.66% to 95.51%.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed for people using a2a-sdk
&lt;/h2&gt;

&lt;p&gt;If your client calls agents you do not control, version 1.3.0 behaves differently in three ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An agent that adds its own field to an error object, or leaves out &lt;code&gt;message&lt;/code&gt;, now raises the mapped A2A error, so your &lt;code&gt;except TaskNotFoundError:&lt;/code&gt; branch runs.&lt;/li&gt;
&lt;li&gt;A malformed body, such as a list, a string, a missing &lt;code&gt;result&lt;/code&gt; or a non-object &lt;code&gt;error&lt;/code&gt;, raises &lt;code&gt;A2AClientError&lt;/code&gt;, so one &lt;code&gt;except&lt;/code&gt; clause covers all of them.&lt;/li&gt;
&lt;li&gt;A stream with a broken &lt;code&gt;data:&lt;/code&gt; line raises &lt;code&gt;A2AClientError&lt;/code&gt; where it used to leak a &lt;code&gt;JSONDecodeError&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One behaviour change is worth knowing. A body with &lt;code&gt;"error": null&lt;/code&gt; and no &lt;code&gt;result&lt;/code&gt; used to parse as an empty object and now raises &lt;code&gt;A2AClientError&lt;/code&gt;. That body breaks JSON-RPC 2.0 anyway. Upgrading is &lt;code&gt;pip install -U a2a-sdk&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The v0.3 compatibility transport still uses the same validator-first pattern on &lt;code&gt;main&lt;/code&gt;. I kept this PR to the 1.0 client and offered a &lt;code&gt;fix(compat)&lt;/code&gt; follow-up in the PR body.&lt;/p&gt;

&lt;h2&gt;
  
  
  A thank-you to Iwaniukooo11 and the A2A maintainers
&lt;/h2&gt;

&lt;p&gt;I opened &lt;a href="https://github.com/a2aproject/a2a-python/pull/1375" rel="noopener noreferrer"&gt;PR #1375&lt;/a&gt; on October 25 with no linked issue, just the table and the tests. On October 26, &lt;a href="https://github.com/Iwaniukooo11" rel="noopener noreferrer"&gt;Mateusz Iwaniuk (@Iwaniukooo11)&lt;/a&gt; brought the branch up to date with &lt;code&gt;main&lt;/code&gt;, approved it and merged it. Thank you. Getting a first PR to a repo reviewed and merged the day after it opens is a generous welcome.&lt;/p&gt;

&lt;p&gt;Thanks also to the A2A maintainers for keeping the door open to outside contributors. Of the 40 merges before mine, 29 came from non-members, with a median of 3.4 days from open to merge. Release 1.3.0 carries fixes from at least three outside contributors, mine among them.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I took away
&lt;/h2&gt;

&lt;p&gt;Where a third-party validator sits in your call chain decides which exceptions your callers see. The stack trace ended inside &lt;code&gt;json-rpc&lt;/code&gt;, but the decision that mattered was where the SDK called it.&lt;/p&gt;

&lt;p&gt;When you own an error contract, check the wire envelope yourself at one choke point and hand libraries only what they can handle. As more vendors open their agents to outside callers, the exception a client can catch by name is part of the interface, and that boundary is the kind of problem I like to work on.&lt;/p&gt;

&lt;p&gt;I'm Shreya Boyane, an ML engineer who builds agent systems, and I'm open to ML and AI engineering roles. You can find my work on &lt;a href="https://github.com/Sonica-B" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;, more about me at &lt;a href="https://shreyaboyane.dev" rel="noopener noreferrer"&gt;shreyaboyane.dev&lt;/a&gt;, and I'm happy to connect on &lt;a href="https://www.linkedin.com/in/shreya-boyane/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>python</category>
      <category>ai</category>
      <category>agents</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Isaac Lab 3.0 is GA. One missing close() locked log files on Windows</title>
      <dc:creator>Shreya Boyane</dc:creator>
      <pubDate>Thu, 29 Oct 2026 01:15:37 +0000</pubDate>
      <link>https://dev.to/sonicab/isaac-lab-30-is-ga-one-missing-close-locked-log-files-on-windows-41oo</link>
      <guid>https://dev.to/sonicab/isaac-lab-30-is-ga-one-missing-close-locked-log-files-on-windows-41oo</guid>
      <description>&lt;p&gt;&lt;strong&gt;GTC Berlin, October 21, 2026.&lt;/strong&gt; NVIDIA &lt;a href="https://nvidianews.nvidia.com/news/nvidia-advances-isaac-platform-for-general-purpose-robotics-with-new-reference-humanoid-frameworks-simulation-pipelines-and-data" rel="noopener noreferrer"&gt;announced that Isaac Lab 3.0 is generally available&lt;/a&gt;, describing it as a framework for training robot policies "from desktop to data center." Three days later I was at the desktop end of that phrase. My desktop was a Windows 11 laptop, and two of Isaac Lab's logging tests were failing on it with an error that had nothing to do with robots.&lt;/p&gt;

&lt;p&gt;Isaac Lab is NVIDIA's open-source framework for training robot policies in GPU-accelerated simulation. Version 3.0 supports the Newton and PhysX physics engines, and the &lt;a href="https://github.com/isaac-sim/IsaacLab/releases/tag/v3.0.0" rel="noopener noreferrer"&gt;v3.0.0 release notes&lt;/a&gt; make Isaac Sim optional for many workflows. This post is about a side quest on that laptop and the one-line fix it produced, now merged as &lt;a href="https://github.com/isaac-sim/IsaacLab/pull/8809" rel="noopener noreferrer"&gt;PR #8809&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;p&gt;A logging handler is the thing that keeps a log file open and writes lines into it. When Isaac Lab reconfigured its logging, it unhooked the old handlers from the logger but never closed them, so the old log file stayed open in the background. Linux lets you delete a file that is still open, so nobody noticed. Windows refuses, so two tests failed on my machine. The fix is one line: close what you unhook.&lt;/p&gt;

&lt;h2&gt;
  
  
  The concept, animated
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvf1pzvxjqi0aj5nn9gyp.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvf1pzvxjqi0aj5nn9gyp.gif" alt="Animated figure. The root logger drawn as a board holding a StreamHandler and a FileHandler; each FileHandler is wired to its log file, whose lamp reads &amp;quot;stream open&amp;quot;. removeHandler lifts the old handler off the board with its wire still attached and os.remove reports PermissionError [WinError 32]; after handler.close the wire is gone, the lamp reads &amp;quot;closed&amp;quot; and os.remove reports &amp;quot;deleted&amp;quot;." width="800" height="602"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 1. Every part carries its real name. The board is the root logger's &lt;code&gt;handlers&lt;/code&gt; list. The wire from a &lt;code&gt;FileHandler&lt;/code&gt; to its &lt;code&gt;.log&lt;/code&gt; file is the handler's &lt;code&gt;.stream&lt;/code&gt;, and the lamp reads "stream open" for as long as that wire exists. The three states are the three calls, in order: &lt;code&gt;configure_logging(file=...)&lt;/code&gt;, &lt;code&gt;removeHandler(old)&lt;/code&gt;, and the fix, &lt;code&gt;handler.close()&lt;/code&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Watch the second state. &lt;code&gt;removeHandler()&lt;/code&gt; does exactly what its name says: the old handler leaves the list, so the logger will never call it again. Nothing about that call touches the wire. The old &lt;code&gt;FileHandler&lt;/code&gt; still owns an open stream to &lt;code&gt;isaaclab_1.log&lt;/code&gt;, and that is what Windows checks when &lt;code&gt;os.remove()&lt;/code&gt; arrives: something holds the file, so the delete fails with &lt;code&gt;PermissionError: [WinError 32]&lt;/code&gt;. Linux lets a delete go through on an open file, which is why the leak was invisible everywhere except Windows.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpzubo4mamrpj2wbp76q8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpzubo4mamrpj2wbp76q8.png" alt="Still from the third state: the old handler's lamp reads &amp;quot;closed&amp;quot;, its wire is gone, and os.remove reports &amp;quot;deleted&amp;quot; while the new FileHandler stays wired to isaaclab_2.log." width="800" height="601"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 2. The third state: &lt;code&gt;handler.close()&lt;/code&gt; ends the stream, the lamp goes out, and the same &lt;code&gt;os.remove()&lt;/code&gt; succeeds. The new handler and its file are untouched.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How I got there
&lt;/h2&gt;

&lt;p&gt;I came to Isaac Lab for a different bug. Issue &lt;a href="https://github.com/isaac-sim/IsaacLab/issues/8534" rel="noopener noreferrer"&gt;#8534&lt;/a&gt; reported that a contrib humanoid task passed wxyz quaternions into xyzw APIs, so the humanoid trained upside down. I wanted that one. Another contributor had already claimed it and opened a PR.&lt;/p&gt;

&lt;p&gt;So I took the fallback on my list. Isaac Lab's docs list Windows 11 as a supported platform, but when I checked in mid-October, no CI job ran its tests on Windows and the &lt;code&gt;windows_ci&lt;/code&gt; test marker was applied to zero test files. I work on Windows, so that gap was mine to poke at.&lt;/p&gt;

&lt;p&gt;My setup was plain: Windows 11, Python 3.12.12 and CPU-only PyTorch 2.11.0, the same Python and PyTorch versions 3.0 targets, minus CUDA. I did not install Isaac Sim, so the real work was sorting failures by cause.&lt;/p&gt;

&lt;p&gt;I ran every unit test in four test folders on &lt;code&gt;main&lt;/code&gt; and got 913 passed, 72 failed, 125 skipped and 5 collection errors. Most of the failures were my environment talking: a missing &lt;code&gt;omni&lt;/code&gt; module (that is Isaac Sim), a CPU-only torch build, no &lt;code&gt;torchvision&lt;/code&gt;. Others were Windows symlink permission errors, because Developer Mode was off, and a few tests with POSIX assumptions about Isaac Sim build trees. I set those aside, since judging them fairly needs Developer Mode or a simulator build.&lt;/p&gt;

&lt;p&gt;Two failures in &lt;code&gt;source/isaaclab/test/utils/test_logger.py&lt;/code&gt; did not fit any of those buckets.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why only Windows complained
&lt;/h2&gt;

&lt;p&gt;Both tests failed with &lt;code&gt;PermissionError: [WinError 32]&lt;/code&gt;. One failed while its temporary directory was being cleaned up, because the log file inside could not be removed. The other failed on its own &lt;code&gt;os.remove&lt;/code&gt; call. These tests are plain Python and run without a simulator or a GPU, so my setup could not explain them.&lt;/p&gt;

&lt;p&gt;WinError 32 is Windows saying that some process still has the file open. The only thing that opens that log file is the file handler installed by &lt;code&gt;configure_logging&lt;/code&gt;, so I read what that function does with the handlers it replaces. Then I wrote a short probe: configure logging with a file, reconfigure without one, and look at the old handler. Its stream was still an open file. The log file had outlived the configuration that created it.&lt;/p&gt;

&lt;p&gt;That is the whole mechanism in figure 1. &lt;code&gt;removeHandler()&lt;/code&gt; detaches a handler from a logger and leaves it open. A detached file handler keeps its file until someone calls &lt;code&gt;close()&lt;/code&gt;, and nobody did. On Linux the leak is invisible, because an open file can still be deleted. On Windows it is a locked file.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix, in one idea
&lt;/h2&gt;

&lt;p&gt;Close what you remove. The loop that unhooks each old handler now also closes it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;handler&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;root_logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;handlers&lt;/span&gt;&lt;span class="p"&gt;[:]:&lt;/span&gt;
    &lt;span class="n"&gt;root_logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the entire code change. Python's own &lt;a href="https://docs.python.org/3/library/logging.html#logging.basicConfig" rel="noopener noreferrer"&gt;&lt;code&gt;logging.basicConfig(force=True)&lt;/code&gt;&lt;/a&gt; already removes and closes existing handlers the same way, so the fix brings Isaac Lab in line with the standard library, and the docstring now says so.&lt;/p&gt;

&lt;p&gt;The tests changed too. Both now reconfigure logging before cleanup, which releases the file, and one of them asserts that the replaced handler's stream is gone. That assertion checks the handle, not the Windows error, so a Linux CI run can catch the regression too. I only ran it on Windows, where it failed against the old &lt;code&gt;logger.py&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed for Isaac Lab users
&lt;/h2&gt;

&lt;p&gt;Any process that calls &lt;code&gt;configure_logging&lt;/code&gt; more than once, such as a test session, now releases the previous log file right away instead of holding it until exit. On Windows, those files can be deleted again.&lt;/p&gt;

&lt;p&gt;Against &lt;code&gt;main&lt;/code&gt; at commit 4a281f3, with the PR's tests, &lt;code&gt;test_logger.py&lt;/code&gt; had 2 failures and 12 passes. With the fix, all 14 pass. Across the &lt;code&gt;source/isaaclab/test/utils&lt;/code&gt; unit sweep, the same 36 environment failures appeared with and without the change, so the fix moved nothing else.&lt;/p&gt;

&lt;p&gt;I flagged one trade-off in the PR. The function now closes every handler it removes, including ones it did not create, such as Kit log bridges or pytest's &lt;code&gt;--log-file&lt;/code&gt; handler. The old code had already detached them. Closing a plain stream handler leaves its underlying stream open, so only file-backed handlers behave differently. A maintainer could reasonably prefer closing only the handlers the function made, and I wanted that choice in front of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  How a first PR got merged in 28 hours
&lt;/h2&gt;

&lt;p&gt;The contributing guide asks for an issue first, so I filed &lt;a href="https://github.com/isaac-sim/IsaacLab/issues/8808" rel="noopener noreferrer"&gt;#8808&lt;/a&gt; and then opened &lt;a href="https://github.com/isaac-sim/IsaacLab/pull/8809" rel="noopener noreferrer"&gt;#8809&lt;/a&gt; with "Fixes #8808". The repo wanted a changelog fragment instead of a &lt;code&gt;CHANGELOG.rst&lt;/code&gt; edit, plus a line in &lt;code&gt;CONTRIBUTORS.md&lt;/code&gt;. Isaac Lab's &lt;code&gt;main&lt;/code&gt; moves fast, with 587 PRs merged in the 30 days before I started, so I rebased from 4a281f3 onto 259e1a8 before pushing. Both files I touched were identical at the two commits, so my Windows baseline still held.&lt;/p&gt;

&lt;p&gt;Three minutes after I opened the PR, the repo's review bot asked for two changes. First, make the cleanup in the file-handler test exception-safe: if an assertion failed halfway, the file would stay open and the cleanup would raise its own &lt;code&gt;PermissionError&lt;/code&gt;, hiding the real failure. I moved the reconfiguring call into a &lt;code&gt;finally&lt;/code&gt; block. Second, name the tested baseline revision and give explicit failure counts. I rewrote the Testing section with the numbers above.&lt;/p&gt;

&lt;p&gt;Then the maintainers took over. AntoineRichard commented &lt;code&gt;run-ci&lt;/code&gt; to start the Docker and GPU test jobs, approved the PR, and merged it on October 26, about 28 hours after I opened it. maxkra15 gave the second approval.&lt;/p&gt;

&lt;h2&gt;
  
  
  Thank you
&lt;/h2&gt;

&lt;p&gt;Thank you to &lt;a href="https://github.com/AntoineRichard" rel="noopener noreferrer"&gt;AntoineRichard&lt;/a&gt; for the review, the CI run and the merge, and to &lt;a href="https://github.com/maxkra15" rel="noopener noreferrer"&gt;maxkra15&lt;/a&gt; for the second review. Thanks also to the Isaac Lab maintainers for a contribution path I could follow end to end from the docs, with an issue template, a PR checklist, changelog fragments, and a review bot that caught my gaps before a human had to.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I took away
&lt;/h2&gt;

&lt;p&gt;Windows is where this bug showed up, but the open file exists on every OS. So the regression test checks the handle instead of the Windows error, and a failure that only Windows could see became a check any CI can run. When a bug appears on one platform, I look for the platform-independent cause and pin the test to that.&lt;/p&gt;

&lt;p&gt;The second lesson is about triage. Two of the 72 failures turned into a fix. Getting there meant sorting the other 70 by cause first and writing down why I left each group alone.&lt;/p&gt;

&lt;p&gt;Code that removes something holding a file should close it too, because Linux hides the leak that Windows reports. Isaac Lab 3.0 promises desktop to data center, and running its tests on a platform its CI did not cover was a cheap way to find a real bug.&lt;/p&gt;

&lt;p&gt;I'm open to roles in robot learning, ML systems and MLOps. You can find my work at &lt;a href="https://shreyaboyane.dev" rel="noopener noreferrer"&gt;shreyaboyane.dev&lt;/a&gt;, my code on GitHub as &lt;a href="https://github.com/Sonica-B" rel="noopener noreferrer"&gt;Sonica-B&lt;/a&gt;, and me on &lt;a href="https://www.linkedin.com/in/shreya-boyane/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>python</category>
      <category>robotics</category>
      <category>opensource</category>
      <category>windows</category>
    </item>
    <item>
      <title>TorchTitan's October update is out. I fixed 2 bugs in its quick-start script</title>
      <dc:creator>Shreya Boyane</dc:creator>
      <pubDate>Thu, 29 Oct 2026 01:15:20 +0000</pubDate>
      <link>https://dev.to/sonicab/torchtitans-october-update-is-out-i-fixed-2-bugs-in-its-quick-start-script-2nbo</link>
      <guid>https://dev.to/sonicab/torchtitans-october-update-is-out-i-fixed-2-bugs-in-its-quick-start-script-2nbo</guid>
      <description>&lt;p&gt;&lt;strong&gt;TorchTitan Update, October 21, 2026.&lt;/strong&gt; During PyTorch Conference North America in San Jose, the PyTorch team published &lt;a href="https://pytorch.org/blog/torchtitan-update-new-sota-models-performance-at-scale-titanrl/" rel="noopener noreferrer"&gt;TorchTitan Update: New SOTA Models, Performance at Scale, TitanRL&lt;/a&gt;. It walks through new support for DeepSeek-V4, Kimi K3 and Qwen3.5, introduces TitanRL for reinforcement learning, and reports DeepSeek-V3 671B training at 2,694,493 tokens per second on 512 GB300 GPUs. The post ends by telling you to clone the repo and follow the quick start.&lt;/p&gt;

&lt;p&gt;Step 3 of that quick start downloads a tokenizer with &lt;code&gt;scripts/download_hf_assets.py&lt;/code&gt;. I found two bugs in that script, and a test that passed the whole time had been hiding one of them. I opened &lt;a href="https://github.com/pytorch/torchtitan/pull/5621" rel="noopener noreferrer"&gt;PR #5621&lt;/a&gt; on October 25, and it merged on October 27.&lt;/p&gt;

&lt;p&gt;The update covers the scale end of torchtitan. My side quest happened at the other end, in the script a new user runs before training a real model.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;p&gt;The download function was handed a list of what to fetch and wrote into that list instead of its own copy, so the caller's list came back one entry longer. Call it twice with the same list and the second call rejects the entry the first call added. The same function also raised an error built from three separate strings, so its message printed as a tuple with literal &lt;code&gt;\n&lt;/code&gt; in it, and the test that checked the message could not tell. The fix copies the list and builds one message.&lt;/p&gt;

&lt;h2&gt;
  
  
  The concept, animated
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa0tqbkooj5it0t6qkaoz.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa0tqbkooj5it0t6qkaoz.gif" alt="Animated figure. Two frames on a plate, the caller and download_hf_assets(). The caller's asset_types list is a tray with one chip, &amp;quot;tokenizer&amp;quot;; a wire from the function's parameter asset_types to that tray reads &amp;quot;same object&amp;quot;. The function's .append(&amp;quot;additional_patterns&amp;quot;) drops a second chip into the caller's tray; on the second call the ASSET_PATTERNS table (tokenizer, safetensors, index, config) rejects it with ValueError: Unknown asset type additional_patterns. After #5621 a copy, list(asset_types), appears inside the function, the wire points to it, and the caller's tray keeps one chip." width="800" height="602"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 1. Trays are list objects and chips are their entries; the wire shows which object the parameter name &lt;code&gt;asset_types&lt;/code&gt; refers to. The three states: the first call before the fix, the second call with the same list, and a call after the fix.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;download_hf_assets.py&lt;/code&gt; is the torchtitan script that downloads a model's tokenizer, config, safetensors index or weights from the Hugging Face Hub before training. You tell it which assets you want as a list, and when you also pass extra file patterns it adds a placeholder entry, &lt;code&gt;additional_patterns&lt;/code&gt;, to that list so the download loop picks them up.&lt;/p&gt;

&lt;p&gt;The catch is in the word "that". If you passed a string, the function wrapped it in a fresh list and the placeholder went into the fresh list. If you passed a list, the placeholder went into your list. Your list is now &lt;code&gt;['tokenizer', 'additional_patterns']&lt;/code&gt;. The first call still works, because the table of known assets is rebuilt every call and the placeholder is matched during that same call. The second call, with the same list, meets an entry nobody asked for, finds no pattern for it, and stops: &lt;code&gt;Unknown asset type additional_patterns&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Anyone looping over several repos with one shared list of asset types hit this on the second repo.&lt;/p&gt;

&lt;h2&gt;
  
  
  The second bug, and why a passing test hid it
&lt;/h2&gt;

&lt;p&gt;I found the function through a typo. I typed &lt;code&gt;--assets tokenizers&lt;/code&gt;, with a stray "s", and instead of a readable message I got this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ValueError: ('Unknown asset type tokenizers. Available uses: --assets tokenizer safetensors index config \n', "Or specify exact patterns to download. Example: --additional_patterns '*.safetensors' README.md '*.json' \n", 'Or use --all to download all files')
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is a tuple, with literal &lt;code&gt;\n&lt;/code&gt; escapes inside it. The error was raised with three comma-separated strings, and when an exception has more than one argument Python prints the whole tuple as its message.&lt;/p&gt;

&lt;p&gt;The existing test for this case used &lt;code&gt;assertIn&lt;/code&gt; on a piece of the message. A substring of a tuple's printed form is still a substring, so the test stayed green for as long as the bug existed. A short repro with the Hub calls mocked confirmed both bugs: it printed &lt;code&gt;args: 3&lt;/code&gt;, and after one call my list read &lt;code&gt;['tokenizer', 'additional_patterns']&lt;/code&gt;. Before writing any code, I searched torchtitan's issues and PRs for the function and both messages. Neither bug had been reported.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix, in one idea
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpuwkorzx09tzz9lrt5y9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpuwkorzx09tzz9lrt5y9.png" alt="Still from the third state: a second tray labelled list(asset_types), the copy, sits inside the function holding &amp;quot;tokenizer&amp;quot; and &amp;quot;additional_patterns&amp;quot;, the wire from the parameter points to it, and the caller's tray holds &amp;quot;tokenizer&amp;quot; alone." width="800" height="601"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 2. After #5621 the function copies the list first. The copy gets the placeholder entry and the download loop reads the copy; the caller's list is exactly as it was passed in.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Work on a copy. The function now copies the list it was given before adding anything to it, so the caller's list is untouched: &lt;code&gt;list(asset_types)&lt;/code&gt; instead of using the caller's object. And the error is now one message built from one string, so it prints as three readable lines. Two edits in one file, 35 lines added and 8 removed across the script and its tests.&lt;/p&gt;

&lt;p&gt;The tests changed with it. The existing one now asserts that the exception has exactly one argument before it checks the text, which is what would have caught the tuple. A new one calls the function twice with one list and &lt;code&gt;additional_patterns=["*.txt"]&lt;/code&gt;, then checks that the list is still &lt;code&gt;["tokenizer"]&lt;/code&gt; and that both calls downloaded the same files.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed for torchtitan users
&lt;/h2&gt;

&lt;p&gt;A mistyped asset type now prints three readable lines: &lt;code&gt;Unknown asset type tokenizers. Available uses: --assets tokenizer safetensors index config&lt;/code&gt;, then the &lt;code&gt;--additional_patterns&lt;/code&gt; example, then the &lt;code&gt;--all&lt;/code&gt; option. Scripts that loop over several repos with one shared &lt;code&gt;asset_types&lt;/code&gt; list now work past the first repo. Which files get downloaded does not change.&lt;/p&gt;

&lt;p&gt;The one behaviour change is for a caller that relied on the mutation. I searched the repo for callers of the function and found none that did. &lt;code&gt;test_tokenizer.py&lt;/code&gt; passes a string, which still gets wrapped in a list.&lt;/p&gt;

&lt;p&gt;I ran everything on Windows 11 with Python 3.11 and torch 2.16.0.dev20261024+cpu. &lt;code&gt;pre-commit&lt;/code&gt; passed flake8, ufmt, pydoclint, codespell and Pyrefly on both files. The lychee link checker needs &lt;code&gt;/bin/bash&lt;/code&gt;, so it could not start on Windows. I did not run anything on Linux or GPU, and the PR description says so.&lt;/p&gt;

&lt;h2&gt;
  
  
  Thank you, tianyu-l
&lt;/h2&gt;

&lt;p&gt;Thank you to &lt;a href="https://github.com/tianyu-l" rel="noopener noreferrer"&gt;@tianyu-l&lt;/a&gt; for approving PR #5621 and merging it on October 27, two days after I opened it. It was my first PR to the repo, so CI also needed a maintainer to approve the workflow runs before it could start.&lt;/p&gt;

&lt;p&gt;Thanks as well to the PyTorch distributed team at Meta who maintain torchtitan. The repo merged 392 PRs in the 30 days before I opened mine, and outside fixes still got a first review within 0 to 2 days.&lt;/p&gt;

&lt;p&gt;I modeled my PR description on &lt;a href="https://github.com/pytorch/torchtitan/pull/5040" rel="noopener noreferrer"&gt;#5040&lt;/a&gt;, an earlier outside fix to the same function by u7k4rs6. Because #5040 had just touched that function, I picked the smallest change over a rewrite.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I took away
&lt;/h2&gt;

&lt;p&gt;I don't count a test as protection until I have watched it fail on the bug.&lt;/p&gt;

&lt;p&gt;The old test stayed green for as long as the tuple message existed. So after writing the new assertions, I reverted only the script fix and kept the tests: 2 failed and 11 passed. With the fix back, all 13 passed, along with the 40 tests in &lt;code&gt;tests/unit_tests/cpu/scripts&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That revert is the cheapest check I know, and it changes what I can put in a PR. "Tests pass" becomes "these 2 tests fail on main and pass with the change," which a reviewer can verify without taking my word for it. Listing what I did not run does the same job for the parts I could not test.&lt;/p&gt;

&lt;p&gt;I'm an ML engineer with a DevOps background, and I'm open to ML engineering roles. My code is on GitHub as &lt;a href="https://github.com/Sonica-B" rel="noopener noreferrer"&gt;Sonica-B&lt;/a&gt;, my projects are at &lt;a href="https://shreyaboyane.dev" rel="noopener noreferrer"&gt;shreyaboyane.dev&lt;/a&gt;, and I'm on &lt;a href="https://www.linkedin.com/in/shreya-boyane/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>pytorch</category>
      <category>python</category>
      <category>opensource</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>GTC Berlin side quest: one import hid 7 NVIDIA agent tools, so I made it lazy</title>
      <dc:creator>Shreya Boyane</dc:creator>
      <pubDate>Wed, 28 Oct 2026 08:46:17 +0000</pubDate>
      <link>https://dev.to/sonicab/gtc-berlin-side-quest-one-import-hid-7-nvidia-agent-tools-so-i-made-it-lazy-inl</link>
      <guid>https://dev.to/sonicab/gtc-berlin-side-quest-one-import-hid-7-nvidia-agent-tools-so-i-made-it-lazy-inl</guid>
      <description>&lt;p&gt;At GTC Berlin on October 21, NVIDIA &lt;a href="https://blogs.nvidia.com/blog/enterprise-platforms-agent-toolkit/" rel="noopener noreferrer"&gt;expanded its Agent Toolkit&lt;/a&gt;: a new Agent Knowledge Foundation for grounding agents in company data, the new Kumo models, the OpenShell secure runtime, and tools like NeMo Relay. Two days later, NeMo Relay turned up somewhere else. The README of NVIDIA's open-source agent library, NeMo Agent Toolkit, gained a &lt;a href="https://github.com/NVIDIA/NeMo-Agent-Toolkit/pull/2437" rel="noopener noreferrer"&gt;deprecation notice&lt;/a&gt; that points users to NeMo Relay, NeMo Helix and NeMo Fabric, with end of life set for the end of 2026.&lt;/p&gt;

&lt;p&gt;That made me curious about the piece people still install today. So I built a fresh environment and looked at what a user actually gets. Seven of the library's 12 built-in tools never registered, and the error message pointed somewhere else entirely.&lt;/p&gt;

&lt;p&gt;The side quest became &lt;a href="https://github.com/NVIDIA/NeMo-Agent-Toolkit/pull/2474" rel="noopener noreferrer"&gt;PR #2474&lt;/a&gt;: +10/-3 lines in one source file plus a regression test, approved by an NVIDIA maintainer and merged about 19 hours after I opened it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;p&gt;The toolkit registers its built-in tools by importing a list of modules, one after another. One of those modules asked for a LangChain class the moment it was imported. LangChain is not part of the core install, so on a plain install that import failed, the list stopped there, and the seven tools after it were never registered. The only trace was one warning in the log. The fix moves that import to the one method that actually uses it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The concept, animated
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Faku92bdtkd1rw3eouj6p.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Faku92bdtkd1rw3eouj6p.gif" alt="Animated figure. nat/tool/register.py drawn as a rack of twelve labelled tool cards in import order, with an &amp;quot;import&amp;quot; pointer walking along it and lighting each card's lamp as that tool registers. The nvidia_rag card has a socket for langchain_core at its top; the langchain_core block at the front reads &amp;quot;not installed&amp;quot; on a plain install, and the pointer stops at nvidia_rag with ModuleNotFoundError, leaving seven lamps dark. After the fix the socket sits inside to_document() and all twelve lamps light." width="800" height="602"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 1. The rack is &lt;code&gt;nat/tool/register.py&lt;/code&gt;, one card per built-in tool, named as in the registry. The pointer is the import pass the &lt;code&gt;nat_tools&lt;/code&gt; entry point runs top to bottom; a lit lamp is a registered tool. The socket on &lt;code&gt;nvidia_rag.py&lt;/code&gt; marks where it needs &lt;code&gt;langchain_core&lt;/code&gt;: at import time before the fix, inside &lt;code&gt;to_document()&lt;/code&gt; after. The three states are a dev install, a plain install before #2474, and a plain install after it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Plugins reach &lt;code&gt;nat&lt;/code&gt;, the toolkit's package, through Python entry points. The &lt;code&gt;nat_tools&lt;/code&gt; entry point loads one file, &lt;code&gt;nat/tool/register.py&lt;/code&gt;, which is a list of imports, and every import registers the tool it names. That list is the rack in figure 1, and the import pass is the pointer.&lt;/p&gt;

&lt;p&gt;Line 21 of &lt;code&gt;nvidia_rag.py&lt;/code&gt; imported LangChain's &lt;code&gt;Document&lt;/code&gt; class at module level, which means it needed &lt;code&gt;langchain-core&lt;/code&gt; just to be imported, before anyone used the tool. That is the socket at the top of the &lt;code&gt;nvidia_rag&lt;/code&gt; card, and the block at the front is the package it asks for.&lt;/p&gt;

&lt;p&gt;On every contributor's machine that block reads "installed", because the test extras pull in &lt;code&gt;langchain-core&lt;/code&gt; through &lt;code&gt;langchain-community&lt;/code&gt;. On a plain install, a virtual environment holding only &lt;code&gt;nvidia-nat-core&lt;/code&gt; and &lt;code&gt;nvidia-nat-atif&lt;/code&gt; and their declared dependencies, it reads "not installed". The import raises &lt;code&gt;ModuleNotFoundError&lt;/code&gt;, the loader catches it for that entry point, writes one warning, and carries on with nothing. Five lamps lit, seven dark: &lt;code&gt;nvidia_rag&lt;/code&gt;, &lt;code&gt;nat_retriever&lt;/code&gt;, &lt;code&gt;current_request_attributes&lt;/code&gt;, &lt;code&gt;code_execution&lt;/code&gt;, &lt;code&gt;add_memory&lt;/code&gt;, &lt;code&gt;delete_memory&lt;/code&gt; and &lt;code&gt;get_memory&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a fresh install actually gives you
&lt;/h2&gt;

&lt;p&gt;I wrote a minimal workflow whose only setting was &lt;code&gt;_type: code_execution&lt;/code&gt;, one of the built-in tools, and ran &lt;code&gt;nat validate&lt;/code&gt; on it. It failed with this:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Input tag 'code_execution' found using discriminator() does not match any of the expected tags&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;So the validator claimed a built-in tool did not exist, and said nothing about imports or missing packages. If the registry did not know &lt;code&gt;code_execution&lt;/code&gt;, I wanted to see what it did know, so I listed every registered function after plugin discovery. Five of twelve. The log held the only real clue, a warning that is easy to scroll past: &lt;code&gt;Failed to import plugin 'nat_tools'&lt;/code&gt;, followed by &lt;code&gt;ModuleNotFoundError: No module named 'langchain_core'&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The same masking had hidden an earlier bug, &lt;a href="https://github.com/NVIDIA/NeMo-Agent-Toolkit/issues/2386" rel="noopener noreferrer"&gt;#2386&lt;/a&gt;, where &lt;code&gt;packaging&lt;/code&gt; was missing on a plain install (&lt;a href="https://github.com/NVIDIA/NeMo-Agent-Toolkit/pull/2392" rel="noopener noreferrer"&gt;#2392&lt;/a&gt; fixed it). The import was also in the released 1.9.0.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix, in one idea
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjw1xfqyxj95gwa07wko3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjw1xfqyxj95gwa07wko3.png" alt="Still from the third state: the socket on the nvidia_rag card now sits inside to_document(), the langchain_core block still reads &amp;quot;not installed&amp;quot;, and all twelve lamps are lit." width="800" height="601"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 2. After the fix the socket sits inside &lt;code&gt;to_document()&lt;/code&gt;: the package is needed when the RAG tool runs, not when its module is imported. A plain install registers all twelve tools with &lt;code&gt;langchain_core&lt;/code&gt; still absent.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Move the import to the moment it is needed. &lt;code&gt;Document&lt;/code&gt; is only used inside one method, the one that turns a RAG result into a LangChain document, so the import now lives inside that method:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;to_document&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Document&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langchain_core.documents&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Document&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Document&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page_content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The type hints stay under &lt;code&gt;TYPE_CHECKING&lt;/code&gt;, so editors and type checkers see the same types as before. The tool's build function already imported &lt;code&gt;langchain_core.prompts&lt;/code&gt; this way. Using the &lt;code&gt;nvidia_rag&lt;/code&gt; tool still needs &lt;code&gt;langchain-core&lt;/code&gt;, exactly as before. Importing the module no longer does.&lt;/p&gt;

&lt;p&gt;I weighed two other fixes. Declaring &lt;code&gt;langchain-core&lt;/code&gt; a core dependency would pull LangChain into the core package for everyone. Wrapping each import in &lt;code&gt;register.py&lt;/code&gt; in try/except would hide real errors.&lt;/p&gt;

&lt;p&gt;The regression test plays the plain install on any machine: it blocks every &lt;code&gt;langchain_core&lt;/code&gt; entry in &lt;code&gt;sys.modules&lt;/code&gt;, imports &lt;code&gt;nat.tool.nvidia_rag&lt;/code&gt; fresh against a mocked registry, and checks that the tool registers. On the old code it fails with the same &lt;code&gt;ModuleNotFoundError&lt;/code&gt;. A second test checks that both RAG response shapes, &lt;code&gt;results&lt;/code&gt; and &lt;code&gt;chunks&lt;/code&gt;, still produce the same LangChain &lt;code&gt;Document&lt;/code&gt; objects. Before the fix, 1 test failed and 2 passed. After it, all 3 pass. Across the core suite, 2,981 tests passed, 310 were skipped and 8 failed; none of the 8 import &lt;code&gt;nat.tool&lt;/code&gt;, and the five I reran on &lt;code&gt;develop&lt;/code&gt; fail there the same way. I ran everything on Windows 11 with Python 3.11.9, not on Linux.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed for users
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Built-in tool on a plain install&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;chat_completion&lt;/code&gt;, &lt;code&gt;current_datetime&lt;/code&gt;, &lt;code&gt;current_timezone&lt;/code&gt;, &lt;code&gt;milvus_document_search&lt;/code&gt;, &lt;code&gt;github_files_tool&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;registered&lt;/td&gt;
&lt;td&gt;registered&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;nvidia_rag&lt;/code&gt;, &lt;code&gt;nat_retriever&lt;/code&gt;, &lt;code&gt;current_request_attributes&lt;/code&gt;, &lt;code&gt;code_execution&lt;/code&gt;, &lt;code&gt;add_memory&lt;/code&gt;, &lt;code&gt;delete_memory&lt;/code&gt;, &lt;code&gt;get_memory&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;missing&lt;/td&gt;
&lt;td&gt;registered&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;nat validate&lt;/code&gt; on the &lt;code&gt;code_execution&lt;/code&gt; workflow went from exit code 1 and the misleading tag error to &lt;code&gt;Configuration file is valid!&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;nat info components -t function&lt;/code&gt; printed the &lt;code&gt;Failed to import plugin 'nat_tools'&lt;/code&gt; warning twice before the fix and zero times after.&lt;/li&gt;
&lt;li&gt;A workflow on a plain install can now configure code execution, the retriever, request attributes and the three memory tools without LangChain installed.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Why fix a library that is being deprecated?
&lt;/h3&gt;

&lt;p&gt;The deprecation notice gives users until the end of 2026 to migrate. Until then, anyone who installs only the core packages, in CI, in a container or on a new laptop, gets the environment I tested. The maintainers merged this fix three days after the deprecation notice landed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Thank you, willkill07
&lt;/h2&gt;

&lt;p&gt;Thank you to &lt;a href="https://github.com/willkill07" rel="noopener noreferrer"&gt;willkill07&lt;/a&gt; (Will Killian) at NVIDIA. He started CI on NVIDIA's runners with &lt;code&gt;/ok to test&lt;/code&gt;, approved the PR about 12 hours after I opened it, and sent it to merge with &lt;code&gt;/merge&lt;/code&gt; about seven hours after that.&lt;/p&gt;

&lt;p&gt;Thanks as well to the NeMo Agent Toolkit maintainers, who keep this repo open to outside contributors. When I checked on October 26, 39 of the last 40 merged PRs came from people outside the NVIDIA GitHub organization, with a median of 13.9 hours from open to merge.&lt;/p&gt;

&lt;p&gt;The PR template asks every PR to close an issue, so I filed &lt;a href="https://github.com/NVIDIA/NeMo-Agent-Toolkit/issues/2473" rel="noopener noreferrer"&gt;#2473&lt;/a&gt; first. DCO and branch checks passed, CodeRabbit left one minor note on test cleanup, and a maintainer added the &lt;code&gt;bug&lt;/code&gt; and &lt;code&gt;non-breaking&lt;/code&gt; labels.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I took away
&lt;/h2&gt;

&lt;p&gt;Your dev environment is a superset of every user's environment, which makes it the one place a missing dependency can never show up.&lt;/p&gt;

&lt;p&gt;A plugin system that imports a list of modules stops at the first missing optional dependency. Here that cost seven tools and produced one log warning. Unit tests could not catch it, because they run where every extra is installed.&lt;/p&gt;

&lt;p&gt;Before my MS in AI at WPI, I was a Lead DevOps Engineer, and this bug sits between packaging and code, where neither side's tests look. I now build the exact install a user gets and import every plugin before I trust a green test run. I want to propose a CI job that does the same for this repo. The bug I'm looking at now is the same class: on a plain install, &lt;code&gt;nat serve&lt;/code&gt; fails with &lt;code&gt;No module named 'sqlalchemy'&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I'm open to ML engineering and agent infrastructure roles. If your team builds AI agents or the tooling under them, I'd be glad to talk. You can find my work at &lt;a href="https://shreyaboyane.dev" rel="noopener noreferrer"&gt;shreyaboyane.dev&lt;/a&gt;, my code on GitHub as &lt;a href="https://github.com/Sonica-B" rel="noopener noreferrer"&gt;Sonica-B&lt;/a&gt;, and me on &lt;a href="https://www.linkedin.com/in/shreya-boyane/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>opensource</category>
      <category>debugging</category>
    </item>
  </channel>
</rss>
