AI-Assisted Development Guide for Python with FastAPI
This guide provides Python and FastAPI-specific implementations of the language-agnostic principles in the AI Development Guide.
For a concise checklist of requirements when working with AI coding assistants on Python/FastAPI projects, see:
CLAUDE.md - Quick reference for AI tools
This stack guide covers:
- Python 3.10+ best practices with AI assistance
- FastAPI API development patterns
- Streamlit app development
- Testing with pytest
- Docker deployment
- CI/CD patterns for Python projects
project/
├── app/ # FastAPI application
│ ├── __init__.py
│ ├── main.py # FastAPI app initialization
│ ├── routes/ # API endpoints
│ ├── dependencies.py # Dependency injection
│ └── middleware.py # Custom middleware
├── services/ # Business logic
│ ├── __init__.py
│ └── processors.py # Data processing functions
├── schemas/ # Pydantic models
│ ├── __init__.py
│ └── models.py # Request/response schemas
├── streamlit_app/ # Streamlit UI (if applicable)
│ ├── __init__.py
│ └── app.py
├── tests/ # Test suite
│ ├── __init__.py
│ ├── test_routes.py
│ ├── test_services.py
│ └── conftest.py # pytest fixtures
├── docs/
│ └── API_REFERENCE.md # API documentation
├── .env.example # Environment variable template
├── .gitignore
├── Dockerfile
├── docker-compose.yml
├── pyproject.toml # Project dependencies (Poetry)
├── requirements.txt # or requirements.txt (pip)
└── README.md
/app - FastAPI routes and API layer
- Request/response handling
- Input validation (via Pydantic)
- HTTP concerns (status codes, headers)
- Route definitions
/services - Business logic
- Data processing algorithms
- External API calls
- Complex calculations
- Domain-specific logic
/schemas - Data validation
- Pydantic models for request/response
- Type definitions
- Validation rules
/streamlit_app - UI layer (if applicable)
- User interface components
- Presentation logic
- Visualization
/tests - Test suite
- Unit tests for services
- Integration tests for API endpoints
- Fixtures and test data
Required tools:
# Linting
flake8
# Formatting
black
# Type checking
mypy
# Import sorting
isortConfiguration:
.flake8:
[flake8]
max-line-length = 88
extend-ignore = E203, W503
exclude = .git,__pycache__,venv,build,distpyproject.toml (for Black and isort):
[tool.black]
line-length = 88
target-version = ['py310']
[tool.isort]
profile = "black"
line_length = 88Always use type hints for public functions:
from typing import List, Optional, Dict, Any
def process_items(
items: List[str],
config: Optional[Dict[str, Any]] = None
) -> List[Dict[str, str]]:
"""
Process items according to configuration.
Args:
items: List of item identifiers to process
config: Optional configuration dictionary
Returns:
List of processed items with metadata
Raises:
ValueError: If items list is empty
"""
if not items:
raise ValueError("Items list cannot be empty")
# [ai-assisted] Implementation
return [{"item": item, "status": "processed"} for item in items]Use Google-style docstrings:
def calculate_score(data: List[float], weights: List[float]) -> float:
"""
Calculate weighted score from input data.
This function computes a weighted sum of the input data using the
provided weights. All inputs must be non-negative.
Args:
data: List of numeric values to score
weights: List of weights (must match length of data)
Returns:
Weighted score as a float between 0.0 and 1.0
Raises:
ValueError: If data and weights have different lengths
ValueError: If any value is negative
Example:
>>> calculate_score([1.0, 2.0, 3.0], [0.5, 0.3, 0.2])
1.9
Notes:
- [ai-assisted] Algorithm based on standard weighted average
- Normalizes output to [0, 1] range
"""
passfrom fastapi import APIRouter, HTTPException, Depends, status
from typing import List
from schemas.models import ItemResponse, ItemCreate
from services.processor import process_item
from app.dependencies import get_current_user
router = APIRouter(prefix="/api/v1/items", tags=["items"])
@router.post(
"/",
response_model=ItemResponse,
status_code=status.HTTP_201_CREATED,
summary="Create new item",
description="Create a new item with validation"
)
async def create_item(
item: ItemCreate,
current_user: str = Depends(get_current_user)
) -> ItemResponse:
"""
Create a new item.
Args:
item: Item data from request body
current_user: Current authenticated user (injected)
Returns:
Created item with generated ID
Raises:
HTTPException: 400 if validation fails
HTTPException: 401 if not authenticated
"""
try:
result = await process_item(item)
return ItemResponse(**result)
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e)
)from pydantic import BaseModel, Field, validator
from typing import Optional
from datetime import datetime
class ItemBase(BaseModel):
"""Base schema for item data."""
name: str = Field(..., min_length=1, max_length=100)
description: Optional[str] = Field(None, max_length=500)
value: float = Field(..., ge=0.0)
class ItemCreate(ItemBase):
"""Schema for creating new items."""
pass
class ItemResponse(ItemBase):
"""Schema for item API responses."""
id: str
created_at: datetime
updated_at: datetime
class Config:
orm_mode = True
class ItemUpdate(BaseModel):
"""Schema for updating items (all fields optional)."""
name: Optional[str] = Field(None, min_length=1, max_length=100)
description: Optional[str] = Field(None, max_length=500)
value: Optional[float] = Field(None, ge=0.0)
@validator('value')
def value_must_be_positive(cls, v):
if v is not None and v < 0:
raise ValueError('value must be non-negative')
return v# app/dependencies.py
from fastapi import Header, HTTPException, status
from typing import Optional
async def get_current_user(
authorization: Optional[str] = Header(None)
) -> str:
"""
Extract and validate current user from auth header.
Args:
authorization: Authorization header value
Returns:
User identifier
Raises:
HTTPException: 401 if authentication fails
"""
if not authorization:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Missing authorization header"
)
# [ai-assisted] Validate token and extract user
# Simplified example - use proper auth in production
try:
token = authorization.split(" ")[1]
user_id = validate_token(token)
return user_id
except Exception:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid authentication credentials"
)# tests/test_services.py
import pytest
from services.processor import process_item
from schemas.models import ItemCreate
class TestItemProcessing:
"""Test suite for item processing."""
def test_process_valid_item(self):
"""Test processing with valid input."""
# Arrange
item = ItemCreate(name="Test", description="Test item", value=10.0)
# Act
result = process_item(item)
# Assert
assert result["name"] == "Test"
assert result["value"] == 10.0
assert "id" in result
def test_process_item_empty_name(self):
"""Test that empty name raises ValueError."""
# Arrange
item = ItemCreate(name="", description="Test", value=10.0)
# Act & Assert
with pytest.raises(ValueError, match="name cannot be empty"):
process_item(item)
@pytest.mark.parametrize("value,expected", [
(0.0, 0.0),
(10.0, 10.0),
(100.0, 100.0),
])
def test_process_item_various_values(self, value, expected):
"""Test processing with various value inputs."""
item = ItemCreate(name="Test", value=value)
result = process_item(item)
assert result["value"] == expected# tests/test_routes.py
import pytest
from fastapi.testclient import TestClient
from app.main import app
client = TestClient(app)
class TestItemRoutes:
"""Test suite for item API endpoints."""
def test_create_item_success(self):
"""Test successful item creation."""
# Arrange
payload = {
"name": "Test Item",
"description": "A test item",
"value": 42.0
}
# Act
response = client.post("/api/v1/items/", json=payload)
# Assert
assert response.status_code == 201
data = response.json()
assert data["name"] == "Test Item"
assert data["value"] == 42.0
assert "id" in data
def test_create_item_invalid_data(self):
"""Test that invalid data returns 400."""
# Arrange
payload = {
"name": "", # Invalid: empty name
"value": -1.0 # Invalid: negative value
}
# Act
response = client.post("/api/v1/items/", json=payload)
# Assert
assert response.status_code == 400
def test_get_item_not_found(self):
"""Test that missing item returns 404."""
response = client.get("/api/v1/items/nonexistent-id")
assert response.status_code == 404# tests/conftest.py
import pytest
from typing import Generator
from fastapi.testclient import TestClient
from app.main import app
@pytest.fixture
def client() -> Generator[TestClient, None, None]:
"""Provide test client for API testing."""
with TestClient(app) as test_client:
yield test_client
@pytest.fixture
def sample_item() -> dict:
"""Provide sample item data for tests."""
return {
"name": "Sample Item",
"description": "A sample item for testing",
"value": 10.0
}
@pytest.fixture
def mock_database(monkeypatch):
"""Mock database for isolated testing."""
# [ai-assisted] Database mocking implementation
passFROM python:3.10-slim
WORKDIR /app
# Install dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY . .
# Run as non-root user
RUN useradd -m appuser && chown -R appuser:appuser /app
USER appuser
# Expose port
EXPOSE 8000
# Run application
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]version: '3.8'
services:
api:
build: .
ports:
- "8000:8000"
environment:
- DATABASE_URL=${DATABASE_URL}
- SECRET_KEY=${SECRET_KEY}
env_file:
- .env
depends_on:
- db
volumes:
- ./app:/app/app
command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
db:
image: postgres:15
environment:
- POSTGRES_USER=${DB_USER}
- POSTGRES_PASSWORD=${DB_PASSWORD}
- POSTGRES_DB=${DB_NAME}
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
streamlit:
build:
context: .
dockerfile: Dockerfile.streamlit
ports:
- "8501:8501"
environment:
- API_URL=http://api:8000
depends_on:
- api
volumes:
postgres_data:# Application
APP_NAME=My FastAPI App
DEBUG=false
SECRET_KEY=your-secret-key-here
# Database
DATABASE_URL=postgresql://user:password@localhost:5432/dbname
DB_USER=user
DB_PASSWORD=password
DB_NAME=dbname
# API
API_V1_PREFIX=/api/v1
CORS_ORIGINS=http://localhost:3000,http://localhost:8501
# External Services
EXTERNAL_API_KEY=your-api-key
EXTERNAL_API_URL=https://api.example.com
# Note: Never commit actual secrets to version control
# Copy this to .env and fill in actual values-
Always run type checking:
mypy app/ services/ schemas/
-
Run linting and formatting:
black . isort . flake8
-
Run tests:
pytest tests/ -v --cov=app --cov=services
-
Verify Pydantic models:
- Check that validation rules are appropriate
- Test with invalid data to ensure errors are raised
- Verify response_model matches actual return type
1. Incorrect async/await usage:
# ❌ BAD - AI might forget await
async def get_data():
result = fetch_from_db() # Missing await!
return result
# ✅ GOOD
async def get_data():
result = await fetch_from_db()
return result2. Pydantic validation bypassing:
# ❌ BAD - AI might use dict directly
def process(data: dict):
return data["field"]
# ✅ GOOD - Use Pydantic for validation
def process(data: ItemSchema):
return data.field3. Missing exception handling:
# ❌ BAD - AI might not handle exceptions
@router.get("/items/{item_id}")
async def get_item(item_id: str):
return get_item_from_db(item_id) # What if not found?
# ✅ GOOD - Proper error handling
@router.get("/items/{item_id}")
async def get_item(item_id: str):
try:
item = get_item_from_db(item_id)
if not item:
raise HTTPException(status_code=404, detail="Item not found")
return item
except Exception as e:
logger.error(f"Error fetching item {item_id}: {e}")
raise HTTPException(status_code=500, detail="Internal server error")name: Python CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install -r requirements-dev.txt
- name: Run linting
run: |
flake8 app/ services/ schemas/
black --check .
isort --check-only .
- name: Run type checking
run: mypy app/ services/ schemas/
- name: Run tests
run: pytest tests/ -v --cov=app --cov=services --cov-report=xml
- name: Upload coverage
uses: codecov/codecov-action@v3- AI Development Guide - Language-agnostic principles
- README - Main repository overview
Last Updated: 2025-11-10