Finishing a Capstone Project

A capstone is your chance to build something non-trivial, end-to-end, with all the skills the course has taught. It is deliberately open: pick a problem that interests you, scope it so you can finish in a few sessions, and use the opportunity to practice the full loop — design, implement, test, document, polish, ship. What you build matters less than proving to yourself that you can take something from empty folder to working program.

Good capstones share a few traits. They solve a real (if small) problem. They have a clear user-facing entry point: a CLI, a small web page, or a library with a tidy API. They persist something across runs — a file, a database, a remote service. They have at least a handful of tests. They come with a README that explains what, why, and how.

Typical shapes that work well: a personal CLI tool (expense tracker, bookmark saver, meal planner); a data-processing pipeline (scrape, clean, visualize); a mini web app (FastAPI + a small HTML template); a game (text adventure, tic-tac-toe with AI); a library that wraps an API you care about. Pick one, scope it to what you can finish in 10–20 hours total, and start with the riskiest part.

The hardest 10% is finishing. Resist the urge to add features past your scope. Write a short README, one diagram, two examples. Run the tests. Fix the obvious rough edges. Ship. A done project you are mildly proud of teaches more than three unfinished ambitious ones.

Scoping and risk-first order

Write down one sentence for what the project does and one paragraph for what it explicitly does not do. Strike out anything you can't ship this week. Short scope, solid implementation, is much better than long scope with hand-waving.

Start with the riskiest unknown. If it's the external API, prove you can authenticate and call it first. If it's the algorithm, prototype in a notebook. Don't polish the CLI before you know the core works.

Ship the polish

README: what, why, how to install, how to run, one screenshot or transcript. Tests: at least for the two or three most critical paths. CI: run the tests on every push. Version: 0.1.0 is enough. Publish: GitHub + optional pip install via pyproject.toml.

After shipping, write a short retrospective. What took longer than expected? What surprised you? The next capstone starts with those notes.

Capstone-building resources.

ToolPurpose
GitHub
platform
Host code and collaborate.
PyPI
registry
Publish Python packages.
venv
module
Isolate dependencies per project.
pyproject.toml
spec
Modern project configuration.
ruff
tool
Lint + format in CI.
pytest
tool
Tests for the project.
shields.io
site
README status badges.
CHANGELOG.md
convention
Track versions.

Finishing a Capstone Project code example

The script is a compact, end-to-end capstone example: a small bookmark manager with add/list/search commands, JSON persistence, and tests.

# Lesson: Finishing a Capstone Project  (Bookmarks)
import json
import re
import unittest
from dataclasses import asdict, dataclass
from datetime import datetime, timezone
from pathlib import Path
from tempfile import TemporaryDirectory


@dataclass
class Bookmark:
    id: int
    url: str
    title: str
    tags: list[str]
    created_at: str


def _now() -> str:
    return datetime.now(timezone.utc).isoformat()


def load(db: Path) -> list[Bookmark]:
    if not db.exists():
        return []
    return [Bookmark(**row) for row in json.loads(db.read_text(encoding="utf-8"))]


def save(db: Path, bookmarks: list[Bookmark]) -> None:
    db.write_text(json.dumps([asdict(b) for b in bookmarks], indent=2), encoding="utf-8")


def add(db: Path, url: str, title: str, tags: list[str]) -> Bookmark:
    bks = load(db)
    bid = (max((b.id for b in bks), default=0)) + 1
    bk = Bookmark(id=bid, url=url, title=title, tags=tags, created_at=_now())
    bks.append(bk)
    save(db, bks)
    return bk


def search(db: Path, query: str) -> list[Bookmark]:
    q = query.lower()
    return [
        b for b in load(db)
        if q in b.title.lower() or q in b.url.lower() or any(q == t.lower() for t in b.tags)
    ]


# ---- demo ----
with TemporaryDirectory() as tmp:
    db = Path(tmp) / "bookmarks.json"
    add(db, "https://docs.python.org", "Python Docs", ["python", "reference"])
    add(db, "https://realpython.com", "Real Python", ["python", "tutorial"])
    add(db, "https://fastapi.tiangolo.com", "FastAPI", ["python", "web", "framework"])

    print("all:", len(load(db)))
    for b in search(db, "tutorial"):
        print("  -", b.id, b.title, b.tags)

    class T(unittest.TestCase):
        def test_add_then_search(self):
            self.assertTrue(search(db, "fastapi"))
            self.assertFalse(search(db, "nope"))
    unittest.TextTestRunner(verbosity=2).run(unittest.TestLoader().loadTestsFromTestCase(T))

A small but complete capstone:

1) Model, storage, and service layers are separated.
2) Everything is testable because nothing touches real filesystems at import time.
3) The core API is six functions; a CLI/web UI would wrap them.
4) Documentation + tests + a tidy README are what turns this into a capstone.

Add a list-by-tag feature in five lines.

def list_by_tag(db: Path, tag: str):
    return [b for b in load(db) if tag.lower() in (t.lower() for t in b.tags)]
# print(list_by_tag(db, "python"))

Capstone behavior contract.

from tempfile import TemporaryDirectory
from pathlib import Path
with TemporaryDirectory() as d:
    db = Path(d)/"b.json"
    b = add(db, "https://a", "A", ["x"])
    assert b.id == 1
    assert len(search(db, "A")) == 1
    assert search(db, "zz") == []

Running prints:

all: 3
  - 2 Real Python ['python', 'tutorial']
test_add_then_search (__main__.T.test_add_then_search) ... ok

----------------------------------------------------------------------
Ran 1 test in 0.001s

OK