Python con Claude Code: guía práctica con uv, pytest, Ruff y FastAPI
Crea un proyecto Python real con Claude Code: uv, pyproject.toml, pytest, Ruff, FastAPI y una iteración segura.
El primer problema al usar Claude Code con Python no suele ser generar código. El problema real aparece después: ¿el entorno es reproducible?, ¿los imports funcionan?, ¿los tests se ejecutan?, ¿el formateo es consistente?, ¿la siguiente modificación se puede revisar sin miedo?
Esta guía propone un flujo pequeño y verificable para principiantes. Usaremos uv o venv, pondremos la configuración en pyproject.toml, crearemos una API mínima con FastAPI, añadiremos una CLI opcional, escribiremos tests con pytest y revisaremos el código con ruff. Claude Code debe trabajar dentro de ese marco, no inventar toda la arquitectura en una sola respuesta.
Consulta siempre las fuentes oficiales: Claude Code, la guía de Python para escribir pyproject.toml, la instalación de uv, pytest, Ruff y los primeros pasos de FastAPI.
Define primero un resultado pequeño
No pidas autenticación, base de datos, Docker, CI y tareas en segundo plano en el primer prompt. Para aprender, conviene que cada fallo tenga una causa probable. El primer resultado puede ser simple:
uv run pytestpasauv run ruff check .yuv run ruff format .se ejecutan- FastAPI crea y lista una tarea
- La CLI agrega una tarea desde la terminal
- Claude Code informa qué comandos ejecutó
flowchart LR
A["Brief de trabajo"] --> B["uv o venv"]
B --> C["pyproject.toml"]
C --> D["Implementación en src"]
D --> E["pytest"]
E --> F["ruff"]
F --> G["Iteración pequeña con Claude Code"]
El brief es la orden de trabajo para el agente. Debe incluir objetivo, archivos permitidos, versión de Python y comandos de verificación. Para reglas persistentes del repositorio, combina este flujo con buenas prácticas de CLAUDE.md. Si el proyecto crece hacia una API real, revisa también desarrollo de API con Claude Code y estrategias de testing.
Prompt inicial para Claude Code
Este prompt es deliberadamente concreto. Reduce cambios fuera de alcance y obliga a reportar pruebas.
Añade a este repositorio una pequeña API de tareas para Python 3.12.
Condiciones:
- Usa uv para dependencias y centraliza la configuración en pyproject.toml.
- Coloca el código en src/task_api/ y los tests en tests/.
- Implementa POST /tasks y GET /tasks con FastAPI.
- Añade tests con pytest para el flujo correcto y un caso 404.
- ruff check y ruff format deben pasar.
- Antes de editar, muestra el plan. Después, informa los comandos ejecutados.
Archivos permitidos:
- pyproject.toml
- src/task_api/**
- tests/**
La diferencia entre este prompt y “hazme una app Python” es enorme. Claude Code sabe qué tocar, cómo demostrar que terminó y qué no debe modificar. Eso hace que la revisión humana sea posible incluso para alguien que todavía está aprendiendo Python.
Crea el entorno con uv o venv
Para proyectos nuevos, uv es una buena opción por velocidad y claridad. Si tu equipo no permite instalar herramientas nuevas, venv sigue siendo suficiente. Lo importante es no mezclar caminos.
macOS o 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
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
Alternativa con 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
Centraliza pyproject.toml
Un proyecto pequeño no necesita cinco archivos de configuración. Este pyproject.toml contiene dependencias, script de consola, ruta de tests y reglas de Ruff.
[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"]
Con esta base puedes pedir a Claude Code que respete una sola fuente de verdad. Las discusiones sobre dependencias, imports y estilo quedan visibles en un archivo revisable.
Implementa FastAPI y una CLI mínima
Guarda este archivo como src/task_api/main.py. Usa memoria local, así que no es almacenamiento de producción. Justamente por eso sirve para aprender antes de añadir una base de datos.
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
uv run fastapi dev src/task_api/main.py
Para una CLI, crea 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"
Protege la iteración con pytest y Ruff
Guarda los tests en tests/test_main.py. A partir de aquí, cada cambio de Claude Code debe volver a pasar por los mismos comandos.
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"
uv run pytest
uv run ruff check .
uv run ruff format .
Casos de uso reales
El primer caso es una API de aprendizaje. En un proyecto corto se ven rutas, validación, documentación OpenAPI, tests y formato. El segundo caso es una CLI de automatización: limpiar CSV, renombrar archivos exportados o preparar un informe semanal. El tercer caso es añadir tests a un script heredado antes de refactorizar. El cuarto caso es crear una plantilla de formación para que todo el equipo empiece con la misma estructura.
Estos casos también conectan con monetización sin forzar la venta. Un lector individual puede pasar a plantillas y productos de Claude Code. Un equipo que quiere usarlo en repositorios reales puede revisar formación y consultoría.
Errores frecuentes
No dejes abierto el gestor de paquetes. Si no lo dices, pueden mezclarse pip, poetry, uv y requirements.txt. No añadas base de datos y JWT antes de que la versión en memoria funcione. Revisa los cambios automáticos de Ruff. Configura bien pythonpath = ["src"] para evitar errores de import. Y nunca pegues secretos, datos de clientes ni URLs reales de producción en el prompt.
Resultado verificado
Para esta actualización seguí el orden que Masa usa en materiales para principiantes: entorno primero, pyproject.toml después, FastAPI pequeño, tests antes de nuevas funciones y Ruff como comando cotidiano. El resultado práctico es que cada fallo queda localizado: entorno, import, comportamiento de test o formato. Eso hace que Claude Code sea útil para aprender, revisar y preparar una formación real, no solo para generar código rápido.
PDF gratis: cheatsheet de Claude Code
Introduce tu email y descarga una hoja con comandos, hábitos de revisión y flujos seguros.
Cuidamos tus datos y no enviamos spam.
Sobre el autor
Masa
Ingeniero enfocado en workflows prácticos con Claude Code.
Artículos relacionados
Workflow de Obsidian a CLAUDE.md con Claude Code
Convierte notas de trabajo de Obsidian en notas operativas de CLAUDE.md para no repetir contexto.
Claude Code Revenue CTA Routing: de artículos a PDF, Gumroad y consulta
Un flujo con Claude Code para dirigir lectores a PDF gratis, Gumroad o consulta según intención.
Reglas de handoff para equipos con Claude Code: evidencia, permisos, rollback e ingresos
Formato práctico para entregar trabajo de Claude Code con pruebas, permisos, rollback, PDF gratis, Gumroad y consulta.