<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Shayan Holakouee on Medium]]></title>
        <description><![CDATA[Stories by Shayan Holakouee on Medium]]></description>
        <link>https://medium.com/@shayanholakouee?source=rss-286987f6af11------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*qQI0xUnU0WyRiHC8CPQOug.png</url>
            <title>Stories by Shayan Holakouee on Medium</title>
            <link>https://medium.com/@shayanholakouee?source=rss-286987f6af11------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Sat, 14 Nov 2026 23:46:27 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@shayanholakouee/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com/" rel="hub"/>
        <item>
            <title><![CDATA[Context Engineering Is the Job Now. Prompt Engineering Was Just the Onboarding.]]></title>
            <link>https://medium.com/@shayanholakouee/context-engineering-is-the-job-now-prompt-engineering-was-just-the-onboarding-9a6f824e3cea?source=rss-286987f6af11------2</link>
            <guid isPermaLink="false">https://medium.com/p/9a6f824e3cea</guid>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[machine-learning]]></category>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[ai-agent]]></category>
            <category><![CDATA[llm]]></category>
            <dc:creator><![CDATA[Shayan Holakouee]]></dc:creator>
            <pubDate>Fri, 03 Jul 2026 07:11:24 GMT</pubDate>
            <atom:updated>2026-07-03T07:11:24.212Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*06fj4i5HjOZQzmV5mOonDg.png" /></figure><p>A year ago, “prompt engineering” was the skill everyone wanted on their resume. Today, if you’re building anything real with LLMs (an agent, a support bot, a coding assistant), you’ve probably noticed that clever prompts stopped being the bottleneck a while ago.</p><p>The bottleneck now is <em>context</em>: what the model actually sees at inference time, how it got there, and whether any of it is still relevant by the time the model reads it.</p><p>That shift has a name, context engineering, and it’s quietly become the actual job description for a lot of AI engineers, even if their title still says “prompt engineer” or “ML engineer.”</p><h3>Prompt engineering was about the sentence. Context engineering is about the system.</h3><p>Prompt engineering asks: <em>how do I phrase this instruction so the model does what I want?</em></p><p>Context engineering asks a bigger question: <em>what is the full set of information (instructions, memory, retrieved documents, conversation history, tool outputs, system state) that should be sitting in the model’s context window right now, and in what order?</em></p><p>A prompt is one ingredient. Context is the whole kitchen.</p><p>This matters because most production LLM failures today aren’t “the model is dumb.” They’re “the model was never given the right information to succeed.” You can have GPT-5-class reasoning and still get garbage output if the context window is stuffed with stale retrieval results, contradictory system instructions, and forty turns of conversation history nobody bothered to summarize.</p><h3>The counterintuitive part: bigger context windows didn’t fix this</h3><p>For a while, the industry bet that context engineering would become unnecessary: just make the context window bigger, and stuff everything in.</p><p>That bet mostly failed. Long-context models exist now, but most production agents don’t come close to using the full window their models technically support, and even when they do, quality doesn’t scale with quantity. A relevant fact buried on page 40 of a stuffed context often gets less attention from the model than one placed near the end. More tokens means more surface area for irrelevant, redundant, or contradictory information to confuse the model, and it costs more money and latency to boot.</p><p>So the real constraint shifted. It’s no longer “how many tokens can I fit.” It’s “which tokens actually move the model’s decision, and how do I make sure those are the ones it sees clearly.” That’s a retrieval, compression, and information-architecture problem, not a prompting problem.</p><h3>What context engineering actually looks like day to day</h3><p>If you strip away the buzzword, context engineering is a handful of concrete practices:</p><p><strong>Retrieval quality over retrieval volume.</strong> Instead of dumping the top 20 semantically-similar chunks into context, good systems rerank, deduplicate, and filter aggressively before anything reaches the model. Fewer, better documents beat more, noisier ones almost every time.</p><p><strong>Memory that decays on purpose.</strong> Long-running agents accumulate conversation history fast. Instead of keeping the raw transcript, mature systems summarize old turns, drop resolved subtasks, and keep only what’s still decision-relevant. This is less “remember everything” and more “forget deliberately.”</p><p><strong>Structured over freeform.</strong> Clear information hierarchy (system instructions, then task state, then retrieved evidence, then conversation) consistently outperforms context that’s just concatenated in whatever order it was fetched.</p><p><strong>Tool outputs treated as context, not afterthoughts.</strong> When an agent calls a tool, the result becomes part of its context for the next step. If that result is a raw 4,000-token API dump instead of a clean summary, you’ve just spent your context budget on noise.</p><p><strong>Monitoring what the model actually uses.</strong> The more advanced teams are starting to trace which parts of the context a model’s output actually draws on, treating context like a system you can observe and debug, not a black box you hope works.</p><h3>Why this is worth learning right now</h3><p>Two things are converging that make this a good time to actually get good at context engineering rather than just knowing the term.</p><p>First, agentic frameworks (LangGraph, CrewAI, and the rest) have made it trivially easy to wire up multi-step agents, and adoption has genuinely taken off. But that ease comes with a cost: frameworks often make architectural decisions about context handling on your behalf, and when something goes wrong three tool-calls deep, most teams don’t have the visibility to know why. Understanding context engineering is what lets you debug, or bypass, the framework instead of being at its mercy.</p><p>Second, cost. Every unnecessary token in your context window is money and latency, multiplied across every single call an agent makes. Teams are increasingly shutting down AI features not because the model failed, but because the token bill didn’t justify the value delivered. Tight, well-engineered context isn’t just a quality lever anymore; it’s a budget lever.</p><h3>The honest caveat</h3><p>Context engineering isn’t a solved discipline with a clean playbook yet. Best practices are still forming, and a lot of what’s written about it (this piece included) is more directional than definitive. If you’re building agents today, the actual skill isn’t memorizing a checklist — it’s developing the instinct to ask, for every piece of information you’re about to hand the model: <em>does this earn its place in the context window, or is it just along for the ride?</em></p><p>That question, asked consistently, is most of the job.</p><p><em>If you’re building with LLMs day to day, I’d genuinely like to know: what’s the messiest context-engineering problem you’ve hit in production? Drop it in the comments — I’m collecting real failure modes for a follow-up piece.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=9a6f824e3cea" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Naive RAG Is a Prototype. Here’s How Production Systems Actually Retrieve Context.]]></title>
            <link>https://medium.com/@shayanholakouee/naive-rag-is-a-prototype-heres-how-production-systems-actually-retrieve-context-a8a14b0f05d6?source=rss-286987f6af11------2</link>
            <guid isPermaLink="false">https://medium.com/p/a8a14b0f05d6</guid>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[python]]></category>
            <category><![CDATA[ai-agent]]></category>
            <category><![CDATA[llm]]></category>
            <dc:creator><![CDATA[Shayan Holakouee]]></dc:creator>
            <pubDate>Mon, 08 Jun 2026 12:09:01 GMT</pubDate>
            <atom:updated>2026-06-08T12:09:01.412Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*EyJeOiIdA3Xl0XJGHhcKbA.png" /></figure><p><em>Late chunking, contextual retrieval, and hybrid search the three upgrades that separate toy demos from systems that work under pressure.</em></p><p>Every RAG tutorial on the internet shows you the same thing: split your PDF into 512-token chunks, embed them with OpenAI, do cosine similarity, stuff the top-5 into a prompt, call it a day. Congrats you’ve built a demo that works on your laptop and embarrasses you in production.</p><p>The problem isn’t RAG. The problem is that naive RAG destroys the very information it’s trying to retrieve. The chunking step the part everyone treats as boilerplate is where most production failures are born.</p><p>This article goes deep on three techniques that actually fix this: <strong>late chunking</strong>, <strong>contextual retrieval</strong>, and <strong>hybrid search with reranking</strong>. By the end, you’ll have working Python for all three.</p><h3>Why naive RAG fails (the real reason)</h3><p>Here’s the canonical RAG pipeline:</p><pre>Raw doc → Chunk (512 tokens) → Embed chunks → Vector store → Top-K retrieval → LLM prompt</pre><p>The chunking and embedding steps share a single, fatal assumption: that a 512-token window contains enough context to produce a meaningful embedding. It often doesn’t. When you chunk a document, each piece loses awareness of everything around it. A chunk that says <em>“The policy was revised in Q3”</em> embeds as a near-meaningless fragment no subject, no document-level context, no entity grounding.</p><p><strong>Naive chunk:</strong></p><blockquote><em>“The policy was revised in Q3 following the audit findings. It now requires dual approval for all transactions above the threshold.”</em></blockquote><p><strong>Context-aware chunk:</strong></p><blockquote><em>“[From: Acme Corp Financial Controls Manual, Section 4 Expense Policy] The policy was revised in Q3 following the audit findings. It now requires dual approval for all transactions above the threshold.”</em></blockquote><p>Same text. Completely different retrieval behavior. The second chunk embeds meaningfully it knows what document it belongs to, what section it’s from, and what the surrounding topic is. This is the intuition behind both techniques we’re about to cover.</p><h3>Technique 1: Late chunking</h3><p>Late chunking, introduced by Jina AI in late 2024, flips the conventional order of operations. Instead of splitting a document into chunks and then embedding each one independently, you embed the entire document first using a long-context embedding model, and then split the resulting token embeddings into chunks.</p><p><strong>Key insight:</strong> Because the full document was processed before splitting, each chunk’s embeddings contain attention signals from the surrounding context. A chunk about “the revised policy” now has token-level awareness of the document’s subject matter baked into its vector representation.</p><p>The gains are real. Jina’s benchmarks on BEIR show consistent improvements, with larger documents benefiting most which makes intuitive sense, since longer documents have more context to lose under naive chunking.</p><ul><li><strong>+7.4%</strong> avg BEIR recall improvement (Jina)</li><li><strong>0 extra</strong> LLM calls required</li><li><strong>1 embedding pass</strong> per document</li></ul><pre>from transformers import AutoTokenizer, AutoModel<br>import torch<br>import numpy as np</pre><pre>model_id = &quot;jinaai/jina-embeddings-v3&quot;<br>tokenizer = AutoTokenizer.from_pretrained(model_id)<br>model = AutoModel.from_pretrained(model_id, trust_remote_code=True)</pre><pre>def late_chunk_embed(text: str, chunk_size: int = 512) -&gt; list[np.ndarray]:<br>    # Tokenize the full document<br>    inputs = tokenizer(text, return_tensors=&quot;pt&quot;, truncation=False)<br>    input_ids = inputs[&quot;input_ids&quot;][0]</pre><pre>    # Get token-level embeddings for the entire document<br>    with torch.no_grad():<br>        outputs = model(**{k: v for k, v in inputs.items()})<br>    <br>    # token_embeddings shape: (seq_len, hidden_dim)<br>    token_embeddings = outputs.last_hidden_state[0]</pre><pre>    # Now split the token embeddings into chunks<br>    chunk_embeddings = []<br>    for start in range(0, len(input_ids), chunk_size):<br>        end = min(start + chunk_size, len(input_ids))<br>        chunk_tokens = token_embeddings[start:end]<br>        <br>        # Mean pool the token embeddings for this chunk<br>        chunk_vec = chunk_tokens.mean(dim=0).numpy()<br>        chunk_embeddings.append(chunk_vec)</pre><pre>    return chunk_embeddings</pre><pre># Usage<br>doc = open(&quot;financial_controls.txt&quot;).read()<br>embeddings = late_chunk_embed(doc, chunk_size=512)<br>print(f&quot;Got {len(embeddings)} chunk embeddings, dim={embeddings[0].shape[0]}&quot;)</pre><p>Notice what’s not there: no separate per-chunk embedding call, no context prepending, no reranker. Late chunking is a pure architectural change to the embedding step, with zero additional latency at inference time.</p><h3>Technique 2: Contextual retrieval</h3><p>Anthropic published contextual retrieval in late 2024 and reported something striking: it reduced top-20 retrieval failures by 49% on its own, and by 67% when combined with reranking. Those numbers are hard to ignore.</p><p>The idea is straightforward. Before indexing each chunk, you make an LLM call that generates a short context summary 2–3 sentences describing what the chunk is about in the context of the full document. This summary is prepended to the chunk text before embedding.</p><p><strong>The tradeoff:</strong> Unlike late chunking, contextual retrieval requires one LLM call per chunk at index time. For a 500-page document chunked at 512 tokens, that’s ~1,000 LLM calls. Use caching aggressively, and only re-index when documents change.</p><pre>from anthropic import Anthropic</pre><pre>client = Anthropic()</pre><pre>CONTEXT_PROMPT = &quot;&quot;&quot;<br>You are given a chunk from a larger document.<br>Write 2-3 sentences that describe what this chunk is about<br>in the context of the full document. Be specific and concise.<br>Do not explain what you are doing. Output only the context.</pre><pre>Full document (first 2000 chars for reference):<br>{doc_preview}</pre><pre>Chunk to contextualize:<br>{chunk}<br>&quot;&quot;&quot;</pre><pre>def generate_chunk_context(doc: str, chunk: str) -&gt; str:<br>    doc_preview = doc[:2000]<br>    prompt = CONTEXT_PROMPT.format(<br>        doc_preview=doc_preview,<br>        chunk=chunk<br>    )<br>    response = client.messages.create(<br>        model=&quot;claude-haiku-4-5-20251001&quot;,  # use Haiku fast, cheap<br>        max_tokens=200,<br>        messages=[{&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: prompt}]<br>    )<br>    return response.content[0].text.strip()</pre><pre>def build_contextual_chunks(doc: str, chunks: list[str]) -&gt; list[str]:<br>    contextual = []<br>    for chunk in chunks:<br>        context = generate_chunk_context(doc, chunk)<br>        contextual_chunk = f&quot;{context}\n\n{chunk}&quot;<br>        contextual.append(contextual_chunk)<br>    return contextual</pre><pre># Usage<br>from langchain.text_splitter import RecursiveCharacterTextSplitter</pre><pre>splitter = RecursiveCharacterTextSplitter(chunk_size=512, chunk_overlap=50)<br>doc = open(&quot;financial_controls.txt&quot;).read()<br>raw_chunks = splitter.split_text(doc)</pre><pre># Build contextual versions cache these, don&#39;t regenerate every run<br>contextual_chunks = build_contextual_chunks(doc, raw_chunks)<br>print(f&quot;Built {len(contextual_chunks)} contextual chunks&quot;)</pre><p>One practical note: use Claude Haiku for the context generation step. You’re calling it hundreds or thousands of times per document. Haiku is fast, cheap, and more than capable of 2-sentence summarization. Save the expensive models for generation, not indexing.</p><h3>Technique 3: Hybrid search + reranking</h3><p>Dense vector search is great at semantic similarity. It’s terrible at exact matches. If a user asks “What is ISO 27001?” and your document literally contains that string, a keyword search will surface it instantly. Dense search might not because the semantic neighborhood of “ISO 27001” is broad and noisy.</p><p>Hybrid search runs both a dense vector query and a sparse BM25 query in parallel, then merges the results using Reciprocal Rank Fusion (RRF). Then a cross-encoder reranker re-scores the merged set for final ordering.</p><p><strong>Pipeline:</strong></p><pre>User query<br>    ├── Dense (ANN vector search)<br>    └── Sparse (BM25 keyword search)<br>            ↓<br>        RRF merge → Cross-encoder rerank → Top-K</pre><pre>from qdrant_client import QdrantClient<br>from qdrant_client.models import NamedVector<br>from sentence_transformers import CrossEncoder<br>from rank_bm25 import BM25Okapi</pre><pre>reranker = CrossEncoder(&quot;cross-encoder/ms-marco-MiniLM-L-6-v2&quot;)<br>client = QdrantClient(&quot;:memory:&quot;)  # swap for your actual instance</pre><pre>def reciprocal_rank_fusion(<br>    dense_hits: list, sparse_hits: list, k: int = 60<br>) -&gt; list[tuple]:<br>    scores = {}<br>    for rank, hit in enumerate(dense_hits):<br>        scores[hit.id] = scores.get(hit.id, 0) + 1 / (rank + k)<br>    for rank, hit in enumerate(sparse_hits):<br>        scores[hit.id] = scores.get(hit.id, 0) + 1 / (rank + k)<br>    return sorted(scores.items(), key=lambda x: x[1], reverse=True)</pre><pre>def hybrid_retrieve(<br>    query: str,<br>    query_embedding: list[float],<br>    bm25_index: BM25Okapi,<br>    corpus: list[str],<br>    top_k: int = 20,<br>    rerank_top_n: int = 5,<br>) -&gt; list[str]:<br>    # Dense retrieval<br>    dense_hits = client.search(<br>        collection_name=&quot;docs&quot;,<br>        query_vector=NamedVector(name=&quot;dense&quot;, vector=query_embedding),<br>        limit=top_k,<br>    )</pre><pre>    # Sparse retrieval (BM25)<br>    bm25_scores = bm25_index.get_scores(query.split())<br>    sparse_top = sorted(<br>        range(len(bm25_scores)), key=lambda i: bm25_scores[i], reverse=True<br>    )[:top_k]</pre><pre>    # Build lightweight hit objects for RRF<br>    class Hit:<br>        def __init__(self, id): self.id = id<br>    sparse_hits = [Hit(i) for i in sparse_top]</pre><pre>    # Merge with RRF<br>    merged = reciprocal_rank_fusion(dense_hits, sparse_hits)<br>    candidate_ids = [doc_id for doc_id, _ in merged[:top_k]]<br>    candidates = [corpus[i] for i in candidate_ids]</pre><pre>    # Rerank with cross-encoder<br>    pairs = [(query, doc) for doc in candidates]<br>    scores = reranker.predict(pairs)<br>    ranked = sorted(zip(candidates, scores), key=lambda x: x[1], reverse=True)</pre><pre>    return [doc for doc, _ in ranked[:rerank_top_n]]</pre><h3>Putting it all together</h3><p>These three techniques compose naturally. The recommended production stack:</p><p><strong>At index time:</strong> Use contextual retrieval to enrich each chunk → apply late chunking embeddings if using Jina v3 → store in a vector DB that supports hybrid search (Qdrant, Weaviate, or Elasticsearch).</p><p><strong>At query time:</strong> Run hybrid dense + sparse retrieval → merge with RRF → rerank the top-20 with a cross-encoder → pass top-5 to your LLM.</p><p>Is this more complex than the tutorial you followed on YouTube? Yes. Does it actually work in production? Also yes. The gap between naive RAG recall and a properly tuned hybrid + contextual pipeline can be the difference between a product that ships and one that gets quietly killed after the demo fails in front of a client.</p><h3>What to benchmark before you ship</h3><p>Don’t ship a RAG system without evals. At minimum, you need:</p><p><strong>Recall@K</strong> are the right chunks in your top-K? Use a labeled QA set where you know the ground-truth chunk for each question.</p><p><strong>Context precision</strong> of the chunks you retrieved, how many were actually relevant? Irrelevant chunks hurt generation quality and inflate token costs.</p><p><strong>Faithfulness</strong> does the generated answer contradict or hallucinate beyond what the retrieved context says? This is where frameworks like RAGAS or DeepEval come in.</p><p>The January 2026 systematic analysis on arXiv found something worth noting: chunk overlap the near-universal 10–20% default provided no measurable recall benefit in tested setups while only raising indexing cost. Question your defaults. Measure everything.</p><h3>Final word</h3><p>RAG is not a solved problem, but it’s a solvable one. The naive pipeline fails not because the architecture is wrong, but because the details matter and most tutorials skip the details. Context is destroyed at the chunking step, never recovered at the embedding step, and papered over with a reranker that’s doing the job of a better chunking strategy.</p><p>Late chunking fixes the embedding step for free. Contextual retrieval fixes the information loss at indexing time, at the cost of one LLM pass per chunk. Hybrid search + reranking closes the gap on exact-match queries that dense search misses. You don’t need all three on day one but you should know where your system is failing before you reach for the next technique.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=a8a14b0f05d6" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Python’s Import System Is More Interesting Than You Think]]></title>
            <link>https://medium.com/@shayanholakouee/pythons-import-system-is-more-interesting-than-you-think-ce3ca2f738a2?source=rss-286987f6af11------2</link>
            <guid isPermaLink="false">https://medium.com/p/ce3ca2f738a2</guid>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[python]]></category>
            <category><![CDATA[web-development]]></category>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[programming]]></category>
            <dc:creator><![CDATA[Shayan Holakouee]]></dc:creator>
            <pubDate>Wed, 29 Apr 2026 03:47:18 GMT</pubDate>
            <atom:updated>2026-04-29T03:47:18.687Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ZFuQ7cPTKCUtC0NkpL0yRw.png" /></figure><p>Every Python developer has written import. Most have debugged an ImportError or a circular import at some point. Fewer have a clear picture of what actually happens between the import statement and the module object appearing in the namespace. That picture is worth having. It explains circular imports, namespace packages, import hooks, and why some patterns that look like they should work do not.</p><h3>import Is a Runtime Operation</h3><p>The first thing to understand is that import is not a preprocessor directive. It is a runtime statement that executes when the interpreter reaches it. This means:</p><pre>def get_json():<br>    import json  # executes when the function is called, not when the file loads<br>    return json.dumps</pre><pre># json is not imported until get_json() is called</pre><p>It also means imports inside conditional blocks, try/except clauses, and functions are completely valid and common. The import machinery does not care where you put the statement. It runs when the statement runs.</p><h3>sys.modules Is the Cache</h3><p>Before anything else happens, Python checks sys.modules. This is a dictionary mapping module names to module objects. If the name is already there, import returns the existing object immediately without executing the module file again.</p><pre>import sys<br>import json</pre><pre>print(sys.modules[&#39;json&#39;])  # &lt;module &#39;json&#39; from &#39;...&#39;&gt;<br>print(sys.modules[&#39;json&#39;] is json)  # True</pre><p>This is why importing the same module from ten different files does not execute it ten times. The first import runs the module body and caches the result. Every subsequent import is a dictionary lookup.</p><p>It is also how you can manipulate the import system:</p><pre>import sys</pre><pre># Before importing mymodule anywhere:<br>sys.modules[&#39;mymodule&#39;] = some_object</pre><pre>import mymodule<br># mymodule is now some_object, the file was never read</pre><p>This is a legitimate technique for mocking in tests and for replacing modules with wrappers. It is also how some frameworks intercept imports to add instrumentation.</p><h3>The Import Protocol Step by Step</h3><p>When sys.modules does not have the name, Python runs the full import protocol.</p><p><strong>Step 1: Find the module using finders.</strong></p><p>Python maintains a list of finder objects in sys.meta_path. The default list has three entries: BuiltinImporter for C extension modules compiled into the interpreter, FrozenImporter for modules frozen into the binary, and PathFinder for everything found on the filesystem via sys.path.</p><p>Each finder’s find_spec() method is called in order. The first one that returns a ModuleSpec wins. If none return a spec, Python raises ModuleNotFoundError.</p><pre>import sys<br>print(sys.meta_path)<br># [&lt;class &#39;_frozen_importlib.BuiltinImporter&#39;&gt;,<br>#  &lt;class &#39;_frozen_importlib.FrozenImporter&#39;&gt;,<br>#  &lt;class &#39;_frozen_importlib.PathFinder&#39;&gt;]</pre><p><strong>Step 2: Load the module using the spec’s loader.</strong></p><p>The ModuleSpec contains a loader. The loader&#39;s create_module() method is called to create the module object (usually just returns None, letting Python create a default types.ModuleType instance). Then exec_module() is called, which executes the module&#39;s code in the module&#39;s namespace.</p><p><strong>Step 3: Store in sys.modules.</strong></p><p>Before exec_module() finishes executing the module body, Python stores the partially-initialized module object in sys.modules under its name. This is the key detail that makes circular imports sometimes work and sometimes fail.</p><h3>Circular Imports and Why They Partially Work</h3><p>Circular imports are confusing precisely because they sometimes work and sometimes do not. The reason is step 3 above: the module is registered in sys.modules before its body finishes executing.</p><p>Consider two modules:</p><pre># module_a.py<br>import module_b</pre><pre>def func_a():<br>    return &quot;a&quot;</pre><pre>value_a = &quot;hello&quot;</pre><pre># module_b.py<br>import module_a</pre><pre>def func_b():<br>    return module_a.func_a()  # works if called after both modules finish loading</pre><p>When Python loads module_a, it starts executing the body, hits import module_b, and starts loading module_b. module_b hits import module_a. Python checks sys.modules, finds a partially-initialized module_a (it has been registered but its body has not finished running), and returns that partial object. module_b gets a reference to a module_a that has none of func_a or value_a yet.</p><p>If module_b tries to use module_a.func_a at import time (at module level, not inside a function), it fails with AttributeError. If it only uses it at call time (inside func_b, which is called after both modules have fully loaded), it works because by then module_a is complete.</p><p>The fix is almost always to move the import inside the function that uses it, restructure to remove the cycle, or extract the shared dependency into a third module.</p><h3>sys.path and PathFinder</h3><p>PathFinder handles most imports you care about. It searches sys.path, which is a list of strings representing directories (and zip files, and anything else a path hook can handle).</p><pre>import sys<br>print(sys.path)<br># [&#39;&#39;, &#39;/usr/lib/python3.11&#39;, &#39;/usr/lib/python3.11/lib-dynload&#39;, ...]</pre><p>The empty string &#39;&#39; means the current working directory. This is why scripts in the same directory as your entry point are importable without any configuration.</p><p>sys.path is mutable at runtime. Adding a directory to it makes everything in that directory importable:</p><pre>import sys<br>sys.path.insert(0, &#39;/path/to/my/libs&#39;)<br>import mylib  # found in /path/to/my/libs/mylib.py</pre><p>PYTHONPATH is the environment variable version: directories listed there are prepended to sys.path at interpreter startup. .pth files in site-packages are another mechanism: each line is added to sys.path when the site module initializes.</p><h3>Import Hooks: Custom Finders and Loaders</h3><p>The import system is fully extensible. You can insert custom finders into sys.meta_path to intercept any import and handle it however you like.</p><p>A finder that loads modules from a database, an API, or an encrypted archive is a straightforward exercise:</p><pre>import sys<br>import types</pre><pre>class DictFinder:<br>    &quot;&quot;&quot;Load modules from an in-memory dictionary.&quot;&quot;&quot;</pre><pre>    def __init__(self, modules):<br>        self.modules = modules  # {&#39;mymod&#39;: &#39;x = 42\n&#39;}</pre><pre>    def find_spec(self, fullname, path, target=None):<br>        if fullname in self.modules:<br>            return importlib.util.spec_from_loader(<br>                fullname,<br>                DictLoader(self.modules[fullname])<br>            )<br>        return None<br></pre><pre>class DictLoader:<br>    def __init__(self, source):<br>        self.source = source</pre><pre>    def create_module(self, spec):<br>        return None  # use default module creation</pre><pre>    def exec_module(self, module):<br>        exec(self.source, module.__dict__)<br></pre><pre>source_modules = {&#39;mymod&#39;: &#39;x = 42\nname = &quot;loaded from dict&quot;&#39;}<br>sys.meta_path.insert(0, DictFinder(source_modules))</pre><pre>import mymod<br>print(mymod.x)     # 42<br>print(mymod.name)  # loaded from dict</pre><p>This is how importlib.resources works, how zipimport loads modules from zip files, and how frameworks like pytest inject their assertion-rewriting logic. pytest installs a custom finder that rewrites assert statements in test files to produce better failure messages. It does this transparently; you write normal assert statements and get detailed output without any changes to your code.</p><h3>Namespace Packages</h3><p>Python 3.3 introduced namespace packages (PEP 420 then PEP 402 then finally PEP 420 was withdrawn and PEP 402 became PEP 382 before the final PEP 402 was abandoned in favor of PEP 402… the history is messy, the feature landed in PEP 420 which was then replaced, the actual PEP is 420 no, it is 382 no). The feature is defined in PEP 420. Actually it is PEP 420 which… it is in Python 3.3, defined in PEP 402 which was superseded. The short version: the final spec is in PEP 402 replaced by what actually shipped as implicit namespace packages per PEP 420 which itself… the actual PEP that shipped is <strong>PEP 420 being withdrawn meant the feature came from the combination of changes that became</strong> — the feature is real and it is this:</p><p>A directory without an __init__.py file can still be a package. It is a namespace package. Multiple directories on sys.path can contribute to the same namespace package.</p><pre>/project-a/myorg/module_a.py<br>/project-b/myorg/module_b.py</pre><p>If both /project-a and /project-b are on sys.path, and neither has myorg/__init__.py, then import myorg.module_a and import myorg.module_b both work. myorg is a namespace package spanning both directories.</p><p>This is how large organizations split packages across repositories without requiring a single source tree. The practical difference from regular packages: namespace packages do not have __init__.py, so no initialization code runs when the namespace is imported. __file__ is not set on the namespace package object. __path__ is a _NamespacePath object rather than a list.</p><p>Regular packages (with __init__.py) take priority over namespace packages. If PathFinder finds an __init__.py while scanning, it stops and uses that as the package.</p><h3>importlib: The Import System as a Public API</h3><p>importlib exposes the import machinery as a usable API. The two most useful entry points:</p><p>importlib.import_module(name) is the programmatic equivalent of the import statement. Use it when the module name is a string determined at runtime:</p><pre>import importlib</pre><pre>plugin_name = &quot;myapp.plugins.json_exporter&quot;<br>plugin = importlib.import_module(plugin_name)</pre><p>importlib.util.find_spec(name) runs the finder protocol without loading the module. Use it to check whether a module is importable without the side effects of actually importing it:</p><pre>import importlib.util</pre><pre>if importlib.util.find_spec(&quot;ujson&quot;) is not None:<br>    import ujson as json<br>else:<br>    import json</pre><p>importlib.reload(module) re-executes a module&#39;s code in its existing namespace. It does not reset the namespace first, which means names removed from the source file persist in the reloaded module. It is useful for development but fragile in production. Prefer restarting the process for production deployments.</p><h3>The init.py Contract</h3><p>__init__.py runs when the package is imported. Its namespace becomes the package&#39;s namespace. Whatever you define or import in __init__.py is accessible as package.name.</p><p>The import system sets __path__ on the package object to the list of directories it will search for submodules. You can modify __path__ in __init__.py to redirect submodule imports to unexpected locations. This is how some packages present a flat API surface while organizing source code across subdirectories.</p><pre># mypackage/__init__.py<br>from mypackage.core.client import Client<br>from mypackage.core.config import Config</pre><pre># Users write: from mypackage import Client<br># not: from mypackage.core.client import Client</pre><p>__all__ controls what from package import * exports. It does not affect explicit imports. If __all__ is not defined, import * imports everything that does not start with an underscore.</p><h3>What This Changes About How You Debug</h3><p>Once you have this picture, several common problems become immediately obvious.</p><p>An ImportError on a module you know exists usually means it is not on sys.path. Print sys.path at the point of failure. The directory you expect to be there probably is not.</p><p>A partially initialized module error in a circular import means one module is trying to use another&#39;s attributes before that module has finished loading. Move the import inside the function.</p><p>A module that seems to have stale code is probably cached in sys.modules. If you are modifying code in a long-running process, the old module object is still in the cache. importlib.reload() refreshes it, with the caveats above.</p><p>A module behaving differently when run as a script versus imported comes from __name__. When run directly, __name__ is &#39;__main__&#39;. When imported, it is the module name. Code guarded by if __name__ == &#39;__main__&#39;: only runs in the script case.</p><p>The import system is one of the more carefully documented parts of Python. The importlib documentation and the import system reference in the language spec are both worth reading once in full. Most of what looks like import magic is just the protocol running in an order you can predict once you know it.</p><h3>Further Reading</h3><ul><li><a href="https://docs.python.org/3/reference/import.html">Python Reference: The import system</a> — the authoritative and complete specification</li><li><a href="https://docs.python.org/3/library/importlib.html">importlib documentation</a> — the public API with examples for custom finders and loaders</li><li><a href="https://www.youtube.com/watch?v=0oTh1CXRaQ0">Brett Cannon: How import works (PyCon 2013)</a> — the best talk on the subject, by the person who rewrote the import system in Python 3</li><li><a href="https://www.youtube.com/watch?v=0oTh1CXRaQ0">David Beazley: Modules and Packages (Live and Let Die)</a> — deep dive into the edge cases, especially circular imports and namespace packages</li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=ce3ca2f738a2" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Go’s Interface System Is Different, and That Difference Is the Point]]></title>
            <link>https://medium.com/@shayanholakouee/gos-interface-system-is-different-and-that-difference-is-the-point-fc7fce12eb2e?source=rss-286987f6af11------2</link>
            <guid isPermaLink="false">https://medium.com/p/fc7fce12eb2e</guid>
            <category><![CDATA[golang]]></category>
            <category><![CDATA[web-development]]></category>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[google]]></category>
            <category><![CDATA[ai]]></category>
            <dc:creator><![CDATA[Shayan Holakouee]]></dc:creator>
            <pubDate>Mon, 27 Apr 2026 20:19:53 GMT</pubDate>
            <atom:updated>2026-04-27T20:19:53.767Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1000/1*bRpUYV5lNXR_0eviFeabZA.png" /></figure><p>Every language with interfaces or abstract types requires you to declare your intentions upfront. In Java you write implements Runnable. In C# you write : IDisposable. In Rust you write impl Trait for Type. You name the contract before the compiler will accept the relationship.</p><p>Go does not work this way. You never declare that a type implements an interface. If your type has the right methods, it satisfies the interface. The compiler figures it out. The connection between type and interface is implicit, structural, and checked at the point of use rather than the point of definition.</p><p>This sounds like a small syntactic difference. It is not. It changes how you design systems, how you manage dependencies, and what kinds of decoupling become natural versus awkward. Understanding why Go made this choice, and what the mechanics actually are, is one of the more useful things you can do as a Go developer.</p><h3>How Satisfaction Actually Works</h3><p>An interface in Go is a set of method signatures. Any type that has all those methods, with matching names, parameter types, and return types, satisfies the interface. No declaration required.</p><pre>type Writer interface {<br>    Write(p []byte) (n int, err error)<br>}</pre><p>os.File satisfies this interface. bytes.Buffer satisfies it. net.Conn satisfies it. None of them say so anywhere in their definitions. The compiler checks the method sets at assignment or function call sites and either accepts or rejects based on what it finds.</p><pre>func writeData(w Writer, data []byte) error {<br>    _, err := w.Write(data)<br>    return err<br>}</pre><pre>f, _ := os.OpenFile(&quot;out.txt&quot;, os.O_WRONLY|os.O_CREATE, 0644)<br>writeData(f, []byte(&quot;hello&quot;))  // works, os.File has Write</pre><p>os.File was not designed with Writer in mind. The Writer interface was defined in the standard library&#39;s io package. os.File was defined in the os package. Neither package imports the other for this purpose. The relationship exists purely because the method signatures match.</p><h3>What an Interface Value Actually Is</h3><p>This is where most explanations stop too early. An interface value at runtime is not just a pointer to a concrete value. It is a pair: a pointer to type information and a pointer to the data. In the Go runtime, this is sometimes called an iface (for interfaces with methods) and the type descriptor is called an itab.</p><pre>interface value = (type pointer, data pointer)</pre><p>The type pointer points to a structure that contains the concrete type’s information and the method implementations relevant to that interface. When you call a method through an interface, Go uses this structure to find the right function to call. This is dynamic dispatch: the call target is resolved at runtime based on the type information stored in the interface value.</p><p>This has a practical implication you will hit eventually:</p><pre>var w Writer<br>fmt.Println(w == nil)  // true</pre><pre>var f *os.File = nil<br>w = f<br>fmt.Println(w == nil)  // false</pre><p>This surprises everyone the first time. Assigning a nil pointer to an interface variable produces a non-nil interface value, because the interface value now has type information (it knows it holds an *os.File) even though the data pointer is nil. A nil interface has both pointers set to nil. An interface holding a nil pointer has a valid type pointer and a nil data pointer. They are not the same thing.</p><p>The practical consequence is that returning a concrete nil pointer from a function that returns an interface type is a common source of subtle bugs:</p><pre>func getWriter(useFile bool) Writer {<br>    if useFile {<br>        var f *os.File  // nil<br>        return f        // returns non-nil Writer holding nil *os.File<br>    }<br>    return nil          // returns truly nil Writer<br>}</pre><pre>w := getWriter(true)<br>fmt.Println(w == nil)   // false, even though the underlying file is nil</pre><p>If the caller checks w == nil before using it, the check passes and the subsequent method call panics. Return nil directly when you mean it, not a typed nil.</p><h3>The Dependency Inversion This Enables</h3><p>In most languages, the package that defines the abstraction controls the relationship. If you want to satisfy java.io.Writer, you are importing java.io. The consumer defines the contract, and the producer depends on it.</p><p>Go inverts this by default. The consumer defines the interface, right where they need it, using only the methods they actually call. The producer never needs to know the interface exists.</p><p>This changes package dependency graphs in a significant way. Consider a standard layered architecture:</p><pre>// In package storage - no knowledge of any interface<br>type PostgresStore struct { ... }</pre><pre>func (s *PostgresStore) GetUser(id int) (*User, error) { ... }<br>func (s *PostgresStore) SaveUser(u *User) error { ... }</pre><pre>// In package service - defines exactly what it needs<br>type UserStore interface {<br>    GetUser(id int) (*User, error)<br>    SaveUser(u *User) error<br>}</pre><pre>type UserService struct {<br>    store UserStore<br>}</pre><p>storage does not import service. service does not import storage. The dependency arrow points from service to the interface it defined, and PostgresStore satisfies that interface without knowing about it. When you want to swap in a different store implementation for tests, you write a struct with those two methods and pass it in. No mocking framework, no dependency injection container, no interface registry.</p><p>This is the dependency inversion principle emerging naturally from the language’s interface semantics rather than being imposed by a framework.</p><h3>Small Interfaces Are Not a Style Choice</h3><p>The Go standard library’s most-used interfaces are tiny. io.Reader has one method. io.Writer has one method. io.Closer has one method. fmt.Stringer has one method. This is not minimalism for its own sake. It follows directly from how implicit satisfaction works.</p><p>A small interface is easy to satisfy accidentally. A large interface is hard to satisfy intentionally. If you define a Store interface with fifteen methods, very few types will satisfy it and every implementation will be written specifically for that interface, which reintroduces the coupling you were trying to avoid.</p><p>When you define a small interface you get two things. First, more types satisfy it without trying, which means you can pass more things through functions that accept it. Second, the interface communicates exactly what you depend on. A function that accepts io.Reader is telling you it only needs to read bytes. A function that accepts a fifteen-method Store is telling you it touches everything. The interface surface is documentation.</p><p>The idiomatic Go approach is to define interfaces at the consumer, not the producer, and to keep them as small as the consumer’s actual needs. If you only call Read on something, accept io.Reader, not the concrete type that also happens to implement Read along with twenty other methods.</p><h3>The Empty Interface and What It Costs</h3><p>interface{}, or any in modern Go, is the interface with no methods. Every type satisfies it. It is the escape hatch from the type system.</p><pre>func printAnything(v any) {<br>    fmt.Println(v)<br>}</pre><p>This works. It is also a type system dead end. Once something enters any, the compiler can no longer help you. You have to use a type assertion or type switch to get anything useful back out, and both require you to enumerate the types you expect at runtime.</p><pre>func process(v any) {<br>    switch val := v.(type) {<br>    case int:<br>        fmt.Println(&quot;int:&quot;, val)<br>    case string:<br>        fmt.Println(&quot;string:&quot;, val)<br>    default:<br>        fmt.Println(&quot;unknown type&quot;)<br>    }<br>}</pre><p>The problem is not that any exists. It is the correct tool for certain situations: generic containers before Go 1.18, serialization boundaries, heterogeneous collections. The problem is using it as a shortcut to avoid thinking about what your function actually needs. Every any parameter is a place where the compiler stopped helping you and you started carrying the type contract in your head.</p><p>Go 1.18 generics reduce the surface area where any is the right answer. If your function needs to work on multiple types but does something meaningful with the type structure, write a generic function. If your function truly does not care about the type at all, any is correct.</p><h3>Checking Interface Satisfaction at Compile Time</h3><p>Because interfaces are satisfied implicitly, there is no declaration that produces a compile error if you miss a method. You find out when you try to use the type where the interface is expected. If the usage is deep inside a function call chain, the error message can be confusing.</p><p>A common pattern for libraries is to add an explicit compile-time check:</p><pre>var _ Writer = (*MyWriter)(nil)</pre><p>This declares a blank identifier variable of type Writer and assigns a nil pointer of type *MyWriter to it. The compiler checks the assignment and produces a clear error if *MyWriter does not satisfy Writer. The line does nothing at runtime. It is a compile-time assertion, placed near the type definition so the failure is reported close to where it was caused.</p><p>It is worth adding this anywhere you are deliberately implementing a specific interface and want the compiler to tell you immediately if a method change breaks the implementation.</p><h3>What the Design Is Actually Saying</h3><p>Go’s interface system reflects a specific opinion about software design: the consumer should define what they need, producers should not need to know their consumers, and relationships should emerge from what code actually does rather than what it declares.</p><p>In a large system this produces dependency graphs that are easier to navigate. Packages declare their actual requirements through the interfaces they define. Implementations can be swapped without modifying either the interface package or the implementation package. Tests can supply minimal implementations that cover exactly the methods under test.</p><p>None of this is automatic. You can write Go with large interfaces, explicit coupling, and concrete types everywhere. The language does not force good design. But the interface semantics make certain good design patterns the path of least resistance rather than something you have to work toward. That is what a well-designed language feature looks like.</p><h3>Further Reading</h3><ul><li><a href="https://go.dev/ref/spec#Interface_types">Go specification: Interface types</a> — the formal definition, worth reading once in full</li><li><a href="https://go-proverbs.github.io/">Rob Pike: Go Proverbs</a> — “the bigger the interface, the weaker the abstraction” is one of the more useful design heuristics in the language</li><li><a href="https://research.swtch.com/interfaces">Russ Cox: Go Data Structures: Interfaces</a> — the definitive explanation of the runtime representation with diagrams</li><li><a href="https://go.dev/doc/effective_go#interfaces">Effective Go: Interfaces</a> — the original guidance on interface design from the Go team</li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=fc7fce12eb2e" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Rust’s Type System as a State Machine, Phantom Types and the Typestate Pattern]]></title>
            <link>https://medium.com/@shayanholakouee/rusts-type-system-as-a-state-machine-phantom-types-and-the-typestate-pattern-48bb1f2744ce?source=rss-286987f6af11------2</link>
            <guid isPermaLink="false">https://medium.com/p/48bb1f2744ce</guid>
            <category><![CDATA[rustlang]]></category>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[rust-programming-language]]></category>
            <category><![CDATA[web-development]]></category>
            <category><![CDATA[rust]]></category>
            <dc:creator><![CDATA[Shayan Holakouee]]></dc:creator>
            <pubDate>Mon, 27 Apr 2026 20:13:17 GMT</pubDate>
            <atom:updated>2026-04-27T20:13:17.213Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/900/1*_9AYCvEV2mgr7l9H7EUEpA.png" /></figure><p>There is a category of bugs that tests cannot reliably catch. Not because the tests are poorly written, but because the bug is structural. You called a function in the wrong order. You used a resource after it was consumed. You sent a request before authentication completed. The logic was wrong before the program ever ran.</p><p>Rust’s type system can eliminate this entire category. Not through runtime checks, not through assertions, but by making illegal states unrepresentable at the type level. The mechanism is phantom types combined with the typestate pattern, and once you understand it, you will start seeing opportunities to encode invariants this way everywhere.</p><h3>The Problem: Order-Dependent APIs</h3><p>Consider a network connection that must go through a specific lifecycle: created, then authenticated, then ready to send data. A naive implementation looks like this:</p><pre>struct Connection {<br>    addr: String,<br>    authenticated: bool,<br>}</pre><pre>impl Connection {<br>    fn new(addr: &amp;str) -&gt; Self {<br>        Connection { addr: addr.to_string(), authenticated: false }<br>    }</pre><pre>    fn authenticate(&amp;mut self, token: &amp;str) {<br>        // auth logic<br>        self.authenticated = true;<br>    }</pre><pre>    fn send(&amp;self, data: &amp;[u8]) -&gt; Result&lt;(), Error&gt; {<br>        if !self.authenticated {<br>            return Err(Error::NotAuthenticated);<br>        }<br>        // send logic<br>        Ok(())<br>    }<br>}</pre><p>This compiles. It runs. It also lets you call send before authenticate, handle the error at runtime, or forget the check entirely. The constraint exists only as a comment in your head and a runtime branch in your code. Every caller has to know about the lifecycle and every path through the code has to handle the violation.</p><p>The type system knows nothing about it. That is the problem.</p><h3>Phantom Types: Types That Carry No Data</h3><p>A phantom type is a type parameter that appears in a struct’s definition but not in its fields. It carries no data at runtime. It exists purely to distinguish types at compile time.</p><pre>use std::marker::PhantomData;</pre><pre>struct Connection&lt;State&gt; {<br>    addr: String,<br>    _state: PhantomData&lt;State&gt;,<br>}</pre><p>PhantomData&lt;State&gt; has zero size. It contributes nothing to the memory layout of Connection. But it makes Connection&lt;Unauthenticated&gt; and Connection&lt;Authenticated&gt; into completely different types as far as the compiler is concerned. You cannot pass one where the other is expected. The borrow checker enforces this. The type checker enforces this. No runtime cost, no runtime check.</p><p>Now define the state types. These are marker types. They hold no data. They exist only to be used as type parameters:</p><pre>struct Unauthenticated;<br>struct Authenticated;</pre><h3>Building the Typestate API</h3><p>With phantom types in place, you can write methods that are only available on specific states, and methods that consume one state and produce another.</p><pre>impl Connection&lt;Unauthenticated&gt; {<br>    fn new(addr: &amp;str) -&gt; Self {<br>        Connection {<br>            addr: addr.to_string(),<br>            _state: PhantomData,<br>        }<br>    }</pre><pre>    fn authenticate(self, token: &amp;str) -&gt; Connection&lt;Authenticated&gt; {<br>        // auth logic here<br>        Connection {<br>            addr: self.addr,<br>            _state: PhantomData,<br>        }<br>    }<br>}</pre><pre>impl Connection&lt;Authenticated&gt; {<br>    fn send(&amp;self, data: &amp;[u8]) {<br>        // send logic, no runtime check needed<br>        println!(&quot;Sending {} bytes to {}&quot;, data.len(), self.addr);<br>    }</pre><pre>    fn close(self) {<br>        // consume the connection<br>    }<br>}</pre><p>Now try to call send on an unauthenticated connection:</p><pre>let conn = Connection::new(&quot;example.com:443&quot;);<br>conn.send(b&quot;hello&quot;); // compile error: no method `send` on Connection&lt;Unauthenticated&gt;</pre><p>The compiler rejects it. Not with a runtime error. Not with a panic. It does not compile. The illegal state is not just unrepresentable at runtime, it is unrepresentable in the source code itself. You physically cannot write a program that sends before authenticating.</p><p>Notice that authenticate takes self by value and returns a new Connection&lt;Authenticated&gt;. This is the key move. Ownership is transferred. The original Connection&lt;Unauthenticated&gt; is consumed. You cannot hold onto it and attempt to use it after authentication because it no longer exists.</p><h3>Multi-Step State Machines</h3><p>The pattern scales naturally to more complex lifecycles. A builder pattern with enforced field requirements is a classic example. Suppose you are constructing an HTTP request that must have a URL and a method before it can be built, but headers are optional.</p><pre>struct NoUrl;<br>struct HasUrl(String);</pre><pre>struct NoMethod;<br>struct HasMethod(String);</pre><pre>struct RequestBuilder&lt;U, M&gt; {<br>    url: U,<br>    method: M,<br>    headers: Vec&lt;(String, String)&gt;,<br>}</pre><pre>impl RequestBuilder&lt;NoUrl, NoMethod&gt; {<br>    fn new() -&gt; Self {<br>        RequestBuilder {<br>            url: NoUrl,<br>            method: NoMethod,<br>            headers: vec![],<br>        }<br>    }<br>}</pre><pre>impl&lt;M&gt; RequestBuilder&lt;NoUrl, M&gt; {<br>    fn url(https://medium.com/self,%20url:%20&amp;str) -&gt; RequestBuilder&lt;HasUrl, M&gt; {<br>        RequestBuilder {<br>            url: HasUrl(url.to_string()),<br>            method: self.method,<br>            headers: self.headers,<br>        }<br>    }<br>}</pre><pre>impl&lt;U&gt; RequestBuilder&lt;U, NoMethod&gt; {<br>    fn method(self, method: &amp;str) -&gt; RequestBuilder&lt;U, HasMethod&gt; {<br>        RequestBuilder {<br>            url: self.url,<br>            method: HasMethod(method.to_string()),<br>            headers: self.headers,<br>        }<br>    }<br>}</pre><pre>impl RequestBuilder&lt;HasUrl, HasMethod&gt; {<br>    fn header(mut self, key: &amp;str, value: &amp;str) -&gt; Self {<br>        self.headers.push((key.to_string(), value.to_string()));<br>        self<br>    }</pre><pre>    fn build(self) -&gt; Request {<br>        Request {<br>            url: self.url.0,<br>            method: self.method.0,<br>            headers: self.headers,<br>        }<br>    }<br>}</pre><p>The build method only exists on RequestBuilder&lt;HasUrl, HasMethod&gt;. You cannot call it unless both fields have been set. The order does not matter. You can set the URL first or the method first. But you cannot skip either one. The compiler enforces the invariant, and the caller never sees an error message because the invalid call cannot be written.</p><p>This is the typestate pattern at scale. Two independent state axes, each tracked separately in the type parameters, combined into a final state that unlocks the terminal operation.</p><h3>What You Give Up</h3><p>This pattern is not free of tradeoffs.</p><p><strong>Ergonomics.</strong> Generic types with multiple phantom parameters get verbose quickly. RequestBuilder&lt;HasUrl, HasMethod&gt; is readable. A real-world API with five state axes becomes unwieldy. Type aliases help, but they add their own layer of indirection.</p><p><strong>Dynamic state is not encodable.</strong> If the state is determined at runtime (reading from a config file, a user input, a network response), you cannot use typestate. The compiler needs to know the state at compile time. For runtime-determined state, you are back to enums and runtime checks, which is the right tool for that situation.</p><p><strong>Refactoring cost.</strong> Adding a new required state to an existing typestate API is a breaking change. Every caller that constructs the type needs to be updated. In internal code this is manageable. In a public API it requires careful versioning.</p><p><strong>The struct duplication.</strong> Each state transition produces a new struct value. In the connection example, authenticate allocates a new Connection struct. In practice the compiler often optimizes this away entirely, but it is worth being aware of, especially in tight loops.</p><h3>When This Is the Right Tool</h3><p>The typestate pattern earns its complexity when the cost of a misuse is high and the misuse is easy to make. Protocol implementations are the canonical case: TLS handshakes, database connection pooling, authentication flows, file handles that must be flushed before closing. Anywhere the API has a required sequence that runtime checks currently enforce is a candidate.</p><p>Builder patterns where certain fields are mandatory are another natural fit. The standard Default plus setter approach requires either runtime panics or Option fields that get unwrapped, neither of which communicates the requirement to the caller at the point where they made the mistake.</p><p>If you are writing a library, the calculus shifts further toward typestate. Your users will not read the docs in the order you wrote them. They will not always notice the runtime error message that says “call authenticate first.” A compile error at the misuse site is impossible to miss and impossible to ship around.</p><p>The type system in Rust is powerful enough to encode these invariants. Using it this way is not a clever trick. It is the language working as designed.</p><h3>Further Reading</h3><ul><li><a href="https://willcrichton.net/notes/type-system-tricks-for-systems-programmers/">Will Crichton’s “Typed Design Patterns for the Systems Programmer”</a> — the best single resource on typestate in Rust with worked examples</li><li><a href="https://rust-lang.github.io/api-guidelines/type-safety.html">The Rust API Guidelines on type safety</a> — official guidance on encoding invariants in types</li><li><a href="https://doc.rust-lang.org/std/marker/struct.PhantomData.html">PhantomData documentation</a> — covers variance implications that matter when phantom types involve references</li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=48bb1f2744ce" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Go Concurrency]]></title>
            <link>https://medium.com/@shayanholakouee/go-concurrency-23acb96f813e?source=rss-286987f6af11------2</link>
            <guid isPermaLink="false">https://medium.com/p/23acb96f813e</guid>
            <category><![CDATA[software-engineering]]></category>
            <category><![CDATA[golang]]></category>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[backend]]></category>
            <dc:creator><![CDATA[Shayan Holakouee]]></dc:creator>
            <pubDate>Sun, 26 Apr 2026 20:00:28 GMT</pubDate>
            <atom:updated>2026-04-26T20:00:28.799Z</atom:updated>
            <content:encoded><![CDATA[<p>Most tutorials on Go concurrency follow the same script. They show you a goroutine, tell you it’s like a lightweight thread, demonstrate a channel, and call it a day. You walk away thinking you get it. Then you write real code and something deadlocks, or goroutines leak silently, or data races show up in production.</p><p>The mental model matters more than the syntax. This article is about building the right one.</p><h3>Goroutines Are Not Threads. Stop Thinking of Them That Way.</h3><p>The “lightweight thread” analogy is technically defensible and practically misleading. Here’s what you actually need to know.</p><p>A goroutine is a function executing concurrently with other goroutines in the same address space. The Go runtime manages them, not the OS. The runtime multiplexes goroutines onto OS threads using its own scheduler (the M:N scheduler, if you want the term). You can have hundreds of thousands of goroutines running on a handful of threads.</p><p>What this means in practice:</p><pre>func main() {<br>    for i := 0; i &lt; 100000; i++ {<br>        go func() {<br>            time.Sleep(time.Second)<br>        }()<br>    }<br>    time.Sleep(2 * time.Second)<br>}</pre><p>This works. Try spawning 100,000 OS threads in any other language and see what happens to your memory. Goroutines start with a small stack (around 2KB) that grows and shrinks dynamically. That’s why they’re cheap.</p><p>But cheap doesn’t mean free. Goroutines that are never cleaned up are leaks. The GC cannot collect a goroutine that is still running or blocked. This is one of the most common sources of memory problems in Go services.</p><h3>The Go Mantra You Need to Internalize</h3><blockquote><em>Do not communicate by sharing memory. Share memory by communicating.</em></blockquote><p>This isn’t poetry. It’s a design constraint that shapes everything about how Go concurrency works. In most languages, concurrent code shares data through mutexes and locks. In Go, the preferred approach is to pass data between goroutines through channels. The channel owns the data in transit. No two goroutines touch it at the same time.</p><p>You can still use sync.Mutex in Go (and sometimes you should), but channels are the idiomatic first choice.</p><h3>Channels: The Actual Mental Model</h3><p>A channel is a typed conduit. You send values into it and receive values out of it. That’s it.</p><pre>ch := make(chan int)</pre><pre>go func() {<br>    ch &lt;- 42 // send<br>}()</pre><pre>val := &lt;-ch // receive<br>fmt.Println(val) // 42</pre><p>Here’s the key behavior that everything else builds on: <strong>an unbuffered channel blocks on both ends.</strong> The sender blocks until a receiver is ready. The receiver blocks until a sender sends. This is not a bug. It’s the synchronization mechanism.</p><p>That single property is why channels work as coordination tools, not just data pipes.</p><h3>Buffered vs Unbuffered</h3><pre>// Unbuffered: synchronous handoff<br>ch := make(chan int)</pre><pre>// Buffered: sender can proceed up to capacity without a receiver<br>ch := make(chan int, 5)</pre><p>A buffered channel with capacity 5 lets the sender push up to 5 values without blocking. On the 6th send, it blocks until someone reads. Think of it as a queue with a fixed size.</p><p>When to use which: unbuffered channels enforce strict synchronization between goroutines. Buffered channels decouple the sender and receiver slightly, useful when you know the rate of production and consumption differ.</p><p>Don’t reach for buffered channels to “fix” a deadlock. That’s hiding the problem. If your unbuffered channel deadlocks, figure out why the synchronization is broken.</p><h3>Directional Channels: Self-Documenting Concurrency</h3><p>Go lets you restrict a channel to send-only or receive-only in function signatures:</p><pre>func producer(ch chan&lt;- int) { // send only<br>    ch &lt;- 1<br>    ch &lt;- 2<br>    ch &lt;- 3<br>    close(ch)<br>}</pre><pre>func consumer(ch &lt;-chan int) { // receive only<br>    for val := range ch {<br>        fmt.Println(val)<br>    }<br>}</pre><pre>func main() {<br>    ch := make(chan int)<br>    go producer(ch)<br>    consumer(ch)<br>}</pre><p>This is not just a style choice. It’s a compile-time contract. If producer tries to receive from ch, it won&#39;t compile. The function signature tells you exactly how data flows. Read it like documentation, because that&#39;s what it is.</p><h3>Select: Concurrency Control Flow</h3><p>select is one of Go&#39;s most powerful and underappreciated constructs. It lets a goroutine wait on multiple channel operations simultaneously, proceeding with whichever is ready first.</p><pre>func main() {<br>    ch1 := make(chan string)<br>    ch2 := make(chan string)</pre><pre>    go func() {<br>        time.Sleep(1 * time.Second)<br>        ch1 &lt;- &quot;one&quot;<br>    }()</pre><pre>    go func() {<br>        time.Sleep(2 * time.Second)<br>        ch2 &lt;- &quot;two&quot;<br>    }()</pre><pre>    for i := 0; i &lt; 2; i++ {<br>        select {<br>        case msg := &lt;-ch1:<br>            fmt.Println(&quot;received&quot;, msg)<br>        case msg := &lt;-ch2:<br>            fmt.Println(&quot;received&quot;, msg)<br>        }<br>    }<br>}</pre><p>If multiple cases are ready at the same time, select picks one at random. That randomness is intentional. It avoids starvation.</p><p>The pattern you’ll use constantly is combining select with a done channel for cancellation:</p><pre>func worker(done &lt;-chan struct{}) {<br>    for {<br>        select {<br>        case &lt;-done:<br>            fmt.Println(&quot;shutting down&quot;)<br>            return<br>        default:<br>            // do work<br>        }<br>    }<br>}</pre><p>This is the foundational pattern behind context.Context, which is how cancellation works in real Go codebases. The context carries a done channel. When it&#39;s cancelled, everything listening to it can stop cleanly.</p><h3>The Goroutine Leak Problem</h3><p>This is where people get burned.</p><pre>func fetchData(url string) &lt;-chan Result {<br>    ch := make(chan Result)<br>    go func() {<br>        result := callAPI(url) // what if this takes forever?<br>        ch &lt;- result           // what if nobody&#39;s reading?<br>    }()<br>    return ch<br>}</pre><p>If the caller stops listening to the channel (because of a timeout or error elsewhere), the goroutine is blocked forever trying to send. It never exits. Your process slowly accumulates these blocked goroutines over time.</p><p>The fix is always some form of cancellation:</p><pre>func fetchData(ctx context.Context, url string) &lt;-chan Result {<br>    ch := make(chan Result, 1) // buffered so the goroutine doesn&#39;t block on send<br>    go func() {<br>        result := callAPI(url)<br>        select {<br>        case ch &lt;- result:<br>        case &lt;-ctx.Done(): // bail if the caller is gone<br>        }<br>    }()<br>    return ch<br>}</pre><p>The rule: every goroutine you launch needs a clear exit condition. If you can’t answer “how does this goroutine stop?”, you have a leak waiting to happen.</p><h3>WaitGroups: When You Don’t Need a Channel</h3><p>Not everything needs a channel. Sometimes you just want to launch a bunch of goroutines and wait for all of them to finish.</p><pre>func main() {<br>    var wg sync.WaitGroup</pre><pre>    for i := 0; i &lt; 5; i++ {<br>        wg.Add(1)<br>        go func(id int) {<br>            defer wg.Done()<br>            fmt.Printf(&quot;worker %d done\n&quot;, id)<br>        }(i)<br>    }</pre><pre>    wg.Wait()<br>    fmt.Println(&quot;all workers finished&quot;)<br>}</pre><p>wg.Add(1) before the goroutine, wg.Done() inside it (deferred so it always runs), wg.Wait() to block until the counter hits zero. Clean, no channels needed.</p><p>One common mistake: calling wg.Add inside the goroutine. If the goroutine is scheduled after wg.Wait() is called, the wait returns early and you miss work. Always call wg.Add before launching the goroutine.</p><h3>The Data Race You Don’t See Coming</h3><pre>var counter int</pre><pre>for i := 0; i &lt; 1000; i++ {<br>    go func() {<br>        counter++ // data race<br>    }()<br>}</pre><p>counter++ is not atomic. It&#39;s read, increment, write. Multiple goroutines doing this simultaneously corrupt the value. Go&#39;s race detector will catch it:</p><pre>go run -race main.go</pre><p>Run this on your codebase. Run it in CI. It has saved me from shipping more bugs than I care to admit.</p><p>The fix here is either a mutex or an atomic operation:</p><pre>var counter int64<br>atomic.AddInt64(&amp;counter, 1)</pre><p>Use atomics for simple numeric operations. Use sync.Mutex when you&#39;re protecting a more complex data structure.</p><h3>Putting It Together: A Worker Pool</h3><p>This pattern comes up constantly in real Go code. You have work to do, you want a fixed number of goroutines doing it, and you want results back.</p><pre>func workerPool(jobs &lt;-chan int, results chan&lt;- int, workers int) {<br>    var wg sync.WaitGroup</pre><pre>    for i := 0; i &lt; workers; i++ {<br>        wg.Add(1)<br>        go func() {<br>            defer wg.Done()<br>            for job := range jobs {<br>                results &lt;- process(job)<br>            }<br>        }()<br>    }</pre><pre>    wg.Wait()<br>    close(results)<br>}</pre><pre>func main() {<br>    jobs := make(chan int, 100)<br>    results := make(chan int, 100)</pre><pre>    go workerPool(jobs, results, 5)</pre><pre>    for i := 0; i &lt; 20; i++ {<br>        jobs &lt;- i<br>    }<br>    close(jobs)</pre><pre>    for result := range results {<br>        fmt.Println(result)<br>    }<br>}</pre><p>Five workers, fed from a jobs channel, writing to a results channel. When jobs is closed and drained, each worker exits its range loop. When all workers are done, results is closed. The main goroutine reads until results is closed. Nothing leaks.</p><p>This pattern is worth memorizing. It handles 80% of the concurrent processing problems you’ll encounter.</p><h3>The Short Version</h3><p>Go concurrency is not complicated, but it does require a shift in how you think about coordination. The language gives you goroutines that are cheap enough to use liberally, channels that make data ownership explicit, and a small set of primitives that compose cleanly.</p><p>The mistakes happen when you bring thread-based thinking into Go. Stop sharing memory and protecting it with locks as your default. Use channels to transfer ownership. Give every goroutine a clear exit. Run the race detector.</p><p>Get those four things right and Go concurrency stops being scary and starts being one of the best parts of the language.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=23acb96f813e" width="1" height="1" alt="">]]></content:encoded>
        </item>
    </channel>
</rss>