Build a Code Explainer

Su Suriya Ravichandran Updated 10 Nov 2026
5 min read ·Lesson 193 of 200

The Goal

A code explainer reads a piece of source code and describes it in plain language, for a reader who is learning. It is useful in training, in code review, and in documentation, and it is a good project because the output has a clear structure that the application can check. An explanation that skips the risks of a piece of code is incomplete in a way the reader may not notice, so the application verifies that every required section is present, and asks again when one is missing.

The project combines three earlier ideas: input validation (Lesson 174), prompting with tagged content (Lesson 37), and a checked response with a bounded retry (Lesson 105). The model is asked for free text, and the application treats the required headings as a contract.


What You Will Build

An explain function that:

  1. accepts a source file's name and contents,
  2. rejects inputs that are empty or too large,
  3. identifies the language from the file extension,
  4. asks for an explanation with required headings,
  5. checks the answer for the headings, and asks again, with the missing headings named, up to a limit.

Project Structure

code_explainer/
├── explainer.py    # validation, language detection, the check, and the retry loop
└── demo.py         # a stand-in client that first forgets a heading, then includes it

Step 1: Validating and Identifying the Input

The explainer accepts only input it can handle. The contents must be non-empty and within a size limit, and the language is taken from the extension. An unknown extension is labeled as plain text rather than guessed, because a wrong language label would mislead the explanation:

"""explainer.py: explain source code with a required structure, checked before it is returned."""

from pathlib import Path

MAX_CHARS = 20_000
REQUIRED_HEADINGS = ["Purpose", "How it works", "Risks"]
LANGUAGES = {".py": "Python", ".js": "JavaScript", ".sql": "SQL", ".java": "Java"}


def language_for(filename: str) -> str:
    return LANGUAGES.get(Path(filename).suffix.lower(), "plain text")


def validate(code: str) -> list[str]:
    problems = []
    if not code.strip():
        problems.append("the file is empty")
    if len(code) > MAX_CHARS:
        problems.append(f"the file has {len(code)} characters; the limit is {MAX_CHARS}")
    return problems

Step 2: Asking for the Structure

The prompt names the headings and wraps the code in tags, so the model knows exactly what to produce and what is data. Each retry adds one sentence naming the headings that were missing, which is more useful to the model than repeating the whole request:

def build_prompt(code: str, language: str, missing: list[str] | None = None) -> str:
    prompt = (f"Explain this {language} code for a beginner. Use exactly these headings, "
              f"in this order: {', '.join(REQUIRED_HEADINGS)}.\n\n<code>\n{code}\n</code>")
    if missing:
        prompt += f"\n\nYour previous answer lacked these headings: {', '.join(missing)}. Include them."
    return prompt


def missing_headings(answer: str) -> list[str]:
    return [heading for heading in REQUIRED_HEADINGS if heading not in answer]

Step 3: The Checked Loop

The loop asks, checks, and retries. Each attempt's missing headings are recorded, so the caller can see how the explanation was reached. The loop stops after a fixed number of attempts, and then returns the last answer with its gaps stated, rather than pretending the answer is complete:

def explain(client, filename: str, code: str, attempts: int = 2) -> dict:
    problems = validate(code)
    if problems:
        return {"ok": False, "problems": problems}
    language = language_for(filename)
    missing: list[str] | None = None
    answer = ""
    for attempt in range(1, attempts + 1):
        response = client.messages.create(
            model="claude-sonnet-5-5",
            max_tokens=800,
            messages=[{"role": "user", "content": build_prompt(code, language, missing)}],
        )
        answer = "".join(block.text for block in response.content if block.type == "text")
        missing = missing_headings(answer)
        if not missing:
            return {"ok": True, "language": language, "explanation": answer, "attempts": attempt}
    return {"ok": False, "language": language, "explanation": answer,
            "attempts": attempts, "missing": missing}

How It Works

  1. Validation runs before any call, so an empty or oversized file costs nothing.
  2. The language comes from the extension, and an unknown one becomes plain text.
  3. Each answer is checked for every heading, using the exact names from the contract.
  4. A retry names only the missing headings, which keeps the correction short and specific.
  5. After the last attempt, the answer is returned with its gaps. The caller sees ok: False and the missing list, so the explanation is never presented as complete when it is not.

Step 4: A Demonstration

The stand-in client returns a first answer that omits the risks, and a second answer that includes every heading, which exercises the retry path:

"""demo.py: the explainer, with a stand-in that first omits the Risks heading."""

from types import SimpleNamespace

from explainer import explain


class ForgetfulClient:
    def __init__(self):
        self.messages = self
        self.turn = 0

    def create(self, **request):
        self.turn += 1
        if self.turn == 1:
            text = "Purpose: sums two numbers.\n\nHow it works: it adds them."
        else:
            text = ("Purpose: sums two numbers.\n\nHow it works: it adds them.\n\n"
                    "Risks: none for this small function.")
        return SimpleNamespace(content=[SimpleNamespace(type="text", text=text)])


if __name__ == "__main__":
    code = "def add(a, b):\n    return a + b\n"
    result = explain(ForgetfulClient(), "add.py", code)
    print("ok:", result["ok"], "language:", result["language"], "attempts:", result["attempts"])
    print(result["explanation"].splitlines()[0])

Output:

ok: True language: Python attempts: 2
Purpose: sums two numbers.

How It Works

  1. The first answer fails the check, because it has no Risks heading. The loop does not return it.
  2. The retry prompt names the missing heading, and the stand-in responds with the complete structure.
  3. The result reports two attempts, so a cost report can see that this explanation required a second call.
  4. The language is Python, because the file ends in .py, and the first line of the explanation confirms the answer is the one that passed the check.

Going Live

The stand-in is replaced by the SDK client in the same call, explain(Anthropic(), filename, code). The validation, the language detection, and the checked loop are unchanged.


Testing the Project

The validation and the checks are pure functions, so they are tested as Lesson 181 described. Cases to cover include an empty file, a file over the limit, an unknown extension, an answer with all headings, and an answer missing one. The explanation's accuracy is evaluated separately with a set of reviewed examples (Lesson 110), since a structured answer can still be wrong.

0 Comments

Reviewed before they appear

No comments yet.

Anthropic SDK Using Python
Anthropic SDK Intro Claude API Intro SDK Requirements Install Anthropic SDK Upgrade the SDK Create an API Key Set API Keys Use Environment Variables Import Anthropic Create a Client Send Your First Message Choose a Claude Model Set Max Tokens Read a Response Print Response Text Understand Message Blocks Use System Prompts Send User Messages Set Temperature Handle Stop Reasons Messages API Intro Create Messages Use Message Roles Send Conversation History Continue a Conversation Build a Chat Loop Reset Chat History Use Multiple Turns Inspect Message IDs Read Usage Data Write Clear Prompts Set a System Prompt Add Context to Prompts Use Prompt Templates Add Examples to Prompts Request JSON Output Use XML Tags Control Response Length Improve Prompt Quality Avoid Prompt Injection Read Text Blocks Read Content Blocks Check Stop Reasons Count Input Tokens Count Output Tokens Count Tokens Before Sending Limit Output Tokens Use Stop Sequences Read Request IDs Parse API Responses Streaming Intro Stream Text Output Use Stream Events Use Stream Helpers Get Final Messages Stream Async Responses Handle Stream Errors Build a Live Chat Stream Long Responses Close Streams Safely Build a Chatbot Keep Chat History Manage Chat Sessions Add Chat Memory Build a CLI Chatbot Build a Web Chatbot Use Flask with Claude Use FastAPI with Claude Return JSON from an API Add Chat Error Messages Async SDK Intro Create an Async Client Send Async Messages Use asyncio Run Parallel Requests Stream Async Text Handle Async Errors Set Async Timeouts Close Async Clients Choose Sync or Async Tool Use Intro Define a Tool Create Tool Schemas Send Tools to Claude Read Tool Calls Run Python Tools Return Tool Results Continue After Tool Use Control Tool Choice Handle Tool Errors Use the beta_tool Decorator Create a Tool Runner Add Tool Docstrings Validate Tool Inputs Use Multiple Tools Limit Tool Calls Log Tool Activity Build a Weather Tool Build a Search Tool Build a Calculator Tool Structured Output Intro Request JSON Data Validate JSON Output Use Pydantic Models Handle Invalid JSON Extract Fields from Text Generate Typed Data Use Image URLs Send Base64 Images Read PDFs with Claude Use Output Schemas Build Data Extraction Test Structured Responses Send Image Inputs Upload Files Use File IDs Process Text Files Summarize Documents Extract Table Data Handle File Errors Read Cache Usage Reduce Repeated Input Costs Debug Cache Misses Prompt Caching Intro Enable Prompt Caching Cache System Prompts Cache Long Context Test Cache Performance Use Cache Breakpoints Cache Best Practices Message Batches Intro Create a Message Batch Set Batch IDs Check Batch Status List Message Batches Get Batch Results Process Batch Outputs Handle Failed Batch Items Use Batches for Bulk Tasks Compare Batches and Requests Handle API Errors Handle Rate Limits Retry Failed Requests Set Request Timeouts Handle Network Errors Use Exponential Backoff Build Reliable API Calls Use Extended Thinking Log API Errors Track Request IDs Paginate API Results Set Thinking Budgets Read Thinking Blocks Use Tool Search Use Web Search Tools Use Computer Tools Use Citations Use Token Counting Read Raw Responses Set Custom Headers Use Claude on AWS Bedrock Use Claude on Vertex AI Use Claude on Microsoft Foundry Configure Cloud Credentials Choose a Cloud Provider Compare API Integrations Configure Provider Clients Handle Cloud Errors Use Cloud SDK Extras Deploy Cloud Apps Protect API Keys Use a .env File Ignore Secrets in Git Validate User Input Reduce Prompt Injection Limit Tool Permissions Avoid Unsafe Tool Calls Sanitize Tool Results Set Usage Limits Review Security Logs Test SDK Functions Mock API Responses Test Error Handling Test Streaming Code Measure Token Usage Track API Costs Configure Logging Deploy with Docker Deploy a FastAPI App Monitor Production Apps Build a Claude Chatbot Build a Text Summarizer Build a Code Explainer Build a JSON Extractor Build a PDF Assistant Build a Streaming Chat App Build a Tool-Calling Agent Build a Batch Processor Build a Research Assistant Build a Claude API Dashboard
Ask about this post
AI Ask about this post

Ask questions about Build a Code Explainer and get answers drawn from it.

Signed-in readers only.