Skip to content

Commit 36bffff

Browse files
RPBotclaude
andcommitted
Sample code for: How to Use Claude Code to Write and Refactor Python
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent d4c1d27 commit 36bffff

10 files changed

Lines changed: 307 additions & 0 deletions

File tree

‎claude-code/README.md‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# How to Use Claude Code to Write and Refactor Python
2+
3+
This folder contains code associated with the Real Python tutorial [How to Use Claude Code to Write and Refactor Python](https://realpython.com/how-to-use-claude-code/).
4+
5+
The `mini-contacts/` project is the finished state of the command-line contact manager that you build with Claude Code in the tutorial. The prompts that produced it are collected in [`prompts.md`](prompts.md), in the order they appear.
6+
7+
Because Claude Code is nondeterministic, your own run won't match this code line for line. Expect the same structure, a storage module for CSV operations and a CLI module for argument parsing, with different naming and implementation details.
8+
9+
## Run the Project
10+
11+
```sh
12+
$ cd mini-contacts/
13+
$ python -m mini_contacts add --name "Alice" --email "alice@example.com" --phone "555-1234"
14+
$ python -m mini_contacts list
15+
```
16+
17+
Contacts are stored at `~/.mini-contacts.csv` by default. Pass `--path` to use a different file.
18+
19+
## Run the Tests
20+
21+
```sh
22+
$ cd mini-contacts/
23+
$ python -m unittest
24+
```
25+
26+
The suite uses `unittest` from the standard library, so there's nothing to install.
27+
28+
## About the Author
29+
30+
Real Python - Email: office@realpython.com
31+
32+
## License
33+
34+
Distributed under the MIT license. See `LICENSE` for more information.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Project Conventions
2+
3+
- Python 3.14+, four-space indentation, PEP 8 compliant
4+
- Prefer the standard library; use third-party packages only when needed
5+
- Use type hints on all public functions

‎claude-code/mini-contacts/mini_contacts/__init__.py‎

Whitespace-only changes.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from mini_contacts.cli import main
2+
3+
main()
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import argparse
2+
import sys
3+
4+
from mini_contacts import storage
5+
6+
7+
def _print_table(contacts: list[dict[str, str]]) -> None:
8+
if not contacts:
9+
print("No contacts found.")
10+
return
11+
headers = {"name": "Name", "email": "Email", "phone": "Phone"}
12+
widths = {
13+
field: max(len(headers[field]), max(len(row[field]) for row in contacts))
14+
for field in storage.FIELDNAMES
15+
}
16+
header_line = " | ".join(headers[f].ljust(widths[f]) for f in storage.FIELDNAMES)
17+
separator = "-+-".join("-" * widths[f] for f in storage.FIELDNAMES)
18+
print(header_line)
19+
print(separator)
20+
for row in contacts:
21+
print(" | ".join(row[f].ljust(widths[f]) for f in storage.FIELDNAMES))
22+
23+
24+
def main(argv: list[str] | None = None) -> None:
25+
parser = argparse.ArgumentParser(description="Minimal contact manager")
26+
parser.add_argument(
27+
"--path", default="~/.mini-contacts.csv", help="Path to the CSV file"
28+
)
29+
subparsers = parser.add_subparsers(dest="command")
30+
subparsers.required = True
31+
add_parser = subparsers.add_parser("add", help="Add a new contact")
32+
add_parser.add_argument("--name", required=True)
33+
add_parser.add_argument("--email", required=True)
34+
add_parser.add_argument("--phone", required=True)
35+
subparsers.add_parser("list", help="List all contacts")
36+
args = parser.parse_args(argv)
37+
38+
try:
39+
if args.command == "add":
40+
storage.add_contact(args.path, args.name, args.email, args.phone)
41+
print(f"Added contact: {args.name}")
42+
elif args.command == "list":
43+
contacts = storage.read_contacts(args.path)
44+
_print_table(contacts)
45+
except (ValueError, OSError) as e:
46+
print(f"Error: {e}", file=sys.stderr)
47+
sys.exit(1)
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import csv
2+
import os
3+
4+
FIELDNAMES = ("name", "email", "phone")
5+
6+
7+
def read_contacts(path: str) -> list[dict[str, str]]:
8+
path = os.path.expanduser(path)
9+
try:
10+
with open(path, newline="") as f:
11+
reader = csv.DictReader(f, restval="")
12+
if reader.fieldnames is None:
13+
return []
14+
if tuple(reader.fieldnames) != FIELDNAMES:
15+
raise ValueError(
16+
f"Expected CSV headers {FIELDNAMES}, got {tuple(reader.fieldnames)}"
17+
)
18+
return list(reader)
19+
except FileNotFoundError:
20+
return []
21+
22+
23+
def add_contact(path: str, name: str, email: str, phone: str) -> None:
24+
if not all([name, email, phone]):
25+
raise ValueError("name, email, and phone must not be empty")
26+
path = os.path.expanduser(path)
27+
write_header = not os.path.exists(path) or os.path.getsize(path) == 0
28+
with open(path, "a", newline="") as f:
29+
writer = csv.DictWriter(f, fieldnames=FIELDNAMES)
30+
if write_header:
31+
writer.writeheader()
32+
writer.writerow({"name": name, "email": email, "phone": phone})

‎claude-code/mini-contacts/tests/__init__.py‎

Whitespace-only changes.
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import contextlib
2+
import io
3+
import tempfile
4+
import unittest
5+
from pathlib import Path
6+
7+
from mini_contacts.cli import main
8+
9+
10+
class CLITests(unittest.TestCase):
11+
def setUp(self):
12+
tmp = tempfile.TemporaryDirectory()
13+
self.addCleanup(tmp.cleanup)
14+
self.tmp_path = Path(tmp.name)
15+
16+
def run_cli(self, *argv):
17+
stdout = io.StringIO()
18+
with contextlib.redirect_stdout(stdout):
19+
main(list(argv))
20+
return stdout.getvalue()
21+
22+
def test_add_then_list(self):
23+
path = str(self.tmp_path / "contacts.csv")
24+
self.run_cli(
25+
"--path",
26+
path,
27+
"add",
28+
"--name",
29+
"Alice",
30+
"--email",
31+
"alice@example.com",
32+
"--phone",
33+
"555-1234",
34+
)
35+
out = self.run_cli("--path", path, "list")
36+
self.assertIn("Alice", out)
37+
self.assertIn("alice@example.com", out)
38+
39+
def test_list_empty(self):
40+
out = self.run_cli("--path", str(self.tmp_path / "empty.csv"), "list")
41+
self.assertIn("No contacts found.", out)
42+
43+
def test_list_short_row_exits_cleanly(self):
44+
path = self.tmp_path / "contacts.csv"
45+
path.write_text("name,email,phone\nAlice,alice@example.com\n")
46+
out = self.run_cli("--path", str(path), "list")
47+
self.assertIn("Alice", out)
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import tempfile
2+
import unittest
3+
from pathlib import Path
4+
5+
from mini_contacts import storage
6+
7+
8+
class StorageTests(unittest.TestCase):
9+
def setUp(self):
10+
tmp = tempfile.TemporaryDirectory()
11+
self.addCleanup(tmp.cleanup)
12+
self.tmp_path = Path(tmp.name)
13+
14+
def test_round_trip(self):
15+
path = self.tmp_path / "contacts.csv"
16+
storage.add_contact(str(path), "Alice", "alice@example.com", "555-1234")
17+
expected = {
18+
"name": "Alice",
19+
"email": "alice@example.com",
20+
"phone": "555-1234",
21+
}
22+
self.assertEqual(storage.read_contacts(str(path)), [expected])
23+
24+
def test_header_written_once(self):
25+
path = self.tmp_path / "contacts.csv"
26+
storage.add_contact(str(path), "Alice", "alice@example.com", "555-1234")
27+
storage.add_contact(str(path), "Bob", "bob@example.com", "555-5678")
28+
self.assertEqual(path.read_text().count("name,email,phone"), 1)
29+
30+
def test_read_missing_file_returns_empty(self):
31+
missing = self.tmp_path / "missing.csv"
32+
self.assertEqual(storage.read_contacts(str(missing)), [])
33+
34+
def test_short_row_filled_with_blanks(self):
35+
path = self.tmp_path / "contacts.csv"
36+
path.write_text("name,email,phone\nAlice,alice@example.com\n")
37+
contacts = storage.read_contacts(str(path))
38+
self.assertEqual(contacts[0]["phone"], "")
39+
40+
def test_add_blank_field_raises(self):
41+
with self.assertRaises(ValueError):
42+
storage.add_contact(str(self.tmp_path / "c.csv"), "Alice", "", "555-1234")

‎claude-code/prompts.md‎

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
# Prompts
2+
3+
These are the prompts used in the Real Python tutorial
4+
[How to Use Claude Code to Write and Refactor Python](https://realpython.com/how-to-use-claude-code/),
5+
in the order they appear.
6+
7+
Claude Code is nondeterministic, so your results will differ in wording and
8+
detail. Expect the same general shape, not identical output.
9+
10+
## 1. Plan the project structure (Plan Mode)
11+
12+
```text
13+
I want to build a minimal command-line contact manager in Python.
14+
15+
It should have two commands:
16+
17+
- `add` to append a new contact with name, email, and phone to a CSV file
18+
- `list` to print all contacts in a readable table format
19+
20+
Use `argparse` for the CLI. Store the CSV at ~/.mini-contacts.csv
21+
by default with a --path flag to override it.
22+
23+
Split the project into a mini_contacts/ package with storage.py for CSV
24+
read/write and cli.py for the `argparse` interface.
25+
```
26+
27+
## 2. Implement the plan
28+
29+
```text
30+
Implement the plan.
31+
```
32+
33+
## 3. Exercise the happy path
34+
35+
```text
36+
Add a contact named "John Doe" with email john@example.com and
37+
phone 555-0100, then list all contacts to verify it was saved.
38+
```
39+
40+
## 4. Write the tests
41+
42+
```text
43+
Write tests for the storage and CLI modules. Cover the happy path for
44+
add and list, and test what happens when the CSV file doesn't exist yet.
45+
```
46+
47+
## 5. Run the tests with Shell mode
48+
49+
```text
50+
! python -m unittest
51+
```
52+
53+
## 6. Commit the initial implementation
54+
55+
```text
56+
Create a commit with the message "Add initial mini-contacts implementation".
57+
```
58+
59+
## 7. Explore the codebase in a fresh session
60+
61+
```text
62+
Review this codebase. Tell me what the application does, describe
63+
each module and how they connect, and summarize the test coverage.
64+
```
65+
66+
## 8. Hunt for bugs and edge cases
67+
68+
```text
69+
Now look for bugs, security issues, and edge cases that could cause
70+
crashes or data loss. Be specific about each finding.
71+
```
72+
73+
## 9. Plan the fixes (Plan Mode)
74+
75+
```text
76+
Fix the crash bugs and the edge case for empty-string validation.
77+
For the crash on short CSV rows, handle rows with missing fields gracefully.
78+
For the empty-string fields, reject blank values for name, email, and phone.
79+
```
80+
81+
## 10. Implement the fixes
82+
83+
```text
84+
Implement the fixes.
85+
```
86+
87+
## 11. Re-run the tests
88+
89+
```text
90+
Run the tests and show me the results.
91+
```
92+
93+
## 12. Commit the fixes
94+
95+
```text
96+
Create a commit with the message "Add input validation and fix crash bugs".
97+
```

0 commit comments

Comments
 (0)