<?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: Dhanush Kumar Sivaji</title>
    <description>The latest articles on DEV Community by Dhanush Kumar Sivaji (@dhanushkumarsivaji).</description>
    <link>https://dev.to/dhanushkumarsivaji</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%2F909684%2Ff0e49f96-ee40-4b7b-a54a-f6694ecf40f1.png</url>
      <title>DEV Community: Dhanush Kumar Sivaji</title>
      <link>https://dev.to/dhanushkumarsivaji</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dhanushkumarsivaji"/>
    <language>en</language>
    <item>
      <title>The path to AI FDE #05: AI, ML and LLMs in plain words, and how an LLM writes</title>
      <dc:creator>Dhanush Kumar Sivaji</dc:creator>
      <pubDate>Fri, 23 Oct 2026 17:16:40 +0000</pubDate>
      <link>https://dev.to/dhanushkumarsivaji/the-path-to-ai-fde-05-ai-ml-and-llms-in-plain-words-and-how-an-llm-writes-370h</link>
      <guid>https://dev.to/dhanushkumarsivaji/the-path-to-ai-fde-05-ai-ml-and-llms-in-plain-words-and-how-an-llm-writes-370h</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on my newsletter, The Path to AI FDE. &lt;a href="https://dhanushkumarsivaji.substack.com/subscribe" rel="noopener noreferrer"&gt;Subscribe free&lt;/a&gt; to get each issue by email.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This is a &lt;strong&gt;“From Zero” issue&lt;/strong&gt;. If you’ve never written code or used an AI API, start here. If you’re a developer, the second half (how text actually gets generated) explains most of the weird behaviour you’ll meet later. No maths and no code: just the words people throw around, and what’s going on underneath.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjlmmqf5byrmyhjjawmb7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjlmmqf5byrmyhjjawmb7.png" alt="Two panels. Part A, the words: nested boxes showing AI containing machine learning, containing deep learning, containing large language models, plus cards on foundation models, generative AI and hallucinations. Part B, how an LLM writes: text split into tokens, a ranked list of likely next tokens, a loop that adds one token at a time, a temperature dial, training stages, and a reminder that each call starts with no memory" width="800" height="1145"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Part A · The words, in plain English
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1773g98uo615wad72qgq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1773g98uo615wad72qgq.png" alt="Nested, not synonyms: artificial intelligence contains machine learning, which contains deep learning, which contains large language models, each with everyday examples" width="800" height="709"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. AI is the broad umbrella.&lt;/strong&gt; The OECD’s definition, widely cited in AI policy, boils down to this: an AI system is a machine that works out, from the input it gets, how to produce outputs such as predictions, content, recommendations or decisions. Systems differ in how independently they act and whether they keep adapting after launch.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb5yaxhuffm6s0599h9ue.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb5yaxhuffm6s0599h9ue.png" alt="Rules vs learning from examples: a spam filter built from hand-written rules next to one trained on labelled emails, plus Google's four ML families: supervised, unsupervised, reinforcement and generative AI" width="800" height="629"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Machine learning means learning from examples instead of hand-written rules.&lt;/strong&gt; You &lt;em&gt;train&lt;/em&gt; a piece of software called a &lt;strong&gt;model&lt;/strong&gt; on data, and it learns to make predictions or generate content. Google’s crash course sorts ML into four families: supervised (learning from labelled examples), unsupervised (finding patterns without labels), reinforcement (learning from rewards) and generative AI.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Deep learning is ML with large neural networks.&lt;/strong&gt; Many-layered networks learn their own features from raw input (pixels, characters) instead of humans designing them. The approach took off around 2010 and powers nearly everything that follows.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. A large language model (LLM) is a deep network trained on a huge amount of text to predict what comes next.&lt;/strong&gt; Do that across a large slice of the written internet, and the model picks up grammar, facts, style and a surprising amount of reasoning-like behaviour along the way.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiul3yf5krkvo71etpcoc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiul3yf5krkvo71etpcoc.png" alt="Foundation models and generative AI: one big model trained on broad data feeds many tasks; a generative model writes a new paragraph while a fraud detector only labels a payment" width="800" height="531"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. A foundation model is one big model reused for many jobs.&lt;/strong&gt; Researchers at Stanford coined the term in 2021 for models trained on broad data at scale and then adapted to a wide range of tasks. That’s why companies build products &lt;em&gt;on top of&lt;/em&gt; a few such models instead of training their own.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Generative AI creates new content.&lt;/strong&gt; Text, images, audio or code from your input, instead of just a label. Every LLM chatbot is generative AI, but not all AI is generative: a fraud detector that says “suspicious: yes/no” isn’t.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5v5xwe514j5tjg2e8coj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5v5xwe514j5tjg2e8coj.png" alt="Hallucinations: a confident wrong birthday; why guessing beats " width="800" height="653"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7. A hallucination is a confident answer that’s wrong.&lt;/strong&gt; OpenAI defines it as a plausible but false statement. Part of the cause is that training and evaluation reward a good guess over “I don’t know”, and rare facts, like one person’s birthday, can’t be inferred from patterns in text. So an LLM is a brilliant pattern engine, not a database of verified facts. This is why later issues ground answers in real documents.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part B · How an LLM writes text (no maths)
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fduc55q5ovlojx19erfyy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fduc55q5ovlojx19erfyy.png" alt="Tokens in, scores out: " width="800" height="594"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;8. Text becomes tokens first.&lt;/strong&gt; The model reads &lt;strong&gt;tokens&lt;/strong&gt;: common words, word pieces and punctuation, not letters or whole words. Everything is counted in tokens, including limits and prices.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;9. At each step it scores every possible next token.&lt;/strong&gt; Given everything so far, each token in the vocabulary gets a likelihood. (The figure’s bars are illustrative, not from a real model.)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1qkfmesu4002krxsoa6o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1qkfmesu4002krxsoa6o.png" alt="How an LLM writes: a loop of score, pick and append that repeats until an end token, stop sequence or length limit; low temperature is focused, high temperature is varied" width="800" height="646"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;10. It writes one token at a time, in a loop.&lt;/strong&gt; Each new token depends on all the ones before it, and the loop stops at a special end token, a stop sequence you set, or a length limit. That’s why answers stream in word by word, and why long answers take longer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;11. A little randomness is on purpose.&lt;/strong&gt; The model doesn’t always take the top choice. &lt;strong&gt;Temperature&lt;/strong&gt; is the dial (higher is more varied, lower more focused), and settings like top-p limit picks to the most likely candidates. So the same question can give different answers, and that’s normal.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fedzjh4vcgn6oc5hflsht.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fedzjh4vcgn6oc5hflsht.png" alt="Two training stages: pre-training on raw text to predict the next token with no human labels, then tuning on examples and feedback to follow instructions, as in InstructGPT (2022)" width="800" height="477"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;12. Two training stages: read a lot, then learn to help.&lt;/strong&gt; Pre-training is self-supervised: the model reads raw text and learns to predict the next token, no human labelling needed. Then it’s tuned on examples and feedback so it follows instructions instead of just continuing text. InstructGPT (2022) was an early public example.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0pn1b3jiurze36xl7n5i.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0pn1b3jiurze36xl7n5i.png" alt="Every call starts from zero: on turn 2 the chat app resends turn 1 so the model can answer " width="799" height="487"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;13. Every call starts from zero.&lt;/strong&gt; The model doesn’t remember your last message. Chat apps feel continuous because they resend the whole conversation with each request. (That’s the stateless API from #01: memory is the app’s job, not the model’s.)&lt;/p&gt;




&lt;h3&gt;
  
  
  Try it (10 minutes, no code)
&lt;/h3&gt;

&lt;p&gt;In any chatbot, ask the same open question three times in fresh chats (”Give me a name for a bakery”) and compare the answers. Then ask something obscure and specific, such as a small-town council member’s birthday, and notice how confident the answer sounds. You’ve just seen points 11 and 7.&lt;/p&gt;

&lt;h3&gt;
  
  
  Common beginner mistakes
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Treating “AI”, “ML” and “LLM” as synonyms. They’re nested, not the same.&lt;/li&gt;
&lt;li&gt;Assuming the model looks facts up. By default it predicts; it doesn’t retrieve.&lt;/li&gt;
&lt;li&gt;Thinking the chatbot remembers you. The app resends history.&lt;/li&gt;
&lt;li&gt;Counting words instead of tokens when estimating limits or cost.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Takeaway
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;An LLM is a next-token predictor, trained on a lot of text and then tuned to be helpful. It writes one token at a time with a bit of chosen randomness, and it remembers nothing between calls.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where this fits in my roadmap:&lt;/strong&gt; this is on-ramp material, not part of my current study block. I already work with these ideas as a TypeScript engineer, so I’m writing it for readers starting from zero. It also gives later issues shared vocabulary. The deeper versions (tokenizers, context windows, prefill and decode, sampling, how training works) come in Section 6, once my roadmap gets there.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Over to you:&lt;/strong&gt; which AI term did you nod along to for longest before you really knew what it meant?&lt;/p&gt;




&lt;h3&gt;
  
  
  Sources
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;OECD.AI: Updates to the OECD’s definition of an AI system explained (2023). &lt;a href="https://oecd.ai/en/wonk/ai-system-definition-update" rel="noopener noreferrer"&gt;https://oecd.ai/en/wonk/ai-system-definition-update&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Google ML Crash Course: What is machine learning? (training a model on data; supervised, unsupervised, reinforcement, generative AI). &lt;a href="https://developers.google.com/machine-learning/intro-to-ml/what-is-ml" rel="noopener noreferrer"&gt;https://developers.google.com/machine-learning/intro-to-ml/what-is-ml&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Bommasani et al., On the Opportunities and Risks of Foundation Models (Stanford CRFM, 2021): defines “foundation model”; history of deep learning. &lt;a href="https://arxiv.org/abs/2108.07258" rel="noopener noreferrer"&gt;https://arxiv.org/abs/2108.07258&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Hugging Face LLM Course, Ch. 1: How do Transformers work? (self-supervised pre-training on raw text; causal language modelling; fine-tuning; InstructGPT, Jan 2022). &lt;a href="https://huggingface.co/learn/llm-course/chapter1/4" rel="noopener noreferrer"&gt;https://huggingface.co/learn/llm-course/chapter1/4&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Hugging Face LLM Course, Ch. 1: Deep dive into text generation inference (one token at a time; temperature, top-p, top-k; stop conditions). &lt;a href="https://huggingface.co/learn/llm-course/chapter1/8" rel="noopener noreferrer"&gt;https://huggingface.co/learn/llm-course/chapter1/8&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Hugging Face LLM Course, Ch. 2: Tokenizers (”token” + “ization”). &lt;a href="https://huggingface.co/learn/llm-course/chapter2/4" rel="noopener noreferrer"&gt;https://huggingface.co/learn/llm-course/chapter2/4&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;OpenAI: Why language models hallucinate (Sep 5, 2025). &lt;a href="https://openai.com/index/why-language-models-hallucinate/" rel="noopener noreferrer"&gt;https://openai.com/index/why-language-models-hallucinate/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Anthropic: Working with the Messages API (stateless; resend the conversation). &lt;a href="https://platform.claude.com/docs/en/build-with-claude/working-with-messages" rel="noopener noreferrer"&gt;https://platform.claude.com/docs/en/build-with-claude/working-with-messages&lt;/a&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;em&gt;Sources checked Oct 18, 2026.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Free: &lt;a href="https://dhanushkumarsivaji.substack.com/p/the-path-to-ai-fde-the-free-roadmap" rel="noopener noreferrer"&gt;the 5-page roadmap from Novice to AI FDE&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If this was useful, &lt;a href="https://dhanushkumarsivaji.substack.com/subscribe" rel="noopener noreferrer"&gt;subscribe to The Path to AI FDE&lt;/a&gt; for the next issue.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>machinelearning</category>
      <category>programming</category>
    </item>
    <item>
      <title>The path to AI FDE #04: Many calls at once, and a FastAPI service you can lean on</title>
      <dc:creator>Dhanush Kumar Sivaji</dc:creator>
      <pubDate>Thu, 22 Oct 2026 17:14:51 +0000</pubDate>
      <link>https://dev.to/dhanushkumarsivaji/the-path-to-ai-fde-04-many-calls-at-once-and-a-fastapi-service-you-can-lean-on-22l9</link>
      <guid>https://dev.to/dhanushkumarsivaji/the-path-to-ai-fde-04-many-calls-at-once-and-a-fastapi-service-you-can-lean-on-22l9</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on my newsletter, The Path to AI FDE. &lt;a href="https://dhanushkumarsivaji.substack.com/subscribe" rel="noopener noreferrer"&gt;Subscribe free&lt;/a&gt; to get each issue by email.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Issue #03 ended with tasks: the way async work actually overlaps. Real AI backends fan out, calling a model several times or fetching a few sources in parallel. So this issue starts with how to do that without losing errors or hanging forever. Then it turns to the FastAPI features that move an endpoint past hello-world. As always, every example is a toy one (a notes app), not my roadmap exercises.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F14l7frqyjv1dhqiujmcu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F14l7frqyjv1dhqiujmcu.png" alt="Two panels. Part A, many calls at once: six cards on gather, TaskGroup, timeouts, semaphores and cancellation. Part B, FastAPI properly: six cards on request and response models, HTTPException, dependencies, def versus async def, and settings" width="800" height="1118"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Part A · Many calls at once
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F41v148xpxsngtj1ul9b6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F41v148xpxsngtj1ul9b6.png" alt="gather: three fetches finish in a different order than they were started, but gather returns results in argument order; by default the first error is raised and the others keep running, while return_exceptions=True puts errors in the list" width="800" height="676"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1.&lt;/strong&gt; &lt;code&gt;gather&lt;/code&gt; &lt;strong&gt;returns results in the order you asked.&lt;/strong&gt; &lt;code&gt;a, b = await asyncio.gather(fetch(1), fetch(2))&lt;/code&gt; runs both concurrently, and the result list matches the order of the arguments, not the order the calls finished in. It’s the closest thing to &lt;code&gt;Promise.all&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Know what&lt;/strong&gt; &lt;code&gt;gather&lt;/code&gt; &lt;strong&gt;does with an error.&lt;/strong&gt; By default, the first exception is raised straight to you, and the other calls are &lt;em&gt;not&lt;/em&gt; cancelled: they keep running in the background. With &lt;code&gt;return_exceptions=True&lt;/code&gt;, errors come back as items in the result list instead, which suits best-effort work.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Faon0vs0x7yd69lxl5ynr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Faon0vs0x7yd69lxl5ynr.png" alt="TaskGroup: when one of three tasks fails the other two are cancelled, and the failures are raised together as an ExceptionGroup handled with except-star" width="800" height="553"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3.&lt;/strong&gt; &lt;code&gt;TaskGroup&lt;/code&gt; &lt;strong&gt;is all-or-nothing.&lt;/strong&gt; Inside &lt;code&gt;async with asyncio.TaskGroup() as tg:&lt;/code&gt;, you start work with &lt;code&gt;tg.create_task(...)&lt;/code&gt;. The first failure cancels the rest, and when the block exits the failures are raised together as an &lt;code&gt;ExceptionGroup&lt;/code&gt;, which you handle with &lt;code&gt;except* SomeError:&lt;/code&gt;. Use it when partial results are useless. It also keeps task references for you (#03, point 11).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5epbw834sctebnw3p7ey.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5epbw834sctebnw3p7ey.png" alt="Every network wait needs a deadline: a slow call is cancelled at 10 seconds and TimeoutError is raised; catch it outside the async with asyncio.timeout block, not inside" width="800" height="647"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Every network wait needs a deadline.&lt;/strong&gt; &lt;code&gt;async with asyncio.timeout(10):&lt;/code&gt; cancels whatever is inside after 10 seconds and raises &lt;code&gt;TimeoutError&lt;/code&gt;, which you can only catch &lt;em&gt;outside&lt;/em&gt; the block. For a single awaitable, &lt;code&gt;await asyncio.wait_for(coro, timeout=10)&lt;/code&gt; does the same job. Without a deadline, one slow provider call can hold a request open indefinitely.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fme3d746o05vbpyqydc11.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fme3d746o05vbpyqydc11.png" alt="A semaphore caps how many run at once: items wait in a queue while at most five calls are in flight in Semaphore(5)'s slots" width="800" height="552"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. A semaphore caps how many run at once.&lt;/strong&gt; &lt;code&gt;sem = asyncio.Semaphore(5)&lt;/code&gt;, then &lt;code&gt;async with sem:&lt;/code&gt; around each call, lets at most five run concurrently while the rest wait their turn. That’s how you fan out over 200 items while keeping concurrency low enough to stay under a provider’s rate limit (the 429s from #01).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3i0ai1rswix3w0sl3mig.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3i0ai1rswix3w0sl3mig.png" alt="Don't swallow cancellation: timeout and TaskGroup cancel a task, CancelledError must travel up; catching BaseException and passing hides it, while catching specific errors with try/finally lets it through" width="800" height="608"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Don’t swallow cancellation.&lt;/strong&gt; &lt;code&gt;TaskGroup&lt;/code&gt; and &lt;code&gt;timeout()&lt;/code&gt; work by cancelling tasks, and the asyncio docs warn that they can misbehave if your code catches and hides &lt;code&gt;asyncio.CancelledError&lt;/code&gt;. A bare &lt;code&gt;except:&lt;/code&gt;, an &lt;code&gt;except BaseException:&lt;/code&gt; or a re-raise you forgot quietly breaks your deadlines. (&lt;code&gt;except Exception:&lt;/code&gt; doesn’t catch it: since Python 3.8, &lt;code&gt;CancelledError&lt;/code&gt; is a &lt;code&gt;BaseException&lt;/code&gt;.) Catch specific errors (#02, point 4).&lt;/p&gt;

&lt;h2&gt;
  
  
  Part B · FastAPI properly
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsoz9710gp3x32oy6ioyf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsoz9710gp3x32oy6ioyf.png" alt="FastAPI validates both directions: a toy notes app request passes NoteIn validation (invalid gives 422), the handler, and a NoteOut response model that drops owner_id; HTTPException returns a 404 for a missing note" width="800" height="559"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7. Request models give you 422s for free.&lt;/strong&gt; Declare a body parameter as a Pydantic model (&lt;code&gt;note: NoteIn&lt;/code&gt;), and FastAPI validates the request, answering &lt;code&gt;422&lt;/code&gt; with a list of what’s wrong before your function even runs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;8. Response models validate and filter what goes out.&lt;/strong&gt; Set a return type or &lt;code&gt;response_model=NoteOut&lt;/code&gt;, and FastAPI checks your output and drops any field the model doesn’t declare. That’s the docs’ security point: an internal field (an owner ID, a password hash) can’t leak just because it was on the object you returned. If you set both, &lt;code&gt;response_model&lt;/code&gt; takes priority.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;9.&lt;/strong&gt; &lt;code&gt;HTTPException&lt;/code&gt; &lt;strong&gt;for errors you mean.&lt;/strong&gt; &lt;code&gt;raise HTTPException(status_code=404, detail="Note not found")&lt;/code&gt; stops the handler and returns that status with a JSON body. Raise it for expected failures: a missing item, a bad state, an upstream provider that’s down.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5oc2t692zoaw5zcvagng.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5oc2t692zoaw5zcvagng.png" alt="Dependencies: get_db, get_current_user and get_settings feed an endpoint through an Annotated Depends alias; settings read from environment variables are validated by BaseSettings, cached with lru_cache and overridden in tests" width="800" height="700"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;10. Dependencies hold the shared plumbing.&lt;/strong&gt; A dependency is a function FastAPI calls for you before your endpoint runs, declared as a parameter with &lt;code&gt;Depends(get_db)&lt;/code&gt;, usually through an &lt;code&gt;Annotated&lt;/code&gt; alias so it’s written once. Shared logic, database sessions and auth live there instead of in every handler. Dependencies can be &lt;code&gt;async def&lt;/code&gt; or plain &lt;code&gt;def&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy60guuf4hagwtui6z63q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy60guuf4hagwtui6z63q.png" alt="def vs async def endpoints: async def runs on the shared event loop and suits async clients; plain def runs in an external threadpool and suits synchronous libraries; avoid blocking calls inside async def" width="799" height="527"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;11.&lt;/strong&gt; &lt;code&gt;def&lt;/code&gt; &lt;strong&gt;versus&lt;/strong&gt; &lt;code&gt;async def&lt;/code&gt; &lt;strong&gt;endpoints.&lt;/strong&gt; An &lt;code&gt;async def&lt;/code&gt; endpoint runs on the event loop, so anything blocking inside it stalls every request (#03, point 12). A plain &lt;code&gt;def&lt;/code&gt; endpoint runs in a threadpool instead, so blocking calls there are survivable. Rule of thumb: &lt;code&gt;async def&lt;/code&gt; with async clients, plain &lt;code&gt;def&lt;/code&gt; when your libraries are synchronous.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;12. Settings come from the environment, validated.&lt;/strong&gt; A &lt;code&gt;pydantic-settings&lt;/code&gt; &lt;code&gt;BaseSettings&lt;/code&gt; class reads environment variables (case-insensitively) and validates their types when it’s created. A missing API key or &lt;code&gt;TIMEOUT_S=abc&lt;/code&gt; fails right there, not halfway through a request that uses it: at startup if you create the settings at import time, or on first use if you serve them the way the FastAPI docs do, from an &lt;code&gt;@lru_cache&lt;/code&gt;‘d &lt;code&gt;get_settings()&lt;/code&gt; dependency. That dependency also lets tests swap settings with &lt;code&gt;app.dependency_overrides&lt;/code&gt;.&lt;/p&gt;




&lt;h3&gt;
  
  
  Try it (20 minutes, toy example)
&lt;/h3&gt;

&lt;p&gt;Make &lt;code&gt;async def fetch(i)&lt;/code&gt; that sleeps for a random 0–2 seconds and sometimes raises. Run 20 of them three ways: &lt;code&gt;gather&lt;/code&gt;, &lt;code&gt;gather(return_exceptions=True)&lt;/code&gt; and a &lt;code&gt;TaskGroup&lt;/code&gt;. Then add &lt;code&gt;Semaphore(5)&lt;/code&gt; and a 1-second &lt;code&gt;timeout&lt;/code&gt;, and compare what you get back each time. For the FastAPI half, give a toy notes app a &lt;code&gt;NoteOut&lt;/code&gt; model without an internal &lt;code&gt;owner_id&lt;/code&gt; field, return an object that has one, and check that it doesn’t appear in the response.&lt;/p&gt;

&lt;h3&gt;
  
  
  Common beginner mistakes
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Assuming &lt;code&gt;gather&lt;/code&gt; stops the other calls when one fails.&lt;/li&gt;
&lt;li&gt;Writing &lt;code&gt;try/except TimeoutError&lt;/code&gt; &lt;em&gt;inside&lt;/em&gt; &lt;code&gt;async with asyncio.timeout()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Fanning out with no concurrency limit and getting rate-limited.&lt;/li&gt;
&lt;li&gt;Returning a database object without a response model, and leaking fields.&lt;/li&gt;
&lt;li&gt;Doing blocking calls in &lt;code&gt;async def&lt;/code&gt; endpoints.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Takeaway
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Fan out with a limit and a deadline. Let FastAPI validate both directions, and keep shared plumbing in dependencies.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where this fits in my roadmap:&lt;/strong&gt; this is the second half of my foundations block (async in practice, plus FastAPI and Pydantic). In my own study, I’m building that block’s small LLM endpoint myself, with real timeouts and error handling. So this post sticks to the concepts and toy examples, not my solution. Next comes testing (pytest, plus mocking the provider), and then structured outputs, where the Pydantic schema from #03 becomes the contract the model must follow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Over to you:&lt;/strong&gt; &lt;code&gt;gather(return_exceptions=True)&lt;/code&gt; or &lt;code&gt;TaskGroup&lt;/code&gt;: which do you reach for by default, and why?&lt;/p&gt;




&lt;h3&gt;
  
  
  Sources
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Python docs: Coroutines and Tasks (&lt;code&gt;gather&lt;/code&gt; order, errors and &lt;code&gt;return_exceptions&lt;/code&gt;; &lt;code&gt;TaskGroup&lt;/code&gt; cancellation and &lt;code&gt;ExceptionGroup&lt;/code&gt;; &lt;code&gt;timeout()&lt;/code&gt; raising &lt;code&gt;TimeoutError&lt;/code&gt; catchable only outside; &lt;code&gt;wait_for&lt;/code&gt;; the warning against swallowing &lt;code&gt;CancelledError&lt;/code&gt;). &lt;a href="https://docs.python.org/3/library/asyncio-task.html" rel="noopener noreferrer"&gt;https://docs.python.org/3/library/asyncio-task.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Python tutorial 8.9: Raising and handling multiple unrelated exceptions (&lt;code&gt;ExceptionGroup&lt;/code&gt;, &lt;code&gt;except*&lt;/code&gt;). &lt;a href="https://docs.python.org/3/tutorial/errors.html" rel="noopener noreferrer"&gt;https://docs.python.org/3/tutorial/errors.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Python docs: Synchronization primitives (&lt;code&gt;Semaphore&lt;/code&gt;). &lt;a href="https://docs.python.org/3/library/asyncio-sync.html" rel="noopener noreferrer"&gt;https://docs.python.org/3/library/asyncio-sync.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;FastAPI: Request Body (Pydantic models; invalid data returns an error). &lt;a href="https://fastapi.tiangolo.com/tutorial/body/" rel="noopener noreferrer"&gt;https://fastapi.tiangolo.com/tutorial/body/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;FastAPI: Response Model (validates returned data; filters output to the declared type for security; &lt;code&gt;response_model&lt;/code&gt; priority). &lt;a href="https://fastapi.tiangolo.com/tutorial/response-model/" rel="noopener noreferrer"&gt;https://fastapi.tiangolo.com/tutorial/response-model/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;FastAPI: Handling Errors (&lt;code&gt;HTTPException&lt;/code&gt;; &lt;code&gt;RequestValidationError&lt;/code&gt; and 422). &lt;a href="https://fastapi.tiangolo.com/tutorial/handling-errors/" rel="noopener noreferrer"&gt;https://fastapi.tiangolo.com/tutorial/handling-errors/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;FastAPI: Dependencies (&lt;code&gt;Depends&lt;/code&gt;, shared logic, DB connections, auth; &lt;code&gt;Annotated&lt;/code&gt; aliases; &lt;code&gt;async def&lt;/code&gt; or &lt;code&gt;def&lt;/code&gt;). &lt;a href="https://fastapi.tiangolo.com/tutorial/dependencies/" rel="noopener noreferrer"&gt;https://fastapi.tiangolo.com/tutorial/dependencies/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;FastAPI: Concurrency and async/await (plain &lt;code&gt;def&lt;/code&gt; path operations run in an external threadpool). &lt;a href="https://fastapi.tiangolo.com/async/" rel="noopener noreferrer"&gt;https://fastapi.tiangolo.com/async/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;FastAPI: Settings and Environment Variables (&lt;code&gt;BaseSettings&lt;/code&gt;, case-insensitive env vars, &lt;code&gt;lru_cache&lt;/code&gt;, &lt;code&gt;dependency_overrides&lt;/code&gt;, &lt;code&gt;.env&lt;/code&gt;). &lt;a href="https://fastapi.tiangolo.com/advanced/settings/" rel="noopener noreferrer"&gt;https://fastapi.tiangolo.com/advanced/settings/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Python docs: asyncio Exceptions (&lt;code&gt;CancelledError&lt;/code&gt; is a &lt;code&gt;BaseException&lt;/code&gt; since 3.8; must almost always be re-raised). &lt;a href="https://docs.python.org/3/library/asyncio-exceptions.html" rel="noopener noreferrer"&gt;https://docs.python.org/3/library/asyncio-exceptions.html&lt;/a&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;em&gt;Sources checked Oct 18, 2026.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://dhanushkumarsivaji.substack.com/p/the-path-to-ai-fde-04-many-calls" rel="noopener noreferrer"&gt;The Path to AI FDE&lt;/a&gt; on Substack. Subscribe for the next issue: &lt;a href="https://dhanushkumarsivaji.substack.com/subscribe" rel="noopener noreferrer"&gt;https://dhanushkumarsivaji.substack.com/subscribe&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If this was useful, &lt;a href="https://dhanushkumarsivaji.substack.com/subscribe" rel="noopener noreferrer"&gt;subscribe to The Path to AI FDE&lt;/a&gt; for the next issue.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>fastapi</category>
      <category>programming</category>
    </item>
    <item>
      <title>The path to AI FDE #03: Python types you can trust, and async waiting done well</title>
      <dc:creator>Dhanush Kumar Sivaji</dc:creator>
      <pubDate>Wed, 21 Oct 2026 17:16:57 +0000</pubDate>
      <link>https://dev.to/dhanushkumarsivaji/the-path-to-ai-fde-03-python-types-you-can-trust-and-async-waiting-done-well-52h0</link>
      <guid>https://dev.to/dhanushkumarsivaji/the-path-to-ai-fde-03-python-types-you-can-trust-and-async-waiting-done-well-52h0</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on my newsletter, The Path to AI FDE. &lt;a href="https://dhanushkumarsivaji.substack.com/subscribe" rel="noopener noreferrer"&gt;Subscribe free&lt;/a&gt; to get each issue by email.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Issue #02 was the quick TypeScript-to-Python map. This issue goes one level deeper on the two ideas every AI backend leans on: &lt;strong&gt;types&lt;/strong&gt; (what data is allowed in) and &lt;strong&gt;async&lt;/strong&gt; (how one process waits on many slow network calls). Twelve short points, each with the trap I’d fall into first.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwljnms3ee5tlqmmysd74.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwljnms3ee5tlqmmysd74.png" alt="Two panels. Part A, types you can trust: six cards from type hints to Pydantic errors and JSON Schema. Part B, waiting done well: six cards from the event loop to not blocking it" width="800" height="1110"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Part A · Types you can trust
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3cv974k2ez77cjxm8zde.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3cv974k2ez77cjxm8zde.png" alt="Type hints: the parts of def count_words(text: str) -&gt; int, collection hints like list[str], str | None as the Python 3.10 spelling of Optional[str], and count_words(42) running anyway unless mypy or Pyright flags it" width="800" height="510"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Hints describe intent.&lt;/strong&gt; &lt;code&gt;def count_words(text: str) -&amp;gt; int&lt;/code&gt; tells readers, editors and type checkers what goes in and what comes out. Collections take their item types in brackets: &lt;code&gt;list[str]&lt;/code&gt;, &lt;code&gt;dict[str, int]&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. “Maybe missing” is spelled&lt;/strong&gt; &lt;code&gt;X | None&lt;/code&gt;&lt;strong&gt;.&lt;/strong&gt; Since Python 3.10 (PEP 604) you write &lt;code&gt;str | None&lt;/code&gt; instead of &lt;code&gt;Optional[str]&lt;/code&gt;. Trap: in Pydantic v2, &lt;code&gt;subtitle: "str | None&lt;/code&gt; with no default is still a &lt;em&gt;required&lt;/em&gt; field. Add &lt;code&gt;= None&lt;/code&gt; if callers may leave it out."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Checked before running, not while running.&lt;/strong&gt; As covered in #02, Python ignores hints at runtime, so &lt;code&gt;count_words(42)&lt;/code&gt; runs anyway and only mypy or Pyright flags it. Run one of them in CI.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwyu4ny6jxh491evjos2w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwyu4ny6jxh491evjos2w.png" alt="Pydantic models check data when it arrives: incoming data passes through a toy Book model with Field constraints and becomes a Book or a ValidationError; a table shows str | None without a default is still required" width="800" height="576"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Pydantic models check data when it arrives.&lt;/strong&gt; A model is a class that inherits from &lt;code&gt;BaseModel&lt;/code&gt;, with typed fields, and creating an instance runs validation. &lt;code&gt;Field()&lt;/code&gt; adds constraints such as &lt;code&gt;gt=0&lt;/code&gt; or &lt;code&gt;max_length=200&lt;/code&gt;. If no error is raised, the fields are guaranteed to match their types.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9r1fllrrr87xnkczcjzo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9r1fllrrr87xnkczcjzo.png" alt="Lax by default: the same input with pages as the string " width="800" height="530"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Lax by default: know what it lets through.&lt;/strong&gt; Pydantic coerces when it can (&lt;code&gt;pages="412"&lt;/code&gt; becomes the integer &lt;code&gt;412&lt;/code&gt;) and silently ignores extra keys. That’s handy for query strings and environment variables, but risky for data from a model or another service. For those, opt into &lt;code&gt;strict=True&lt;/code&gt; and &lt;code&gt;extra='forbid'&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy65814zvf4y3cpwwjd9h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy65814zvf4y3cpwwjd9h.png" alt="Errors that list everything: one ValidationError reports title, pages and tags.2 at once; model_json_schema exports the model as a JSON Schema with exclusiveMinimum for gt=0" width="800" height="529"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. One error lists every problem, and a model can describe itself.&lt;/strong&gt; A &lt;code&gt;ValidationError&lt;/code&gt; reports &lt;em&gt;all&lt;/em&gt; failing fields with their locations (such as &lt;code&gt;tags.2&lt;/code&gt;), not just the first. &lt;code&gt;Book.model_validate_json(raw)&lt;/code&gt; validates a JSON string directly, and &lt;code&gt;Book.model_json_schema()&lt;/code&gt; exports a JSON Schema, which is how structured outputs get their contract (coming later in the series).&lt;/p&gt;

&lt;h2&gt;
  
  
  Part B · Waiting done well
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhpb8dxhgx4worwwkxy3p.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhpb8dxhgx4worwwkxy3p.png" alt="Waiting, overlapped: one LLM call is mostly waiting on the provider; an event loop runs tasks A, B and C one at a time, switching whenever a task awaits a reply" width="800" height="674"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7. AI backends mostly wait.&lt;/strong&gt; An LLM call spends most of its time waiting on someone else’s server, not computing on yours. Async lets one process overlap those waits instead of standing idle through each one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;8. The event loop runs one task at a time.&lt;/strong&gt; asyncio uses cooperative scheduling: whenever the running task hits an &lt;code&gt;await&lt;/code&gt; on something not yet ready, the loop switches to another. Nothing is truly simultaneous; the overlap comes only from the waiting.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fst9qfs2j409ghhzhusrr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fst9qfs2j409ghhzhusrr.png" alt="await waits, tasks overlap: the asyncio docs' say_after example takes 3 seconds with two back-to-back awaits and about 2 seconds with two tasks" width="800" height="768"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;9.&lt;/strong&gt; &lt;code&gt;async def&lt;/code&gt; &lt;strong&gt;makes a coroutine, and&lt;/strong&gt; &lt;code&gt;asyncio.run()&lt;/code&gt; &lt;strong&gt;starts the loop.&lt;/strong&gt; &lt;code&gt;asyncio.run(main())&lt;/code&gt; is the usual entry point in a script. Calling &lt;code&gt;main()&lt;/code&gt; on its own only creates a coroutine object, which does nothing until awaited (#02, point 6). In FastAPI, the framework runs the loop for you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;10. Back-to-back awaits are still sequential.&lt;/strong&gt; This is the trap I’d hit first. In the asyncio docs’ example, &lt;code&gt;await say_after(1, ...)&lt;/code&gt; then &lt;code&gt;await say_after(2, ...)&lt;/code&gt; takes 3 seconds: async syntax alone doesn’t make anything concurrent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;11. Tasks are how work overlaps.&lt;/strong&gt; &lt;code&gt;asyncio.create_task(coro)&lt;/code&gt; schedules the coroutine to run soon, alongside the current one, which brings the same example down to about 2 seconds. Keep a reference to every task: the loop holds tasks only weakly, so an unreferenced task can be garbage-collected before it finishes. &lt;code&gt;TaskGroup&lt;/code&gt; handles that for you (next issue).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foyjee3x68xhrpsrc96o4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foyjee3x68xhrpsrc96o4.png" alt="One blocking call stalls everyone: two tasks finish in 2 seconds with await asyncio.sleep but take 3 seconds with time.sleep; asyncio.to_thread moves unavoidable blocking I/O off the loop" width="799" height="577"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;12. One blocking call stalls everyone.&lt;/strong&gt; &lt;code&gt;time.sleep(1)&lt;/code&gt; or a synchronous HTTP client inside &lt;code&gt;async def&lt;/code&gt; freezes the whole loop, while &lt;code&gt;await asyncio.sleep(1)&lt;/code&gt; always suspends just the current task. For blocking I/O you can’t avoid, &lt;code&gt;await asyncio.to_thread(fn)&lt;/code&gt; moves it off the loop.&lt;/p&gt;




&lt;h3&gt;
  
  
  Try it (15 minutes, toy example)
&lt;/h3&gt;

&lt;p&gt;Write &lt;code&gt;async def nap(n)&lt;/code&gt; that does &lt;code&gt;await asyncio.sleep(n)&lt;/code&gt;. Time &lt;code&gt;await nap(1); await nap(2)&lt;/code&gt;, then the same two calls wrapped in &lt;code&gt;create_task&lt;/code&gt;. Then swap in &lt;code&gt;time.sleep&lt;/code&gt; and watch the overlap vanish. Separately, define a &lt;code&gt;Book&lt;/code&gt; model with &lt;code&gt;title: str&lt;/code&gt; and &lt;code&gt;pages: int = Field(gt=0)&lt;/code&gt;, and feed it one good dict and one bad dict.&lt;/p&gt;

&lt;h3&gt;
  
  
  Common beginner mistakes
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Assuming hints validate input. Only Pydantic (or your own checks) does that.&lt;/li&gt;
&lt;li&gt;Writing &lt;code&gt;x: str | None&lt;/code&gt; and expecting it to be optional in Pydantic v2. It needs &lt;code&gt;= None&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Thinking &lt;code&gt;async def&lt;/code&gt; plus &lt;code&gt;await&lt;/code&gt; equals parallel. It doesn’t without tasks.&lt;/li&gt;
&lt;li&gt;Creating tasks and dropping the reference.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Takeaway
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Hints describe, Pydantic enforces.&lt;/strong&gt; &lt;code&gt;await&lt;/code&gt; &lt;strong&gt;waits; tasks overlap.&lt;/strong&gt; That’s the whole issue in two lines.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where this fits in my roadmap:&lt;/strong&gt; these are the concepts behind the foundations block of my roadmap (Python typing, errors and async; FastAPI and Pydantic models). That block’s exercises (a typed document loader, a small validated LLM endpoint and its tests) are mine to build, so nothing above is that code; the examples are deliberately toy ones. Next issue: running many calls at once safely (&lt;code&gt;gather&lt;/code&gt;, &lt;code&gt;TaskGroup&lt;/code&gt;, timeouts) and FastAPI beyond hello-world.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Over to you:&lt;/strong&gt; which bit you first: a missing &lt;code&gt;await&lt;/code&gt;, or an &lt;code&gt;await&lt;/code&gt; that should have been a task?&lt;/p&gt;




&lt;h3&gt;
  
  
  Sources
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Python docs: &lt;code&gt;typing&lt;/code&gt; (annotations aren’t enforced at runtime). &lt;a href="https://docs.python.org/3/library/typing.html" rel="noopener noreferrer"&gt;https://docs.python.org/3/library/typing.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;PEP 604: Allow writing union types as &lt;code&gt;X | Y&lt;/code&gt; (Python 3.10). &lt;a href="https://peps.python.org/pep-0604/" rel="noopener noreferrer"&gt;https://peps.python.org/pep-0604/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;mypy documentation. &lt;a href="https://mypy.readthedocs.io/en/stable/" rel="noopener noreferrer"&gt;https://mypy.readthedocs.io/en/stable/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Pyright. &lt;a href="https://github.com/microsoft/pyright" rel="noopener noreferrer"&gt;https://github.com/microsoft/pyright&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Pydantic: Models (validation on instantiation, guarantees, coercion, extra data ignored by default, &lt;code&gt;ValidationError&lt;/code&gt; lists all errors, &lt;code&gt;model_validate_json&lt;/code&gt;, &lt;code&gt;model_json_schema&lt;/code&gt;). &lt;a href="https://pydantic.dev/docs/validation/latest/concepts/models/" rel="noopener noreferrer"&gt;https://pydantic.dev/docs/validation/latest/concepts/models/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Pydantic: Fields (&lt;code&gt;Field()&lt;/code&gt; constraints such as &lt;code&gt;gt&lt;/code&gt; and &lt;code&gt;max_length&lt;/code&gt;; v2 no longer gives &lt;code&gt;Optional&lt;/code&gt; an implicit &lt;code&gt;None&lt;/code&gt; default). &lt;a href="https://pydantic.dev/docs/validation/latest/concepts/fields/" rel="noopener noreferrer"&gt;https://pydantic.dev/docs/validation/latest/concepts/fields/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Pydantic: Strict Mode (default coercion of &lt;code&gt;'123'&lt;/code&gt; to &lt;code&gt;123&lt;/code&gt;; enabling strict mode). &lt;a href="https://pydantic.dev/docs/validation/latest/concepts/strict_mode/" rel="noopener noreferrer"&gt;https://pydantic.dev/docs/validation/latest/concepts/strict_mode/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Python docs: Coroutines and Tasks (calling a coroutine doesn’t schedule it; &lt;code&gt;asyncio.run&lt;/code&gt;; the 3-second vs 2-second example; &lt;code&gt;create_task&lt;/code&gt;; keep task references; &lt;code&gt;sleep&lt;/code&gt; always suspends; &lt;code&gt;to_thread&lt;/code&gt;; cooperative scheduling). &lt;a href="https://docs.python.org/3/library/asyncio-task.html" rel="noopener noreferrer"&gt;https://docs.python.org/3/library/asyncio-task.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Python docs: Developing with asyncio (”Running Blocking Code”). &lt;a href="https://docs.python.org/3/library/asyncio-dev.html" rel="noopener noreferrer"&gt;https://docs.python.org/3/library/asyncio-dev.html&lt;/a&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;em&gt;Sources checked Oct 18, 2026.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://dhanushkumarsivaji.substack.com/p/the-path-to-ai-fde-03-python-types" rel="noopener noreferrer"&gt;The Path to AI FDE&lt;/a&gt; on Substack. Subscribe for the next issue: &lt;a href="https://dhanushkumarsivaji.substack.com/subscribe" rel="noopener noreferrer"&gt;https://dhanushkumarsivaji.substack.com/subscribe&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If this was useful, &lt;a href="https://dhanushkumarsivaji.substack.com/subscribe" rel="noopener noreferrer"&gt;subscribe to The Path to AI FDE&lt;/a&gt; for the next issue.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>beginners</category>
      <category>programming</category>
    </item>
    <item>
      <title>The path to AI FDE #02: Python for AI backends: a TypeScript engineer's refresher</title>
      <dc:creator>Dhanush Kumar Sivaji</dc:creator>
      <pubDate>Wed, 21 Oct 2026 17:16:38 +0000</pubDate>
      <link>https://dev.to/dhanushkumarsivaji/the-path-to-ai-fde-02-python-for-ai-backends-a-typescript-engineers-refresher-238p</link>
      <guid>https://dev.to/dhanushkumarsivaji/the-path-to-ai-fde-02-python-for-ai-backends-a-typescript-engineers-refresher-238p</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on my newsletter, The Path to AI FDE. &lt;a href="https://dhanushkumarsivaji.substack.com/subscribe" rel="noopener noreferrer"&gt;Subscribe free&lt;/a&gt; to get each issue by email.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Most AI engineering tutorials assume Python. I’ve written TypeScript for years and have working, not deep, Python. My roadmap starts with a short Python + FastAPI refresher rather than a full course, and this post is the map I’m following through it: eight concepts AI backends lean on, each mapped to TypeScript I already know, plus the spot where my TS instincts are most likely to mislead me.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3wv8kfhbkpx9bm6zypol.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3wv8kfhbkpx9bm6zypol.png" alt="Python for AI backends: eight TypeScript-to-Python comparisons, each with a gotcha" width="800" height="1170"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;Each concept gets its own figure first, then the short version in words.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup and types: concepts 1–3
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F55rjckjdiggspakymdxf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F55rjckjdiggspakymdxf.png" alt="One environment per project: two project folders each with their own .venv and different versions of the same library; create, activate, install; warnings about skipping activation and PEP 668 externally managed Pythons" width="800" height="723"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Isolated dependencies:&lt;/strong&gt; &lt;code&gt;node_modules&lt;/code&gt; &lt;strong&gt;↔ virtual environments.&lt;/strong&gt; A &lt;em&gt;virtual environment&lt;/em&gt; is a self-contained directory with its own interpreter and packages, so two projects can pin different versions of the same library. Create it with &lt;code&gt;python -m venv .venv&lt;/code&gt;, then activate it: activation changes your shell so &lt;code&gt;python&lt;/code&gt; points into that environment. The TS instinct that misleads: there’s no automatic per-folder install. Forget to activate, and &lt;code&gt;pip&lt;/code&gt; targets whichever Python comes first on your PATH. On Pythons marked “externally managed” (PEP 668), as some OS-provided ones are, pip refuses to install outside a virtual environment and exits with an error.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F50vjlcbtx7c82plkuujg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F50vjlcbtx7c82plkuujg.png" alt="Types vanish at runtime in both languages: TypeScript types are checked by tsc then erased; Python annotations are checked by mypy or Pyright but not enforced, so f(" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Type hints are for tools, just like TS types.&lt;/strong&gt; The TypeScript handbook shows types being &lt;em&gt;erased&lt;/em&gt; when &lt;code&gt;tsc&lt;/code&gt; compiles to JavaScript. Python’s &lt;code&gt;typing&lt;/code&gt; docs say the runtime &lt;em&gt;does not enforce&lt;/em&gt; annotations; they exist for type checkers, IDEs and linters. So &lt;code&gt;def f(x: float) -&amp;gt; str&lt;/code&gt; happily accepts a string at runtime. Run mypy or Pyright in CI, the way you’d run &lt;code&gt;tsc --noEmit&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj2mlulutrwsebbk0btl8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj2mlulutrwsebbk0btl8.png" alt="Pydantic guards the edges: HTTP request bodies and LLM output pass through a Pydantic model before becoming typed objects; the OpenAI Python SDK turns the model class into a JSON Schema and parses the reply back" width="800" height="646"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Runtime validation: Zod ↔ Pydantic.&lt;/strong&gt; Since annotations aren’t enforced, untrusted data needs a validator. Zod describes itself as TypeScript-first schema validation with static type inference. Pydantic plays the same role in Python: validation is driven by type annotations, and once data passes, the fields are guaranteed to match the declared types. For LLM work, FastAPI uses it to validate requests and responses, and OpenAI’s Python SDK accepts a Pydantic class directly (&lt;code&gt;client.responses.parse(..., text_format=MyModel)&lt;/code&gt;): it converts the class to a JSON Schema, sends it and parses the reply back into the model. Only the subset of JSON Schema that structured outputs supports will work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Errors and cleanup: concepts 4–5
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyk7wuq4ap5y70renjszw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyk7wuq4ap5y70renjszw.png" alt="Errors in Python: which of try, except, else and finally run with no error, a matching error or another error; FastAPI's HTTPException is swallowed by a broad except Exception" width="800" height="587"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Errors:&lt;/strong&gt; &lt;code&gt;throw&lt;/code&gt;&lt;strong&gt;/&lt;/strong&gt;&lt;code&gt;catch&lt;/code&gt; &lt;strong&gt;↔&lt;/strong&gt; &lt;code&gt;raise&lt;/code&gt;&lt;strong&gt;/&lt;/strong&gt;&lt;code&gt;except&lt;/code&gt;&lt;strong&gt;.&lt;/strong&gt; The shape maps closely, plus one extra: &lt;code&gt;else&lt;/code&gt;, which runs only if the &lt;code&gt;try&lt;/code&gt; block raised nothing. Idiomatic Python catches &lt;em&gt;specific&lt;/em&gt; exception classes. In FastAPI, &lt;code&gt;raise HTTPException(...)&lt;/code&gt; returns an HTTP error and ends the request, even from deep inside a helper. But FastAPI’s docs call &lt;code&gt;HTTPException&lt;/code&gt; a normal Python exception, so a broad &lt;code&gt;except Exception:&lt;/code&gt; between the raise and FastAPI swallows it. One more reason to catch narrowly.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fse7m4hc20vgpzuvd4sxy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fse7m4hc20vgpzuvd4sxy.png" alt="Cleanup that always runs: acquire, then whether the block finishes or raises, cleanup runs; TypeScript using with Symbol.dispose next to Python with and async with" width="800" height="545"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Guaranteed cleanup:&lt;/strong&gt; &lt;code&gt;using&lt;/code&gt; &lt;strong&gt;↔&lt;/strong&gt; &lt;code&gt;with&lt;/code&gt;&lt;strong&gt;.&lt;/strong&gt; TypeScript 5.2 added &lt;code&gt;using&lt;/code&gt; declarations (explicit resource management). Python has had the &lt;code&gt;with&lt;/code&gt; statement for a long time: the cleanup step runs whether the block finishes normally or raises, and &lt;code&gt;async with&lt;/code&gt; is the asynchronous version. In AI backends, that’s the home for HTTP clients, open files and database sessions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Async: concepts 6–8
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F78m7iivy8ylg7kr21sxe.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F78m7iivy8ylg7kr21sxe.png" alt="Starting async work: in TypeScript calling an async function starts it immediately; in Python calling an async def only creates a coroutine until it is awaited; a forgotten await in Python means the work never runs and asyncio warns" width="800" height="640"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Starting async work: the subtle one.&lt;/strong&gt; The syntax looks the same; what happens at call time doesn’t. MDN: an &lt;code&gt;async&lt;/code&gt; function runs synchronously up to its first &lt;code&gt;await&lt;/code&gt;, so calling &lt;code&gt;fetchDoc()&lt;/code&gt; starts the work immediately. Python’s asyncio docs: &lt;em&gt;simply calling a coroutine will not schedule it&lt;/em&gt;. Nothing runs until you &lt;code&gt;await&lt;/code&gt; it or wrap it in a task, so a forgotten &lt;code&gt;await&lt;/code&gt; means the work never happens. asyncio emits a &lt;code&gt;RuntimeWarning&lt;/code&gt; (”coroutine ... was never awaited”); treat it as a bug.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F444ocedggn2ovrrcvx5k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F444ocedggn2ovrrcvx5k.png" alt="One blocking call stalls everything: a timeline where time.sleep(1) on the event loop delays three ready requests; fixes for blocking I/O, CPU-bound work and FastAPI" width="800" height="702"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7. Blocking the event loop: same trap, both runtimes.&lt;/strong&gt; Node’s docs warn against blocking the event loop. Python’s asyncio docs say a CPU-intensive calculation that takes 1 second delays &lt;em&gt;every&lt;/em&gt; concurrent task by 1 second, and blocking I/O (&lt;code&gt;time.sleep()&lt;/code&gt;, a synchronous HTTP client inside &lt;code&gt;async def&lt;/code&gt;) does the same. The fix depends on the work: for blocking I/O, an awaitable client or &lt;code&gt;asyncio.to_thread()&lt;/code&gt;, which the docs say is mainly for I/O-bound work because the GIL usually stops threads speeding up CPU-bound Python; for CPU-bound work, a process pool via &lt;code&gt;loop.run_in_executor(ProcessPoolExecutor(), fn)&lt;/code&gt;; in FastAPI, a plain &lt;code&gt;def&lt;/code&gt; endpoint runs in a threadpool instead of on the loop.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9lq0yz50m3exzy5fajmk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9lq0yz50m3exzy5fajmk.png" alt="Fan-out when one call fails: Promise.all and asyncio.gather let the other calls keep running, gather with return_exceptions returns results and errors like Promise.allSettled, and TaskGroup cancels the rest" width="800" height="559"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;8. Fan-out:&lt;/strong&gt; &lt;code&gt;Promise.all&lt;/code&gt; &lt;strong&gt;↔&lt;/strong&gt; &lt;code&gt;asyncio.gather&lt;/code&gt; &lt;strong&gt;/&lt;/strong&gt; &lt;code&gt;TaskGroup&lt;/code&gt;&lt;strong&gt;.&lt;/strong&gt; You’ll want this for parallel calls, like embedding several chunks or querying two retrievers. &lt;code&gt;Promise.all&lt;/code&gt; rejects with the first rejection but doesn’t stop the other operations: MDN notes promises have no built-in cancellation (you’d reach for &lt;code&gt;AbortController&lt;/code&gt;). &lt;code&gt;asyncio.gather&lt;/code&gt; behaves the same by default, and with &lt;code&gt;return_exceptions=True&lt;/code&gt; it collects every result and error, much like &lt;code&gt;Promise.allSettled&lt;/code&gt;. &lt;code&gt;asyncio.TaskGroup&lt;/code&gt; (Python 3.11+) is the real difference: when one task fails, the rest of the group is cancelled, and the failures are raised together as an &lt;code&gt;ExceptionGroup&lt;/code&gt;. That’s usually what you want when partial results are useless.&lt;/p&gt;




&lt;h3&gt;
  
  
  Takeaway
&lt;/h3&gt;

&lt;p&gt;Coming from TypeScript, the biggest shift isn’t syntax. It’s &lt;strong&gt;where the guarantees live&lt;/strong&gt;. In both languages, types vanish at runtime. Real safety comes from validating at the edges (Pydantic, the way Zod does on the front end), closing resources deterministically (&lt;code&gt;with&lt;/code&gt;) and respecting the event loop. Points 6 and 8 are the two I expect to trip over most, so they go on my checklist.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where this fits in my roadmap:&lt;/strong&gt; the foundations block (Python typing, errors, environments and async, plus the FastAPI tutorial’s request/response models, errors, dependencies and testing), building toward a small typed LLM API. Next post: structured outputs, from “please return JSON” to contracts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Over to you:&lt;/strong&gt; if you moved from TypeScript to Python, which habit took longest to unlearn?&lt;/p&gt;




&lt;h3&gt;
  
  
  Sources
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Python Tutorial: Virtual Environments and Packages. &lt;a href="https://docs.python.org/3/tutorial/venv.html" rel="noopener noreferrer"&gt;https://docs.python.org/3/tutorial/venv.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;PEP 668: Marking Python base environments as “externally managed”. &lt;a href="https://peps.python.org/pep-0668/" rel="noopener noreferrer"&gt;https://peps.python.org/pep-0668/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;TypeScript Handbook: The Basics, “Erased Types”. &lt;a href="https://www.typescriptlang.org/docs/handbook/2/basic-types.html" rel="noopener noreferrer"&gt;https://www.typescriptlang.org/docs/handbook/2/basic-types.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Python docs: &lt;code&gt;typing&lt;/code&gt; (”The Python runtime does not enforce function and variable type annotations”). &lt;a href="https://docs.python.org/3/library/typing.html" rel="noopener noreferrer"&gt;https://docs.python.org/3/library/typing.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;mypy documentation (”a static type checker for Python”). &lt;a href="https://mypy.readthedocs.io/en/stable/" rel="noopener noreferrer"&gt;https://mypy.readthedocs.io/en/stable/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Pyright (”a full-featured, standards-based static type checker for Python”). &lt;a href="https://github.com/microsoft/pyright" rel="noopener noreferrer"&gt;https://github.com/microsoft/pyright&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Zod documentation (TypeScript-first schema validation).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://zod.dev/" rel="noopener noreferrer"&gt;https://zod.dev/&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Pydantic: Why use Pydantic (type-hint-driven validation, JSON Schema). &lt;a href="https://pydantic.dev/docs/validation/latest/get-started/why/" rel="noopener noreferrer"&gt;https://pydantic.dev/docs/validation/latest/get-started/why/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Pydantic: Models (post-validation guarantees). &lt;a href="https://pydantic.dev/docs/validation/latest/concepts/models/" rel="noopener noreferrer"&gt;https://pydantic.dev/docs/validation/latest/concepts/models/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;OpenAI: Structured model outputs (&lt;code&gt;pydantic.BaseModel&lt;/code&gt; schemas, &lt;code&gt;text_format&lt;/code&gt;, supported JSON Schema subset). &lt;a href="https://developers.openai.com/api/docs/guides/structured-outputs" rel="noopener noreferrer"&gt;https://developers.openai.com/api/docs/guides/structured-outputs&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;openai-python &lt;code&gt;helpers.md&lt;/code&gt; (&lt;code&gt;.parse()&lt;/code&gt; converts a Pydantic model to JSON Schema and parses the reply; &lt;code&gt;output_parsed&lt;/code&gt;). &lt;a href="https://github.com/openai/openai-python/blob/main/helpers.md" rel="noopener noreferrer"&gt;https://github.com/openai/openai-python/blob/main/helpers.md&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Python Tutorial: Errors and Exceptions (&lt;code&gt;else&lt;/code&gt;, &lt;code&gt;finally&lt;/code&gt;). &lt;a href="https://docs.python.org/3/tutorial/errors.html" rel="noopener noreferrer"&gt;https://docs.python.org/3/tutorial/errors.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;FastAPI: Handling Errors (&lt;code&gt;HTTPException&lt;/code&gt; is a normal Python exception; raising it from a helper ends the request). &lt;a href="https://fastapi.tiangolo.com/tutorial/handling-errors/" rel="noopener noreferrer"&gt;https://fastapi.tiangolo.com/tutorial/handling-errors/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;TypeScript 5.2 release notes: &lt;code&gt;using&lt;/code&gt; declarations and explicit resource management. &lt;a href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-2.html" rel="noopener noreferrer"&gt;https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-2.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Python Language Reference: the &lt;code&gt;with&lt;/code&gt; statement. &lt;a href="https://docs.python.org/3/reference/compound_stmts.html#the-with-statement" rel="noopener noreferrer"&gt;https://docs.python.org/3/reference/compound_stmts.html#the-with-statement&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;MDN: &lt;code&gt;async function&lt;/code&gt; (runs synchronously up to the first &lt;code&gt;await&lt;/code&gt;). &lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function" rel="noopener noreferrer"&gt;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Python docs: Coroutines and Tasks (calling a coroutine doesn’t schedule it; &lt;code&gt;create_task&lt;/code&gt;; &lt;code&gt;gather&lt;/code&gt;; &lt;code&gt;TaskGroup&lt;/code&gt; and &lt;code&gt;ExceptionGroup&lt;/code&gt;; &lt;code&gt;to_thread&lt;/code&gt; and the GIL). &lt;a href="https://docs.python.org/3/library/asyncio-task.html" rel="noopener noreferrer"&gt;https://docs.python.org/3/library/asyncio-task.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Python docs: Developing with asyncio (”Running Blocking Code”; “Detect never-awaited coroutines”). &lt;a href="https://docs.python.org/3/library/asyncio-dev.html" rel="noopener noreferrer"&gt;https://docs.python.org/3/library/asyncio-dev.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Python docs: Event loop, &lt;code&gt;run_in_executor&lt;/code&gt; with &lt;code&gt;ProcessPoolExecutor&lt;/code&gt;. &lt;a href="https://docs.python.org/3/library/asyncio-eventloop.html" rel="noopener noreferrer"&gt;https://docs.python.org/3/library/asyncio-eventloop.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Node.js: Don’t Block the Event Loop (or the Worker Pool). &lt;a href="https://nodejs.org/learn/asynchronous-work/dont-block-the-event-loop" rel="noopener noreferrer"&gt;https://nodejs.org/learn/asynchronous-work/dont-block-the-event-loop&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;FastAPI: Concurrency and async / await (plain &lt;code&gt;def&lt;/code&gt; path operations run in a threadpool). &lt;a href="https://fastapi.tiangolo.com/async/" rel="noopener noreferrer"&gt;https://fastapi.tiangolo.com/async/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;MDN: &lt;code&gt;Promise.all()&lt;/code&gt;. &lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all" rel="noopener noreferrer"&gt;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;MDN: &lt;code&gt;Promise&lt;/code&gt; (no first-class cancellation; use &lt;code&gt;AbortController&lt;/code&gt;). &lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise" rel="noopener noreferrer"&gt;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;MDN: &lt;code&gt;Promise.allSettled()&lt;/code&gt;. &lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/allSettled" rel="noopener noreferrer"&gt;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/allSettled&lt;/a&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;em&gt;Sources checked Oct 18, 2026.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://dhanushkumarsivaji.substack.com/p/the-path-to-ai-fde-02-python-for" rel="noopener noreferrer"&gt;The Path to AI FDE&lt;/a&gt; on Substack. Subscribe for the next issue: &lt;a href="https://dhanushkumarsivaji.substack.com/subscribe" rel="noopener noreferrer"&gt;https://dhanushkumarsivaji.substack.com/subscribe&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If this was useful, &lt;a href="https://dhanushkumarsivaji.substack.com/subscribe" rel="noopener noreferrer"&gt;subscribe to The Path to AI FDE&lt;/a&gt; for the next issue.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>typescript</category>
      <category>beginners</category>
    </item>
    <item>
      <title>The path to AI FDE #01: Anatomy of an LLM app request</title>
      <dc:creator>Dhanush Kumar Sivaji</dc:creator>
      <pubDate>Mon, 19 Oct 2026 19:22:12 +0000</pubDate>
      <link>https://dev.to/dhanushkumarsivaji/the-path-to-ai-fde-01-anatomy-of-an-llm-app-request-3c3e</link>
      <guid>https://dev.to/dhanushkumarsivaji/the-path-to-ai-fde-01-anatomy-of-an-llm-app-request-3c3e</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on my newsletter, The Path to AI FDE. &lt;a href="https://dhanushkumarsivaji.substack.com/subscribe" rel="noopener noreferrer"&gt;Subscribe free&lt;/a&gt; to get each issue by email.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I've spent 8+ years building React/TypeScript front ends. Now I'm learning applied AI in public, working toward AI Engineer, then AI Solution Architect, and (I hope) AI Forward Deployed Engineer. Before tokens, RAG or agents, I wanted one map: what actually happens when a user clicks "Summarize"?&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvgaljigdwufjph6j4a4e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvgaljigdwufjph6j4a4e.png" alt="Anatomy of an LLM app request: a nine-step flow from client to logging, with steps 5 and 6 (prefill and decode) inside the model provider" width="800" height="1135"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;Each step below gets its own figure first, then the short version in words.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before the call: steps 1–4
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F196de03z1hvndk6192z2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F196de03z1hvndk6192z2.png" alt="Where each piece runs: the browser posts to your FastAPI backend, which holds the API key and calls the model provider; inside the backend a Pydantic model either lets a valid body through or FastAPI returns a 422 before any tokens are spent" width="800" height="642"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. The client sends a request.&lt;/strong&gt; From the browser's side, an LLM feature is just an HTTP call to &lt;em&gt;your&lt;/em&gt; backend: &lt;code&gt;POST /summarize&lt;/code&gt; with the user's text, plus a conversation id if it's a chat. OpenAI's docs say never to expose an API key in client-side code, so the key lives on your server and only the server talks to the provider.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Your API validates the input.&lt;/strong&gt; FastAPI checks the body against a Pydantic model. Invalid data never reaches your handler: FastAPI raises a &lt;code&gt;RequestValidationError&lt;/code&gt;, and its default handler returns HTTP 422. Once validation passes, Pydantic guarantees the fields match their declared types. It's the cheapest place to stop bad input.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5z54bra8cdbhuf1oduyf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5z54bra8cdbhuf1oduyf.png" alt="Each request stands alone: three successive requests resend the system prompt and the whole history, so input grows each turn; optional server-side state still bills earlier input tokens" width="800" height="565"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. You assemble the prompt.&lt;/strong&gt; The model sees only what you send. Anthropic's Messages API is stateless, so you resend the full history every time. OpenAI also calls each request independent and stateless, and offers optional server-side conversation state, but notes that earlier input tokens in the chain are still billed on each turn. Later in this series, retrieved documents (RAG) join the prompt here.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzw722jhphulijer3xn7g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzw722jhphulijer3xn7g.png" alt="Tokens and the context window: " width="800" height="559"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Count tokens and check the budget.&lt;/strong&gt; Models read &lt;em&gt;tokens&lt;/em&gt;, not characters, and tokenizers are tied to the model, so counts differ between models. System prompt, tool definitions, history and the output all share the context window. If the input alone is over the limit, the request fails (Anthropic returns a 400, "prompt is too long"). Estimate tokens before sending and set a maximum output length.&lt;/p&gt;

&lt;h2&gt;
  
  
  Inside the model: steps 5–6
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff84nmt1qsd889unweti4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff84nmt1qsd889unweti4.png" alt="Prefill vs decode: a timeline from request sent through network and queue, a prefill block, the first token, then one decode step per token until a stop condition; TTFT and time per output token are marked" width="799" height="549"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Prefill: the model reads all the input at once.&lt;/strong&gt; Inference has two phases. In &lt;em&gt;prefill&lt;/em&gt;, the model processes every input token in parallel. It's compute-heavy, and it's the main model-side driver of &lt;strong&gt;time to first token (TTFT)&lt;/strong&gt;. Network time and provider queueing add to that pause.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Decode: one token at a time.&lt;/strong&gt; Then the model generates &lt;em&gt;autoregressively&lt;/em&gt;: score every token in the vocabulary, pick one, append it, repeat. A KV cache keeps earlier work so each step doesn't start over, and the per-step pace is the time per output token. It stops at an end-of-sequence token, a stop sequence or the token limit.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp5qz4f0b143uqrsdv5v4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp5qz4f0b143uqrsdv5v4.png" alt="How the next token gets picked: a toy probability distribution for " width="800" height="808"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;How&lt;/em&gt; it picks is a sampling setting: greedy, or sampled with temperature, top-k or top-p. Don't assume those settings exist on every model: Anthropic's docs say Claude 4.7 and later return a 400 if you set them to non-default values.&lt;/p&gt;

&lt;h2&gt;
  
  
  After the call: steps 7–9
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6er5qlw29t6rjwdamfd8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6er5qlw29t6rjwdamfd8.png" alt="Streaming with server-sent events: a sequence diagram where the provider sends token deltas to your server, which relays them to the browser; the stream ends with a final event or with an error event after a 200" width="800" height="795"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7. Stream the tokens back.&lt;/strong&gt; OpenAI and Anthropic both stream tokens as server-sent events (SSE) while decoding is still running, so steps 6 and 7 overlap. Two catches from the docs: an error can arrive &lt;em&gt;mid-stream&lt;/em&gt;, after a 200 status, and partial output is harder to moderate than a finished response.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvan47uh0xiof3z9s15mz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvan47uh0xiof3z9s15mz.png" alt="Validating the model's output: a decision flow that checks for a refusal, a response cut off at the token limit, and whether the output parses into the Pydantic model, before FastAPI's response_model check returns typed JSON" width="800" height="664"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;8. Validate the output too.&lt;/strong&gt; Structured outputs constrain the model to a JSON Schema you supply (OpenAI supports a subset), but OpenAI's guide says they can still contain mistakes, and documents two cases to handle: refusals and responses cut off at the token limit. If you stream, validate once the stream completes. FastAPI's &lt;code&gt;response_model&lt;/code&gt; is a second check: a mismatch becomes a server error, not wrong data. A valid shape isn't a correct answer; evals cover correctness later in this series.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjsmrrxnhslc4y3i24yeg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjsmrrxnhslc4y3i24yeg.png" alt="Retry or not: a decision tree that sends rate limits, overloads and connection errors to limited retries, quota, billing and spend-cap errors to no retry, and bad requests or credentials to fix-first" width="800" height="670"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;9. Retry safely, then record.&lt;/strong&gt; The same status can mean different things: a 429 can be a rate limit, but Anthropic also uses it when you hit your monthly spend cap, so read the error body before retrying. Never retry quota or billing errors. For transient errors, honor &lt;code&gt;Retry-After&lt;/code&gt; when present, otherwise use exponential backoff with jitter, and cap attempts and total time. Official SDKs already retry some errors (Anthropic's twice by default), so count that in your limits rather than nesting loops. Then log input/output token usage, latency and the provider's request id. Those records feed debugging, cost tracking and, eventually, evaluation.&lt;/p&gt;




&lt;h3&gt;
  
  
  Takeaway
&lt;/h3&gt;

&lt;p&gt;As a front-end engineer, I found this oddly familiar: it's a request lifecycle with an unusual backend in the middle. Two of the nine steps are the model. The other seven are contracts, budgets, streams and failure handling, the parts an engineer owns. My working rule from here on: &lt;strong&gt;treat the model as an unreliable, metered, remote dependency, and put validation on both sides of it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where this fits in my roadmap:&lt;/strong&gt; this post is the map I'm following into the foundations block: Python typing and async, FastAPI and Pydantic, and LLM mechanics (tokens, context, inference). Next post: Python for AI backends: a TypeScript engineer's refresher.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Over to you:&lt;/strong&gt; which of these nine steps broke first in the first LLM feature you built?&lt;/p&gt;




&lt;h3&gt;
  
  
  Sources
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;OpenAI API reference: Authentication (don't expose API keys in client-side code). &lt;a href="https://developers.openai.com/api/reference/overview" rel="noopener noreferrer"&gt;https://developers.openai.com/api/reference/overview&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;FastAPI: Handling Errors (&lt;code&gt;RequestValidationError&lt;/code&gt; and its default handler). &lt;a href="https://fastapi.tiangolo.com/tutorial/handling-errors/" rel="noopener noreferrer"&gt;https://fastapi.tiangolo.com/tutorial/handling-errors/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;FastAPI source: &lt;code&gt;exception_handlers.py&lt;/code&gt; (default validation handler returns 422). &lt;a href="https://github.com/fastapi/fastapi/blob/master/fastapi/exception_handlers.py" rel="noopener noreferrer"&gt;https://github.com/fastapi/fastapi/blob/master/fastapi/exception_handlers.py&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Pydantic: Models (post-validation type guarantees). &lt;a href="https://pydantic.dev/docs/validation/latest/concepts/models/" rel="noopener noreferrer"&gt;https://pydantic.dev/docs/validation/latest/concepts/models/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Anthropic: Working with the Messages API (stateless, resend full history; sampling parameters on Claude 4.7+). &lt;a href="https://platform.claude.com/docs/en/build-with-claude/working-with-messages" rel="noopener noreferrer"&gt;https://platform.claude.com/docs/en/build-with-claude/working-with-messages&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;OpenAI: Conversation state (requests independent and stateless; optional state management; earlier input tokens still billed). &lt;a href="https://developers.openai.com/api/docs/guides/conversation-state" rel="noopener noreferrer"&gt;https://developers.openai.com/api/docs/guides/conversation-state&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Hugging Face LLM Course, Ch. 2: Tokenizers (subword tokenization, "token" + "ization"). &lt;a href="https://huggingface.co/learn/llm-course/chapter2/4" rel="noopener noreferrer"&gt;https://huggingface.co/learn/llm-course/chapter2/4&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Anthropic: Context windows (what counts toward the window; oversize input returns 400; token counting). &lt;a href="https://platform.claude.com/docs/en/build-with-claude/context-windows" rel="noopener noreferrer"&gt;https://platform.claude.com/docs/en/build-with-claude/context-windows&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Hugging Face LLM Course, Ch. 1: Deep dive into text generation inference (prefill/decode, TTFT, TPOT, KV cache, sampling, stop conditions). &lt;a href="https://huggingface.co/learn/llm-course/chapter1/8" rel="noopener noreferrer"&gt;https://huggingface.co/learn/llm-course/chapter1/8&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Vaswani et al., "Attention Is All You Need" (2017), the transformer/attention paper. &lt;a href="https://arxiv.org/abs/1706.03762" rel="noopener noreferrer"&gt;https://arxiv.org/abs/1706.03762&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;OpenAI: Streaming API responses (SSE, moderation risk for partial output). &lt;a href="https://developers.openai.com/api/docs/guides/streaming-responses" rel="noopener noreferrer"&gt;https://developers.openai.com/api/docs/guides/streaming-responses&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Anthropic: Streaming Messages (SSE). &lt;a href="https://platform.claude.com/docs/en/build-with-claude/streaming" rel="noopener noreferrer"&gt;https://platform.claude.com/docs/en/build-with-claude/streaming&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;OpenAI: Structured model outputs (schema adherence, supported subset, refusals, incomplete responses, mistakes still possible). &lt;a href="https://developers.openai.com/api/docs/guides/structured-outputs" rel="noopener noreferrer"&gt;https://developers.openai.com/api/docs/guides/structured-outputs&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;FastAPI: Response Model (server error instead of invalid returned data). &lt;a href="https://fastapi.tiangolo.com/tutorial/response-model/" rel="noopener noreferrer"&gt;https://fastapi.tiangolo.com/tutorial/response-model/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Anthropic: Errors (429 incl. spend caps, 402 billing, 400 spend limit, 529 overloaded, SDK retries, mid-stream errors after a 200, request ids). &lt;a href="https://platform.claude.com/docs/en/api/errors" rel="noopener noreferrer"&gt;https://platform.claude.com/docs/en/api/errors&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;OpenAI: Rate limits (429/503, &lt;code&gt;Retry-After&lt;/code&gt;, backoff with jitter, cap retries, nested SDK retries, don't retry quota/billing errors). &lt;a href="https://developers.openai.com/api/docs/guides/rate-limits" rel="noopener noreferrer"&gt;https://developers.openai.com/api/docs/guides/rate-limits&lt;/a&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;em&gt;Sources checked Oct 18, 2026. Provider APIs change quickly. Re-check model-specific details (status codes, sampling support) right before publishing.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on The Path to AI FDE on Substack. Subscribe for one diagram-led post every weekday: &lt;a href="https://dhanushkumarsivaji.substack.com/p/the-path-to-ai-fde-01-anatomy-of" rel="noopener noreferrer"&gt;https://dhanushkumarsivaji.substack.com/p/the-path-to-ai-fde-01-anatomy-of&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If this was useful, &lt;a href="https://dhanushkumarsivaji.substack.com/subscribe" rel="noopener noreferrer"&gt;subscribe to The Path to AI FDE&lt;/a&gt; for the next issue.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>beginners</category>
      <category>python</category>
    </item>
    <item>
      <title>I built kerf-cli because Claude Code told me not to worry about cost</title>
      <dc:creator>Dhanush Kumar Sivaji</dc:creator>
      <pubDate>Wed, 08 Apr 2026 01:07:15 +0000</pubDate>
      <link>https://dev.to/dhanushkumarsivaji/i-built-kerf-cli-because-claude-code-told-me-not-to-worry-about-cost-1ma7</link>
      <guid>https://dev.to/dhanushkumarsivaji/i-built-kerf-cli-because-claude-code-told-me-not-to-worry-about-cost-1ma7</guid>
      <description>&lt;p&gt;A few weeks ago I logged into Claude Code, typed &lt;code&gt;/cost&lt;/code&gt;, and got back this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;With your Claude Max subscription, no need to monitor cost.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Two days later I had used 91% of my weekly limit in a single morning of work. I had no idea which session, which project, or which model was responsible. I tried &lt;code&gt;ccusage&lt;/code&gt; (which is great) and it gave me totals — but I wanted to ask questions like "which of my projects is eating Opus tokens unnecessarily?" and "what's my actual cache hit rate over the last 30 days?" Those answers weren't there.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;kerf-cli&lt;/strong&gt; — a local-first cost intelligence tool for Claude Code. This post is about why it exists, what it does, and what I learned about Claude Code billing along the way.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual problem
&lt;/h2&gt;

&lt;p&gt;Anthropic gives you a lot of data and almost no analytics on top of it. Every Claude Code session is logged to &lt;code&gt;~/.claude/projects/&amp;lt;encoded-cwd&amp;gt;/&amp;lt;session-id&amp;gt;.jsonl&lt;/code&gt; with full token breakdowns: input, output, cache_read, cache_creation, model, timestamp, git branch. The data is rich. The tooling on top of it is thin.&lt;/p&gt;

&lt;p&gt;Here's what existed when I started:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Claude Code's &lt;code&gt;/cost&lt;/code&gt; command&lt;/strong&gt; — current session only, and it actively discourages Max subscribers from looking&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anthropic's web console&lt;/strong&gt; — org-level dashboards for Teams/Enterprise, nothing for solo developers on Pro/Max&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ccusage&lt;/strong&gt; — excellent for quick reports, but parses JSONL on every invocation with no persistence&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A handful of other CLIs and menu-bar apps&lt;/strong&gt; — most are read-only reporters&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What I wanted was three things none of them offered:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A persistent analytical layer I could query with SQL&lt;/li&gt;
&lt;li&gt;Real budget enforcement that blocks Claude Code when I exceed a cap, not just a warning&lt;/li&gt;
&lt;li&gt;Concrete optimization recommendations — not "you spent $47" but "switch these 12 sessions from Opus to Sonnet and you'll save $140/month"&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  How kerf works
&lt;/h2&gt;

&lt;p&gt;Kerf is a TypeScript CLI built on &lt;code&gt;commander&lt;/code&gt;, &lt;code&gt;ink&lt;/code&gt;, and &lt;code&gt;better-sqlite3&lt;/code&gt;. The architecture is dead simple: it reads Claude Code's existing JSONL session files, ingests them into a local SQLite database, and then every command and the web dashboard query that database directly. You run &lt;code&gt;kerf sync&lt;/code&gt; once and it ingests every Claude Code session you've ever had. Subsequent syncs are incremental — only changed files are re-parsed. Then everything else is fast queries against the local DB.&lt;/p&gt;

&lt;h2&gt;
  
  
  The commands that matter
&lt;/h2&gt;

&lt;h3&gt;
  
  
  kerf summary
&lt;/h3&gt;

&lt;p&gt;The bread and butter — what did I spend?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ kerf summary --period week --by-project

For week (Apr 1 → Apr 7):
  Total cost:     $178.04
  Sessions:       25
  Tokens:         454.0M
  Cache hit:      98%

By project:
  projects       $117.00  (66%)   5 sessions
  subagents       $42.50  (24%)  14 sessions
  kerf            $14.54   (8%)   4 sessions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  kerf query
&lt;/h3&gt;

&lt;p&gt;The SQL escape hatch I built mostly for myself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="err"&gt;$&lt;/span&gt; &lt;span class="n"&gt;kerf&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="nv"&gt;"SELECT date(timestamp) as day, ROUND(SUM(cost_usd), 2) as cost
              FROM messages
              WHERE timestamp &amp;gt; date('now', '-7 days')
              GROUP BY day ORDER BY day DESC"&lt;/span&gt;

&lt;span class="k"&gt;day&lt;/span&gt;         &lt;span class="n"&gt;cost&lt;/span&gt;
&lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;04&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;07&lt;/span&gt;  &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;46&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;62&lt;/span&gt;
&lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;04&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;06&lt;/span&gt;  &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;84&lt;/span&gt;
&lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;04&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;04&lt;/span&gt;  &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;33&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;51&lt;/span&gt;
&lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;04&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;03&lt;/span&gt;  &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;28&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;
&lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;04&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;02&lt;/span&gt;  &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;33&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;--examples&lt;/code&gt; flag prints a dozen useful queries to copy. The &lt;code&gt;--schema&lt;/code&gt; flag prints the database schema. Writes are blocked — only SELECT statements are allowed.&lt;/p&gt;

&lt;h3&gt;
  
  
  kerf efficiency
&lt;/h3&gt;

&lt;p&gt;The command that actually saves money. This is the one I use every Monday morning.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyu4g1k8nlo4lh7qnm46v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyu4g1k8nlo4lh7qnm46v.png" alt="kerf efficiency terminal output" width="732" height="275"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This was the moment kerf paid for itself. I had been blindly using Opus for everything because it was the default. The analyzer pointed out that most of my Opus sessions had patterns that would have been fine on Sonnet — file edits, small refactors, dependency bumps. I switched those workflows, cut a meaningful chunk of my monthly Claude bill, and noticed zero quality difference.&lt;/p&gt;

&lt;h3&gt;
  
  
  kerf budget + kerf init --enforce-budgets
&lt;/h3&gt;

&lt;p&gt;The killer feature. &lt;code&gt;kerf budget set 50 --period weekly&lt;/code&gt; sets a cap, then &lt;code&gt;kerf init --enforce-budgets&lt;/code&gt; installs a Claude Code PreToolUse hook that runs &lt;code&gt;kerf budget check&lt;/code&gt; before every tool call. If you're over budget, the hook returns exit code 2 and Claude Code blocks the action.&lt;/p&gt;

&lt;p&gt;This is the difference between knowing you blew your budget and not being able to. Other tools warn. Kerf enforces.&lt;/p&gt;

&lt;h3&gt;
  
  
  kerf dashboard
&lt;/h3&gt;

&lt;p&gt;The local web UI for visual people. Opens at &lt;code&gt;http://localhost:3847&lt;/code&gt; — SQLite-backed so queries are sub-100ms, three killer-features cards (budget, efficiency, cache) front and center, sortable session table with drill-down, stacked cost-over-time chart by model. Zero auth, zero cloud, zero data leaving your machine. That's the screenshot at the top of this post.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I learned about Claude Code billing
&lt;/h2&gt;

&lt;p&gt;A few non-obvious things from spending too much time staring at JSONL files:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Cache reads can be 60–80% of your total cost.&lt;/strong&gt; This was the biggest surprise. Cache reads are billed at 10% of standard input rate, which sounds cheap until you realize you're caching 50K tokens per turn and reading them on every message. Optimizing your CLAUDE.md and reducing cache invalidation was the biggest single lever I found.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Opus is the default and it almost never needs to be.&lt;/strong&gt; I ran kerf efficiency on a month of data: 90% of my Opus tokens were on sessions that had no complexity signal (no debugging, no architecture decisions, no large refactors — just file edits and small fixes). Switching them to Sonnet was a 4x cost reduction with no measurable quality drop.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Claude Code's JSONL streams partial usage updates.&lt;/strong&gt; When you parse them, you have to keep the MAX value per field across duplicate message IDs, not the latest value. I learned this the hard way — my v2.1 parser was undercounting input tokens because it kept the "last" entry instead of the "max," which meant the final zero-input chunk overwrote the real numbers from earlier chunks. Fixed before launch, but it's a subtle trap anyone parsing these logs will hit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. The 5-hour billing window is real but Anthropic doesn't expose it clearly.&lt;/strong&gt; Max subscribers are billed against a rolling 5-hour window, not a daily quota. If you don't track this, you can get surprised when the window rolls over mid-session.&lt;/p&gt;

&lt;h2&gt;
  
  
  Technical decisions I'd defend
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SQLite over a JSON file.&lt;/strong&gt; JSON is fine for ccusage's "read once, compute, discard" model. For an analytics layer you want sub-100ms queries, joins, aggregations, and a schema migration story. SQLite via better-sqlite3 is the right tool.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Local-only over cloud-first.&lt;/strong&gt; The moment you add cloud sync, you need auth, storage, privacy controls, GDPR compliance, a business model. None of that serves the primary use case of "show me where my money went." Kerf is local-first on purpose. A hosted team tier is on the roadmap but will always be optional.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ink for the terminal UI.&lt;/strong&gt; React components in the terminal feel weird at first but the composability is worth it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hooks as the enforcement mechanism.&lt;/strong&gt; Claude Code ships a native hook system. Using hooks means kerf doesn't have to intercept or proxy Claude Code's traffic — it just responds to events Claude Code already emits.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;v1 is focused on doing one thing well: Claude Code observability for individual developers. The roadmap from here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;v2.x:&lt;/strong&gt; Cursor and Codex support&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;v2.x:&lt;/strong&gt; Slack/Discord alerts on budget thresholds&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;v2.x:&lt;/strong&gt; GitHub Actions integration for cost gates on PRs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;v3.x (paid team tier):&lt;/strong&gt; cloud sync, team aggregation, SSO&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But none of that ships before I'm sure v1 is rock solid. The CLI will always be free and MIT licensed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; kerf-cli
kerf &lt;span class="nb"&gt;sync
&lt;/span&gt;kerf summary
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/dhanushkumarsivaji/kerf-cli" rel="noopener noreferrer"&gt;github.com/dhanushkumarsivaji/kerf-cli&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Show HN discussion:&lt;/strong&gt; &lt;a href="https://news.ycombinator.com/item?id=47683060" rel="noopener noreferrer"&gt;news.ycombinator.com/item?id=47683060&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you've hit billing surprises with Claude Code, I'd love to hear about them in the comments. The more weird patterns I see, the better the analyzer gets.&lt;/p&gt;

</description>
      <category>claude</category>
      <category>ai</category>
      <category>opensource</category>
      <category>typescript</category>
    </item>
  </channel>
</rss>
