Claude Code Python Guide: uv, pytest, Ruff, FastAPI for Beginners
Build a Python project with Claude Code, uv, pyproject.toml, pytest, Ruff, FastAPI, and safe iteration.
The hard part of using Claude Code for Python is rarely the first generated file. The hard part is knowing whether the project is actually healthy: the environment is reproducible, imports work, tests run, formatting is consistent, and the next change can be reviewed without guessing.
This guide builds a small Python starter workflow for beginners. We will use uv or the standard venv, keep project settings in pyproject.toml, write a tiny FastAPI app and an optional CLI, add pytest, run ruff, and then use Claude Code for small, safe iterations.
Keep the official docs nearby because tool behavior changes. Start with the Claude Code overview, the Python Packaging User Guide for writing pyproject.toml, Astral’s uv installation docs, pytest getting started, the Ruff documentation, and the FastAPI first steps.
Start with a small definition of done
Claude Code can edit a codebase, run commands, and explain failures, but beginners still need a small target. Do not ask for authentication, a database, Docker, background jobs, and CI in the first prompt. That creates too many moving parts, and every error looks mysterious.
For a first Python project, the definition of done can be this:
uv run pytestpassesuv run ruff check .anduv run ruff format .run cleanly- FastAPI can create and list one task
- The optional CLI can add one task from the terminal
- Claude Code reports which commands it ran
flowchart LR
A["Task brief"] --> B["uv or venv"]
B --> C["pyproject.toml"]
C --> D["src implementation"]
D --> E["pytest"]
E --> F["ruff"]
F --> G["Small Claude Code iteration"]
Think of the brief as the work order for the coding agent. It should say what to build, what files it may touch, which Python version to target, and how to prove the result. If you use terms such as harness, explain them as “the scaffold the agent works inside” so junior developers can review the plan.
Give Claude Code a concrete brief
A good first prompt does not need to be poetic. It needs boundaries.
Add a small Python 3.12 task management API to this repository.
Requirements:
- Use uv for package management and keep settings in pyproject.toml.
- Put implementation code under src/task_api/ and tests under tests/.
- Implement POST /tasks and GET /tasks with FastAPI.
- Write pytest coverage for the happy path and a 404 path.
- Make ruff check and ruff format pass.
- Before editing, show the plan. After editing, report the commands you ran.
Allowed files:
- pyproject.toml
- src/task_api/**
- tests/**
This prompt turns Claude Code into an implementation partner, not just a code generator. It also gives you a review surface. If the diff touches a frontend folder or rewrites unrelated files, you can reject it immediately.
For durable repository rules, pair this with CLAUDE.md best practices. When the API grows beyond two endpoints, continue with API development with Claude Code. For test strategy, use Claude Code testing strategies.
Create the environment with uv or venv
For new learning projects in 2026, uv is a practical default because it creates environments quickly, manages dependencies, and keeps the workflow easy to explain. Some company laptops may not allow new tools, so the standard library venv fallback is still worth documenting.
On macOS or Linux:
mkdir task-api
cd task-api
uv init --app --python 3.12
uv add "fastapi[standard]"
uv add --dev pytest ruff
mkdir -p src/task_api tests
touch src/task_api/__init__.py
On Windows PowerShell:
mkdir task-api
cd task-api
uv init --app --python 3.12
uv add "fastapi[standard]"
uv add --dev pytest ruff
New-Item -ItemType Directory -Force src/task_api, tests
New-Item -ItemType File -Force src/task_api/__init__.py
If uv is not available, use venv:
python -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install "fastapi[standard]" pytest ruff
python -m venv .venv
.\.venv\Scripts\Activate.ps1
python -m pip install --upgrade pip
python -m pip install "fastapi[standard]" pytest ruff
The important decision is not whether uv is fashionable. The important decision is that the project has one documented way to install, run, test, and format. Tell Claude Code which path you chose before it edits files.
Keep Python settings in pyproject.toml
Beginners often inherit Python projects with requirements.txt, setup.cfg, pytest.ini, .flake8, and .ruff.toml all competing for attention. For a small starter, consolidate the important settings in pyproject.toml.
[project]
name = "task-api"
version = "0.1.0"
description = "Small FastAPI and CLI sample for Claude Code practice"
requires-python = ">=3.11"
dependencies = [
"fastapi[standard]>=0.115.0",
]
[project.scripts]
task-api = "task_api.cli:main"
[dependency-groups]
dev = [
"pytest>=8.0.0",
"ruff>=0.8.0",
]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["src/task_api"]
[tool.pytest.ini_options]
testpaths = ["tests"]
pythonpath = ["src"]
[tool.ruff]
line-length = 100
target-version = "py311"
[tool.ruff.lint]
select = ["E", "F", "I", "B", "UP"]
Now you can ask Claude Code to follow one source of truth. That makes reviews easier: dependency changes, test discovery, import paths, and lint policy are visible in one file.
Add a runnable FastAPI skeleton
Place this in src/task_api/main.py. It uses an in-memory dictionary, so it is not production storage. That is intentional. The goal is to learn request validation, response models, error handling, and tests before adding a database.
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field
app = FastAPI(title="Task API")
class TaskCreate(BaseModel):
title: str = Field(min_length=1, max_length=80)
class Task(BaseModel):
id: int
title: str
done: bool = False
_tasks: dict[int, Task] = {}
_next_id = 1
@app.post("/tasks", response_model=Task, status_code=201)
def create_task(payload: TaskCreate) -> Task:
global _next_id
task = Task(id=_next_id, title=payload.title)
_tasks[task.id] = task
_next_id += 1
return task
@app.get("/tasks", response_model=list[Task])
def list_tasks() -> list[Task]:
return list(_tasks.values())
@app.patch("/tasks/{task_id}/done", response_model=Task)
def mark_done(task_id: int) -> Task:
task = _tasks.get(task_id)
if task is None:
raise HTTPException(status_code=404, detail="Task not found")
updated = task.model_copy(update={"done": True})
_tasks[task_id] = updated
return updated
Run it:
uv run fastapi dev src/task_api/main.py
Then open http://127.0.0.1:8000/docs and create a task through the generated documentation. Once this works, adding SQLite, PostgreSQL, authentication, or deployment becomes a series of smaller changes instead of one large mystery.
If you also want a command-line entry point, place this in src/task_api/cli.py.
import argparse
import json
from pathlib import Path
DB_PATH = Path("tasks.json")
def load_tasks() -> list[dict[str, object]]:
if not DB_PATH.exists():
return []
return json.loads(DB_PATH.read_text(encoding="utf-8"))
def save_tasks(tasks: list[dict[str, object]]) -> None:
DB_PATH.write_text(json.dumps(tasks, ensure_ascii=False, indent=2), encoding="utf-8")
def add_task(title: str) -> dict[str, object]:
tasks = load_tasks()
task = {"id": len(tasks) + 1, "title": title, "done": False}
tasks.append(task)
save_tasks(tasks)
return task
def main() -> None:
parser = argparse.ArgumentParser(description="Task CLI")
subparsers = parser.add_subparsers(dest="command", required=True)
add_parser = subparsers.add_parser("add")
add_parser.add_argument("title")
args = parser.parse_args()
if args.command == "add":
task = add_task(args.title)
print(f"Added #{task['id']}: {task['title']}")
if __name__ == "__main__":
main()
uv run task-api add "write pytest"
Add pytest and Ruff before asking for more features
Tests make Claude Code useful because every later change has a proof command. Add tests/test_main.py.
import pytest
from fastapi.testclient import TestClient
from task_api import main
from task_api.main import app
@pytest.fixture(autouse=True)
def clean_tasks() -> None:
main._tasks.clear()
main._next_id = 1
def test_create_and_list_tasks() -> None:
client = TestClient(app)
response = client.post("/tasks", json={"title": "Write pytest"})
assert response.status_code == 201
assert response.json()["title"] == "Write pytest"
list_response = client.get("/tasks")
assert list_response.status_code == 200
assert len(list_response.json()) == 1
def test_mark_done_returns_404_for_missing_task() -> None:
client = TestClient(app)
response = client.patch("/tasks/999/done")
assert response.status_code == 404
assert response.json()["detail"] == "Task not found"
Use the same commands every time:
uv run pytest
uv run ruff check .
uv run ruff format .
When a command fails, do not paste a vague “fix this” request. Tell Claude Code which command failed, what behavior must stay the same, and which files it may edit. Example: “Fix only the failing uv run pytest result. Keep the API response shape unchanged. Prefer the smallest diff in src/task_api/main.py or tests/test_main.py.”
Three practical use cases
The first use case is a learning API. A beginner can see routing, validation, response models, OpenAPI docs, tests, and formatting in one small project. It is better than starting with a full SaaS backend because every concept is visible.
The second use case is a business automation CLI. Cleaning CSV files, renaming exports, checking folders, or generating a weekly report often does not need a web UI. Claude Code can help you turn a fragile script into a command with arguments, clear errors, and tests.
The third use case is adding tests to legacy Python. If a script is already used by the team but nobody wants to touch it, ask Claude Code to write characterization tests first. A characterization test captures current behavior before refactoring. That is safer than asking for a rewrite on day one.
The fourth use case is training material. A shared starter with pyproject.toml, src layout, pytest, ruff, and proof commands gives every learner the same baseline. For teams, ClaudeCodeLab can adapt this into Claude Code training and consultation so examples match internal tools instead of generic demos.
Failure cases to avoid
The most common mistake is asking “make a Python app” without naming the package manager. Claude Code may mix pip, poetry, uv, and requirements.txt. Choose one and write it into the brief.
The second mistake is a broken import path. With src layout, tests may fail to import the package unless pythonpath = ["src"] is configured or the package is installed correctly. Import errors waste beginner time because the application code can be perfectly fine while the test runner cannot find it.
The third mistake is adding a database and authentication too early. JWT, migrations, async sessions, and environment variables are useful, but they multiply the failure modes. Start in memory, then add SQLite, then move to the production database.
The fourth mistake is accepting automatic lint changes without review. Ruff is excellent, but a lint fix is still a code change. Ask Claude Code to explain the Ruff diff before applying it when the code is part of a real project.
The fifth mistake is pasting secrets into the prompt. Use .env.example names and fake values. Do not include customer data, API keys, production database URLs, or private tokens.
Monetization path
Python tutorials can bring search traffic, but a thin command list is hard to monetize and weak for AdSense quality. A better article gives the reader code, failure cases, verification, and a natural next step.
Solo developers can start with free checklists and reusable prompts, then move to Claude Code products when the same brief is used repeatedly. Teams should consider Claude Code training and consultation when the risk moves from personal learning to shared repositories, onboarding, code review, or production systems.
What I verified
For this update, I used the same order Masa uses for beginner Python materials: fix the environment first, put project rules in pyproject.toml, keep FastAPI small, add tests before features, and make Ruff part of the everyday command loop. The practical result is not that Claude Code writes perfect Python. The result is that each failure becomes local: dependency setup, import path, test behavior, or formatting. That makes the work teachable, reviewable, and reusable for training.
The takeaway is simple: use Claude Code after you define the scaffold. A small project with clear commands beats a large generated backend that nobody can debug.
Free PDF: Claude Code Cheatsheet
Enter your email and download the one-page Claude Code cheatsheet for commands, review habits, and safe workflows.
We handle your data with care and never send spam.
Level up your Claude Code workflow
Start with the free PDF, use Gumroad guides when you need repeatable workflows, and book consultation when rollout or revenue paths need human judgment.
About the Author
Masa
Engineer focused on practical Claude Code workflows. Runs claudecode-lab.com, a 10-language technical media site.
Related Posts
Claude Code Obsidian to CLAUDE.md Workflow: Stop Re-explaining Context
Turn Obsidian working notes into concise CLAUDE.md operating notes that make Claude Code sessions easier to resume.
Claude Code Revenue CTA Routing: Send Articles to PDF, Gumroad, and Consultation
A Claude Code workflow for routing article readers to the free PDF, Gumroad products, or consultation by intent.
Claude Code Team Handoff Rules: Review Evidence, Permissions, Rollback, and Revenue Paths
A practical Claude Code handoff format for team review, proof, permission rules, rollback, free PDF, Gumroad, and consultation paths.
Related Products
50 Battle-Tested Claude Code Prompt Templates
Copy, paste, ship. 50 production-ready prompts.
Use proven prompts for code review, refactoring, testing, documentation, debugging, architecture, and incident response.
The Complete Claude Code Setup & Configuration Guide
From install to team-ready workflow.
A practical guide to installation, CLAUDE.md, hooks, MCP servers, permissions, IDE setup, and CI/CD workflows.