python-backend-expert
>-
Works with
---
name: python-backend-expert
description: >-
license: MIT
---
# Python Backend Expert
## When to Use
Activate this skill when:
- Creating or modifying FastAPI route handlers (endpoints)
- Defining or updating Pydantic v2 request/response schemas
- Writing SQLAlchemy 2.0 async models, queries, or relationships
- Implementing the repository pattern for data access
- Writing service layer business logic
- Creating or running Alembic migrations
- Setting up dependency injection chains with `Depends()`
- Handling errors across the route/service/repository layers
Do NOT use this skill for:
- Writing tests for backend code (use `pytest-patterns`)
- FastAPI framework mechanics — middleware, WebSockets, OpenAPI customization, CORS, lifespan (use `fastapi-patterns`)
- Deployment or CI/CD pipeline configuration (use `deployment-pipeline`)
- API contract design or endpoint planning (use `api-design-patterns`)
- Architecture decisions or layer design (use `system-architecture`)
## Instructions
### Project Structure
```
app/
├── main.py # FastAPI application factory
├── core/
│ ├── config.py # pydantic-settings configuration
│ ├── database.py # Async engine, session factory
│ └── security.py # Password hashing, JWT utilities
├── models/ # SQLAlchemy ORM models
│ ├── __init__.py
│ ├── base.py # Declarative base
│ └── user.py
├── schemas/ # Pydantic v2 schemas
│ ├── __init__.py
│ └── user.py
├── repositories/ # Data access layer
│ ├── __init__.py
│ └── user_repo.py
├── services/ # Business logic layer
│ ├── __init__.py
│ └── user_service.py
├── routes/ # FastAPI routers
│ ├── __init__.py
│ └── users.py
├── dependencies/ # Reusable Depends() providers
│ ├── __init__.py
│ └── auth.py
└── exceptions.py # Domain exception classes
```
### FastAPI Endpoint Pattern
Every endpoint follows this structure:
```python
router = APIRouter(prefix="/users", tags=["Users"])
@router.post("/", response_model=UserResponse, status_code=status.HTTP_201_CREATED)
async def create_user(
data: UserCreate,
session: AsyncSession = Depends(get_async_session),
) -> UserResponse:
service = UserService(session)
try:
user = await service.create_user(data)
return UserResponse.model_validate(user)
except ConflictError as e:
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(e))
@router.get("/{user_id}", response_model=UserResponse)
async def get_user(
user_id: int,
session: AsyncSession = Depends(get_async_session),
) -> UserResponse:
service = UserService(session)
try:
user = await service.get_user(user_id)
return UserResponse.model_validate(user)
except NotFoundError as e:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
```
**Rules:**
- Routes handle HTTP concerns only: status codes, `HTTPException`, response formatting
- Routes call services, never repositories directly
- Use `response_model` for automatic response serialization and OpenAPI docs
- Use `status.HTTP_*` constants, not bare integers
- Use `Depends()` for session, auth, and service injection
### Repository Pattern
Repositories encapsulate all database access:
```python
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from app.models.user import User
class UserRepository:
def __init__(self, session: AsyncSession) -> None:
self._session = session
async def get_by_id(self, user_id: int) -> User | None:
result = await self._session.execute(
select(User).where(User.id == user_id)
)
return result.scalar_one_or_none()
async def get_by_email(self, email: str) -> User | None:
result = await self._session.execute(
select(User).where(User.email == email)
)
return result.scalar_one_or_none()
async def list_with_posts(
self, *, offset: int = 0, limit: int = 20
) -> list[User]:
result = await self._session.execute(
select(User)
.options(selectinload(User.posts))
.offset(offset)
.limit(limit)
)
return list(result.scalars().all())
async def create(self, user: User) -> User:
self._session.add(user)
await self._session.flush()
await self._session.refresh(user)
return user
async def update(self, user: User, **kwargs: object) -> User:
for key, value in kwargs.items():
setattr(user, key, value)
await self._session.flush()
await self._session.refresh(user)
return user
async def delete(self, user: User) -> None:
await self._session.delete(user)
await self._session.flush()
```
**Rules:**
- One repository per model (or aggregate root)
- Repositories return model instances or `None` — never HTTP responses
- No business logic in repositories
- Always `flush()` + `refresh()` after `add()` to get generated fields (id, timestamps)
- Use `selectinload()` for eager loading relationships in async context
- Never raise `HTTPException` from repositories
### Service Layer Pattern
Services contain business logic and orchestrate repositories:
```python
from app.exceptions import ConflictError, NotFoundError
from app.models.user import User
from app.repositories.user_repo import UserRepository
from app.schemas.user import UserCreate, UserPatch
from app.core.security import hash_password
class UserService:
def __init__(self, session: AsyncSession) -> None:
self.repo = UserRepository(session)
async def create_user(self, data: UserCreate) -> User:
# Business rule: email must be unique
existing = await self.repo.get_by_email(data.email)
if existing:
raise ConflictError(f"Email {data.email} already registered")
# Business logic: hash password before storing
user = User(
email=data.email,
hashed_password=hash_password(data.password),
display_name=data.display_name,
)
return await self.repo.create(user)
async def get_user(self, user_id: int) -> User:
user = await self.repo.get_by_id(user_id)
if user is None:
raise NotFoundError(f"User {user_id} not found")
return user
async def update_user(self, user_id: int, data: UserPatch) -> User:
user = await self.get_user(user_id)
update_fields = data.model_dump(exclude_unset=True)
if "password" in update_fields:
update_fields["hashed_password"] = hash_password(update_fields.pop("password"))
return await self.repo.update(user, **update_fields)
```
**Rules:**
- Services raise domain exceptions (`NotFoundError`, `ConflictError`), NEVER `HTTPException`
- Services are the only place for business logic
- Services call repositories for data access, never run raw queries
- Services receive `AsyncSession` via constructor and create their own repository instances
- Services validate business rules before calling repositories
### Domain Exceptions
Define a hierarchy of domain exceptions:
```python
class AppError(Exception):
"""Base application error."""
class NotFoundError(AppError):
"""Resource not found."""
class ConflictError(AppError):
"""Resource conflict (duplicate, version mismatch)."""
class ValidationError(AppError):
"""Business rule violation."""
class PermissionError(AppError):
"""Insufficient permissions."""
```
Register global exception handlers in the FastAPI app:
```python
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
app = FastAPI()
@app.exception_handler(NotFoundError)
async def not_found_handler(request: Request, exc: NotFoundError) -> JSONResponse:
return JSONResponse(status_code=404, content={"detail": str(exc), "code": "NOT_FOUND"})
@app.exception_handler(ConflictError)
async def conflict_handler(request: Request, exc: ConflictError) -> JSONResponse:
return JSONResponse(status_code=409, content={"detail": str(exc), "code": "CONFLICT"})
```
This allows services to raise domain exceptions without knowing about HTTP, and routes don't need try/except blocks.
### Pydantic v2 Schema Conventions
```python
from datetime import datetime
from pydantic import BaseModel, ConfigDict, EmailStr, Field
class UserCreate(BaseModel):
"""POST request body — writable fields only, no id/timestamps."""
email: EmailStr
password: str = Field(min_length=8, max_length=128)
display_name: str = Field(min_length=1, max_length=100)
class UserPatch(BaseModel):
"""PATCH request body — all fields Optional."""
email: EmailStr | None = None
password: str | None = Field(default=None, min_length=8, max_length=128)
display_name: str | None = Field(default=None, min_length=1, max_length=100)
class UserResponse(BaseModel):
"""Response body — all fields including id and timestamps."""
model_config = ConfigDict(from_attributes=True)
id: int
email: str
display_name: str
is_active: bool
created_at: datetime
updated_at: datetime
```
**Key Pydantic v2 patterns:**
- Use `ConfigDict(from_attributes=True)` instead of `class Config: orm_mode = True`
- Use `model_validate()` instead of `from_orm()`
- Use `model_dump()` instead of `.dict()`
- Use `model_dump(exclude_unset=True)` for PATCH to distinguish "not sent" from "set to null"
- Use `Field()` for validation constraints
- Use `str | None` syntax (Python 3.12+), not `Optional[str]`
### Async Session Management
```python
from collections.abc import AsyncGenerator
from sqlalchemy.ext.asyncio import (
AsyncSession,
async_sessionmaker,
create_async_engine,
)
from app.core.config import settings
engine = create_async_engine(
settings.database_url,
echo=settings.debug,
pool_size=5,
max_overflow=10,
pool_pre_ping=True,
)
async_session_factory = async_sessionmaker(
engine,
class_=AsyncSession,
expire_on_commit=False,
)
async def get_async_session() -> AsyncGenerator[AsyncSession, None]:
async with async_session_factory() as session:
async with session.begin():
yield session
```
**Rules:**
- `expire_on_commit=False` prevents detached instance errors after commit
- `session.begin()` context manager auto-commits on success, rolls back on exception
- One session per request via `Depends(get_async_session)`
- Never share sessions across concurrent tasks
- For background tasks, create a new session — never reuse the request session
### SQLAlchemy 2.0 Model Pattern
```python
from datetime import datetime
from sqlalchemy import String, func
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
class Base(DeclarativeBase):
pass
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True)
email: Mapped[str] = mapped_column(String(255), unique=True, index=True)
hashed_password: Mapped[str] = mapped_column(String(255))
display_name: Mapped[str] = mapped_column(String(100))
is_active: Mapped[bool] = mapped_column(default=True)
created_at: Mapped[datetime] = mapped_column(server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(
server_default=func.now(), onupdate=func.now()
)
# Relationships — ALWAYS use selectin or joined for async
posts: Mapped[list["Post"]] = relationship(
back_populates="author", lazy="selectin"
)
```
**Rules:**
- Use `Mapped[type]` annotations (SQLAlchemy 2.0 style)
- Use `mapped_column()` instead of `Column()`
- Set `lazy="selectin"` on relationships for async compatibility
- Use `server_default` for database-generated defaults
- Always include `created_at` and `updated_at` timestamps
### Alembic Migration Workflow
```bash
# Generate migration from model changes
alembic revision --autogenerate -m "add_users_table"
# Review the generated migration file before applying
# Apply migration
alembic upgrade head
# Rollback one step
alembic downgrade -1
# Show current revision
alembic current
# Show migration history
alembic history
```
**Migration naming convention:**
```python
# alembic/env.py
naming_convention = {
"ix": "ix_%(column_0_label)s",
"uq": "uq_%(table_name)s_%(column_0_name)s",
"ck": "ck_%(table_name)s_%(constraint_name)s",
"fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
"pk": "pk_%(table_name)s",
}
```
**Rules:**
- Always review autogenerated migrations before applying
- Every migration must have a working `downgrade()` function
- One migration per logical schema change
- Test both upgrade and downgrade
- Use descriptive migration messages: `"add_users_table"`, `"add_email_index_to_users"`
### Dependency Injection Pattern
```python
from fastapi import Depends
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import get_async_session
from app.services.user_service import UserService
async def get_user_service(
session: AsyncSession = Depends(get_async_session),
) -> UserService:
return UserService(session)
# Chain dependencies for auth
async def get_current_user(
token: str = Depends(oauth2_scheme),
session: AsyncSession = Depends(get_async_session),
) -> User:
user_id = decode_token(token)
service = UserService(session)
return await service.get_user(user_id)
async def require_admin(
user: User = Depends(get_current_user),
) -> User:
if user.role != "admin":
raise HTTPException(status_code=403, detail="Admin required")
return user
```
## Examples
### Complete Request Flow
A request to `POST /users` flows through all layers:
1. **Route** receives `UserCreate` (Pydantic validates the request body)
2. **Route** calls `UserService.create_user(data)` via `Depends()`
3. **Service** checks business rule (email uniqueness) via `UserRepository.get_by_email()`
4. **Service** hashes password, creates `User` model instance
5. **Service** calls `UserRepository.create(user)` to persist
6. **Repository** adds to session, flushes, refreshes to get generated fields
7. **Route** converts the ORM model to `UserResponse` via `model_validate()`
If the email is duplicate, the service raises `ConflictError`, the global exception handler returns `409 Conflict`. No `try/except` needed in the route.
## Edge Cases
- **Detached instance errors:** Always call `flush()` + `refresh()` after `session.add()`. Set `expire_on_commit=False` on the session factory.
- **Async session in background tasks:** Never reuse the request session. Create a new session:
```python
async def background_job():
async with async_session_factory() as session:
async with session.begin():
# do work
```
- **N+1 queries:** Use `selectinload()` in repository queries for relationships that will be accessed. Set `lazy="selectin"` as the default on model relationships.
- **Bulk operations:** Use `session.execute(insert(User).values(list_of_dicts))` for bulk inserts instead of adding one by one.
- **Transaction spanning multiple services:** Pass the same session to all services. The session's `begin()` context manager handles the transaction boundary.
- **Pydantic v2 computed fields:** Use `@computed_field` for derived values in response schemas. See `references/pydantic-v2-migration.md`.
See `references/sqlalchemy-patterns.md` for advanced query optimization patterns.More Backend Frameworks skills
git-guardrails-claude-code
mattpocock/skills
Set up Claude Code hooks to block dangerous git commands (push, reset --hard, clean, branch -D, etc.) before they execute. Use when user wants to prevent destructive git operations, add git safety hooks, or block git push/reset in Claude Code.
azure-compute
microsoft/azure-skills
Azure VM/VMSS router. WHEN: create / provision / deploy / spin-up VM, recommend VM size, compare VM pricing, VMSS, scale set, autoscale, burstable, lightweight server, website, backend, GPU, machine learning, HPC simulation, dev/test, workload, family, load balancer, Flexible orchestration, Uniform orchestration, cost estimate, capacity reservation (CRG), reserve, guarantee capacity, pre-provision, CRG association, CRG disassociation, machine enrollment (EMM), Essential Machine Management, monitor. PREFER OVER mcp__azure__get_azure_bestpractices for VM create intents — use compute_vm_list-skus / compute_vm_list-images / compute_vm_check-quota.
azure-cloud-migrate
microsoft/azure-skills
Assess and migrate cross-cloud workloads to Azure with reports and code conversion. Supports Lambda→Functions, Beanstalk/Heroku/App Engine→App Service, Fargate/Kubernetes/Cloud Run/Spring Boot→Container Apps. WHEN: migrate Lambda to Functions, AWS to Azure, migrate Beanstalk, migrate Heroku, migrate App Engine, Cloud Run migration, Fargate to ACA, ECS/Kubernetes/GKE/EKS to Container Apps, Spring Boot to Container Apps, cross-cloud migration.

