D1.0 What This Teaches
This tutorial combines file reading, streaming output, code review, and test
generation into one production-quality console tool. It covers:
- Building a multi-command CLI with
argparse
- Streaming responses to the terminal as tokens arrive
- Saving streamed output to a file with
--output
- Generating pytest test files and writing them to disk
- Modular design - shared helper functions reduce duplication
- Loading environment variables from a
.env file
D1.1 Tool Design
The tool runs as:
python code_helper.py <command> <file> [options]
Three commands are supported:
| Command | What it does |
explain | Streams an explanation of the file to the terminal |
review | Streams a code review with issues and suggestions |
tests | Generates pytest tests and writes them to test_<filename>.py |
Options: --model overrides the default model name;
--output saves the streamed text to a file.
D1.2 Project Layout
code_helper/
├── code_helper.py ← main CLI
├── .env ← ANTHROPIC_API_KEY=sk-ant-...
└── requirements.txt ← anthropic
python-dotenv
python-dotenv reads the .env file and sets the environment
variable automatically. This avoids exporting the key in every terminal session.
pip install anthropic python-dotenv
D1.3 The argparse CLI
argparse is the standard library module for building command-line
interfaces. It handles argument parsing, type checking, and help text automatically.
# argparse setup for code_helper.py
import argparse
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="AI-powered code helper: explain, review, or generate tests."
)
parser.add_argument(
"command",
choices=["explain", "review", "tests"],
help="What to do with the file",
)
parser.add_argument(
"file",
help="Path to the source file",
)
parser.add_argument(
"--model",
default="claude-sonnet-4-6",
help="Claude model to use (default: claude-sonnet-4-6)",
)
parser.add_argument(
"--output",
help="Save output to this file in addition to printing",
)
return parser
choices=["explain","review","tests"] makes argparse reject any other
command with a clear error message before your code runs.
D1.4 The explain Command
The explain command reads the file, streams the explanation to the terminal, and
optionally writes the full text to --output.
# explain command implementation.
EXPLAIN_SYSTEM = (
"You are a code explainer. Structure your response as: "
"(1) Purpose, (2) Key Components, (3) How It Works, (4) Potential Issues."
)
def cmd_explain(client, args, code: str, language: str):
prompt = f"Explain this {language} code:\n\n```{language}\n{code}\n```"
with client.messages.stream(
model=args.model,
max_tokens=1024,
system=EXPLAIN_SYSTEM,
messages=[{"role": "user", "content": prompt}],
) as stream:
stream_to_output(stream, args.output)
D1.5 The review Command
The review command uses the same streaming pattern but with a code review system
prompt. The only difference is the system prompt and the prompt wording.
# review command implementation.
REVIEW_SYSTEM = (
"You are an expert code reviewer. "
"Structure your response as: (1) Summary, (2) Issues Found, (3) Suggestions."
)
def cmd_review(client, args, code: str, language: str):
prompt = f"Review this {language} code:\n\n```{language}\n{code}\n```"
with client.messages.stream(
model=args.model,
max_tokens=1024,
system=REVIEW_SYSTEM,
messages=[{"role": "user", "content": prompt}],
) as stream:
stream_to_output(stream, args.output)
Both cmd_explain and cmd_review call the same
stream_to_output helper, keeping the streaming and file-writing
logic in one place.
D1.6 The tests Command
Test generation does not stream - you want the complete test file before writing
it to disk. The output filename defaults to test_<original_name>.py.
# tests command implementation.
TESTS_SYSTEM = (
"You are a test generator. Generate pytest tests for the given code. "
"Include at least one test per public function. "
"Output only the Python test file, no explanation."
)
def cmd_tests(client, args, code: str, language: str):
from pathlib import Path
prompt = f"Generate pytest tests for this {language} code:\n\n```{language}\n{code}\n```"
response = client.messages.create(
model=args.model,
max_tokens=2048,
system=TESTS_SYSTEM,
messages=[{"role": "user", "content": prompt}],
)
test_code = response.content[0].text
out_path = args.output or f"test_{Path(args.file).stem}.py"
with open(out_path, "w", encoding="utf-8") as f:
f.write(test_code)
print(f"Tests written to: {out_path}")
D1.7 Complete Implementation
# code_helper.py - AI-powered code helper: explain, review, or generate tests.
# Usage: python code_helper.py <command> <file> [--model MODEL] [--output FILE]
import sys
import argparse
from pathlib import Path
from dotenv import load_dotenv
import anthropic
load_dotenv()
EXPLAIN_SYSTEM = (
"You are a code explainer. Structure your response as: "
"(1) Purpose, (2) Key Components, (3) How It Works, (4) Potential Issues."
)
REVIEW_SYSTEM = (
"You are an expert code reviewer. "
"Structure your response as: (1) Summary, (2) Issues Found, (3) Suggestions."
)
TESTS_SYSTEM = (
"You are a test generator. Generate pytest tests for the given code. "
"Include at least one test per public function. "
"Output only the Python test file, no explanation."
)
EXT_TO_LANG = {
".py": "Python", ".js": "JavaScript", ".cpp": "C++",
".rs": "Rust", ".cs": "C#", ".ts": "TypeScript",
}
def read_source(path: str) -> str:
p = Path(path)
if not p.exists():
print(f"Error: file not found: {path}")
sys.exit(1)
content = p.read_text(encoding="utf-8")
if not content.strip():
print(f"Error: file is empty: {path}")
sys.exit(1)
return content
def detect_language(path: str) -> str:
return EXT_TO_LANG.get(Path(path).suffix.lower(), "code")
def stream_to_output(stream, output_path: str | None):
collected = []
for chunk in stream.text_stream:
print(chunk, end="", flush=True)
collected.append(chunk)
print() # final newline
if output_path:
Path(output_path).write_text("".join(collected), encoding="utf-8")
print(f"\nSaved to: {output_path}")
def cmd_explain(client, args, code: str, language: str):
prompt = f"Explain this {language} code:\n\n```{language}\n{code}\n```"
with client.messages.stream(
model=args.model, max_tokens=1024, system=EXPLAIN_SYSTEM,
messages=[{"role": "user", "content": prompt}],
) as stream:
stream_to_output(stream, args.output)
def cmd_review(client, args, code: str, language: str):
prompt = f"Review this {language} code:\n\n```{language}\n{code}\n```"
with client.messages.stream(
model=args.model, max_tokens=1024, system=REVIEW_SYSTEM,
messages=[{"role": "user", "content": prompt}],
) as stream:
stream_to_output(stream, args.output)
def cmd_tests(client, args, code: str, language: str):
prompt = f"Generate pytest tests for this {language} code:\n\n```{language}\n{code}\n```"
response = client.messages.create(
model=args.model, max_tokens=2048, system=TESTS_SYSTEM,
messages=[{"role": "user", "content": prompt}],
)
test_code = response.content[0].text
out_path = args.output or f"test_{Path(args.file).stem}.py"
Path(out_path).write_text(test_code, encoding="utf-8")
print(f"Tests written to: {out_path}")
def main():
parser = argparse.ArgumentParser(description="AI-powered code helper.")
parser.add_argument("command", choices=["explain", "review", "tests"])
parser.add_argument("file")
parser.add_argument("--model", default="claude-sonnet-4-6")
parser.add_argument("--output", default=None)
args = parser.parse_args()
code = read_source(args.file)
language = detect_language(args.file)
client = anthropic.Anthropic()
{"explain": cmd_explain, "review": cmd_review, "tests": cmd_tests}[
args.command
](client, args, code, language)
if __name__ == "__main__":
main()
D1.8 Sample Session
$ python code_helper.py explain mylib.py
(1) Purpose - mylib.py provides utility functions for string normalization and
text processing used across the project.
(2) Key Components
- normalize(s): strips whitespace and converts to lowercase
- word_count(s): returns the number of words in a string
- truncate(s, n): shortens a string to at most n characters
(3) How It Works
Each function takes a string, applies one transformation, and returns the result.
No state is shared between calls.
(4) Potential Issues
- truncate() does not add an ellipsis; callers may not know text was cut.
- word_count() splits on whitespace only; punctuation-attached words count as one.
$ python code_helper.py review mylib.py --output review.txt
(1) Summary - The module is clean and focused. Three functions, each doing one job.
(2) Issues Found
- No type hints on any function; callers cannot see expected types.
- word_count("") returns 0 correctly but is not tested.
(3) Suggestions
- Add type hints: def normalize(s: str) -> str
- Add a docstring to each function.
- Consider adding an ellipsis option to truncate().
Saved to: review.txt
$ python code_helper.py tests mylib.py
Tests written to: test_mylib.py
$ pytest test_mylib.py -v
collected 6 items
test_mylib.py::test_normalize_strips_whitespace PASSED
test_mylib.py::test_normalize_lowercases PASSED
test_mylib.py::test_word_count_basic PASSED
test_mylib.py::test_word_count_empty PASSED
test_mylib.py::test_truncate_short PASSED
test_mylib.py::test_truncate_long PASSED
6 passed in 0.12s
D1.9 Exercise
Exercise
Add a compare command that takes two filenames, computes a unified diff,
and streams a review of the changes.
- Add
"compare" to the choices list in the parser.
- Add a second positional argument
file2 to the parser. Make it
optional (nargs="?") so the existing commands still work with one
file.
- Define
cmd_compare(client, args, code1, code2, language). Use
difflib.unified_diff() to compute the diff, then stream a review
of it.
- Usage:
python code_helper.py compare old.py new.py
The diff review prompt: "Here is a unified diff between two versions of a
Python file. Review the changes: what was fixed, what was added, and are there
any concerns?"
D1.10 Common Mistakes
Hard-coding the model name
Writing model="claude-sonnet-4-6" directly in the API call means
switching models requires editing the source. The --model argument
passes the name through from the command line, so users and scripts can override
it without changing the code.
Not handling empty files
If read_source() returns an empty string and you send it to Claude,
the model produces a confused response like "I don't see any code to explain."
Check if not content.strip() after reading and exit with a clear
error message.
Silently overwriting existing output files
When --output points to an existing file, write_text()
overwrites it without warning. For a tool used on real projects, check
Path(output_path).exists() before writing and either warn the user
or add an --overwrite flag.
D1.11 Key Terms
| Term | Meaning |
| argparse | Python standard library module for parsing command-line arguments and generating help text |
| command | A positional argument that selects which action to perform (explain, review, tests) |
| choices | An argparse parameter that restricts a positional argument to a fixed set of values |
| streaming output | Printing tokens to the terminal as they arrive rather than buffering the full response first |
| modular CLI | A CLI where each command is a separate function, making it easy to add or change commands |
| .env | A plain text file containing environment variable assignments; read by python-dotenv at startup |
| helper function | A small function (like read_source or stream_to_output) shared by multiple commands to avoid duplication |
| difflib | Python standard library module for computing differences between sequences of text |