Claude
Skills
Sign in
Back

python-project

Included with Lifetime
$97 forever

Modern Python project architecture guide for 2025. Use when creating Python projects (APIs, CLI, data pipelines). Covers uv, Ruff, Pydantic, FastAPI, and async patterns.

Backend & APIs

What this skill does

# Python Project Architecture

## Core Principles

- **Type hints everywhere** — Pydantic for runtime, mypy for static
- **uv for everything** — Package management, virtualenv, Python version
- **Ruff only** — Replace Flake8 + Black + isort with single tool
- **src layout** — All code under `src/` directory
- **pyproject.toml only** — No setup.py, no requirements.txt
- **Async all the way** — Once async, stay async through call chain
- **No backwards compatibility** — Delete, don't deprecate. Change directly
- **LiteLLM for LLM APIs** — Use LiteLLM proxy for all LLM integrations

---

## No Backwards Compatibility

> **Delete unused code. Change directly. No compatibility layers.**

```python
# ❌ BAD: Deprecated decorator kept around
import warnings

def old_function():
    warnings.warn("Use new_function instead", DeprecationWarning)
    return new_function()

# ❌ BAD: Alias for renamed functions
new_name = old_name  # "for backwards compatibility"

# ❌ BAD: Unused parameters with underscore
def process(_legacy_param, data):
    ...

# ❌ BAD: Version checking for old behavior
if version < "2.0":
    # old behavior
    ...

# ✅ GOOD: Just delete and update all usages
def new_function():
    ...
# Then: Find & replace all old_function → new_function

# ✅ GOOD: Remove unused parameters entirely
def process(data):
    ...
```

---

## LiteLLM for LLM APIs

> **Use LiteLLM proxy. Don't call provider APIs directly.**

```python
# src/myapp/llm.py
from openai import AsyncOpenAI

from myapp.config import settings

# Connect to LiteLLM proxy using OpenAI SDK
client = AsyncOpenAI(
    base_url=settings.litellm_url,  # "http://localhost:4000"
    api_key=settings.litellm_api_key,
)


async def complete(prompt: str, model: str = "gpt-4o") -> str:
    """Call any LLM through LiteLLM proxy."""
    response = await client.chat.completions.create(
        model=model,  # "gpt-4o", "claude-3-opus", "gemini-pro", etc.
        messages=[{"role": "user", "content": prompt}],
    )
    return response.choices[0].message.content or ""
```

---

## Quick Start

### 1. Initialize Project

```bash
# Install uv (if not installed)
curl -LsSf https://astral.sh/uv/install.sh | sh

# Create new project
uv init myapp
cd myapp

# Set Python version
echo "3.12" > .python-version

# Add dependencies
uv add fastapi uvicorn pydantic sqlalchemy httpx
uv add --dev pytest pytest-asyncio ruff mypy
```

### 2. Apply Tech Stack

| Layer | Recommendation |
|-------|----------------|
| Package Manager | uv |
| Linting + Format | Ruff |
| Type Checking | mypy |
| Validation | Pydantic v2 |
| Web Framework | FastAPI |
| Database | SQLAlchemy 2.0 + asyncpg |
| HTTP Client | httpx |
| Testing | pytest + pytest-asyncio |
| Logging | structlog |

### Version Strategy

> **Always use latest. Never pin in templates.**

```toml
[project]
dependencies = [
    "fastapi",      # uv resolves to latest
    "pydantic",
    "sqlalchemy",
]
```

- `uv add` fetches latest compatible versions
- `uv.lock` ensures reproducible builds
- `uv sync` installs exact locked versions

### 3. Use Standard Structure (src layout)

```
myapp/
├── pyproject.toml         # Single config file
├── uv.lock                # Lock file (commit this)
├── .python-version        # Python version for uv
├── src/
│   └── myapp/
│       ├── __init__.py
│       ├── __main__.py    # Entry point
│       ├── main.py        # FastAPI app
│       ├── config.py      # Pydantic Settings
│       ├── models/        # Pydantic models
│       │   ├── __init__.py
│       │   └── user.py
│       ├── services/      # Business logic
│       │   ├── __init__.py
│       │   └── user.py
│       ├── repositories/  # Data access
│       │   ├── __init__.py
│       │   └── user.py
│       ├── api/           # HTTP layer
│       │   ├── __init__.py
│       │   ├── deps.py    # Dependencies
│       │   └── routes/
│       │       ├── __init__.py
│       │       └── user.py
│       └── core/          # Shared utilities
│           ├── __init__.py
│           ├── exceptions.py
│           └── logging.py
├── tests/
│   ├── __init__.py
│   ├── conftest.py        # Fixtures
│   └── test_user.py
└── Makefile
```

---

## Architecture Layers

### main.py — FastAPI Application

```python
# src/myapp/main.py
from contextlib import asynccontextmanager

from fastapi import FastAPI

from myapp.api.routes import router
from myapp.config import settings
from myapp.core.logging import setup_logging
from myapp.db import engine


@asynccontextmanager
async def lifespan(app: FastAPI):
    # Startup
    setup_logging()
    yield
    # Shutdown
    await engine.dispose()


app = FastAPI(
    title=settings.app_name,
    lifespan=lifespan,
)

app.include_router(router, prefix="/api/v1")


@app.get("/health")
async def health():
    return {"status": "ok"}
```

### config.py — Pydantic Settings

```python
# src/myapp/config.py
from pydantic_settings import BaseSettings, SettingsConfigDict


class Settings(BaseSettings):
    model_config = SettingsConfigDict(
        env_file=".env",
        env_file_encoding="utf-8",
    )

    app_name: str = "myapp"
    debug: bool = False

    # Database
    database_url: str = "postgresql+asyncpg://localhost/myapp"

    # LiteLLM
    litellm_url: str = "http://localhost:4000"
    litellm_api_key: str = ""


settings = Settings()
```

### models/ — Pydantic Models

```python
# src/myapp/models/user.py
from datetime import datetime
from uuid import UUID

from pydantic import BaseModel, EmailStr, Field


class UserBase(BaseModel):
    email: EmailStr
    name: str = Field(min_length=2, max_length=100)


class UserCreate(UserBase):
    pass


class UserUpdate(BaseModel):
    email: EmailStr | None = None
    name: str | None = Field(default=None, min_length=2, max_length=100)


class User(UserBase):
    id: UUID
    created_at: datetime
    updated_at: datetime

    model_config = {"from_attributes": True}
```

### services/ — Business Logic

```python
# src/myapp/services/user.py
from uuid import UUID

from myapp.core.exceptions import NotFoundError, ConflictError
from myapp.models.user import User, UserCreate, UserUpdate
from myapp.repositories.user import UserRepository


class UserService:
    def __init__(self, repo: UserRepository):
        self.repo = repo

    async def get(self, id: UUID) -> User:
        user = await self.repo.get(id)
        if not user:
            raise NotFoundError("user", str(id))
        return user

    async def create(self, data: UserCreate) -> User:
        existing = await self.repo.get_by_email(data.email)
        if existing:
            raise ConflictError("email already exists")
        return await self.repo.create(data)

    async def update(self, id: UUID, data: UserUpdate) -> User:
        user = await self.get(id)
        return await self.repo.update(user, data)

    async def delete(self, id: UUID) -> None:
        user = await self.get(id)
        await self.repo.delete(user)
```

### api/routes/ — HTTP Handlers

```python
# src/myapp/api/routes/user.py
from uuid import UUID

from fastapi import APIRouter, Depends, status

from myapp.api.deps import get_user_service
from myapp.models.user import User, UserCreate, UserUpdate
from myapp.services.user import UserService

router = APIRouter(prefix="/users", tags=["users"])


@router.get("/{id}", response_model=User)
async def get_user(
    id: UUID,
    service: UserService = Depends(get_user_service),
):
    return await service.get(id)


@router.post("", response_model=User, status_code=status.HTTP_201_CREATED)
async def create_user(
    data: UserCreate,
    service: UserService = Depends(get_user_service),
):
    return await service.create(data)


@router.patch("/{id}", response_model=User)
async def update_user(
    id: UUID,
    data: UserUpdate,
    service: UserService = Depends(get_user_service),
):
    return await service.update(id, data)


@router.delete("/{id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_user(
    id: UUID,

Related in Backend & APIs