Python Dev
Purpose
Take a Python backend feature from requirements to PR on the secondary Python stack:
FastAPI + SQLAlchemy 2.0 + Alembic + Celery + Redis (primary path), or Django 5.x + DRF
where the project already made that call. The conventions live in the std-* skills —
this skill is the workflow that applies them in order.
Framework Choice
One decision, made once per service, not per feature: FastAPI is the default; Django + DRF
only when an admin UI or batteries-included CRUD outweighs FastAPI's leanness. The decision
tree is owned by std-python — do not relitigate it here.
Build Protocol (FastAPI — primary path)
1. Scaffold
- Confirm the house layout exists; create only what is missing:
app/main.py(create_app()factory + lifespan),app/core/config.py(pydantic-settings),app/api/routers/,app/schemas/,app/models/(a package, one module per aggregate — never a singlemodels.py),app/services/,app/db/session.py,alembic/ - New service:
uv init, commituv.lock, all tool config inpyproject.toml - Layout and wiring rules →
std-fastapi; layering and typing →std-python
2. Model + Migration
- Add the SQLAlchemy model:
Mapped[],mapped_column(), explicitindex=Trueon FKs and filtered columns — SQLAlchemy does not index FKs for you uv run alembic revision --autogenerate -m "add_orders", then read the generated migration — autogenerate misses server defaults, constraint names, and data moves- Verify
downgrade()actually reverses; split schema change from backfill - Migration safety (locking, backfills, concurrent indexes) →
std-database
3. Schemas
- Three pydantic models per resource in
app/schemas/<resource>.py:XCreate,XUpdate(optional fields, applied withexclude_unset),XRead(ConfigDict(from_attributes=True)) - The schema module mirrors the router module — one resource, one pair of files
4. Service
- One service per use case in
app/services/, singleexecute()entry point - Return a typed result object (dataclass or pydantic), never a bare dict
- Raise domain exceptions — no
HTTPExceptionhere; the service must stay callable from Celery tasks and scripts - Inject dependencies through
__init__againstProtocols →std-python
5. Router
- One
APIRouterper resource with explicitprefix/tags, included fromcreate_app() - Every route declares
response_model=andstatus_code=explicitly (201 create, 204 delete) - Parse → authorize → one service call →
XRead.model_validate(...)— no logic in routes - Domain exceptions become the house envelope in ONE app-level exception handler —
envelope shape →
std-api-design
6. Celery Task (when the feature has async work)
- Anything slow or retryable — emails, ML inference, exports, third-party calls — is a Celery task, not request work
- Idempotent, takes IDs not objects, re-fetches inside the task; explicit retry limits,
acks_late=True, JSON serializer; queuesdefault/critical/low_priority - Full task rules →
std-fastapi(Background Jobs)
7. Tests
httpx.AsyncClientwithASGITransport(app=app)— in-process, no live serverapp.dependency_overridesswapsget_session(rollback-per-test) andget_current_user(stub) — never patch auth internalstests/mirrors the package, one test module per source module; AAA and coverage targets →std-testing
8. Query-Performance Pass
- Before opening the PR, walk every list endpoint: eager-load with
selectinload()/joinedload()and pin a query-count assertion in a test — an eager load without a pinned count silently regresses - Keyset pagination for deep lists;
EXPLAINanything filtering a large table - The full checklist →
std-python-performance
9. Run the Ladder
uv run ruff format && uv run ruff check && uv run mypy && uv run pytest
- Fix everything the ladder reports — no
--no-verify, no bare# type: ignorewithout an error code - Then branch, conventional commit, and PR per
std-git-workflow
Django + DRF Variant
Same protocol, different spellings — depth in std-django:
- Scaffold
config/(split settings) + one app per bounded context underapps/ - Model with
Meta.constraintsand a custom QuerySet manager;makemigrations --name add_order_status, verify the reverse,makemigrations --checkin CI - DRF serializers validate;
services.pymutates insidetransaction.atomic - Thin ViewSet + router registration; django-filter
FilterSet; global paginator and the envelope via a customEXCEPTION_HANDLER - Celery task: identical idempotency and queue rules as the FastAPI path
- Tests: pytest-django +
APIClient, explicit@pytest.mark.django_db, factory_boy - Performance pass:
select_related/prefetch_related+assertNumQueries - The same ladder:
uv run ruff format && uv run ruff check && uv run mypy && uv run pytest
Owned elsewhere — do not duplicate
This skill sequences the work. These own the rules:
std-python— layout, typing, layering, error hierarchy, toolchainstd-fastapi/std-django— framework wiring, DI, async discipline, Celerystd-python-performance— N+1, eager loading, keyset pagination, poolingstd-python-ai-ml— ML-serving features: model loading, inference endpoints, LLM callsstd-api-design— the error envelope (error/code/status/details/requestId) and pagination response formatstd-database— migration safety, locking, indexing depthstd-testing— AAA structure, coverage targetsstd-security— auth (pyjwt+argon2-cffi, neverpython-jose/passlib), secrets
Done means
- Ladder green: ruff format + check clean,
mypy --strictclean, pytest passing - List endpoints eager-load with a pinned query-count assertion — no N+1
- Every route has explicit
response_modelandstatus_code; errors conform to the house envelope - Migration reversible and read by a human; schema change split from backfill
- Celery tasks idempotent, ID-passing, bounded retries
- Conventional commit, branch naming, and docs/CHANGELOG per
std-git-workflow