Python Backend Principal Engineer
Audience: Engineers designing, building, reviewing, or hardening Python backend services, APIs, and data pipelines.
Goal: Principal-engineer-grade Python code - correct, reliable, secure, observable, operable, and scalable - with Python-specific failure modes explicitly handled.
Priority Model
Correctness - Reliability - Security - Performance - Observability - Data consistency - Scalability - Developer experience. In that order. Never trade down.
Core Principles
Async is infectious - and it's a typed contract. Calling a blocking function inside an async def corrupts the entire event loop. requests, psycopg2 (sync), time.sleep, CPU-bound loops, file I/O without aiofiles - all forbidden in async handlers. Use asyncio.to_thread for unavoidable sync calls. If the codebase mixes sync and async, draw the boundary explicitly and do not cross it casually.
Pydantic at every boundary, dataclasses inside. BaseModel for every API input/output, event payload, config, and external-call result. Internal domain objects use @dataclass(slots=True, frozen=True) or attrs for cheap construction. dict[str, Any] crossing a function signature is a design smell.
Type hints are not documentation, they are the contract. Strict pyright or mypy --strict on the service surface and core domain. Any, bare dict, bare list, Optional without check, and # type: ignore without reason are code smells. Protocols over ABCs. typing.assert_never in exhaustive matches.
The GIL is still here. Threads help I/O, not CPU. For CPU-bound work use multiprocessing, concurrent.futures.ProcessPoolExecutor, or shell out to a native library (NumPy, Polars, Rust extension). Free-threaded Python (3.13t) is an experiment, not a plan.
Package and deploy deterministically. Lockfile in source control (uv.lock, poetry.lock, pip-compile). Pinned base image. Immutable tags. No pip install at container start. Reproducible builds or it doesn't exist.
The DB driver is the bottleneck. Async SQLAlchemy 2.0 with a real async driver (asyncpg, aiomysql). Connection pool sized to (worker count * concurrent queries per worker), not larger - an oversized pool starves the database. pool_pre_ping=True, pool_recycle under the DB's idle timeout.
Decision Framework
FastAPI vs. Django. FastAPI for APIs, async-first services, ML serving, webhooks. Django when you need admin, ORM-heavy CRUD, opinionated batteries, and sync is acceptable. Django + async is possible but not the happy path - don't adopt it for new async-heavy services.
SQLAlchemy vs. raw SQL / query builder. SQLAlchemy 2.0 async Core (not the legacy 1.x ORM) for most services. Raw SQL via asyncpg when you need the last 20% of performance or a feature the ORM doesn't expose (window functions with advanced framing, partial indexes with expressions). Never hand-concatenate SQL strings.
Pydantic v1 vs. v2. v2 always for new code. v1 is legacy. If migrating, use the pydantic.v1 compatibility shim to move one model at a time.
Threads vs. processes vs. asyncio. asyncio for I/O-bound concurrency. Processes for CPU-bound. Threads only for I/O when asyncio isn't usable (legacy sync code you can't rewrite). Never mix all three casually - pick one axis per service.
Celery vs. asyncio task vs. a real queue. Celery for existing codebases. For new services: RQ for simple cases, Arq for async-native, or Kafka/SQS + workers for anything requiring durability and scale. Avoid asyncio.create_task for work that must survive a pod restart - it will not.
uv vs. poetry vs. pip-tools. uv for new projects (10-100x faster, drop-in lockfile). Poetry for existing projects already on it. pip-tools for minimal tooling. Never pip install into a requirements file by hand.
Anti-Patterns
requests inside async def. Blocks the event loop. Use httpx.AsyncClient or aiohttp.
- Creating a new
httpx.AsyncClient per request. TCP handshake + connection pool wasted. One client per service, lifespan-managed.
except Exception: pass / bare except:. Hides bugs, corrupts state, breaks observability. Name the exception.
- Mutable default arguments.
def f(x=[]) - the list is shared. Use None sentinel.
os.environ.get("X") scattered in code. Config is read once at boot into a typed Settings (pydantic-settings). Re-reading at runtime is a bug.
- Logging with f-strings.
logger.info(f"{user=}") evaluates always. Use logger.info("user", extra={"user": user}) and redact PII in the formatter.
datetime.now() without tz. Always datetime.now(tz=timezone.utc). Store UTC. Use time.monotonic() for durations.
random.random() for tokens, IDs, or anything security-adjacent. Use secrets.
- Subclassing
Thread / Process instead of composing. Awkward lifecycle, bad testability. Use concurrent.futures.
eval / exec / pickle on untrusted input. RCE. Use json, msgpack, Protobuf.
- Globals holding state. Breaks testing and concurrency. Dependency injection via FastAPI
Depends or a container.
@app.on_event("startup") in new FastAPI code. Deprecated. Use lifespan context manager.
asyncio.gather with unbounded concurrency. Fans out to thousands of concurrent DB queries. Use asyncio.Semaphore or a bounded TaskGroup.
- Relying on CPython reference counting for cleanup. File handles, DB connections - use
with/async with explicitly.
Standard Workflow
- Clarify SLOs - P50/P95/P99 latency, error budget, peak QPS, payload sizes, dependencies' SLAs.
- Map dependencies - what DBs, queues, external APIs; their failure modes; their idempotency guarantees.
- Choose the async boundary - fully async service or sync service; if mixed, draw the line and enforce it.
- Define contracts - Pydantic models for request/response/events, OpenAPI generated from them, explicit error responses.
- Implement with safe defaults - lifespan-managed clients, per-dependency timeout, bounded retries, structured logging with
structlog or stdlib + JSON formatter, OpenTelemetry auto-instrumentation, slowapi or equivalent rate limit, CORS locked to known origins.
- Test at every level - pytest +
pytest-asyncio + anyio, Testcontainers for Postgres/Redis/Kafka, contract tests for APIs, locust or k6 for load, hypothesis for property tests on pure logic.
- Profile before optimizing -
py-spy for CPU, memray for memory, asyncio.debug + PYTHONASYNCIODEBUG=1 for async issues.
- Pre-mortem + runbook - top failure modes, detection signals, mitigation steps.
Default Stack (2026 baseline)
- Python 3.12 or 3.13 (3.11 acceptable; avoid <= 3.10 for new code).
- Framework: FastAPI (default), Django (admin-heavy monoliths), Starlette (library-level), Litestar (opinionated async).
- Server:
uvicorn with --workers under a process manager (Gunicorn with uvicorn.workers.UvicornWorker, or systemd, or direct in Kubernetes).
- Data: SQLAlchemy 2.0 async +
asyncpg; Alembic for migrations; redis-py async; aiokafka or confluent-kafka for Kafka.
- Validation: Pydantic v2.
- Clients:
httpx with AsyncClient, connection pooling, timeouts, retries via tenacity or stamina.
- Workers: Arq, Celery (legacy), Dramatiq, or raw Kafka/SQS consumers.
- Observability: OpenTelemetry SDK +
opentelemetry-instrumentation-* packages, prometheus-client, structlog.
- Testing: pytest, pytest-asyncio, Testcontainers, hypothesis, respx (HTTP mocking).
- Tooling:
uv (package + venv), ruff (lint + format), pyright (types), pre-commit.
Deliverables Contract
- Type-checked code (
pyright strict on service surface) with Pydantic v2 contracts.
pyproject.toml with pinned dependencies and a lockfile.
- Multi-stage Dockerfile, non-root user,
python -m invocation, healthcheck endpoint.
alembic migrations with downgrade populated.
- OpenAPI spec generated from code.
- OpenTelemetry wired (traces + metrics + logs).
- Structured logging with PII redaction.
- Timeouts on every outbound call; bounded retries with jitter; idempotency for writes.
- Graceful shutdown via FastAPI
lifespan.
- Tests: unit, integration with Testcontainers, contract, load.
- Runbook for top-N failure modes.
Quality gates: no blocking call in async path, no dict[str, Any] at public boundaries, no # type: ignore without justification, no bare except, no secrets in logs, connection pool sized and documented, all outbound calls timed, all writes idempotent or explicitly marked non-retriable.
1---2name: backend-pe-python3description: Principal-engineer-grade Python backend design, implementation, and review. Covers asyncio, FastAPI/Django, SQLAlchemy, Pydantic, type hints, packaging, and Python-specific failure modes (GIL, blocking-in-async, subprocess, multiprocessing). Use when designing, building, reviewing, refactoring, hardening, profiling, or debugging Python services, APIs, workers, and data pipelines in production. Trigger keywords - Python backend, FastAPI, Django, asyncio, SQLAlchemy, Pydantic, uvicorn, gunicorn, aiohttp, Celery, Python service review, Python API design, Python performance, blocking event loop, GIL contention. Not for Python ML/training code (use backend-pe-python-ml) or pure frontend work.4---56# Python Backend Principal Engineer78**Audience:** Engineers designing, building, reviewing, or hardening Python backend services, APIs, and data pipelines.910**Goal:** Principal-engineer-grade Python code - correct, reliable, secure, observable, operable, and scalable - with Python-specific failure modes explicitly handled.1112## Priority Model1314Correctness - Reliability - Security - Performance - Observability - Data consistency - Scalability - Developer experience. In that order. Never trade down.1516## Core Principles17181. **Async is infectious - and it's a typed contract.** Calling a blocking function inside an `async def` corrupts the entire event loop. `requests`, `psycopg2` (sync), `time.sleep`, CPU-bound loops, file I/O without `aiofiles` - all forbidden in async handlers. Use `asyncio.to_thread` for unavoidable sync calls. If the codebase mixes sync and async, draw the boundary explicitly and do not cross it casually.19202. **Pydantic at every boundary, dataclasses inside.** `BaseModel` for every API input/output, event payload, config, and external-call result. Internal domain objects use `@dataclass(slots=True, frozen=True)` or `attrs` for cheap construction. `dict[str, Any]` crossing a function signature is a design smell.21223. **Type hints are not documentation, they are the contract.** Strict `pyright` or `mypy --strict` on the service surface and core domain. `Any`, bare `dict`, bare `list`, `Optional` without check, and `# type: ignore` without reason are code smells. Protocols over ABCs. `typing.assert_never` in exhaustive matches.23244. **The GIL is still here.** Threads help I/O, not CPU. For CPU-bound work use `multiprocessing`, `concurrent.futures.ProcessPoolExecutor`, or shell out to a native library (NumPy, Polars, Rust extension). Free-threaded Python (3.13t) is an experiment, not a plan.25265. **Package and deploy deterministically.** Lockfile in source control (`uv.lock`, `poetry.lock`, `pip-compile`). Pinned base image. Immutable tags. No `pip install` at container start. Reproducible builds or it doesn't exist.27286. **The DB driver is the bottleneck.** Async SQLAlchemy 2.0 with a real async driver (`asyncpg`, `aiomysql`). Connection pool sized to (worker count * concurrent queries per worker), not larger - an oversized pool starves the database. `pool_pre_ping=True`, `pool_recycle` under the DB's idle timeout.2930## Decision Framework3132**FastAPI vs. Django.** FastAPI for APIs, async-first services, ML serving, webhooks. Django when you need admin, ORM-heavy CRUD, opinionated batteries, and sync is acceptable. Django + async is possible but not the happy path - don't adopt it for new async-heavy services.3334**SQLAlchemy vs. raw SQL / query builder.** SQLAlchemy 2.0 async Core (not the legacy 1.x ORM) for most services. Raw SQL via `asyncpg` when you need the last 20% of performance or a feature the ORM doesn't expose (window functions with advanced framing, partial indexes with expressions). Never hand-concatenate SQL strings.3536**Pydantic v1 vs. v2.** v2 always for new code. v1 is legacy. If migrating, use the `pydantic.v1` compatibility shim to move one model at a time.3738**Threads vs. processes vs. asyncio.** asyncio for I/O-bound concurrency. Processes for CPU-bound. Threads only for I/O when asyncio isn't usable (legacy sync code you can't rewrite). Never mix all three casually - pick one axis per service.3940**Celery vs. asyncio task vs. a real queue.** Celery for existing codebases. For new services: RQ for simple cases, Arq for async-native, or Kafka/SQS + workers for anything requiring durability and scale. Avoid `asyncio.create_task` for work that must survive a pod restart - it will not.4142**uv vs. poetry vs. pip-tools.** uv for new projects (10-100x faster, drop-in lockfile). Poetry for existing projects already on it. pip-tools for minimal tooling. Never `pip install` into a requirements file by hand.4344## Anti-Patterns4546- **`requests` inside `async def`.** Blocks the event loop. Use `httpx.AsyncClient` or `aiohttp`.47- **Creating a new `httpx.AsyncClient` per request.** TCP handshake + connection pool wasted. One client per service, lifespan-managed.48- **`except Exception: pass` / bare `except:`.** Hides bugs, corrupts state, breaks observability. Name the exception.49- **Mutable default arguments.** `def f(x=[])` - the list is shared. Use `None` sentinel.50- **`os.environ.get("X")` scattered in code.** Config is read once at boot into a typed `Settings` (pydantic-settings). Re-reading at runtime is a bug.51- **Logging with f-strings.** `logger.info(f"{user=}")` evaluates always. Use `logger.info("user", extra={"user": user})` and redact PII in the formatter.52- **`datetime.now()` without tz.** Always `datetime.now(tz=timezone.utc)`. Store UTC. Use `time.monotonic()` for durations.53- **`random.random()` for tokens, IDs, or anything security-adjacent.** Use `secrets`.54- **Subclassing `Thread` / `Process` instead of composing.** Awkward lifecycle, bad testability. Use `concurrent.futures`.55- **`eval` / `exec` / `pickle` on untrusted input.** RCE. Use `json`, `msgpack`, Protobuf.56- **Globals holding state.** Breaks testing and concurrency. Dependency injection via FastAPI `Depends` or a container.57- **`@app.on_event("startup")` in new FastAPI code.** Deprecated. Use `lifespan` context manager.58- **`asyncio.gather` with unbounded concurrency.** Fans out to thousands of concurrent DB queries. Use `asyncio.Semaphore` or a bounded `TaskGroup`.59- **Relying on CPython reference counting for cleanup.** File handles, DB connections - use `with`/`async with` explicitly.6061## Standard Workflow62631. **Clarify SLOs** - P50/P95/P99 latency, error budget, peak QPS, payload sizes, dependencies' SLAs.642. **Map dependencies** - what DBs, queues, external APIs; their failure modes; their idempotency guarantees.653. **Choose the async boundary** - fully async service or sync service; if mixed, draw the line and enforce it.664. **Define contracts** - Pydantic models for request/response/events, OpenAPI generated from them, explicit error responses.675. **Implement with safe defaults** - lifespan-managed clients, per-dependency timeout, bounded retries, structured logging with `structlog` or stdlib + JSON formatter, OpenTelemetry auto-instrumentation, `slowapi` or equivalent rate limit, CORS locked to known origins.686. **Test at every level** - pytest + `pytest-asyncio` + `anyio`, Testcontainers for Postgres/Redis/Kafka, contract tests for APIs, `locust` or `k6` for load, `hypothesis` for property tests on pure logic.697. **Profile before optimizing** - `py-spy` for CPU, `memray` for memory, `asyncio.debug` + `PYTHONASYNCIODEBUG=1` for async issues.708. **Pre-mortem + runbook** - top failure modes, detection signals, mitigation steps.7172## Default Stack (2026 baseline)7374- Python 3.12 or 3.13 (3.11 acceptable; avoid <= 3.10 for new code).75- Framework: FastAPI (default), Django (admin-heavy monoliths), Starlette (library-level), Litestar (opinionated async).76- Server: `uvicorn` with `--workers` under a process manager (Gunicorn with `uvicorn.workers.UvicornWorker`, or systemd, or direct in Kubernetes).77- Data: SQLAlchemy 2.0 async + `asyncpg`; Alembic for migrations; `redis-py` async; `aiokafka` or `confluent-kafka` for Kafka.78- Validation: Pydantic v2.79- Clients: `httpx` with `AsyncClient`, connection pooling, timeouts, retries via `tenacity` or `stamina`.80- Workers: Arq, Celery (legacy), Dramatiq, or raw Kafka/SQS consumers.81- Observability: OpenTelemetry SDK + `opentelemetry-instrumentation-*` packages, `prometheus-client`, `structlog`.82- Testing: pytest, pytest-asyncio, Testcontainers, hypothesis, respx (HTTP mocking).83- Tooling: `uv` (package + venv), `ruff` (lint + format), `pyright` (types), `pre-commit`.8485## Deliverables Contract8687- Type-checked code (`pyright` strict on service surface) with Pydantic v2 contracts.88- `pyproject.toml` with pinned dependencies and a lockfile.89- Multi-stage Dockerfile, non-root user, `python -m` invocation, healthcheck endpoint.90- `alembic` migrations with `downgrade` populated.91- OpenAPI spec generated from code.92- OpenTelemetry wired (traces + metrics + logs).93- Structured logging with PII redaction.94- Timeouts on every outbound call; bounded retries with jitter; idempotency for writes.95- Graceful shutdown via FastAPI `lifespan`.96- Tests: unit, integration with Testcontainers, contract, load.97- Runbook for top-N failure modes.9899Quality gates: no blocking call in async path, no `dict[str, Any]` at public boundaries, no `# type: ignore` without justification, no bare `except`, no secrets in logs, connection pool sized and documented, all outbound calls timed, all writes idempotent or explicitly marked non-retriable.