Prompting for Code Explanation
Why Code Explanation Needs Careful Prompting Developers read far more code than they write: when joining a project, reviewing a pull request, maintaining old features, or learning a new library. Language models can explain code quickly, but an explanation is only useful if it is at the right level, connected to the reader's purpose, and correct.
The last point is critical. A model explaining code describes what the code appears to do. It can describe the intended behavior instead of the actual behavior, which hides bugs instead of revealing them. This lesson shows how to prompt for useful explanations and how to check them against what the code really does.
Explanation Type Answers
- Overview What is this code for?
- Line-by-line walkthrough What does each part do?
- Execution trace What happens with this specific input?
- Data flow Where does this value come from, and where does it go?
- Design reasoning Why might it be written this way?
- Explanation for a specific purpose What do I need to know to change, review, or use it? The Context an Explanation Needs A function on its own is often ambiguous. Provide:
Context Why Language and framework versions Behavior can differ between versions Where the code is called from Shows what inputs it actually receives Related definitions Models, helper functions, constants used inside it The reader's level Determines depth and vocabulary The reader's purpose Learning, reviewing, debugging, or modifying Number the Lines Line numbers let both the model and the reader refer to exact locations. Add them in code before building the prompt (Lesson 7):
def number_lines(code: str) -> str: return "\n".join(f"{i:>3}: {line}" for i, line in enumerate(code.splitlines(), start=1)) {i:>3} right-aligns the line number in three characters, so the code stays aligned.
Type 1: Overview Use it when: you need to know what code is for before reading it in detail.
Prompt Give an overview of the {language} code in for {reader}.
- Purpose: what this code does, in one or two sentences.
- Inputs and outputs: what it receives and what it returns or changes.
- Main steps: 3 to 6 bullet points.
- Dependencies: other functions, models, or services it relies on.
Describe only what the code actually does. If you are unsure what an external function does, say so instead of guessing.
{numbered_code} Why It Works A fixed structure produces overviews that are quick to scan. "Only what the code actually does" discourages describing the intended purpose based on function names alone. Admitting uncertainty about external functions prevents invented descriptions of code the model cannot see. Type 2: Line-by-Line Walkthrough Use it when: learning, or when a small piece of code is hard to understand.
Prompt Explain the code in line by line for {reader}.
For each line or small group of lines:
- Line numbers
- What it does
- Any language feature used, explained briefly the first time it appears
Then point out any line whose behavior might surprise a reader, and why.
{numbered_code} Why It Works Language features explained once teach concepts like list comprehensions or decorators in context. "Lines that might surprise a reader" directs attention to subtle behavior, which is where bugs and misunderstandings hide. Type 3: Execution Trace With a Concrete Input Use it when: you need to know exactly what happens for a specific input, which is the most reliable way to understand and debug code.
Prompt Trace the execution of the function in for this call: {call}
For each step, show the line number and the value of every variable that changes. Then state the final return value or the exception raised.
Base the trace only on the code. Do not assume what the code is "supposed" to do.
{numbered_code} Why Traces Are Powerful An overview says what the code is meant to do. A trace with real values shows what it actually does. If the traced result differs from what the function's name or docstring suggests, you have found a bug.
Verifying Explanations by Running the Code A model's trace or prediction is itself a claim that can be wrong. When the code is safe to run, such as your own pure functions, compare the model's predictions with real results.
Example The function below is supposed to return the average progress of active students:
SOURCE = '''def average_progress(students): total = 0 for s in students: if s["active"]: total += s["progress"] return total / len(students)''' Suppose a model, asked to explain and predict outputs, says: "It returns the average progress of active students", and predicts 80.0 for one active student at 80% and one inactive student at 20%.
namespace = {} exec(SOURCE, namespace) # our own, known code, used here only for testing average_progress = namespace["average_progress"]
def check_predictions(func, predictions): """Compare predicted results with actual results for each set of arguments.""" results = [] for args, predicted in predictions: try: actual = func(*args) except Exception as error: actual = f"raises {type(error).name}" results.append((predicted == actual, predicted, actual)) return results
predictions = [ (([{"active": True, "progress": 80}, {"active": True, "progress": 60}],), 70.0), (([{"active": True, "progress": 80}, {"active": False, "progress": 20}],), 80.0), (([],), 0), ]
for matches, predicted, actual in check_predictions(average_progress, predictions): print("OK " if matches else "DIFF", "predicted:", predicted, "actual:", actual) Output:
OK predicted: 70.0 actual: 70.0 DIFF predicted: 80.0 actual: 40.0 DIFF predicted: 0 actual: raises ZeroDivisionError Code Explanation exec(SOURCE, namespace) defines the function from a string, so this example is self-contained. In your project, import the function normally. Never use exec on untrusted or model-generated code (Lesson 58). check_predictions() runs the function for each set of arguments, captures exceptions as text, and compares the result with the prediction. What the Mismatches Reveal Second case: the code adds only active students' progress, but divides by all students: (80) ÷ 2 = 40. The model explained the intended behavior, an average of active students, which would be 80. The code has a bug. Third case: an empty list causes division by zero. The model predicted 0, a reasonable intention, but the code crashes. The explanation sounded correct, but running the code exposed two bugs. This is why explanations of important code should be checked against reality.
Prompting to Reduce This Problem Asking for a trace (Type 3) instead of a summary makes the model follow the actual code more closely. Adding "Base the trace only on the code" and asking explicitly "Does the code's behavior match its name and docstring?" further encourages it to notice gaps between intention and implementation.
Type 4: Data Flow Use it when: you need to know where a value comes from and where it goes, for example when changing a field or investigating wrong data.
Prompt In the code in , trace the value {variable_or_field}:
- Where it comes from (input, database, calculation, or external call), with line numbers
- Every place it is changed, with line numbers
- Where it is used or sent (returned, saved, displayed, logged)
- Any place where it could be missing, empty, or of an unexpected type
{numbered_code} Why It Works Data flow questions are common in maintenance work: "If I change how discount is calculated, what else is affected?" A structured trace answers this directly, and the "could be missing" step finds fragile points.
Type 5: Design Reasoning Use it when: you want to understand why code might be written a certain way, for example before changing it.
Prompt For the code in , explain the likely reasons for these design choices: {choices}
For each choice:
- The most likely reason
- One alternative design and its trade-off
Mark these as likely reasons, not facts, since the original author's intent is not known. If the project's documentation or comments state a reason, prefer that. Why It Works The model cannot know the author's intent. Asking it to label reasons as likely, and to prefer documented reasons, keeps the explanation honest. Alternatives with trade-offs help you decide whether a change is safe.
Type 6: Explanation for a Specific Purpose Different tasks need different explanations of the same code.
Purpose What to Ask For Using the code Inputs, outputs, errors, and a usage example Reviewing the code Behavior, risks, edge cases, and assumptions Modifying the code Where to make the change, what else depends on it, and what could break Learning from the code Concepts and patterns used, with explanations Example: Explaining for Modification I need to change the code in so that {change}.
Explain:
- Which lines need to change
- Which other parts of this code depend on those lines
- Which callers might be affected, based on
- Which tests should be added or updated
Do not write the change yet. "Do not write the change yet" separates understanding from implementation, so you can check the plan before any code is generated (Lesson 91).
Explaining Larger Codebases For more code than fits comfortably in a prompt:
Start with structure: provide the folder tree and file names, and ask for a map of what each part likely does. Go file by file: explain the most important files individually. Follow one flow: trace one user action, such as enrolling in a course, through views, services, and models. Collect the results into an onboarding document (Lesson 97). Lessons 101–106 cover retrieval, which allows question answering over large codebases and documentation.
Common Mistakes Mistake 1: Explaining Without Context What it is: Pasting a function without its callers, related models, or versions.
Why it happens: The function seems self-contained.
What it causes: Guesses about inputs and dependencies presented as facts.
How to fix it: Provide versions, related definitions, and callers.
Mistake 2: Accepting Intended Behavior as Actual Behavior What it is: Trusting an explanation that matches the function's name.
Why it happens: The explanation sounds reasonable.
What it causes: Bugs remain hidden, as in the average progress example.
How to fix it: Ask for execution traces, and run the code with test inputs where possible.
Mistake 3: Wrong Level of Detail What it is: A line-by-line explanation when you needed an overview, or the reverse.
Why it happens: The purpose is not stated.
What it causes: Time wasted reading the wrong kind of explanation.
How to fix it: State the reader and purpose, and choose the explanation type.
Mistake 4: Treating Design Reasons as Facts What it is: Believing the model's explanation of why the author made a choice.
Why it happens: The explanation is confident.
What it causes: Wrong assumptions when changing code.
How to fix it: Ask for likely reasons, labeled as such, and check documentation, comments, or commit history.
Best Practices Provide context. Versions, related code, callers, reader, and purpose.
Number the lines. Make explanations precise.
Choose the explanation type for the task. Overview, walkthrough, trace, data flow, design, or purpose-specific.
Prefer traces with concrete inputs for important code. They reveal actual behavior.
Verify by running safe code. Compare predictions with real results.
Label uncertainty. External functions and design intent should not be presented as facts.
Try It Yourself Exercise 1: Overview and Trace Choose a function from one of your projects. Ask for an overview (Type 1) and a trace with a concrete input (Type 3). Run the function with the same input and compare the result with the trace.
Exercise 2: Find the Gap Write a function with a small bug, such as an off-by-one error in a loop. Ask a model to explain it and to predict outputs for three inputs. Use check_predictions() to compare. Then ask "Does the code's behavior match its name?" and check whether the model now finds the bug.
Exercise 3: Explain for Modification Take a view or function that you want to change. Use the "explanation for modification" prompt with the code and at least one caller. Compare the model's list of affected lines and callers with your own analysis.