<?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: Ethan Linden</title>
    <description>The latest articles on DEV Community by Ethan Linden (@ethan_linden_195175e739c9).</description>
    <link>https://dev.to/ethan_linden_195175e739c9</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%2F4095869%2F531d1d20-d1c5-471a-8605-fb29bef4fbf9.png</url>
      <title>DEV Community: Ethan Linden</title>
      <link>https://dev.to/ethan_linden_195175e739c9</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ethan_linden_195175e739c9"/>
    <language>en</language>
    <item>
      <title>AI Is Transforming Software Development Workflows</title>
      <dc:creator>Ethan Linden</dc:creator>
      <pubDate>Wed, 26 Aug 2026 20:42:26 +0000</pubDate>
      <link>https://dev.to/ethan_linden_195175e739c9/ai-is-transforming-software-development-workflows-58o2</link>
      <guid>https://dev.to/ethan_linden_195175e739c9/ai-is-transforming-software-development-workflows-58o2</guid>
      <description>&lt;p&gt;The highest-value use of Claude Code is rarely the one in the demos, and it isn't writing code. It's answering a question like "where does the webhook retry backoff actually get configured?" in a Rails app you've been on for three weeks — and getting it right, pointing at a Sidekiq worker's &lt;code&gt;sidekiq_retry_in&lt;/code&gt; block that four different &lt;code&gt;grep&lt;/code&gt; attempts missed because the constant was named &lt;code&gt;SPACING&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That's the shape of the real productivity gain. Below is what holds up under daily use across the kinds of codebases these tools get pointed at — a Python service, a Go CLI, a legacy Rails monolith — and the parts that reliably waste time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it genuinely changes the day
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Reading code you didn't write.&lt;/strong&gt; This is the biggest one and it gets undersold because it doesn't produce a diff. Dropping into an unfamiliar module and asking "trace what happens when a &lt;code&gt;POST /v2/orders&lt;/code&gt; comes in, list every file involved" gets you a map in under a minute. It's sometimes wrong at the edges, but it's wrong in the way a helpful colleague is wrong — close enough to orient you, and cheap to verify by opening the files it named. Onboarding to a new repo shifts from "read for two days" to "read for two hours with a search assistant."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mechanical migrations with a clear pattern.&lt;/strong&gt; Converting 60 test files from &lt;code&gt;unittest&lt;/code&gt; to &lt;code&gt;pytest&lt;/code&gt; fixtures. Swapping a deprecated &lt;code&gt;moment&lt;/code&gt; call for &lt;code&gt;date-fns&lt;/code&gt; across a frontend. Adding &lt;code&gt;context.Context&lt;/code&gt; as the first parameter to 200 Go functions and threading it through. These are jobs where the transformation is obvious and the tedium is the whole problem. &lt;code&gt;aider&lt;/code&gt; is particularly good here because it commits after each file, so &lt;code&gt;git log --oneline&lt;/code&gt; gives a reviewable trail and &lt;code&gt;git revert&lt;/code&gt; is per-file.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Commit messages and PR descriptions.&lt;/strong&gt; Small, but it removes a real friction point. &lt;code&gt;aider&lt;/code&gt; writes them from the diff by default. Outside an agent session, &lt;code&gt;git diff --cached | claude -p "write a conventional commit message"&lt;/code&gt; does the same job. The messages are reliably better than the ones written by hand at 6pm.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The first draft of tests for code that already works.&lt;/strong&gt; Characterization tests specifically — pinning existing behaviour before a refactor. More on this below, including where it bites.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Debugging as a search problem.&lt;/strong&gt; Paste a stack trace plus the relevant file and ask "what are three things that could cause this?" The model isn't smarter than the developer; it's just faster at enumerating hypotheses, and it doesn't get attached to the first one.&lt;/p&gt;

&lt;h2&gt;
  
  
  A worked example: the characterization test loop
&lt;/h2&gt;

&lt;p&gt;A pricing function nobody wants to touch:&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="c1"&gt;# pricing.py
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;apply_discounts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cart&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;qty&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;cart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tier&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gold&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;*=&lt;/span&gt; &lt;span class="mf"&gt;0.9&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;weekday&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="c1"&gt;# Wednesday promo, added 2019, never removed
&lt;/span&gt;        &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;5.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.05&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The loop, with &lt;code&gt;aider&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aider &lt;span class="nt"&gt;--model&lt;/span&gt; sonnet pricing.py tests/test_pricing.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;gt; Write characterization tests for apply_discounts. Do not change pricing.py.
&amp;gt; Cover: gold tier above and below the 100 threshold, Wednesday vs non-Wednesday,
&amp;gt; and the interaction of both. Use explicit datetime values, no freezegun.
&amp;gt; Assert exact expected floats.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, in the same session:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /run python &lt;span class="nt"&gt;-m&lt;/span&gt; pytest tests/test_pricing.py &lt;span class="nt"&gt;-q&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;aider&lt;/code&gt; feeds the failures back in and iterates. Two rounds is typically enough to go green — six tests, all passing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Then the important step, which the tool will not do for you.&lt;/strong&gt; Green tests prove nothing until they've been proven capable of going red. Break the function on purpose:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;-    if now.weekday() == 2:
&lt;/span&gt;&lt;span class="gi"&gt;+    if now.weekday() == 3:
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four of the six tests still pass. Two of them look like this:&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;test_gold_customer_gets_discount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mock_pricing_client&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;apply_discounts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cart&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;gold_customer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;mock_pricing_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;called_once&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two problems, both classic. &lt;code&gt;assert result &amp;lt; 200&lt;/code&gt; is a tautology given the inputs. And &lt;code&gt;mock.called_once&lt;/code&gt; is not a real attribute — &lt;code&gt;MagicMock&lt;/code&gt; happily returns a new child mock, which is truthy, so that line can never fail. The real spelling is &lt;code&gt;assert_called_once()&lt;/code&gt;. The &lt;code&gt;called_once&lt;/code&gt; mistake is common enough that it turns up in generated tests across unrelated repos.&lt;/p&gt;

&lt;p&gt;The fix is to make "can this test fail?" part of the loop, not a thing anyone remembers to do. For Python:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;mutmut
mutmut run &lt;span class="nt"&gt;--paths-to-mutate&lt;/span&gt; pricing.py &lt;span class="nt"&gt;--tests-dir&lt;/span&gt; tests/
mutmut results
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any surviving mutant is a test that isn't testing. For a lighter version, &lt;code&gt;git stash&lt;/code&gt; the implementation change and confirm red before confirming green. On the Go side, &lt;code&gt;go test -run TestPricing -count=1&lt;/code&gt; plus deliberately inverting a boolean gets 80% of the same signal in ten seconds.&lt;/p&gt;

&lt;p&gt;That loop — generate, run, deliberately break, re-run — is the difference between AI-written tests being useful and being coverage theatre.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it costs you time
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Hallucinated APIs, and specifically plausible ones.&lt;/strong&gt; Not &lt;code&gt;foo.bar_baz_quux()&lt;/code&gt; — that gets caught. It's &lt;code&gt;boto3&lt;/code&gt; calls with a &lt;code&gt;MaxResults&lt;/code&gt; kwarg that the particular client doesn't take, or a &lt;code&gt;pandas&lt;/code&gt; &lt;code&gt;DataFrame.explode(ignore_index=True)&lt;/code&gt; on a version that predates the kwarg, or a Django &lt;code&gt;QuerySet&lt;/code&gt; method that exists on the manager but not the queryset. The failure is a &lt;code&gt;TypeError&lt;/code&gt; at runtime, in a code path that runs monthly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Confident-but-wrong refactors.&lt;/strong&gt; The pattern: you ask for a small change, the agent decides three adjacent things are also wrong, and returns a 400-line diff where the requested change is 12 lines. The other 388 lines look fine. They compile. One of them dropped a &lt;code&gt;nil&lt;/code&gt; check. This is the argument for running agents with a hard scope instruction and reviewing every hunk — and why &lt;code&gt;aider&lt;/code&gt;'s per-change commits matter more than they sound.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Review fatigue.&lt;/strong&gt; This is the sleeper cost. Reading a diff you didn't write is slower and less reliable than reading one you did, because you don't have the model of &lt;em&gt;why&lt;/em&gt; in your head. Generate enough code and you become a full-time reviewer of a colleague who never learns. The common failure is approving an agent's work at the depth normally reserved for a Dependabot bump. That's how the subtle ones get in.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Anything requiring taste.&lt;/strong&gt; Where the module boundary goes. Whether this should be a queue or a cron. Whether the right fix is to delete the feature. Current models will produce a competent implementation of a bad idea, enthusiastically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tool notes, briefly
&lt;/h2&gt;

&lt;p&gt;Copilot's inline completion is still the highest value-per-keystroke thing on the list — it's autocomplete that understands the file, and it costs nothing when it's wrong because you just keep typing. Cursor's agent mode is strong when pointed at exact files and weak when left to search. Claude Code and Codex are better at multi-file work and much better at &lt;em&gt;reading&lt;/em&gt;. &lt;code&gt;aider&lt;/code&gt; wins on git hygiene. &lt;code&gt;continue.dev&lt;/code&gt; with a local model via Ollama is the only option when code can't leave the building; quality is noticeably lower, and it's still worth it for the read-and-explain use case.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to actually measure
&lt;/h2&gt;

&lt;p&gt;Vendor dashboards will show "suggestion acceptance rate." Ignore it. Accepting a suggestion is not a business outcome; it's a keystroke.&lt;/p&gt;

&lt;p&gt;Things that would actually tell you something:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PR size distribution.&lt;/strong&gt; If median diff size is climbing, review quality is falling, whether or not anyone says so.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time from PR open to first substantive review comment.&lt;/strong&gt; Generation is cheap now; review is the bottleneck. If this number is growing, the constraint has moved, not disappeared.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Change failure rate and revert rate&lt;/strong&gt;, from your deploy tooling. This is the one that catches confident-but-wrong.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mutation score on new test files&lt;/strong&gt;, if you can afford to run it in CI weekly. Line coverage will look great and mean nothing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time-to-first-merged-PR for new joiners.&lt;/strong&gt; The onboarding gain is real and this is where it shows up.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And run it as an actual comparison. Pick two similar teams, give one the tooling for a quarter, look at the numbers above. That's not a rigorous trial, but it beats the alternative, which is everyone agreeing it feels faster while the revert rate quietly doubles.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>testing</category>
      <category>devtools</category>
    </item>
  </channel>
</rss>
