<?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: Sergey Dobrov</title>
    <description>The latest articles on DEV Community by Sergey Dobrov (@jbinary).</description>
    <link>https://dev.to/jbinary</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%2F3617962%2Fd0a00be4-2bbf-47c8-9520-da23346408e4.jpeg</url>
      <title>DEV Community: Sergey Dobrov</title>
      <link>https://dev.to/jbinary</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jbinary"/>
    <language>en</language>
    <item>
      <title>AI Can Write the Code. Who Is Going to Learn From Its Bugs?</title>
      <dc:creator>Sergey Dobrov</dc:creator>
      <pubDate>Tue, 13 Oct 2026 16:38:43 +0000</pubDate>
      <link>https://dev.to/jbinary/ai-can-write-the-code-who-is-going-to-learn-from-its-bugs-een</link>
      <guid>https://dev.to/jbinary/ai-can-write-the-code-who-is-going-to-learn-from-its-bugs-een</guid>
      <description>&lt;p&gt;I'd been learning a piece with a piano trainer whose approach worked really well for me, and nothing else did it the same way. Then it went iOS-only. So I described what I wanted to a coding agent, and a few minutes later I had my own.&lt;/p&gt;

&lt;p&gt;I haven't read a single line of its code, and I don't feel guilty about it.&lt;/p&gt;

&lt;p&gt;It has no ads and no annoying UI. It does exactly what I need and nothing else. It changes only should I wish it to. Is its architecture scalable? It will never have a million users. It barely has one: me.&lt;/p&gt;

&lt;p&gt;A lot of software was complicated only because it had to be general. Mine doesn't have to be, so it can be gloriously simple.&lt;/p&gt;

&lt;h2&gt;
  
  
  The case for never reading agent code
&lt;/h2&gt;

&lt;p&gt;The claim goes roughly like this: if you're reading agent code, you're the bottleneck. A recent video, &lt;a href="https://youtu.be/2zLuYU_Ub_0" rel="noopener noreferrer"&gt;Claude Ultracode is super intelligence&lt;/a&gt; by Less Bitter, makes the strongest version of it. It showed a spec of about 2,700 lines, itself written by Claude from a voice memo, turned into a working distributed feature over roughly seven hours of agent work, with critics and tests along the way. The author's conclusion: code quality doesn't matter anymore. Apply enough pressure and keep what survives, like evolution.&lt;/p&gt;

&lt;p&gt;He isn't alone. Gergely Orosz has said he sees code reviews fading away (&lt;a href="https://x.com/GergelyOrosz/status/2081117183002705988" rel="noopener noreferrer"&gt;source&lt;/a&gt;). Santiago (svpino) wrote in August that he'd gone two weeks without reading any AI-generated code and now designs verification for the whole system instead (&lt;a href="https://www.explainx.ai/blog/should-developers-stop-reviewing-ai-generated-code-2026" rel="noopener noreferrer"&gt;source&lt;/a&gt;). A recent piece argues that code is the new assembly: an intermediate representation nobody needs to read (&lt;a href="https://dev.to/max_quimby/code-is-the-new-assembly-stop-reviewing-it-444b"&gt;source&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;I want to give this its due. For my piano trainer, it's simply right. Reading its code would buy me nothing: if it breaks, I'm the only one affected, and I'm not trying to get better at building piano trainers. &lt;/p&gt;

&lt;p&gt;But "you don't need to read this code" and "understanding the implementation no longer matters" are different claims. &lt;/p&gt;

&lt;h2&gt;
  
  
  What code quality actually is
&lt;/h2&gt;

&lt;p&gt;"Code quality doesn't matter" depends on what you think quality was. If it means taste, then sure, an agent doesn't care. But most rules that get dismissed as taste are literal readings of principles, and the principles still matter. Short functions, for example, were never about looks: they're easier to test, and an agent changing those tests later makes fewer mistakes too.&lt;/p&gt;

&lt;p&gt;Or take DRY. It was never "no repeated lines". Its original definition, in Hunt and Thomas's The Pragmatic Programmer (1999), is about knowledge: each piece of it should have one authoritative representation. People called DRY an antipattern long before AI, because they took it literally. Duplicating coincidental code is fine. Duplicating a business rule creates an invariant nobody enforces: these copies must stay equal. Agents break that the same way people do. They update the copy they're looking at and miss the one that drifted after the copy-paste.&lt;/p&gt;

&lt;p&gt;Brevity was never the point either; clarity was. And naming isn't cosmetic. A name is a claim about meaning, and models lean on those claims heavily. Strip the names, and models get measurably worse, even on tasks that should depend only on structure (&lt;a href="https://arxiv.org/html/2510.03178v1" rel="noopener noreferrer"&gt;source&lt;/a&gt;). If anything, names matter more for agents: a human compensates for a bad name with a year of familiarity, while an agent reads the code cold every session.&lt;/p&gt;

&lt;p&gt;So very little of code quality ever existed only for human readers. It was always information and structure, and every reasoner needs both. What AI changes is how its absence fails. A human slogs through bad code and gets slower. An agent reads it fast, misunderstands it confidently, and acts. Which leaves the question the rest of this piece is about: who notices?&lt;/p&gt;

&lt;h2&gt;
  
  
  Where judgement comes from
&lt;/h2&gt;

&lt;p&gt;I didn't learn about race conditions from the definition. I learned when code I was sure was deterministic gave different results, and I spent hours finding out why. After a few of those, you don't just know about synchronization. You feel it. You look at innocent-looking code and think: this will race.&lt;/p&gt;

&lt;p&gt;Same with injection. "Never trust user input" is a sentence. Someone breaking your shiny new homepage by messing with GET parameters is an experience. After that, &lt;code&gt;name = request.args["name"]&lt;/code&gt; makes you flinch.&lt;/p&gt;

&lt;p&gt;That flinch comes from accumulated failed predictions: moments when your mental model collided with reality and lost. It's also why experienced engineers can reject an AI answer before they can explain why. They've seen that movie.&lt;/p&gt;

&lt;p&gt;So I've started to think "should we review the code?" is the wrong question. What I'd rather know is where these collisions happen now, and who's around when they do.&lt;/p&gt;

&lt;p&gt;The standard reply: nobody reads the assembly their compiler emits, so why read what an LLM emits? The standard answer is that compilers are deterministic and LLMs aren't. Yes, but I find a different answer more useful: every abstraction moves where judgement forms, and what matters is how much it leaks.&lt;/p&gt;

&lt;p&gt;Compilers almost never leak in ways you have to reason about. Not reading assembly cost us very little.&lt;/p&gt;

&lt;p&gt;Frameworks leak at the edges, and we can already see what that costs. Modern web frameworks escape output and ORMs parameterize queries by default. That's mostly good. But it means many developers never get burned by injection, so they never form the reflex the burn would have taught. The real lesson was that input is hostile everywhere, not only where HTML gets rendered. Without that reflex, the same developer writes &lt;code&gt;open(os.path.join(root, request.args["file"]))&lt;/code&gt; and walks straight out of the project root. Or builds an SSRF from a URL parameter, or an open redirect. The protection covered one case; the reflex would have covered all of them.&lt;/p&gt;

&lt;p&gt;AI-generated code is an abstraction that can leak anywhere. The cost of never colliding with it scales accordingly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tests aren't the environment
&lt;/h2&gt;

&lt;p&gt;The video's evolution metaphor is appealing: don't demand a beautiful genome, apply pressure and keep what survives. But in evolution, the organism doesn't write its environment. The world judges it, and the organism has no say in what counts as success.&lt;/p&gt;

&lt;p&gt;When the same kind of model interprets the spec, writes the code, writes the tests and fixes the failures, that's not natural selection. It's an optimizer grading its own homework. Adding critic agents helps less than it seems: models from the same family can share blind spots, so their errors are correlated, not independent (&lt;a href="https://hackernoon.com/the-problem-with-using-ai-to-review-ai-written-code" rel="noopener noreferrer"&gt;source&lt;/a&gt;). And sometimes the easiest way to make a test pass is to weaken the test.&lt;/p&gt;

&lt;p&gt;Green CI means the implementation satisfies the tests. It doesn't mean it satisfies reality. Ten thousand tests cover the hypotheses someone thought of, and nothing else. A person who's been burned by concurrency asks what happens if two events arrive in the opposite order, and then an agent can write five hundred tests for it. Without that question, you get five hundred sophisticated tests of the happy path.&lt;/p&gt;

&lt;p&gt;There is a version of this that really is evolution: ship to production, let real users and attackers hit it, read the complaints, fix, repeat. That environment is independent. It's also the most expensive test suite there is. &lt;/p&gt;

&lt;p&gt;In the video, the spec was written by AI too. So the pipeline is vague intent, then an AI spec, AI implementation, AI tests, AI critics, CI.&lt;/p&gt;

&lt;p&gt;Where does engineering knowledge enter that system? In his case the answer is easy: he has it. He's built software for years, knows his product, and can tell whether the result is right. He's the oracle.&lt;/p&gt;

&lt;p&gt;A junior asking for "something like Uber" gets a fluent spec too. Nothing tells them what it's missing, because they don't know what they don't know. AI increasingly removes the need to formulate a solution, but someone still has to judge whether the formulation describes the right problem. The method works because of years of exactly the experience it would deny the next generation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Specs age
&lt;/h2&gt;

&lt;p&gt;The video's example is a single pass: one spec goes in, and seven hours later one working feature comes out. Nobody has to change that spec afterwards. Real systems live through dozens of revisions.&lt;/p&gt;

&lt;p&gt;An AI-written spec of a few thousand lines that nobody fully read is a second codebase nobody owns. When requirements change, someone has to know which parts were real invariants and which were incidental choices. Which tests protect something that matters, and which just pin behavior that happened to exist.&lt;/p&gt;

&lt;p&gt;It gets worse. When the spec changes, the agent updates code and tests together. The environment doesn't just fail to be independent; it co-evolves with the organism, under the same author. Green CI at revision twelve says nothing about an invariant from revision three whose test was "updated to reflect the new requirements".&lt;/p&gt;

&lt;p&gt;Then there's Hyrum's law: with enough users, every observable behavior gets relied upon. We usually discuss it for APIs, where you can argue that consumers shouldn't depend on undocumented behavior. Users of a product are different. They learn where the export button is, what order the list comes in, which filter remembers its value. To them, all observable behavior is the behavior, and you can't tell them they were wrong to rely on it.&lt;/p&gt;

&lt;p&gt;A human changing familiar code tends to make a minimal diff, so incidental behavior stays put by default. An agent satisfying a revised spec is free to restructure. Each regeneration is a fresh sample from the implementations that pass the tests, and everything the tests don't pin can drift: ordering, defaults, wording, timing, edge cases. Not because anyone decided to change them, but because nothing held them in place. Multiply that by every release cycle, and watch your support queue.&lt;/p&gt;

&lt;h2&gt;
  
  
  Nobody debugs it either
&lt;/h2&gt;

&lt;p&gt;One objection I take seriously: you don't need to write code to get burned by it, and engineers on call for systems they didn't build learn plenty. But that only works if someone actually does the debugging. When nobody knows the code, debugging it yourself is expensive, so handing the bug back to the agent is the rational choice. Every time. Each handoff keeps familiarity at zero, which makes the next handoff even more rational. Each choice is sensible on its own. Together, they guarantee that nobody ever gets to know the code.&lt;/p&gt;

&lt;p&gt;Agents are best at bugs that are local and reproducible. They get stuck on emergent ones: timing across services, data-dependent corruption, failures that only appear under real load. Those are the formative bugs, and that's when humans get pulled back in. At the highest stakes, with the least familiarity.&lt;/p&gt;

&lt;p&gt;None of this is new. In 1983, Lisanne Bainbridge described it in "Ironies of Automation": automation takes over the easy cases, leaves operators the hardest ones, and erodes the skills they need for them. Aviation learned this the hard way. I suspect software is about to run the same experiment.&lt;/p&gt;

&lt;p&gt;Addy Osmani described one half of this well: agents made writing code almost free, while understanding it still costs what it always did (&lt;a href="https://addyosmani.com/blog/agentic-code-review/" rel="noopener noreferrer"&gt;source&lt;/a&gt;). I'd add the other half. The expensive part also includes becoming someone who can understand systems at all, and that happens mostly through the collisions we're now automating away.&lt;/p&gt;

&lt;p&gt;Building software is getting cheap much faster than becoming an engineer is. That gap makes judgement more valuable, not less. If you know concurrency is dangerous, you ask for tests that replay events in every hostile order, and an agent writes five hundred of them in minutes. If you don't know, nobody asks, and the agent happily tests the happy path.&lt;/p&gt;

&lt;p&gt;The bottleneck moves from writing code to knowing what should exist, noticing when it's wrong, and knowing which failures matter. Those are exactly the abilities you get from software personally screwing you over.&lt;/p&gt;

&lt;p&gt;Knowing what should exist is the one I've seen junior and even mid-level engineers struggle with most. Many times I've had to ask "why do you need this?" over and over, until they realized they didn't. An agent rarely asks that question. It builds what it's told, quickly and well, so the unnecessary thing now costs minutes instead of days, and nobody is forced to notice it was unnecessary.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I actually do about it
&lt;/h2&gt;

&lt;p&gt;I don't think the answer is reviewing every line. Three things seem to work better, at least for me, though none of them is a complete answer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stay in the reasoning loop.&lt;/strong&gt; As far as I can tell, what builds intuition is being present when reality contradicts your model, more than typing or even reading the code. You can stay present with an agent: predict the cause before it tells you, refuse fixes you can't explain, and have a human write the root cause in the postmortem. None of this happens by default, because "fix it" is always faster than "explain it". It has to be a deliberate choice, for individuals and for teams.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Own the boundaries; let agents own the interiors.&lt;/strong&gt; When I wanted another exercise in my piano trainer, my instinct was to build a separate app instead of growing the first one. That's the Unix way coming back. We drifted away from it partly because many small pieces meant many things to remember and maintain. Cheap generation removes most of that cost, and small pieces also keep each part within what an agent can hold in context. But pieces can only be disposable if the interfaces between them aren't. Unix worked because its contract was dumb and stable: lines of text. So I keep interfaces boring, and I decide them myself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use the agent to raise the bar.&lt;/strong&gt; This part is optimistic. Atomic commits, real commit messages, test plans, small reviewable PRs, a quick first assessment of every incoming traceback, a short loop for the simple bugs: we always knew they were good, and deadlines always gave us an excuse to skip them. Agents make them almost free, so skipping them is now a choice. One caveat: these artifacts are claims about the work. An agent will write a confident test plan that was never run, or split commits that don't build on their own. In experienced hands that's easy to catch, and standards go up. In inexperienced hands you get history that looks exemplary and is subtly fictional.&lt;/p&gt;

&lt;p&gt;My piano trainer exists because I practice piano. Nobody, me included, thinks an app could do the practicing for me. The app was easy to hand over. The hours at the keyboard weren't, because the hours are the point.&lt;/p&gt;

&lt;p&gt;Software engineering is the same. Let the agent build the tool. But someone still has to practice: break things, be surprised, and come out understanding something they didn't before. If AI writes the code, the question isn't whether anyone reads it. It's who's going to learn from its bugs.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>softwareengineering</category>
      <category>career</category>
      <category>programming</category>
    </item>
    <item>
      <title>40 Lines of Python to Fake a Serial Mouse</title>
      <dc:creator>Sergey Dobrov</dc:creator>
      <pubDate>Tue, 10 Mar 2026 15:41:55 +0000</pubDate>
      <link>https://dev.to/jbinary/40-lines-of-python-to-fake-a-serial-mouse-5cob</link>
      <guid>https://dev.to/jbinary/40-lines-of-python-to-fake-a-serial-mouse-5cob</guid>
      <description>&lt;p&gt;During the COVID lockdowns my son and I started playing &lt;em&gt;The Settlers II&lt;/em&gt; in DOSBox.&lt;/p&gt;

&lt;p&gt;One of the coolest features of the game is that two players can play on the same computer in split screen, each controlling their own cursor — a surprisingly social multiplayer mode for a 1996 strategy game.&lt;/p&gt;

&lt;p&gt;The trick is that the second player uses a serial mouse.&lt;/p&gt;

&lt;p&gt;Unfortunately modern operating systems don't really expose the concept of multiple independent mice anymore — they all get merged into a single pointer.&lt;/p&gt;

&lt;p&gt;So if I wanted that old-school multiplayer experience back, I needed an adapter.&lt;/p&gt;

&lt;p&gt;My first thought was simple: maybe I can fake a serial mouse?&lt;/p&gt;

&lt;p&gt;In Unix systems everything is &lt;em&gt;a file&lt;/em&gt;. If DOSBox expects a serial device, perhaps I can generate the right byte stream and feed it to it?&lt;/p&gt;

&lt;p&gt;Reading through the Linux docs and trying to &lt;code&gt;hexdump&lt;/code&gt; the mouse device I realized I need to convert from PS/2 mouse protocol into Microsoft serial mouse protocol. The descriptions of both I quickly found on the Internet: &lt;a href="https://roborooter.com/post/serial-mice/" rel="noopener noreferrer"&gt;https://roborooter.com/post/serial-mice/&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Quickly noticing some key differences:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Both communicate via groups of three bytes;&lt;/li&gt;
&lt;li&gt;The most significant bit of each byte is used for framing in the Microsoft protocol, so the high bits of the coordinates are packed into the first byte;&lt;/li&gt;
&lt;li&gt;Both encode movement deltas, but PS/2 uses sign bits in the status byte while the Microsoft serial protocol splits the high bits of the coordinates into the first byte;&lt;/li&gt;
&lt;li&gt;As a result PS/2 effectively has one extra bit of precision for each axis.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The first idea was to just write the packets into a pipe and connect DOSBox to it.&lt;/p&gt;

&lt;p&gt;Unfortunately, DOSBox expects something that behaves like a real serial device, not just a pipe.&lt;/p&gt;

&lt;p&gt;Eventually I landed on &lt;code&gt;socat&lt;/code&gt;, which can create a pair of pseudo-terminals:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;socat &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; pty,raw,echo&lt;span class="o"&gt;=&lt;/span&gt;0 pty,raw,echo&lt;span class="o"&gt;=&lt;/span&gt;0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates two linked devices:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/dev/pts/24
/dev/pts/25
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Whatever you write to one appears on the other.&lt;/p&gt;

&lt;p&gt;Now DOSBox can connect to one side:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;serial1&lt;/span&gt;=&lt;span class="n"&gt;directserial&lt;/span&gt; &lt;span class="n"&gt;realport&lt;/span&gt;:&lt;span class="n"&gt;pts&lt;/span&gt;/&lt;span class="m"&gt;25&lt;/span&gt; &lt;span class="n"&gt;rxdelay&lt;/span&gt;:&lt;span class="m"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the script writes to the other.&lt;/p&gt;

&lt;p&gt;The first version worked — but the mouse felt strange: movements were extremely smooth and continued even after I stopped moving the mouse.&lt;/p&gt;

&lt;p&gt;Modern mice have extremely high DPI, which means the adapter was sending a huge number of tiny movements.&lt;/p&gt;

&lt;p&gt;DOSBox replayed them with a delay.&lt;/p&gt;

&lt;p&gt;The solution was simple: accumulate movement and send it in larger steps.&lt;/p&gt;

&lt;p&gt;I deliberately didn’t try to make it neat or reusable — it only needed to run on my laptop:&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;struct&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;#
&lt;/span&gt;    &lt;span class="n"&gt;byte1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mb"&gt;0b01000000&lt;/span&gt;
    &lt;span class="n"&gt;byte1&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mb"&gt;0b11000000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;
    &lt;span class="n"&gt;byte1&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mb"&gt;0b11000000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;
    &lt;span class="c1"&gt;#
&lt;/span&gt;    &lt;span class="n"&gt;buff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;byte1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;buff&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mb"&gt;0b00111111&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;buff&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mb"&gt;0b00111111&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buff&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;flush&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;


&lt;span class="n"&gt;BUNDLING&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="n"&gt;SENSITIVITY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;


&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/dev/pts/24&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;wb&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;acc_dx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;acc_dy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/dev/input/mouse1&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;rb&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;mouse&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;let&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s get it started!&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;b1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mouse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&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;b1&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mb"&gt;0b00010000&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;dx&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;256&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;b1&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mb"&gt;0b00100000&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;dy&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;256&lt;/span&gt;
            &lt;span class="n"&gt;acc_dx&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;dx&lt;/span&gt;
            &lt;span class="n"&gt;acc_dy&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;dy&lt;/span&gt;

            &lt;span class="n"&gt;dx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;acc_dx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;BUNDLING&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;dx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;acc_dx&lt;/span&gt; &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="n"&gt;SENSITIVITY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;acc_dx&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="n"&gt;dx&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;SENSITIVITY&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;acc_dy&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;BUNDLING&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;dy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;acc_dy&lt;/span&gt; &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="n"&gt;SENSITIVITY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;acc_dy&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="n"&gt;dy&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;SENSITIVITY&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;dx&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;dy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;dy&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you can see the script is very simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Main loop reads PS/2 packets from &lt;code&gt;/dev/input/mouse1&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;Decodes movement deltas;&lt;/li&gt;
&lt;li&gt;Accumulates them over time;&lt;/li&gt;
&lt;li&gt;Applies &lt;code&gt;BUNDLING&lt;/code&gt; to avoid overwhelming DOSBox with packets;&lt;/li&gt;
&lt;li&gt;Applies &lt;code&gt;SENSITIVITY&lt;/code&gt; so that our high-DPI mouse feels good in small resolution of the old game;&lt;/li&gt;
&lt;li&gt;Every now and then the &lt;code&gt;send&lt;/code&gt; routine builds MS Serial Mouse packets back from the accumulated movements and sends it to the &lt;code&gt;pts&lt;/code&gt; devices so that &lt;code&gt;socat&lt;/code&gt; mirrors it into the other &lt;code&gt;pts&lt;/code&gt; which DOSBox will read.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Note that The Microsoft protocol has slightly lower resolution for movement deltas than PS/2, so very large movements would technically need to be split across multiple packets. In practice this isn't an issue because mice report movement in small increments.&lt;/p&gt;

&lt;h2&gt;
  
  
  The UNIX way
&lt;/h2&gt;

&lt;p&gt;In the end the entire adapter was about 40 lines of Python, no third-party libraries — just the standard library and a bit of Unix plumbing.&lt;/p&gt;

&lt;p&gt;Interestingly, modern DOSBox builds now support this feature natively: &lt;a href="https://www.dosbox-staging.org/releases/release-notes/0.80.0/?utm_source=chatgpt.com#dual-mouse-gaming" rel="noopener noreferrer"&gt;https://www.dosbox-staging.org/releases/release-notes/0.80.0/?utm_source=chatgpt.com#dual-mouse-gaming&lt;/a&gt;, so the little adapter is no longer necessary.&lt;/p&gt;

&lt;p&gt;But solving the problem was half the fun — and it’s a nice illustration of how Unix-style abstractions let you insert a tiny translator between two pieces of software from completely different eras.&lt;/p&gt;

&lt;p&gt;Even if one of them thinks it's talking to a serial mouse from 1995.&lt;/p&gt;

</description>
      <category>linux</category>
      <category>python</category>
      <category>beginners</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>The Best Engineers Can Move Between Abstraction Layers</title>
      <dc:creator>Sergey Dobrov</dc:creator>
      <pubDate>Wed, 18 Feb 2026 10:57:14 +0000</pubDate>
      <link>https://dev.to/jbinary/the-best-engineers-can-move-between-abstraction-layers-ee8</link>
      <guid>https://dev.to/jbinary/the-best-engineers-can-move-between-abstraction-layers-ee8</guid>
      <description>&lt;p&gt;One pattern I’ve noticed over the years: the strongest engineers can move between abstraction layers almost effortlessly.&lt;/p&gt;

&lt;p&gt;They write high-level code comfortably — but when something behaves strangely, they instinctively descend a few layers, reason about what’s happening underneath, and come back with a fix.&lt;/p&gt;

&lt;p&gt;Here’s a simple thought experiment.&lt;/p&gt;

&lt;p&gt;Ask someone why Python code using dense numeric arrays is much faster than similar logic written with regular Python objects.&lt;/p&gt;

&lt;p&gt;A surface-level answer is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Because NumPy is written in C.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A deeper answer mentions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;contiguous memory&lt;/li&gt;
&lt;li&gt;cache locality&lt;/li&gt;
&lt;li&gt;pointer indirection&lt;/li&gt;
&lt;li&gt;object metadata overhead&lt;/li&gt;
&lt;li&gt;how CPUs actually operate on memory&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That explanation travels from Python syntax down to memory layout and hardware behavior — and back.&lt;/p&gt;

&lt;p&gt;That ability to move across layers isn’t academic. It shows up in very ordinary work.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Very Ordinary API Problem
&lt;/h2&gt;

&lt;p&gt;Recently I was integrating with an external REST API that was painfully slow.&lt;/p&gt;

&lt;p&gt;It returned paginated results and also reported the total number of matching records. The task was straightforward: fetch everything.&lt;/p&gt;

&lt;p&gt;The original implementation used broad filters over the entire time range and fetched page by page with large page sizes.&lt;/p&gt;

&lt;p&gt;At the HTTP layer, that approach makes sense:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fewer requests&lt;/li&gt;
&lt;li&gt;Larger pages&lt;/li&gt;
&lt;li&gt;Simpler control flow&lt;/li&gt;
&lt;li&gt;“Minimize round trips”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It feels efficient. But performance was terrible.&lt;/p&gt;

&lt;p&gt;Instead of tweaking page size or adding concurrency, I asked a different question:&lt;/p&gt;

&lt;p&gt;— Why is the API returning total count?&lt;/p&gt;

&lt;p&gt;— That likely means a &lt;code&gt;COUNT(*)&lt;/code&gt; somewhere. And pagination with &lt;code&gt;LIMIT … OFFSET&lt;/code&gt; … usually means something else:&lt;/p&gt;

&lt;p&gt;To serve page N, the database must scan and discard the previous N × page_size rows.&lt;/p&gt;

&lt;p&gt;So with broad filters over a large time range, each successive page becomes more expensive than the previous one.&lt;/p&gt;

&lt;p&gt;At the REST layer, everything looked clean.&lt;/p&gt;

&lt;p&gt;Underneath, it was probably:&lt;/p&gt;

&lt;p&gt;REST&lt;br&gt;
→ controller&lt;br&gt;
→ SQL with &lt;code&gt;COUNT(*)&lt;/code&gt;&lt;br&gt;
→ &lt;code&gt;LIMIT/OFFSET&lt;/code&gt;&lt;br&gt;
→ large scans and discarded rows&lt;/p&gt;

&lt;p&gt;So instead of fetching everything broadly, I changed the access pattern.&lt;/p&gt;

&lt;p&gt;Fetch day by day:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Smaller slices.&lt;/li&gt;
&lt;li&gt;Better index selectivity.&lt;/li&gt;
&lt;li&gt;Much smaller offsets.&lt;/li&gt;
&lt;li&gt;Less discarded work.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Result: roughly a 10× speedup.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No access to their database.&lt;/li&gt;
&lt;li&gt;No vendor escalation.&lt;/li&gt;
&lt;li&gt;No architectural rewrite.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Just switching layers mentally, adjusting the shape of the query, and moving back up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Matters
&lt;/h2&gt;

&lt;p&gt;Most of our daily work lives at high levels of abstraction: frameworks, APIs, cloud services.&lt;/p&gt;

&lt;p&gt;And that’s good — that’s where productivity lives.&lt;/p&gt;

&lt;p&gt;But performance issues, strange behavior, and cost explosions rarely originate at the same layer where they surface.&lt;/p&gt;

&lt;p&gt;The engineers who consistently deliver under pressure are usually the ones who can descend a few layers, reason about what’s actually happening, and then come back with a pragmatic fix.&lt;/p&gt;

&lt;p&gt;Not because they love low-level code.&lt;/p&gt;

&lt;p&gt;But because they’re not confined to a single abstraction.&lt;/p&gt;

&lt;p&gt;And this becomes even more important as more code is generated for us and more infrastructure is abstracted away. When AI writes the boilerplate and platforms hide the machinery, the differentiator isn’t how quickly you can produce code — it’s how well you understand what happens underneath it.&lt;/p&gt;

&lt;p&gt;That ability to move across layers becomes rarer.&lt;/p&gt;

&lt;p&gt;And that skill compounds.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>performance</category>
      <category>architecture</category>
      <category>career</category>
    </item>
    <item>
      <title>Rediscovering Unix Pipelines: Two Backup Problems, One mindset</title>
      <dc:creator>Sergey Dobrov</dc:creator>
      <pubDate>Mon, 08 Dec 2025 11:31:08 +0000</pubDate>
      <link>https://dev.to/jbinary/rediscovering-unix-pipelines-two-backup-problems-one-mindset-59n7</link>
      <guid>https://dev.to/jbinary/rediscovering-unix-pipelines-two-backup-problems-one-mindset-59n7</guid>
      <description>&lt;p&gt;Modern engineers often reach for JSON parsing, temporary files, or orchestration tools. Unix pipelines still outperform them more often than you'd expect. Two recent backup tasks reminded me how often we over-engineer simple data-movement problems.&lt;/p&gt;

&lt;p&gt;Two backup problems, same pattern: both reached for complex solutions involving temporary files, JSON parsing, and extra dependencies. Both could be solved with simple pipelines.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 1: Pruning Old Backups
&lt;/h2&gt;

&lt;p&gt;I needed to keep the latest seven backups in object storage and delete everything older.&lt;/p&gt;

&lt;p&gt;First instinct: treat it like an application problem.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mc &lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;--recursive&lt;/span&gt; &lt;span class="nt"&gt;--json&lt;/span&gt; bucket/ &lt;span class="se"&gt;\&lt;/span&gt;
  | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'. | "\(.lastModified) \(.key)"'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | … &lt;span class="c"&gt;# extract keys, build array, loop, delete&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Parse JSON, build arrays, extract timestamps, loop through objects.&lt;/p&gt;

&lt;p&gt;But backups already encode timestamps in filenames. &lt;code&gt;mc find&lt;/code&gt; already prints one file per line. &lt;code&gt;sort&lt;/code&gt; already orders strings. &lt;code&gt;head&lt;/code&gt; already drops lines.&lt;/p&gt;

&lt;p&gt;The whole task collapses:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mc find bucket/ &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"backup-*.gz"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nt"&gt;-7&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; file&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do &lt;/span&gt;mc &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No JSON. No arrays. No parsing. Just text flowing through composable tools.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 2: Creating Backups
&lt;/h2&gt;

&lt;p&gt;A teammate needed to dump PostgreSQL, compress it, and upload to object storage.&lt;/p&gt;

&lt;p&gt;His approach (roughly):&lt;/p&gt;

&lt;p&gt;First, upload a script to the server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;scp backup.sh db-server:/tmp/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then run it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh db-server /tmp/backup.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The script itself:&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="c"&gt;#!/bin/bash&lt;/span&gt;
pg_dump mydb &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /tmp/backup.sql
&lt;span class="nb"&gt;gzip&lt;/span&gt; /tmp/backup.sql
mc &lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/backup.sql.gz s3://bucket/backup-&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%Y%m%d&lt;span class="si"&gt;)&lt;/span&gt;.sql.gz
&lt;span class="nb"&gt;rm&lt;/span&gt; /tmp/backup.sql.gz
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This requires:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Uploading the script first&lt;/li&gt;
&lt;li&gt;Installing &lt;code&gt;mc&lt;/code&gt; on the database server&lt;/li&gt;
&lt;li&gt;Managing temporary files&lt;/li&gt;
&lt;li&gt;Cleanup logic&lt;/li&gt;
&lt;li&gt;Disk space for both uncompressed and compressed data&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each of these steps adds friction, state, and failure modes.&lt;br&gt;
But none of them are actually required:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh db-server &lt;span class="s2"&gt;"pg_dump mydb | gzip"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | mc pipe s3://bucket/backup-&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%Y%m%d&lt;span class="si"&gt;)&lt;/span&gt;.sql.gz
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From two commands plus a bash script to one line. From five requirements to zero.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bonus: Free Parallelism
&lt;/h2&gt;

&lt;p&gt;That backup pipeline runs three processes simultaneously: &lt;code&gt;pg_dump&lt;/code&gt; generating data, &lt;code&gt;gzip&lt;/code&gt; compressing it, &lt;code&gt;mc&lt;/code&gt; uploading it. No threading code. No coordination.&lt;/p&gt;

&lt;p&gt;The temporary-file version? Strictly sequential. Each step waits for the previous to finish.&lt;/p&gt;

&lt;p&gt;Pipes give you streaming parallelism by default.&lt;/p&gt;

&lt;p&gt;Need more? &lt;code&gt;xargs&lt;/code&gt; and GNU &lt;code&gt;parallel&lt;/code&gt; scale across cores:&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="c"&gt;# Compress 100 files with 4 workers&lt;/span&gt;
find &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s2"&gt;"*.log"&lt;/span&gt; | xargs &lt;span class="nt"&gt;-P&lt;/span&gt; 4 &lt;span class="nt"&gt;-I&lt;/span&gt; &lt;span class="o"&gt;{}&lt;/span&gt; &lt;span class="nb"&gt;gzip&lt;/span&gt; &lt;span class="o"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same pipeline thinking, multiplied across CPUs.&lt;/p&gt;

&lt;h2&gt;
  
  
  When NOT to Use Pipes
&lt;/h2&gt;

&lt;p&gt;Pipelines aren't always the answer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Complex state management&lt;/strong&gt; - multiple passes over data, tracking relationships&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Explicit error handling&lt;/strong&gt; - pipelines can fail silently&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Extreme scale&lt;/strong&gt; - terabytes need Spark/Hadoop&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bash quirkiness&lt;/strong&gt; - arcane quoting, clunky error handling&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Framework requirements&lt;/strong&gt; - ETL tools gain orchestration but lose composability&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Team familiarity&lt;/strong&gt; - if pipelines are cryptic to your team, write Python&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Unix pipes work best for glue code: moving data between systems, transforming formats, filtering streams.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Pattern to Look For
&lt;/h2&gt;

&lt;p&gt;Whenever you find yourself:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Loading data into arrays&lt;/li&gt;
&lt;li&gt;Writing temporary files&lt;/li&gt;
&lt;li&gt;Parsing structured data just to transform it&lt;/li&gt;
&lt;li&gt;Installing tools on systems just to move data&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;…there's probably a pipeline waiting to be discovered.&lt;/p&gt;

&lt;p&gt;Not because pipelines are "better." Because they're simpler. Simple solutions are easier to debug, modify, and maintain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Fifty years later, Unix pipelines still win through elimination.&lt;/p&gt;

&lt;p&gt;They eliminate temporary state. They eliminate dependencies. They eliminate the complexity of treating data movement as a programming exercise.&lt;/p&gt;

&lt;p&gt;Your turn: What's a problem you recently solved with a pipeline instead of code? Or where you wrote code when a pipeline would have worked?&lt;/p&gt;

</description>
      <category>bash</category>
      <category>devops</category>
      <category>productivity</category>
      <category>cli</category>
    </item>
    <item>
      <title>You don't understand GIL</title>
      <dc:creator>Sergey Dobrov</dc:creator>
      <pubDate>Thu, 27 Nov 2025 16:14:07 +0000</pubDate>
      <link>https://dev.to/jbinary/you-dont-understand-gil-2ce7</link>
      <guid>https://dev.to/jbinary/you-dont-understand-gil-2ce7</guid>
      <description>&lt;p&gt;Some time ago I was chatting with a friend about programming languages, and the conversation drifted — inevitably — to why Python is “bad.”&lt;/p&gt;

&lt;p&gt;The first argument was that Python has “no types,” which makes it error-prone.&lt;br&gt;
I pushed back: Python’s modern type system is surprisingly expressive, and meanwhile Java manages to produce endless null-pointer exceptions despite all its ceremony. That point didn’t land well.&lt;/p&gt;

&lt;p&gt;So the next argument came out:&lt;/p&gt;

&lt;p&gt;“Anyway, Python can’t even use more than one CPU core because of the GIL.”&lt;/p&gt;

&lt;p&gt;I didn’t try to debate it.&lt;br&gt;
Instead, I opened a terminal, ran a data-processing script I had lying around, and showed them top.&lt;/p&gt;

&lt;p&gt;One Python process was using exactly 300% CPU. They did not believe it.&lt;/p&gt;

&lt;p&gt;And that moment captures something important: the GIL is one of the most confidently misunderstood ideas in all of programming.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why the GIL Seems Simple but Isn’t
&lt;/h2&gt;

&lt;p&gt;What makes this even trickier is that the GIL actually exists to make Python simple.&lt;br&gt;
It lets most of the runtime behave like a friendly, high-level environment where you never have to think about memory management, object lifetimes, or thread safety inside the interpreter.&lt;/p&gt;

&lt;p&gt;But concurrency is where that abstraction finally hits its boundary.&lt;br&gt;
The GIL doesn’t behave the same way in every situation — it takes different forms under different workloads.&lt;/p&gt;

&lt;p&gt;That’s why so many explanations are technically correct yet still misleading.&lt;/p&gt;

&lt;p&gt;So let’s peel it back one layer at a time.&lt;/p&gt;
&lt;h3&gt;
  
  
  Layer 1: “Threads Are Useless in Python”
&lt;/h3&gt;

&lt;p&gt;A common starting point is the idea that Python threads are “useless” because of the GIL.&lt;/p&gt;

&lt;p&gt;If that were true, CPython wouldn’t bother with real OS threads — it could have simulated concurrency with simple user-level green threads.&lt;/p&gt;

&lt;p&gt;But CPython uses real &lt;code&gt;pthreads&lt;/code&gt; for a reason: threads matter in Python.&lt;/p&gt;
&lt;h3&gt;
  
  
  Layer 2: “Threads Only Help for I/O”
&lt;/h3&gt;

&lt;p&gt;A slightly more sophisticated version of this is:&lt;/p&gt;

&lt;p&gt;“Threads are only useful when something blocks on I/O. Otherwise the GIL stops everything.”&lt;/p&gt;

&lt;p&gt;This sounds reasonable — but it’s still not right.&lt;/p&gt;

&lt;p&gt;And we already saw that in the very beginning: our Python process was happily using 300% CPU, with no I/O involved at all.&lt;/p&gt;

&lt;p&gt;So clearly something else is going on.&lt;/p&gt;
&lt;h3&gt;
  
  
  Layer 3: “Threads Help for I/O &lt;em&gt;or&lt;/em&gt; Native Code — So Problem Solved?”
&lt;/h3&gt;

&lt;p&gt;Once people realize threads aren’t limited to I/O, they usually expand the model:&lt;/p&gt;

&lt;p&gt;“Okay, fine — if a thread is blocked on I/O or running C code, another thread can run. That’s the whole story.”&lt;/p&gt;

&lt;p&gt;Closer, but still not exactly. Because here’s another catch:&lt;/p&gt;

&lt;p&gt;Not &lt;em&gt;all&lt;/em&gt; C libraries release the GIL. &lt;em&gt;Some&lt;/em&gt; explicitly request it.&lt;/p&gt;

&lt;p&gt;And when they do, the interpreter goes right back to single-threaded behavior, even though no Python code is executing.&lt;/p&gt;

&lt;p&gt;This is why you can see one native operation scale beautifully across cores, while another operation — also written in C — completely freezes out every other thread in the process.&lt;/p&gt;
&lt;h3&gt;
  
  
  Layer 4: Why Some C Libraries Must Hold the GIL
&lt;/h3&gt;

&lt;p&gt;So, the “I/O or C computation frees another thread” intuition is still incomplete.&lt;br&gt;
It depends entirely on what the C library is doing under the hood... and whether it needs exclusive access to Python objects or interpreter state.&lt;/p&gt;

&lt;p&gt;Every Python object — even something as small as an &lt;code&gt;int&lt;/code&gt; or a &lt;code&gt;bytes&lt;/code&gt; object — carries a reference count that tracks how many places are using it.&lt;br&gt;
Incrementing and decrementing these counts must happen one at a time.&lt;br&gt;
If two threads updated them independently, you’d get leaked objects, prematurely freed objects, or corrupted memory.&lt;/p&gt;

&lt;p&gt;But refcounts are only the beginning.&lt;/p&gt;

&lt;p&gt;Any C code that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;creates or destroys Python objects&lt;/li&gt;
&lt;li&gt;mutates a Python object (appending to a &lt;code&gt;list&lt;/code&gt;, updating a &lt;code&gt;dict&lt;/code&gt;, modifying a &lt;code&gt;set&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;raises exceptions&lt;/li&gt;
&lt;li&gt;calls back into Python code&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;…relies on parts of the interpreter that assume exclusive access.&lt;/p&gt;

&lt;p&gt;And the only way to guarantee that is to hold the GIL.&lt;/p&gt;

&lt;p&gt;This is why one C extension can run happily across multiple cores while another — also written in C — blocks every thread in the process.&lt;br&gt;
It depends entirely on whether that extension needs to interact with Python objects or whether it can work on its own data structures without consulting the interpreter.&lt;/p&gt;
&lt;h3&gt;
  
  
  Layer 5: Why “Just Use Multiprocessing” Is an Oversimplification
&lt;/h3&gt;

&lt;p&gt;When people get frustrated with the GIL, the next instinct is usually: “Okay, forget threads. Just use multiprocessing.”&lt;/p&gt;

&lt;p&gt;On paper it sounds perfect — each process has its own GIL, so you get true parallelism, and sometimes that is the right approach.&lt;/p&gt;

&lt;p&gt;But as a general rule, it’s still an oversimplification.&lt;/p&gt;

&lt;p&gt;Multiprocessing has real costs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Most data still has to be serialized to move between processes (Shared memory only works for a limited set of data types)&lt;/li&gt;
&lt;li&gt;Even with copy-on-write on Unix, large Python objects often end up duplicated anyway — refcount updates alone are enough to break CoW&lt;/li&gt;
&lt;li&gt; Starting a process has much higher overhead than starting a thread (a new interpreter instance, new memory space, new OS structures — not just a new stack)&lt;/li&gt;
&lt;li&gt;Context switching between processes is heavier for the OS than between threads&lt;/li&gt;
&lt;li&gt;Coordinating state is harder when it can’t be freely shared&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So yes — multiprocessing sidesteps the GIL for CPU-bound Python bytecode.&lt;br&gt;
But it brings fully separate runtimes, higher startup costs, heavier context switches, and more complicated data sharing.&lt;/p&gt;
&lt;h3&gt;
  
  
  How to Tell Whether a C Library Releases the GIL
&lt;/h3&gt;

&lt;p&gt;At this point a natural question comes up:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;“So how do I know whether a C library actually releases the GIL?”&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Unfortunately, you can’t always tell from the outside — two libraries that look identical in Python can have completely different GIL behavior.&lt;br&gt;
But there are a few practical ways to reason about it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. If a C extension touches Python objects, it must hold the GIL — but only for those parts&lt;/strong&gt;&lt;br&gt;
This doesn’t mean it needs the GIL for the entire function.&lt;br&gt;
A well-written C extension typically does this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Acquire GIL&lt;/li&gt;
&lt;li&gt;Inspect or convert Python inputs (refcounts, type checks, copies, etc.)&lt;/li&gt;
&lt;li&gt;Release GIL&lt;/li&gt;
&lt;li&gt;Run the heavy native computation&lt;/li&gt;
&lt;li&gt;Reacquire GIL&lt;/li&gt;
&lt;li&gt;Build Python output objects&lt;/li&gt;
&lt;li&gt;Return&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;touching Python objects → requires the GIL&lt;/li&gt;
&lt;li&gt;heavy inner computation → often does not&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is why you can see “C code + threads” giving real multi-core speedups despite Python objects being involved at the boundaries.&lt;/p&gt;

&lt;p&gt;Some of the standard library’s own C extensions follow this pattern.&lt;br&gt;
For example, &lt;code&gt;zlib&lt;/code&gt;, &lt;code&gt;bz2&lt;/code&gt;, and &lt;code&gt;hashlib&lt;/code&gt; all parse Python arguments and allocate Python objects while holding the GIL, then drop the GIL around the inner compression or hashing loop, and reacquire it only to wrap up the result.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Pure native computation usually releases the GIL cleanly&lt;br&gt;
Libraries like:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;zlib / bz2 / lzma&lt;/li&gt;
&lt;li&gt;hashing&lt;/li&gt;
&lt;li&gt;crypto&lt;/li&gt;
&lt;li&gt;NumPy (when you do &lt;code&gt;array1 + array2&lt;/code&gt;, NumPy releases the GIL around the actual arithmetic loop. But when you do something like &lt;code&gt;array.tolist()&lt;/code&gt;, it can't because it's creating Python objects)&lt;/li&gt;
&lt;li&gt;many image codecs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;operate on raw buffers or on their own internal data structures.&lt;br&gt;
They typically wrap the expensive part with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;Py_BEGIN_ALLOW_THREADS&lt;/span&gt;
    &lt;span class="cm"&gt;/* heavy computation */&lt;/span&gt;
&lt;span class="n"&gt;Py_END_ALLOW_THREADS&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is how you get things like “Python using 300% CPU” from a single process.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Some native libraries can’t release the GIL much — because their core work is Python interaction&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;regex engines working on Python string objects (standard &lt;code&gt;re&lt;/code&gt; module &lt;a href="https://bugs.python.org/issue1366311" rel="noopener noreferrer"&gt;doesn't release GIL&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Python-level parsing loops (e.g. &lt;code&gt;json&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;libraries that mutate lists or dicts internally (e.g. &lt;code&gt;py-radix&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;things that allocate many intermediate Python objects (e.g. &lt;code&gt;csv&lt;/code&gt;, &lt;code&gt;json&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These must hold the GIL almost the whole time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Docs might mention it — but inconsistently&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Some libraries explicitly say “this releases the GIL,” but many don’t. (e.g. &lt;a href="https://numpy.org/doc/stable/reference/thread_safety.html" rel="noopener noreferrer"&gt;NumPy&lt;/a&gt;, &lt;a href="https://docs.scipy.org/doc/scipy-1.15.0/tutorial/thread_safety.html" rel="noopener noreferrer"&gt;SciPy&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;If documentation does say it, you can trust it.&lt;br&gt;
If it doesn’t—no conclusion can be drawn.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. You can measure it&lt;/strong&gt;&lt;br&gt;
A simple test:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;run the operation in many threads&lt;/li&gt;
&lt;li&gt;watch CPU usage in top or htop&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you see a single core saturated → it's likely holding the GIL&lt;br&gt;
If you see multiple cores fully used → it's releasing the GIL&lt;br&gt;
If you see partial scaling → it's partial GIL release (very common)&lt;/p&gt;

&lt;h3&gt;
  
  
  With Big Power Comes Big Responsibility
&lt;/h3&gt;

&lt;p&gt;Releasing the GIL — or running code that doesn’t use it — doesn’t magically make concurrency “safe.” It just gives you more freedom and more ways to shoot yourself in the foot.&lt;/p&gt;

&lt;p&gt;If two threads start modifying the same NumPy array, or the same shared buffer, or the same Python object through a C extension, nothing protects you anymore. You can get classic race conditions, torn writes, and inconsistent data just like in any other multithreaded language.&lt;/p&gt;

&lt;p&gt;The GIL wasn’t only a limitation — it was also a guardrail.&lt;br&gt;
Once it’s out of the way, you have to be just as careful as you would be in C++ or Java.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The GIL has sharp edges and real limitations — but it also hides a lot of complexity and keeps Python usable without turning every piece of code into a concurrency puzzle.&lt;/p&gt;

&lt;p&gt;Once you understand the layers behind it, the whole picture becomes much clearer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;threads aren’t “useless,”&lt;/li&gt;
&lt;li&gt;I/O isn’t the whole story,&lt;/li&gt;
&lt;li&gt;native code can run in parallel,&lt;/li&gt;
&lt;li&gt;native code sometimes can’t,&lt;/li&gt;
&lt;li&gt;and “just use multiprocessing” solves one problem while introducing several others.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In practice, Python can make excellent use of multiple cores — it just depends on what kind of work you’re doing and which libraries you’re using.&lt;/p&gt;

&lt;p&gt;I’d love to hear your own experiences:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;times when &lt;code&gt;multiprocessing&lt;/code&gt; was total overkill,&lt;/li&gt;
&lt;li&gt;or when a single Python process maxed out all your cores&lt;/li&gt;
&lt;li&gt;or situations where threads surprised you (for better or worse)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Leave a comment, share a story, or correct me if I got something wrong — the whole point of this post is to make the conversation around the GIL more honest and less magical.&lt;/p&gt;

&lt;h2&gt;
  
  
  tl;dr
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Your workload is:
├─ Pure Python computation → multiprocessing
├─ I/O bound (network, disk) → threading or asyncio
├─ Calling C libraries
│  ├─ Don't know if GIL-safe → measure with top/htop or check docs
│  ├─ Releases GIL → threading is fine
│  └─ Requests GIL → multiprocessing or asyncio
└─ Many small tasks → consider overhead cost
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>python</category>
      <category>gil</category>
      <category>performance</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
