A5.0 What This Teaches
- Generating a unified diff with Python's standard library
difflib - Embedding the diff in a prompt for risk assessment
- Short-circuiting the API call when the files are identical
- Combining stdlib tools with LLMs to get insights that neither provides alone
A5.1 What a Diff Shows
- Lines starting with
-were in the old file and removed - Lines starting with
+are in the new file and added - Lines with no prefix are context - unchanged lines that appear on both sides
--- old.py
+++ new.py
@@ -4,7 +4,10 @@
def load_data(filename):
- with open(filename) as f:
- return f.read()
+ try:
+ with open(filename, encoding="utf-8") as f:
+ return f.read()
+ except FileNotFoundError:
+ return None
@@ line shows which line numbers changed. This format is the same
one used by git diff - learning to read it here also teaches you to
read git output.
A5.2 Generating the Diff with difflib
difflib.unified_diff() takes two lists of lines and produces a generator
of diff lines. Join them into a single string to embed in a prompt.
# gen_diff.py - compute a unified diff between two files.
import difflib
from pathlib import Path
def compute_diff(old_path: str, new_path: str) -> str:
old_lines = Path(old_path).read_text(encoding="utf-8").splitlines(keepends=True)
new_lines = Path(new_path).read_text(encoding="utf-8").splitlines(keepends=True)
diff = difflib.unified_diff(
old_lines,
new_lines,
fromfile=old_path,
tofile=new_path,
lineterm="",
)
return "".join(diff)
keepends=True preserves the newline character at the end of each line,
which unified_diff requires to produce correct output.
lineterm="" prevents it from adding an extra newline to each diff line.
A5.3 The Review Prompt
# diff_prompt.py - build the review prompt.
def build_review_prompt(diff_text: str) -> str:
return (
"Review this unified diff. Explain:\n"
"(1) What changed and why it likely changed.\n"
"(2) Any risks introduced (bugs, security issues, breaking changes).\n"
"(3) Whether the change looks complete or if anything is missing.\n\n"
f"```diff\n{diff_text}\n```"
)
diff fence label tells the model to interpret the content as a
diff rather than as source code, which improves how it reasons about the
- and + prefixes.
A5.4 Complete Implementation
# diff_review.py - compare two files and review the changes with Claude.
# Usage: python diff_review.py old.py new.py
import sys
import difflib
from pathlib import Path
import anthropic
def read_file(path: str) -> list[str]:
try:
return Path(path).read_text(encoding="utf-8").splitlines(keepends=True)
except FileNotFoundError:
print(f"Error: file not found: {path}")
sys.exit(1)
def compute_diff(old_path: str, new_path: str) -> str:
old_lines = read_file(old_path)
new_lines = read_file(new_path)
return "".join(difflib.unified_diff(
old_lines, new_lines,
fromfile=old_path, tofile=new_path, lineterm="",
))
def main():
if len(sys.argv) < 3:
print("Usage: python diff_review.py old.py new.py")
sys.exit(1)
old_path, new_path = sys.argv[1], sys.argv[2]
diff = compute_diff(old_path, new_path)
if not diff:
print("Files are identical.")
sys.exit(0)
prompt = (
"Review this unified diff. Explain:\n"
"(1) What changed and why it likely changed.\n"
"(2) Any risks introduced (bugs, security issues, breaking changes).\n"
"(3) Whether the change looks complete or if anything is missing.\n\n"
f"```diff\n{diff}\n```"
)
client = anthropic.Anthropic()
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
messages=[{"role": "user", "content": prompt}],
)
print(response.content[0].text)
if __name__ == "__main__":
main()
A5.5 Sample Output
$ python diff_review.py old.py new.py
(1) What changed and why it likely changed.
The load_data function gained an explicit encoding argument ("utf-8") and a
try/except block that returns None when the file is not found. This is a
defensive improvement - the original would raise an unhandled FileNotFoundError
on a missing file.
(2) Risks introduced.
Returning None on error means callers must now check the return value before
using it. Any code that does `data.split(...)` on the result will raise
AttributeError if the file is missing. The change shifts responsibility for
error handling to the caller without documenting that contract.
(3) Completeness.
The change looks complete for the stated goal of avoiding a crash. A type hint
on the return value (str | None) would make the None case explicit and would
help tools like mypy catch callers that forget to check.
A5.6 Handling Empty Diffs
unified_diff returns
an empty generator and "".join(...) produces an empty string. Check
for this before calling the API.
# empty_diff_guard.py - avoid an unnecessary API call.
diff = compute_diff(old_path, new_path)
if not diff:
print("Files are identical.")
sys.exit(0)
# only reaches here if there are actual changes
response = client.messages.create(...)
A5.7 Exercise
Exercise
Add a
After printing the full review, scan the response text for the rating and print
it on its own line labeled
--risk flag using argparse. When present, append
this sentence to the prompt:
risk_instruction = (
"Rate the overall risk of this change on a scale of 1-5 "
"where 1 is trivial and 5 is high-risk. "
"Give one sentence justification."
)
Risk rating:. A simple approach: look
for the pattern "Risk: N" or "N/5" in the response using
a regular expression, or just print the last paragraph separately.
python diff_review.py old.py new.py --risk
A5.8 Common Mistakes
Sending very large diffs
Not handling the case where one file does not exist
old.py does not exist, read_text() raises
FileNotFoundError. The tool wraps each read in a try/except and calls
sys.exit(1) with a clear message. If you remove that guard to simplify
the code, a typo in a filename produces a confusing Python traceback instead of
a usable error.
Comparing binary files
difflib operates on text. Passing a compiled .pyc file,
a PNG, or any other binary file causes read_text() to raise
UnicodeDecodeError or produce garbage output. Add a check on the
file extension, or catch UnicodeDecodeError and print an error
before any diff is computed.
Key Terms
| Term | Meaning |
|---|---|
| unified diff | A standard format showing removed lines with -, added lines with +, and unchanged context lines |
| difflib | Python standard library module for computing differences between sequences |
| unified_diff() | difflib function that produces a unified diff as a generator of strings |
| keepends=True | splitlines() option that preserves the newline at the end of each line |
| lineterm="" | unified_diff() option that suppresses the extra newline it would otherwise add per line |
| breaking change | A change that alters a public interface in a way that requires callers to update their code |
| UnicodeDecodeError | Exception raised when read_text() encounters bytes that cannot be decoded as UTF-8 text |