<?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: Ayoub Hammoudi</title>
    <description>The latest articles on DEV Community by Ayoub Hammoudi (@ayoub_hammoudi).</description>
    <link>https://dev.to/ayoub_hammoudi</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%2F4180380%2Fa457e956-ceff-4c53-940a-fefc088d2199.png</url>
      <title>DEV Community: Ayoub Hammoudi</title>
      <link>https://dev.to/ayoub_hammoudi</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ayoub_hammoudi"/>
    <language>en</language>
    <item>
      <title>AI did not erase deep understanding. It made it rare.</title>
      <dc:creator>Ayoub Hammoudi</dc:creator>
      <pubDate>Thu, 15 Oct 2026 12:56:49 +0000</pubDate>
      <link>https://dev.to/ayoub_hammoudi/ai-did-not-erase-deep-understanding-it-made-it-rare-3npd</link>
      <guid>https://dev.to/ayoub_hammoudi/ai-did-not-erase-deep-understanding-it-made-it-rare-3npd</guid>
      <description>&lt;p&gt;I hated AI.&lt;/p&gt;

&lt;p&gt;Not on principle. Because I had spent years digging. Stack Overflow at two in the morning, GitHub issues, reading a file until I understood why it actually broke rather than why it appeared to. Effort was the filter. It was the thing that separated the people who could from the people who could not, and I had paid for my side of that line.&lt;/p&gt;

&lt;p&gt;Then a kid with two months of experience started shipping a working project in five minutes.&lt;/p&gt;

&lt;p&gt;I want to be precise about what I felt, because I think a lot of people feel it and do not say it. It was not fear of being replaced. It was something smaller and uglier: the suspicion that the thing I had bought with my nights had just been handed out for free.&lt;/p&gt;

&lt;h2&gt;
  
  
  The sentence that moved me
&lt;/h2&gt;

&lt;p&gt;Two people changed my mind. One was a teacher of mine. The other was a friend of his. Separately, weeks apart, they said the same thing:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You are used to a manual gearbox. Somebody just handed you an automatic. Your advantage is not that you can drive. It is that you know what is happening under the hood.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I nodded politely and did not understand it for months.&lt;/p&gt;

&lt;p&gt;What finally landed was the inversion hidden inside it. I had been reading the situation as a loss, as though a scarce skill had been devalued by mass production. The opposite was true. The skill had not been devalued. It had been made optional, which is a very different thing, and optional skills do not disappear. They become uncommon.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scarcity, not erasure
&lt;/h2&gt;

&lt;p&gt;Before, everyone had to dig. There was no other route from a stack trace to a fix. Because the effort was compulsory, it was also ordinary. Everybody around you had paid the same price, so the price bought you nothing in particular.&lt;/p&gt;

&lt;p&gt;Today nobody is forced to. You can ship a feature without ever asking why the library behaves that way, and most of the time it works, which is exactly what makes it dangerous. The understanding is still there to be had. Almost nobody acquires it, because nothing in the daily loop requires it anymore.&lt;/p&gt;

&lt;p&gt;That is the whole shift, and it runs the other way from how it is usually told. The supply of people who understand deeply is shrinking. The demand for them has not moved. Anyone who says this makes expertise worthless has the arrow backwards.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it actually looks like
&lt;/h2&gt;

&lt;p&gt;This is the part that sounds abstract until you have done it, so here is the concrete shape of it.&lt;/p&gt;

&lt;p&gt;A model will hand you code that is syntactically perfect, idiomatic, well named, and wrong in a way that will not show up for six months. It will confidently call a method that behaves differently than its name suggests. It will reproduce a pattern from the project's most-copied file without noticing that the file is the one everybody complains about. It will give you the answer that is most common on the internet, which is not the same as the answer that is correct here.&lt;/p&gt;

&lt;p&gt;None of that is caught by reading the output carefully. It is caught by already knowing the terrain. By having read the library, by recognising that two siblings in the same class disagree with each other, by having a sense of what this codebase does when it is healthy.&lt;/p&gt;

&lt;p&gt;That sense cannot be prompted into existence. It is the residue of having dug, and it is the only part of the job that is getting scarcer rather than cheaper.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where that leaves me
&lt;/h2&gt;

&lt;p&gt;I use AI every day now. I stopped resisting it a while ago, and resisting it was never the interesting position anyway.&lt;/p&gt;

&lt;p&gt;But I am not ahead because I use it. Plenty of people use it. I am ahead, on the days when I am ahead, because I can tell when it is wrong, and because telling when it is wrong is a thing I can only do about the parts of the stack I actually took the time to learn.&lt;/p&gt;

&lt;p&gt;So the advice I would give my past self, the one who was quietly resentful watching a beginner ship in five minutes, is not "learn the tool". Everyone learns the tool. It is: keep digging in the places nobody is forced to dig anymore. That is where the margin moved.&lt;/p&gt;

&lt;p&gt;I am Ayoub Hammoudi, a backend and AI platforms engineer in Paris. More of my work is at &lt;a href="https://github.com/Ayoubhm07" rel="noopener noreferrer"&gt;github.com/Ayoubhm07&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>career</category>
      <category>softwareengineering</category>
      <category>productivity</category>
    </item>
    <item>
      <title>How I got seven fixes merged into Prefect, SQLGlot, Apache Beam, Kafka and Microsoft by asking one question</title>
      <dc:creator>Ayoub Hammoudi</dc:creator>
      <pubDate>Wed, 14 Oct 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/ayoub_hammoudi/how-i-got-five-fixes-merged-into-prefect-sqlglot-apache-beam-and-microsoft-pyrit-by-asking-one-3m5p</link>
      <guid>https://dev.to/ayoub_hammoudi/how-i-got-five-fixes-merged-into-prefect-sqlglot-apache-beam-and-microsoft-pyrit-by-asking-one-3m5p</guid>
      <description>&lt;p&gt;I had seven pull requests merged upstream: one in Prefect, one in SQLGlot, one in Apache Beam, two in Microsoft PyRIT, one in Microsoft Agent Framework and one in Apache Kafka. The largest was 78 added lines. Two of them added nothing at all and only deleted. The fastest went from issue to merge in 34 minutes.&lt;/p&gt;

&lt;p&gt;None of that came from reading code hoping something would look wrong. It came from one question, asked over and over:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where does this project do the same thing more than once, and which one disagrees with the others?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Mature codebases are full of families. Dialect generators. Notification blocks. Scorers. Serializers. Every family is a contract implemented N times, and a contract implemented N times is a contract that drifts. When one sibling drifts, the other siblings become your oracle. You are no longer arguing about what the code should do. The project already answered that, N minus one times.&lt;/p&gt;

&lt;h2&gt;
  
  
  The clearest case: a seed that never reached the query
&lt;/h2&gt;

&lt;p&gt;SQLGlot transpiles SQL between dialects. Databricks has a &lt;code&gt;UNIFORM(min, max, seed)&lt;/code&gt; function. So does Snowflake. Both are generated by sibling classes.&lt;/p&gt;

&lt;p&gt;The Databricks parser stores that third argument in the &lt;code&gt;seed&lt;/code&gt; slot of &lt;code&gt;exp.Uniform&lt;/code&gt;. The Snowflake generator reads both &lt;code&gt;gen&lt;/code&gt; and &lt;code&gt;seed&lt;/code&gt;. The DuckDB generator reads only &lt;code&gt;gen&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So a query transpiled to DuckDB silently lost its seed. &lt;code&gt;ABS(HASH())&lt;/code&gt; hashed nothing. Seed 5, seed 99 and no seed at all produced identical SQL. No exception, no warning, and a result that looks completely plausible until you need reproducibility.&lt;/p&gt;

&lt;p&gt;Here is the part that matters for the method. I did not have to decide what the correct output was. I wrote in the pull request:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The two expected strings are not mine: &lt;code&gt;tests/dialects/test_snowflake.py:3312-3334&lt;/code&gt; already asserts them for the equivalent Snowflake input.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The project had already written down the right answer for the sibling. My test asserted the same strings for the dialect that got it wrong. A reviewer does not have to trust my judgement about SQL semantics. They only have to agree that two dialects reading the same parsed expression should not disagree.&lt;/p&gt;

&lt;p&gt;Issue opened at 15:46. Merged at 16:20. Thirty four minutes, for a three line change.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same shape, three more times
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Prefect.&lt;/strong&gt; Notification blocks let you set a bot name. One of them, &lt;code&gt;NotifyMattermost&lt;/code&gt;, accepted the setting and never passed it to the request. Its siblings all did. The fix was sixteen lines with a test, and the expected behaviour came from the other blocks, not from me.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Microsoft PyRIT.&lt;/strong&gt; &lt;code&gt;SubStringScorer&lt;/code&gt; includes its text matcher's configuration in the scorer identity. &lt;code&gt;DecodingScorer&lt;/code&gt; holds the same kind of matcher, at the same point of the same method, and did not. The consequence is not cosmetic: two scorers that return opposite verdicts for the same input shared one identity hash, so cached evaluation metrics could be reused across different behaviour. The sibling had already been fixed in a pull request that was merged. I copied the two lines it added into the class that had been missed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Apache Beam.&lt;/strong&gt; This one breaks the pattern, and it is worth including for that reason. A debug log reading &lt;code&gt;### Labels&lt;/code&gt; arrived in the Python BigQuery connector with the commit that added BigQuery Read API support, merged on 6 October 2021. My one line removal merged on 6 October 2026. Five years to the day, in a release path, printing a dataset label dictionary at warning level on every temporary dataset setup. There was no sibling to compare against here. There was just a line that no release should have contained. Zero additions, one deletion, one approval, no requested changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this method is not
&lt;/h2&gt;

&lt;p&gt;It is not scanning for theoretical defects.&lt;/p&gt;

&lt;p&gt;I learned that bluntly, and at my own expense. On another project I prepared a clean, exhaustive change that removed a parameter no code reads across seven model families. Every call site checked, every live use preserved, the project's own type checker measured at the version they pin, the full suite run. Only after all of that did I sit down and read their contributing guide properly. It says, in writing, that they will close changes like mine:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Code agents often zero in on "theoretical bugs" found by code analysis that rarely if ever cause problems in practice. These are generally not worth fixing unless they can be exploited by an attacker.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I had spent a twenty three hour test run before reading the one page that decides whether the work was admissible at all. I opened it anyway. A maintainer closed it two minutes after a bot flagged it for review, without a word, which is exactly what that page says they will do. The CI on it was entirely green: sixteen jobs, 199,469 tests, zero failures.&lt;/p&gt;

&lt;p&gt;That is a fair outcome and I would have done the same in their position. The lesson is not that the policy is wrong. It is that reading it costs ten minutes and belongs at minute zero, not after the suite.&lt;/p&gt;

&lt;p&gt;A maintainer of a different project put the same bar as a question on the issue tracker: &lt;em&gt;Did we verify if this is a real-world scenario before drafting this PR?&lt;/em&gt; He was reviewing one of five pull requests, from three contributors, all proposing the same category of change to the same family of classes. None of the five has been merged, and four are still open. Correctness of an API contract is not enough. Something has to actually go wrong.&lt;/p&gt;

&lt;p&gt;The sibling question clears that bar by construction. If one implementation out of five behaves differently with the same data in hand, the breakage is reachable by definition. Someone is on that path right now.&lt;/p&gt;

&lt;h2&gt;
  
  
  The five questions, in the order I ask them
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Where is the family?&lt;/strong&gt; The interface, the base class, the registry, the dialect map. Anything implemented more than three times.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Does the project already state the right answer?&lt;/strong&gt; An existing test asserting it for the sibling, a docstring that promises it, a schema that declares it. If nothing in the repository states it, you are about to argue about taste with someone who has merge rights, and you will lose.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Are the siblings actually in the same situation?&lt;/strong&gt; This is where most candidates die, and it is the question people skip. If the sibling that behaves correctly never receives the data in question, it is not a witness, and you have no case at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Does it fail before and pass after?&lt;/strong&gt; Write the test first, run it, keep the exact failure message. It costs one command and it is the most persuasive thing you can put in front of a reviewer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Did you run what they run, at the versions they pin?&lt;/strong&gt; Their linter version lives in &lt;code&gt;.pre-commit-config.yaml&lt;/code&gt;, &lt;code&gt;setup.py&lt;/code&gt; or the CI workflow, not in your head. One number produced with a newer tool invites a reviewer to dismiss the entire report.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The part nobody writes about
&lt;/h2&gt;

&lt;p&gt;Everything above is the visible half. The other half is the work that produced nothing, and it is most of it.&lt;/p&gt;

&lt;p&gt;That repository was Kafka, and I chased three separate defect families there before finding anything worth proposing. I wrote a scanner for malformed log placeholders and ran it across 3,911 production files: zero cases. I grepped the whole tree for leftover JUnit 4 imports: zero files. The third was the family of five pull requests above, already dead on a bar a maintainer had set.&lt;/p&gt;

&lt;p&gt;What survived those three dead ends was three test dependencies declared twice in the same Gradle block. Zero lines added, three removed.&lt;/p&gt;

&lt;p&gt;That is the method working, not failing. Seven merges did not come from being prolific. They came from killing candidates early, before writing anything, so the few that reached a maintainer were ones they could not reasonably refuse. Five of the seven were under forty lines. The effort was never in the fixing.&lt;/p&gt;

&lt;p&gt;So if you take one thing from this: the skill being tested is not finding bugs. It is deciding, quickly and honestly, which of the bugs you found are worth another person's attention.&lt;/p&gt;

&lt;p&gt;Everything I shipped is public and linked below. Every claim in this post can be checked against it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/PrefectHQ/prefect/pull/23281" rel="noopener noreferrer"&gt;PrefectHQ/prefect#23281&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/tobymao/sqlglot/pull/8519" rel="noopener noreferrer"&gt;tobymao/sqlglot#8519&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/apache/beam/pull/40430" rel="noopener noreferrer"&gt;apache/beam#40430&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/microsoft/PyRIT/pull/3014" rel="noopener noreferrer"&gt;microsoft/PyRIT#3014&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/microsoft/PyRIT/pull/3075" rel="noopener noreferrer"&gt;microsoft/PyRIT#3075&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/microsoft/agent-framework/pull/9139" rel="noopener noreferrer"&gt;microsoft/agent-framework#9139&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/apache/kafka/pull/23786" rel="noopener noreferrer"&gt;apache/kafka#23786&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I am Ayoub Hammoudi, a backend and AI platforms engineer in Paris. More of this work is at &lt;a href="https://github.com/Ayoubhm07" rel="noopener noreferrer"&gt;github.com/Ayoubhm07&lt;/a&gt;.&lt;/p&gt;

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