Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions claude-code/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# How to Use Claude Code to Write and Refactor Python

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/).

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.

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.

## Run the Project

```sh
$ cd mini-contacts/
$ python -m mini_contacts add --name "Alice" --email "alice@example.com" --phone "555-1234"
$ python -m mini_contacts list
```

Contacts are stored at `~/.mini-contacts.csv` by default. Pass `--path` to use a different file.

## Run the Tests

```sh
$ cd mini-contacts/
$ python -m unittest
```

The suite uses `unittest` from the standard library, so there's nothing to install.

## About the Author

Real Python - Email: office@realpython.com

## License

Distributed under the MIT license. See `LICENSE` for more information.
5 changes: 5 additions & 0 deletions claude-code/mini-contacts/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Project Conventions

- Python 3.14+, four-space indentation, PEP 8 compliant
- Prefer the standard library; use third-party packages only when needed
- Use type hints on all public functions
Empty file.
3 changes: 3 additions & 0 deletions claude-code/mini-contacts/mini_contacts/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from mini_contacts.cli import main

main()
51 changes: 51 additions & 0 deletions claude-code/mini-contacts/mini_contacts/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import argparse
import sys

from mini_contacts import storage


def _print_table(contacts: list[dict[str, str]]) -> None:
if not contacts:
print("No contacts found.")
return
headers = {"name": "Name", "email": "Email", "phone": "Phone"}
widths = {
field: max(
len(headers[field]), max(len(row[field]) for row in contacts)
)
for field in storage.FIELDNAMES
}
header_line = " | ".join(
headers[f].ljust(widths[f]) for f in storage.FIELDNAMES
)
separator = "-+-".join("-" * widths[f] for f in storage.FIELDNAMES)
print(header_line)
print(separator)
for row in contacts:
print(" | ".join(row[f].ljust(widths[f]) for f in storage.FIELDNAMES))


def main(argv: list[str] | None = None) -> None:
parser = argparse.ArgumentParser(description="Minimal contact manager")
parser.add_argument(
"--path", default="~/.mini-contacts.csv", help="Path to the CSV file"
)
subparsers = parser.add_subparsers(dest="command")
subparsers.required = True
add_parser = subparsers.add_parser("add", help="Add a new contact")
add_parser.add_argument("--name", required=True)
add_parser.add_argument("--email", required=True)
add_parser.add_argument("--phone", required=True)
subparsers.add_parser("list", help="List all contacts")
args = parser.parse_args(argv)

try:
if args.command == "add":
storage.add_contact(args.path, args.name, args.email, args.phone)
print(f"Added contact: {args.name}")
elif args.command == "list":
contacts = storage.read_contacts(args.path)
_print_table(contacts)
except (ValueError, OSError) as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
32 changes: 32 additions & 0 deletions claude-code/mini-contacts/mini_contacts/storage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import csv
import os

FIELDNAMES = ("name", "email", "phone")


def read_contacts(path: str) -> list[dict[str, str]]:
path = os.path.expanduser(path)
try:
with open(path, newline="") as f:
reader = csv.DictReader(f, restval="")
if reader.fieldnames is None:
return []
if tuple(reader.fieldnames) != FIELDNAMES:
raise ValueError(
f"Expected CSV headers {FIELDNAMES}, got {tuple(reader.fieldnames)}"
)
return list(reader)
except FileNotFoundError:
return []


def add_contact(path: str, name: str, email: str, phone: str) -> None:
if not all([name, email, phone]):
raise ValueError("name, email, and phone must not be empty")
path = os.path.expanduser(path)
write_header = not os.path.exists(path) or os.path.getsize(path) == 0
with open(path, "a", newline="") as f:
writer = csv.DictWriter(f, fieldnames=FIELDNAMES)
if write_header:
writer.writeheader()
writer.writerow({"name": name, "email": email, "phone": phone})
Empty file.
47 changes: 47 additions & 0 deletions claude-code/mini-contacts/tests/test_cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import contextlib
import io
import tempfile
import unittest
from pathlib import Path

from mini_contacts.cli import main


class CLITests(unittest.TestCase):
def setUp(self):
tmp = tempfile.TemporaryDirectory()
self.addCleanup(tmp.cleanup)
self.tmp_path = Path(tmp.name)

def run_cli(self, *argv):
stdout = io.StringIO()
with contextlib.redirect_stdout(stdout):
main(list(argv))
return stdout.getvalue()

def test_add_then_list(self):
path = str(self.tmp_path / "contacts.csv")
self.run_cli(
"--path",
path,
"add",
"--name",
"Alice",
"--email",
"alice@example.com",
"--phone",
"555-1234",
)
out = self.run_cli("--path", path, "list")
self.assertIn("Alice", out)
self.assertIn("alice@example.com", out)

def test_list_empty(self):
out = self.run_cli("--path", str(self.tmp_path / "empty.csv"), "list")
self.assertIn("No contacts found.", out)

def test_list_short_row_exits_cleanly(self):
path = self.tmp_path / "contacts.csv"
path.write_text("name,email,phone\nAlice,alice@example.com\n")
out = self.run_cli("--path", str(path), "list")
self.assertIn("Alice", out)
48 changes: 48 additions & 0 deletions claude-code/mini-contacts/tests/test_storage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import tempfile
import unittest
from pathlib import Path

from mini_contacts import storage


class StorageTests(unittest.TestCase):
def setUp(self):
tmp = tempfile.TemporaryDirectory()
self.addCleanup(tmp.cleanup)
self.tmp_path = Path(tmp.name)

def test_round_trip(self):
path = self.tmp_path / "contacts.csv"
storage.add_contact(
str(path), "Alice", "alice@example.com", "555-1234"
)
expected = {
"name": "Alice",
"email": "alice@example.com",
"phone": "555-1234",
}
self.assertEqual(storage.read_contacts(str(path)), [expected])

def test_header_written_once(self):
path = self.tmp_path / "contacts.csv"
storage.add_contact(
str(path), "Alice", "alice@example.com", "555-1234"
)
storage.add_contact(str(path), "Bob", "bob@example.com", "555-5678")
self.assertEqual(path.read_text().count("name,email,phone"), 1)

def test_read_missing_file_returns_empty(self):
missing = self.tmp_path / "missing.csv"
self.assertEqual(storage.read_contacts(str(missing)), [])

def test_short_row_filled_with_blanks(self):
path = self.tmp_path / "contacts.csv"
path.write_text("name,email,phone\nAlice,alice@example.com\n")
contacts = storage.read_contacts(str(path))
self.assertEqual(contacts[0]["phone"], "")

def test_add_blank_field_raises(self):
with self.assertRaises(ValueError):
storage.add_contact(
str(self.tmp_path / "c.csv"), "Alice", "", "555-1234"
)
97 changes: 97 additions & 0 deletions claude-code/prompts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# Prompts

These are the prompts used in the Real Python tutorial
[How to Use Claude Code to Write and Refactor Python](https://realpython.com/how-to-use-claude-code/),
in the order they appear.

Claude Code is nondeterministic, so your results will differ in wording and
detail. Expect the same general shape, not identical output.

## 1. Plan the project structure (Plan Mode)

```text
I want to build a minimal command-line contact manager in Python.

It should have two commands:

- `add` to append a new contact with name, email, and phone to a CSV file
- `list` to print all contacts in a readable table format

Use `argparse` for the CLI. Store the CSV at ~/.mini-contacts.csv
by default with a --path flag to override it.

Split the project into a mini_contacts/ package with storage.py for CSV
read/write and cli.py for the `argparse` interface.
```

## 2. Implement the plan

```text
Implement the plan.
```

## 3. Exercise the happy path

```text
Add a contact named "John Doe" with email john@example.com and
phone 555-0100, then list all contacts to verify it was saved.
```

## 4. Write the tests

```text
Write tests for the storage and CLI modules. Cover the happy path for
add and list, and test what happens when the CSV file doesn't exist yet.
```

## 5. Run the tests with Shell mode

```text
! python -m unittest
```

## 6. Commit the initial implementation

```text
Create a commit with the message "Add initial mini-contacts implementation".
```

## 7. Explore the codebase in a fresh session

```text
Review this codebase. Tell me what the application does, describe
each module and how they connect, and summarize the test coverage.
```

## 8. Hunt for bugs and edge cases

```text
Now look for bugs, security issues, and edge cases that could cause
crashes or data loss. Be specific about each finding.
```

## 9. Plan the fixes (Plan Mode)

```text
Fix the crash bugs and the edge case for empty-string validation.
For the crash on short CSV rows, handle rows with missing fields gracefully.
For the empty-string fields, reject blank values for name, email, and phone.
```

## 10. Implement the fixes

```text
Implement the fixes.
```

## 11. Re-run the tests

```text
Run the tests and show me the results.
```

## 12. Commit the fixes

```text
Create a commit with the message "Add input validation and fix crash bugs".
```
Loading