Production Python API Client Checklist
A compliance checklist for any Python client calling an external API: configuration, resilience, contracts, observability, security, testing and architecture.
Quick Navigation
Difficulty: Intermediate
Estimated Time: 20-30 minutes
Prerequisites: Python 3 experience, HTTP and REST basics, Familiarity with pip packages, Basic testing knowledge
Every service eventually calls someone else's API, and that call is where most production incidents originate. This is a review checklist to run before shipping any external API client, grouped by the failure mode each item prevents.
The items are deliberately binary. If you cannot tick one, that is a finding, not a preference.
1. Configuration and Secrets
- No URL, key or token hardcoded in the source
- Configuration through environment variables (
pydantic-settings), with.envexcluded from Git - Secrets stored in a vault (Vault, AWS Secrets Manager) and rotatable
- Secrets never written to logs, with masking applied systematically
The last one is the item most often missed. It is not enough to avoid logging the token explicitly; a full request dump in a debug log will include the Authorization header.
2. HTTP Client and Resilience
- Session and connection reuse via pooling (
httpx.Clientorrequests.Session), never a barerequests.get - Explicit connect and read timeouts on every call
- Retry on 429, 5xx and network errors, with exponential backoff plus jitter and a bounded maximum (
tenacity) - No blind replay of non-idempotent POSTs, use an
Idempotency-Keyheader - Client-side rate limiting that respects the provider's limits, and reads
Retry-After - Circuit breaker after N consecutive failures, with a half-open state (
pybreaker) - Auth token lifecycle handled: automatic refresh, and thread-safe
A retry without jitter turns a provider blip into a synchronised stampede from every one of your instances. A retry without a bound turns it into an outage.
import httpx
from tenacity import retry, stop_after_attempt, wait_exponential_jitter
client = httpx.Client(timeout=httpx.Timeout(connect=3.0, read=10.0))
@retry(stop=stop_after_attempt(4), wait=wait_exponential_jitter(initial=0.5, max=10))
def fetch_user(user_id: str) -> dict:
response = client.get(f"/users/{user_id}")
response.raise_for_status()
return response.json()
3. Data and Contracts
- Input and output schemas validated (
pydanticv2), no rawdictpropagated into business logic - Complete type annotations, with
mypy --strictin CI - Pagination handled through lazy iteration (generators or cursors), never loading everything into memory
- Stable responses cached with a defined invalidation strategy (
ETagandIf-None-Match) - API version pinned, and an identifiable application
User-Agent
Propagating a raw dict from an external API into your domain logic means the provider gets to change your internal data model without telling you. A validated model turns that into an immediate, localised error.
4. Errors and Observability
- A dedicated business exception hierarchy (
ApiErrorleading toApiTimeout,ApiRateLimitand so on), and no bareexcept: pass - Structured JSON logs with a correlation ID, correct levels, and PII masked
- Latency, error rate and volume metrics exposed
- Distributed tracing (OpenTelemetry)
5. Security
- TLS verified, never
verify=False, minimum TLS 1.2 - System certificates or an up-to-date
certifi - Dependency vulnerability auditing (
pip-audit) with a pinned lockfile
verify=False appears in a surprising number of production codebases, usually added once to get past a local certificate problem and never removed. Grep for it before every release.
6. Testing and Quality
- Unit tests with mocked HTTP (
respxorresponses) - Contract tests against the provider (
vcrpyor a shared schema) - Degraded cases covered: timeout, 500, invalid JSON, connection drop
- Coverage at 80 percent or above
- Automated lint and formatting (
ruff,black) in CI
The degraded cases are the ones that matter. A client tested only against happy-path responses is a client that has never been tested.
7. Architecture and Operations
- Client layer isolated from business logic, injectable and mockable
- Bounded concurrency (
asynciowith aSemaphore), never unlimited parallel calls - Complete docstrings, an integration README, and a maintained
CHANGELOG.md
Using This Checklist
Run it as a pre-merge review on the client module, not as a one-off audit before launch. Most of these items are cheap to add on day one and expensive to retrofit once the client is embedded across several services.
If you only have time for four, take the explicit timeout, the bounded retry with jitter, the schema validation at the boundary, and the secret masking in logs. Those four prevent the majority of incidents that this list exists to catch.
Tags: #Python #API #Resilience #ProductionReadiness #Checklist