Claude Code Python 입문 가이드: uv, pytest, Ruff, FastAPI 실습
Claude Code로 Python 프로젝트를 안전하게 시작하는 방법. uv, pyproject, pytest, Ruff, FastAPI를 실습으로 설명합니다.
Claude Code로 Python을 처음 다룰 때 어려운 지점은 코드 생성 자체가 아닙니다. 더 자주 막히는 부분은 “이 프로젝트가 정말 실행 가능한 상태인가”입니다. 가상환경이 다르고, 테스트에서 import가 깨지고, 포맷터 설정이 없고, API는 뜨지만 확인 명령이 없는 상태가 초보자를 지치게 합니다.
이 글은 작은 작업 관리 프로젝트를 기준으로 안전한 시작 순서를 정리합니다. uv 또는 표준 venv로 환경을 만들고, pyproject.toml에 설정을 모으고, FastAPI API와 선택적 CLI를 구현한 뒤, pytest와 ruff로 검증합니다. Claude Code는 큰 코드를 한 번에 만드는 도구가 아니라, 검증 가능한 작은 변경을 빠르게 반복하게 해 주는 도구로 쓰는 편이 안전합니다.
도구의 세부 동작은 공식 문서를 확인하세요. Claude Code는 공식 개요, Python 프로젝트 설정은 Python Packaging User Guide, uv는 Astral uv 문서, 테스트는 pytest 문서, lint와 포맷은 Ruff 문서, API는 FastAPI first steps를 기준으로 삼습니다.
작은 완료 기준부터 정한다
처음부터 로그인, 데이터베이스, Docker, CI까지 요청하면 실패 지점이 너무 많아집니다. 초보자에게 필요한 첫 목표는 작고 확인 가능한 상태입니다.
uv run pytest가 통과한다uv run ruff check .와uv run ruff format .가 실행된다- FastAPI에서 작업을 만들고 목록을 조회할 수 있다
- CLI에서 작업 하나를 추가할 수 있다
- Claude Code가 실행한 확인 명령을 보고한다
flowchart LR
A["작업 브리프"] --> B["uv 또는 venv"]
B --> C["pyproject.toml"]
C --> D["src 구현"]
D --> E["pytest"]
E --> F["ruff"]
F --> G["Claude Code로 작은 수정"]
작업 브리프는 코딩 에이전트에게 주는 작업 지시서입니다. 목표, 수정 가능한 파일, Python 버전, 검증 명령을 적어 둡니다. 저장소 규칙을 계속 유지하려면 CLAUDE.md 작성 가이드와 함께 쓰는 것이 좋습니다.
Claude Code에 줄 첫 요청
아래처럼 범위를 명확히 적으면 Claude Code가 임의로 구조를 넓히는 일을 줄일 수 있습니다.
이 저장소에 Python 3.12용 작은 작업 관리 API를 추가해 주세요.
조건:
- 패키지 관리는 uv를 사용하고 설정은 pyproject.toml에 모읍니다.
- 구현은 src/task_api/ 아래, 테스트는 tests/ 아래에 둡니다.
- FastAPI로 POST /tasks와 GET /tasks를 구현합니다.
- pytest로 정상 생성과 404 케이스를 테스트합니다.
- ruff check와 ruff format이 통과해야 합니다.
- 수정 전 계획을 보여 주고, 수정 후 실행한 확인 명령을 보고합니다.
수정 가능 범위:
- pyproject.toml
- src/task_api/**
- tests/**
이 요청은 Claude Code를 단순 생성기가 아니라 검증까지 수행하는 구현 담당자로 만듭니다. API가 커질 때는 Claude Code API 개발, 테스트 관점은 Claude Code 테스트 전략을 함께 참고하세요.
uv 또는 venv로 환경 만들기
새 학습 프로젝트라면 uv가 편합니다. 환경 생성과 의존성 설치가 빠르고 명령이 짧습니다. 회사 장비에서 새 도구 설치가 어렵다면 표준 venv를 쓰면 됩니다. 중요한 것은 둘을 섞지 않는 것입니다.
macOS 또는 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
uv를 못 쓰는 경우:
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
pyproject.toml에 설정 모으기
초보자는 설정 파일이 여러 개로 흩어질수록 길을 잃기 쉽습니다. 작은 프로젝트에서는 의존성, 테스트 경로, Ruff 규칙을 pyproject.toml에 모아 두면 Claude Code도 같은 기준을 따르기 쉽습니다.
[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"]
FastAPI와 CLI 최소 구현
src/task_api/main.py에 아래 코드를 둡니다. 메모리 딕셔너리를 사용하므로 운영용 저장소는 아니지만, 라우팅, 검증, 응답 모델, 오류 처리를 배우기에는 충분합니다.
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
CLI가 필요하면 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"
pytest와 Ruff로 반복을 보호한다
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"
uv run pytest
uv run ruff check .
uv run ruff format .
실제 활용 사례
첫 번째는 학습용 API입니다. 라우팅, Pydantic 검증, 자동 문서, 테스트를 한 번에 볼 수 있습니다. 두 번째는 업무 자동화 CLI입니다. CSV 정리, 파일명 변경, 주간 보고서 생성은 웹 화면보다 CLI가 빠른 경우가 많습니다. 세 번째는 레거시 Python 테스트 추가입니다. 바로 리팩터링하지 말고 현재 동작을 고정하는 테스트부터 만듭니다. 네 번째는 팀 교육 템플릿입니다. 같은 pyproject.toml, src 구조, 확인 명령을 공유하면 온보딩 품질이 안정됩니다.
팀에 맞춘 실습 자료가 필요하다면 Claude Code 교육과 상담으로 연결할 수 있습니다. 개인 학습자는 Claude Code 제품 템플릿으로 반복되는 요청과 리뷰 문구를 줄일 수 있습니다.
실패 사례와 주의점
가장 흔한 실패는 패키지 관리자를 정하지 않는 것입니다. 그러면 pip, poetry, uv, requirements.txt가 섞일 수 있습니다. 두 번째는 src 구조에서 import가 깨지는 문제입니다. pythonpath = ["src"] 또는 패키지 설치 설정을 확인하세요. 세 번째는 DB와 인증을 너무 빨리 넣는 것입니다. 네 번째는 Ruff 자동 수정을 diff 없이 받아들이는 것입니다. 다섯 번째는 실제 API 키나 고객 데이터를 프롬프트에 붙여 넣는 것입니다.
직접 확인한 결과
이번 정리는 Masa가 초보자용 Python 자료를 만들 때 쓰는 순서대로 확인했습니다. 환경을 먼저 고정하고, pyproject.toml에 규칙을 모으고, FastAPI를 작게 실행한 다음, 테스트와 Ruff로 반복을 보호하면 Claude Code의 결과를 훨씬 쉽게 리뷰할 수 있습니다. 중요한 것은 거대한 백엔드를 한 번에 만드는 것이 아니라, 실패 원인을 환경, import, 테스트, 포맷 중 하나로 좁힐 수 있게 만드는 것입니다.
무료 PDF: Claude Code 치트시트
이메일을 입력하면 명령, 리뷰 습관, 안전한 워크플로를 정리한 PDF를 받을 수 있습니다.
개인정보를 안전하게 관리하며 스팸을 보내지 않습니다.
작성자 소개
Masa
Claude Code 실무 워크플로와 팀 도입을 검증하는 엔지니어입니다.
관련 글
Obsidian 메모를 CLAUDE.md로 바꾸는 Claude Code 워크플로
Obsidian 작업 메모를 CLAUDE.md 운영 노트로 정리해 Claude Code 세션의 문맥 반복을 줄입니다.
Claude Code Revenue CTA Routing: 글에서 PDF, Gumroad, 상담으로 보내기
독자 의도에 따라 무료 PDF, Gumroad 상품, 상담으로 나누는 Claude Code CTA 설계입니다.
Claude Code 팀 인계 규칙: 리뷰 증거, 권한, 롤백, 수익 경로까지 넘기는 법
Claude Code 작업을 팀에 넘길 때 필요한 증거, 권한 규칙, 롤백, 무료 PDF, Gumroad, 상담 경로 체크리스트.