Refactor high-complexity React components in Dify frontend. Use when `pnpm analyze-component...
npx skills add majiayu000/claude-arsenal --skill "python-project"
Install specific skill from multi-skill repository
# Description
Modern Python project architecture guide for 2025. Use when creating Python projects (APIs, CLI, data pipelines). Covers uv, Ruff, Pydantic, FastAPI, and async patterns.
# SKILL.md
name: python-project
description: Modern Python project architecture guide for 2025. Use when creating Python projects (APIs, CLI, data pipelines). Covers uv, Ruff, Pydantic, FastAPI, and async patterns.
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.
# β 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.
# 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
# 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.
[project]
dependencies = [
"fastapi", # uv resolves to latest
"pydantic",
"sqlalchemy",
]
uv addfetches latest compatible versionsuv.lockensures reproducible buildsuv syncinstalls 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
# 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
# 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
# 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
# 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
# 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,
service: UserService = Depends(get_user_service),
):
await service.delete(id)
core/exceptions.py β Custom Exceptions
# src/myapp/core/exceptions.py
from fastapi import HTTPException, status
class AppError(Exception):
"""Base application error."""
def __init__(self, message: str, code: str):
self.message = message
self.code = code
super().__init__(message)
class NotFoundError(AppError):
def __init__(self, resource: str, id: str):
super().__init__(f"{resource} not found: {id}", "NOT_FOUND")
class ConflictError(AppError):
def __init__(self, message: str):
super().__init__(message, "CONFLICT")
class ValidationError(AppError):
def __init__(self, message: str):
super().__init__(message, "VALIDATION_ERROR")
# FastAPI exception handler
def app_error_to_http(error: AppError) -> HTTPException:
status_map = {
"NOT_FOUND": status.HTTP_404_NOT_FOUND,
"CONFLICT": status.HTTP_409_CONFLICT,
"VALIDATION_ERROR": status.HTTP_400_BAD_REQUEST,
}
return HTTPException(
status_code=status_map.get(error.code, status.HTTP_500_INTERNAL_SERVER_ERROR),
detail={"message": error.message, "code": error.code},
)
pyproject.toml
[project]
name = "myapp"
version = "0.1.0"
description = "My application"
requires-python = ">=3.12"
dependencies = [
"fastapi",
"uvicorn[standard]",
"pydantic",
"pydantic-settings",
"sqlalchemy[asyncio]",
"asyncpg",
"httpx",
"structlog",
]
[tool.uv]
dev-dependencies = [
"pytest",
"pytest-asyncio",
"pytest-cov",
"ruff",
"mypy",
]
[tool.ruff]
line-length = 100
target-version = "py312"
[tool.ruff.lint]
select = [
"E", # pycodestyle errors
"F", # pyflakes
"I", # isort
"UP", # pyupgrade
"B", # flake8-bugbear
"SIM", # flake8-simplify
]
[tool.ruff.lint.isort]
known-first-party = ["myapp"]
[tool.mypy]
strict = true
python_version = "3.12"
[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]
Testing
# tests/conftest.py
import pytest
from httpx import ASGITransport, AsyncClient
from myapp.main import app
@pytest.fixture
async def client():
async with AsyncClient(
transport=ASGITransport(app=app),
base_url="http://test",
) as client:
yield client
# tests/test_user.py
import pytest
@pytest.mark.asyncio
async def test_create_user(client):
response = await client.post(
"/api/v1/users",
json={"email": "[email protected]", "name": "Test User"},
)
assert response.status_code == 201
data = response.json()
assert data["email"] == "[email protected]"
@pytest.mark.asyncio
async def test_get_user_not_found(client):
response = await client.get("/api/v1/users/00000000-0000-0000-0000-000000000000")
assert response.status_code == 404
Makefile
.PHONY: dev test lint fmt check clean
# Run development server
dev:
uv run uvicorn myapp.main:app --reload
# Run tests
test:
uv run pytest
# Run tests with coverage
test-cov:
uv run pytest --cov=myapp --cov-report=html
# Lint code
lint:
uv run ruff check src tests
# Format code
fmt:
uv run ruff format src tests
uv run ruff check --fix src tests
# Type check
typecheck:
uv run mypy src
# Run all checks
check: fmt lint typecheck test
@echo "All checks passed!"
# Clean
clean:
rm -rf .pytest_cache .mypy_cache .ruff_cache htmlcov .coverage
find . -type d -name __pycache__ -exec rm -rf {} +
# Sync dependencies
sync:
uv sync
# Upgrade dependencies
upgrade:
uv lock --upgrade
uv sync
Checklist
## Project Setup
- [ ] uv initialized with pyproject.toml
- [ ] .python-version set (3.12+)
- [ ] src/ layout structure
- [ ] Ruff configured
- [ ] mypy strict mode
## Architecture
- [ ] Pydantic models for validation
- [ ] Services for business logic
- [ ] Repositories for data access
- [ ] Custom exceptions
- [ ] Dependency injection
## Quality
- [ ] pytest with pytest-asyncio
- [ ] Type hints everywhere
- [ ] Structured logging
- [ ] Error handling middleware
## CI
- [ ] ruff check
- [ ] ruff format --check
- [ ] mypy
- [ ] pytest
See Also
- reference/architecture.md β Project structure patterns
- reference/tech-stack.md β Tool comparisons
- reference/patterns.md β Python design patterns
# Supported AI Coding Agents
This skill is compatible with the SKILL.md standard and works with all major AI coding agents:
Learn more about the SKILL.md standard and how to use these skills with your preferred AI coding agent.