Skillquality 0.46
python-project
Modern Python project architecture guide for 2025. Use when creating Python projects (APIs, CLI, data pipelines). Covers uv, Ruff, Pydantic, FastAPI, and async patterns.
What it 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.
# ❌ 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": "test@example.com", "name": "Test User"},
)
assert response.status_code == 201
data = response.json()
assert data["email"] == "test@example.com"
@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
Capabilities
skillsource-majiayu000skill-python-projecttopic-agent-skillstopic-ai-agentstopic-ai-coding-assistanttopic-automationtopic-claudetopic-claude-codetopic-code-reviewtopic-developer-toolstopic-devopstopic-productivitytopic-prompt-engineeringtopic-python
Install
Installnpx skills add majiayu000/claude-arsenal
Transportskills-sh
Protocolskill
Quality
0.46/ 1.00
deterministic score 0.46 from registry signals: · indexed on github topic:agent-skills · 29 github stars · SKILL.md body (12,519 chars)
Provenance
Indexed fromgithub
Enriched2026-05-01 07:01:15Z · deterministic:skill-github:v1 · v1
First seen2026-04-18
Last seen2026-05-01