A2.0 What This Teaches
This tutorial builds a command-line tool that reads a source file and asks Claude
to explain it. It covers:
- Reading files with
open() and handling FileNotFoundError
- Using
sys.argv to accept command-line arguments
- Embedding source code inside a prompt with an f-string template
- Detecting the programming language from the file extension
- Producing structured explanation output via a system prompt
A2.1 Application Design
The tool follows five steps in sequence:
- Read the filename from
sys.argv[1]
- Read the file contents into a string
- Build a prompt that embeds the code in a fenced code block
- Call the API with a system prompt that enforces a structured response format
- Print the result to the terminal
Each step is small enough to understand and test independently.
Keeping them separate also makes it easy to extend the tool later - for example,
by writing the explanation to a file instead of printing it.
A2.2 Reading the File
Read the file with open() inside a try/except block so a bad path
produces a readable error instead of a traceback. Also warn early if the file
is large enough to generate a significant token bill.
# read_file.py - isolated file reading with error handling and size check.
import sys
MAX_BYTES = 50_000 # roughly 10,000-15,000 tokens depending on content
def read_source_file(path: str) -> str:
try:
with open(path, "r", encoding="utf-8") as f:
source = f.read()
except FileNotFoundError:
print(f"Error: file not found: {path}")
sys.exit(1)
if len(source) > MAX_BYTES:
print(
f"Warning: {path} is {len(source):,} bytes. "
"Token cost will be high. Consider explaining a smaller section."
)
return source
sys.exit(1) stops the program immediately with a non-zero exit code,
which signals failure to any shell script or CI system that called the tool.
A2.3 Building the Prompt
Wrap the source code in a fenced code block inside the prompt. This gives the model
clear boundaries around the code and lets it apply syntax-aware reasoning.
Detect the language from the file extension so the fence label is accurate.
# detect_language.py - map file extension to language name.
from pathlib import Path
EXT_TO_LANG = {
".py": "Python",
".js": "JavaScript",
".cpp": "C++",
".rs": "Rust",
".cs": "C#",
".java": "Java",
".go": "Go",
}
def detect_language(filename: str) -> str:
ext = Path(filename).suffix.lower()
return EXT_TO_LANG.get(ext, "code")
def build_prompt(filename: str, code: str) -> str:
language = detect_language(filename)
return f"Explain this {language} code:\n\n```{language}\n{code}\n```"
If the extension is not in the table, the language falls back to
"code", which is a valid fence label and still gives the model
enough context to work with.
A2.4 The System Prompt
A system prompt sets the model's role and output format before the user message
arrives. Specifying the exact structure here means every explanation follows the
same four-part layout, regardless of which file you pass.
# system_prompt.py - the explanation format.
SYSTEM = (
"You are a code explainer. Structure your response as:\n"
"(1) Purpose - one sentence.\n"
"(2) Key Components - bulleted list.\n"
"(3) How It Works - step-by-step.\n"
"(4) Potential Issues - any risks or gotchas."
)
Numbered labels like (1) are more reliable than prose section headers
when you want consistent output - the model treats them as required fields to fill in.
A2.5 Complete Implementation
# explain.py - explain a source file using Claude.
# Usage: python explain.py <filename>
import sys
from pathlib import Path
import anthropic
MAX_BYTES = 50_000
EXT_TO_LANG = {
".py": "Python", ".js": "JavaScript", ".cpp": "C++",
".rs": "Rust", ".cs": "C#", ".java": "Java", ".go": "Go",
}
SYSTEM = (
"You are a code explainer. Structure your response as:\n"
"(1) Purpose - one sentence.\n"
"(2) Key Components - bulleted list.\n"
"(3) How It Works - step-by-step.\n"
"(4) Potential Issues - any risks or gotchas."
)
def detect_language(filename: str) -> str:
return EXT_TO_LANG.get(Path(filename).suffix.lower(), "code")
def main():
if len(sys.argv) < 2:
print("Usage: python explain.py <filename>")
sys.exit(1)
filename = sys.argv[1]
try:
with open(filename, "r", encoding="utf-8") as f:
code = f.read()
except FileNotFoundError:
print(f"Error: file not found: {filename}")
sys.exit(1)
if len(code) > MAX_BYTES:
print(f"Warning: {filename} is {len(code):,} bytes - token cost will be high.")
language = detect_language(filename)
prompt = f"Explain this {language} code:\n\n```{language}\n{code}\n```"
client = anthropic.Anthropic()
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
system=SYSTEM,
messages=[{"role": "user", "content": prompt}],
)
print(response.content[0].text)
if __name__ == "__main__":
main()
A2.6 Sample Output
Running the tool on a small Python file that defines a clamp() function
produces output like this:
$ python explain.py clamp.py
(1) Purpose - Constrains a numeric value to lie within a specified minimum and maximum range.
(2) Key Components
- clamp(value, lo, hi): the single public function
- Parameters: value (the number to constrain), lo (lower bound), hi (upper bound)
- Return: the nearest boundary value if out of range, otherwise value unchanged
(3) How It Works
1. Compare value against lo - if below, return lo immediately.
2. Compare value against hi - if above, return hi.
3. If neither condition triggers, return value as-is.
(4) Potential Issues
- No type checking: passing a string raises TypeError at the comparison.
- lo > hi is not validated; results are undefined in that case.
- No docstring or type hints, which makes the expected types ambiguous.
A2.7 Detecting Language from Extension
The mapping used in this tool covers the most common compiled and scripted languages.
Add entries as needed for your own projects.
| Extension | Language label |
| .py | Python |
| .js | JavaScript |
| .cpp | C++ |
| .rs | Rust |
| .cs | C# |
| .java | Java |
| .go | Go |
| (anything else) | code |
A2.8 Exercise
Exercise
Extend explain.py to accept an optional second argument: a question
about the code. If provided, append it to the prompt after the code block:
if len(sys.argv) >= 3:
question = sys.argv[2]
prompt += f"\n\nAfter your explanation, answer this specific question: {question}"
Usage example:
python explain.py myfile.py "What does the main function do?"
Test with a file you wrote yourself - ask a question whose answer you already know,
then check whether the model's answer is accurate.
A2.9 Common Mistakes
Not handling FileNotFoundError
If you call open(filename) without a try/except and the path is wrong,
Python raises an unformatted traceback. Users of a CLI tool expect a clean error
message, not a stack trace. Always catch FileNotFoundError and print
something actionable before calling sys.exit(1).
Sending an entire large codebase file
Files over 1,000 lines often produce poor explanations - the model has to skim
rather than reason carefully. If a file is that large, either explain individual
functions by slicing the text, or ask the model to focus on one specific section
per call.
Not telling the model the language
Sending raw code without a language label forces the model to guess. For a
.cs file that uses LINQ, the model might misread it as Java and
produce a subtly wrong explanation. The fenced code block label and the phrase
"Explain this C# code" together eliminate the ambiguity.
Key Terms
| Term | Meaning |
| sys.argv | List of command-line arguments; argv[0] is the script name, argv[1] is the first user argument |
| pathlib.Path | Object-oriented path manipulation; Path(f).suffix returns the file extension including the dot |
| system prompt | An instruction passed to the model before the user message; controls tone and output format |
| fenced code block | Markdown syntax wrapping code in triple backticks with an optional language label |
| FileNotFoundError | Python exception raised when open() cannot locate the specified path |
| sys.exit(1) | Terminates the program immediately with exit code 1, signaling failure |
| token cost | The number of tokens consumed by a request; large files increase cost and may degrade quality |