Use requests.post with json for JSON APIs, data for form fields, files for multipart uploads, and always add a timeout plus explicit error handling before the call reaches production.
A one-line POST is easy. A dependable integration also defines what it sends, how long it waits, which failures it retries, and what evidence it records without leaking credentials.
Table of contents
- Choose the request body deliberately
- Send forms, headers, and files
- Treat responses as a protocol
- Retry only operations that are safe to repeat
- Make the integration observable and testable
- How this fits the rest of the stack
- FAQ
Choose the request body deliberately
The json argument serializes a Python object and sets the JSON content type. The data argument sends form-encoded fields when given a mapping, or sends raw bytes or text when given a string. Do not manually JSON-encode a dictionary into data unless the API specifically requires that wire shape; it is easy to forget the matching content type.
import requests
payload = {"name": "worker", "enabled": True}
response = requests.post(
"https://api.example.com/services",
json=payload,
timeout=(3.05, 20),
)
response.raise_for_status()
A tuple timeout separates connection time from read time. Requests has no default timeout, so omitting it can leave a worker blocked indefinitely. Pick values from the service contract and job budget rather than copying one number into every integration.
Send forms, headers, and files
Traditional HTML endpoints often expect application/x-www-form-urlencoded data. File uploads use multipart encoding and should pass open file objects through files so Requests can generate the boundary. Use a context manager to close handles even when the network call fails.
with open("report.csv", "rb") as handle:
response = requests.post(
"https://api.example.com/imports",
data={"project": "alpha"},
files={"file": ("report.csv", handle, "text/csv")},
headers={"Authorization": f"Bearer {token}"},
timeout=(3.05, 60),
)
response.raise_for_status()
Do not set the multipart Content-Type yourself because the generated boundary must match the encoded body. Keep tokens in environment-backed secret configuration, not source files, and never log the full Authorization header.
Treat responses as a protocol
A completed HTTP exchange is not automatically success. Check status codes, call raise_for_status when non-2xx responses are exceptional, and parse the response according to its declared content type. A 204 response has no JSON body; an error page from an intermediary may be HTML even when the API normally returns JSON.
try:
response = requests.post(url, json=payload, timeout=10)
response.raise_for_status()
result = response.json() if response.content else None
except requests.Timeout:
raise RuntimeError("API timed out")
except requests.HTTPError as exc:
raise RuntimeError(f"API returned {exc.response.status_code}") from exc
Limit logged response bodies and redact sensitive fields. Capture request IDs and status codes because they are more useful for support than a megabyte of raw payload.
Retry only operations that are safe to repeat
POST often creates or triggers work, so an automatic retry can create duplicates after the server completed the first request but the client missed its response. Use an idempotency key when the API supports one, and retry only transient failures such as selected 429 and 5xx responses with bounded exponential backoff. Respect Retry-After.
A Session reuses connections and centralizes headers. Mount a retrying adapter only after defining allowed methods and statuses for that API. The retry policy is business logic: charging a card, creating a deployment, and submitting an analytics event do not have the same duplicate cost.
Make the integration observable and testable
- Log destination host, operation name, duration, status, and request ID
- Never log secrets or complete personal payloads
- Mock transport failures as well as successful JSON
- Test malformed JSON and empty responses
- Budget retries within the caller timeout
- Expose failure counts and latency percentiles
Wrap third-party calls behind a small application interface rather than scattering requests.post across route handlers. That gives tests one seam, makes authentication rotation manageable, and lets background jobs apply consistent timeouts and retries. The HTTP call is simple; the failure contract is the real integration.
How this fits the rest of the stack
Outbound API work consumes runtime, network, and worker capacity. The RunxBuild hosting calculator puts those pieces on one estimate, and the RunxBuild dashboard keeps environment variables and logs close to the deployed Python service.
Useful related references:
- Python Not Equal: != vs is not, and Why the Difference Bites
- Python Integer Division: Why // Floors and Why -7 // 2 Is -4
- The Python return Statement: What It Does, and What None Tells You
- Python services on RunxBuild
FAQ
What is the difference between json and data in requests.post?
json serializes an object as JSON and sets the matching content type. data normally sends form fields or an already encoded body.
Does requests.post have a default timeout?
No. Add a timeout to every production call so a stalled peer cannot occupy the worker indefinitely.
How do I upload a file?
Pass a file tuple through files and use a context manager. Let Requests generate the multipart content type and boundary.
Should I retry POST requests?
Only when the operation is idempotent or protected by an idempotency key. Otherwise a retry can create duplicate side effects.
How should I handle API errors?
Check status, use raise_for_status when appropriate, catch specific timeout and HTTP exceptions, and log redacted diagnostic context.