curl to Python requests — Complete Conversion Guide
curl commands are the universal language of API testing. This guide shows you how to convert any curl command to Python requests — GET, POST, headers, auth, cookies, files, and more — with side-by-side comparisons for every pattern. We also cover Sessions, retry logic, async with httpx, and real-world error handling so your converted code is production-ready.
100%
curl flags covered
requests
the Python HTTP standard library
httpx
async alternative covered too
Session
for persistent connections + cookies
Basic GET Request
| Item | curl | Python requests |
|---|---|---|
| Simple GET | curl https://api.example.com/users | requests.get("https://api.example.com/users") |
| Verbose | curl -v https://api.example.com | resp = requests.get(url) print(resp.status_code, resp.headers) |
| Save to file | curl -o output.json https://... | with open("output.json","wb") as f: f.write(resp.content) |
| Follow redirects | curl -L https://... | requests.get(url, allow_redirects=True) # default |
| HEAD request | curl -I https://... | requests.head(url) |
| Silent output | curl -s https://... | # requests is silent by default |
import requests
# curl "https://api.example.com/users?page=2&limit=10"
resp = requests.get(
"https://api.example.com/users",
params={"page": 2, "limit": 10} # auto-encoded into URL
)
print(resp.status_code) # 200
data = resp.json() # parse JSON response
# Always check status before using data
resp.raise_for_status() # raises HTTPError for 4xx/5xx
# Access specific response parts
print(resp.headers["Content-Type"])
print(resp.elapsed.total_seconds()) # request timingPOST with JSON Body
data= sends form encoding
# curl -X POST https://api.example.com/users \
# -H "Content-Type: application/json" \
# -d '{"name":"Alice","email":"alice@example.com"}'
# Wrong — sends as form data, not JSON
requests.post(url, data={"name": "Alice"})json= sends JSON with correct Content-Type
# Correct — use json= parameter (sets Content-Type automatically)
resp = requests.post(
"https://api.example.com/users",
json={"name": "Alice", "email": "alice@example.com"}
)
# requests automatically sets: Content-Type: application/json
print(resp.status_code) # 201
print(resp.json()) # {"id": 42, "name": "Alice", ...}import requests
BASE = "https://api.example.com"
# POST — create resource
resp = requests.post(f"{BASE}/users", json={"name": "Alice"})
# PUT — replace resource
resp = requests.put(f"{BASE}/users/42", json={"name": "Alice Updated"})
# PATCH — partial update
resp = requests.patch(f"{BASE}/users/42", json={"email": "new@example.com"})
# DELETE — remove resource
resp = requests.delete(f"{BASE}/users/42")
print(resp.status_code) # 204 No Content
# All methods support the same parameters: headers, json, data, params, timeoutHeaders and Auth
# curl -H "Authorization: Bearer mytoken123" \
# -H "X-Custom-Header: value" \
# https://api.example.com/protected
resp = requests.get(
"https://api.example.com/protected",
headers={
"Authorization": "Bearer mytoken123",
"X-Custom-Header": "value",
}
)
# Basic Auth:
# curl -u username:password https://api.example.com
resp = requests.get(url, auth=("username", "password"))
# Or with requests.auth:
from requests.auth import HTTPBasicAuth
resp = requests.get(url, auth=HTTPBasicAuth("user", "pass"))
# Digest Auth:
from requests.auth import HTTPDigestAuth
resp = requests.get(url, auth=HTTPDigestAuth("user", "pass"))
# API Key in header:
resp = requests.get(url, headers={"X-API-Key": "your_api_key_here"})POST Form Data and File Upload
# Form data (URL-encoded):
# curl -X POST -d "username=alice&password=secret" https://...
resp = requests.post(url, data={"username": "alice", "password": "secret"})
# Content-Type: application/x-www-form-urlencoded
# Multipart form data:
# curl -X POST -F "username=alice" -F "password=secret" https://...
resp = requests.post(url, data={"username": "alice", "password": "secret"})
# File upload:
# curl -X POST -F "file=@photo.jpg" https://api.example.com/upload
with open("photo.jpg", "rb") as f:
resp = requests.post(
"https://api.example.com/upload",
files={"file": f}
)
# File upload with custom filename and content type:
resp = requests.post(
"https://api.example.com/upload",
files={"file": ("custom_name.jpg", open("photo.jpg", "rb"), "image/jpeg")},
data={"user_id": "123", "description": "Profile photo"}
)
# Multiple files:
resp = requests.post(url, files=[
("images", ("photo1.jpg", open("photo1.jpg", "rb"), "image/jpeg")),
("images", ("photo2.jpg", open("photo2.jpg", "rb"), "image/jpeg")),
])Session — Persistent Cookies and Headers
# curl handles cookies automatically — Python needs Session
import requests
session = requests.Session()
# Set default headers for all requests in this session
session.headers.update({
"Authorization": "Bearer mytoken",
"User-Agent": "MyApp/1.0",
"Accept": "application/json",
})
# Login (sets cookies automatically via Set-Cookie headers)
login_resp = session.post(
"https://api.example.com/login",
json={"user": "alice", "pass": "secret"}
)
login_resp.raise_for_status()
# Subsequent requests reuse cookies and headers automatically
profile = session.get("https://api.example.com/me")
orders = session.get("https://api.example.com/orders")
# Override headers for a single request
admin_resp = session.get(
"https://api.example.com/admin",
headers={"X-Admin-Token": "special-token"} # merged with session headers
)
# Always close session when done (or use as context manager)
with requests.Session() as s:
s.headers.update({"Authorization": "Bearer token"})
data = s.get("https://api.example.com/data").json()Timeout, Retry, SSL
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
# Timeout (connect timeout, read timeout):
# curl --connect-timeout 5 --max-time 30 https://...
resp = requests.get(url, timeout=(5, 30)) # (connect, read) in seconds
resp = requests.get(url, timeout=10) # single value applies to both
# Retry with exponential backoff:
session = requests.Session()
retry = Retry(
total=3,
backoff_factor=1, # sleep 1s, 2s, 4s between retries
status_forcelist=[500, 502, 503, 504],
allowed_methods=["GET", "POST"],
)
adapter = HTTPAdapter(max_retries=retry)
session.mount("https://", adapter)
session.mount("http://", adapter)
# Skip SSL verification (dev only! Never in production):
# curl -k https://...
resp = requests.get(url, verify=False)
import urllib3
urllib3.disable_warnings() # suppress SSL warning
# Use a custom CA bundle:
resp = requests.get(url, verify="/path/to/ca-bundle.crt")
# Client certificate authentication:
# curl --cert client.crt --key client.key https://...
resp = requests.get(url, cert=("client.crt", "client.key"))Async with httpx — Drop-in Replacement
When to use httpx
httpx is a near-identical API to requests but with full async/await support and HTTP/2. Use it in FastAPI, async Django, or any asyncio-based application. Install with: pip install httpx
import asyncio
import httpx
# Sync httpx (drop-in requests replacement):
with httpx.Client() as client:
resp = client.get("https://api.example.com/users")
print(resp.json())
# Async httpx (concurrent requests — much faster than sequential):
async def fetch_all():
async with httpx.AsyncClient() as client:
# Fire all requests concurrently
tasks = [
client.get("https://api.example.com/users/1"),
client.get("https://api.example.com/users/2"),
client.get("https://api.example.com/users/3"),
]
responses = await asyncio.gather(*tasks)
return [r.json() for r in responses]
# Run:
results = asyncio.run(fetch_all())
# Streaming large responses (equivalent to curl piped to file):
async def stream_download():
async with httpx.AsyncClient() as client:
async with client.stream("GET", "https://example.com/large-file") as r:
async for chunk in r.aiter_bytes(chunk_size=8192):
process_chunk(chunk)Quick curl Flag Reference
| Item | curl flag | requests equivalent |
|---|---|---|
| Method | -X POST / -X PUT | requests.post() / requests.put() |
| Header | -H "Key: Value" | headers={"Key": "Value"} |
| JSON body | -d '{"key":"val"}' | json={"key": "val"} |
| Form data | -F "key=val" | data={"key": "val"} |
| Basic auth | -u user:pass | auth=("user", "pass") |
| Bearer token | -H "Authorization: Bearer ..." | headers={"Authorization": "Bearer ..."} |
| Skip SSL | -k / --insecure | verify=False |
| Timeout | --max-time 30 | timeout=30 |
| Follow redirects | -L (default on) | allow_redirects=True (default) |
| Verbose | -v | print(resp.status_code, resp.headers) |
| Cookies | -b "key=val" | cookies={"key": "val"} |
| Proxy | --proxy http://proxy:8080 | proxies={"http": "http://proxy:8080"} |
Complete Error Handling Pattern
import requests
from requests.exceptions import (
ConnectionError, Timeout, HTTPError, RequestException
)
def safe_api_call(url: str, **kwargs) -> dict | None:
"""
Production-ready GET request with full error handling.
Equivalent to: curl -s --max-time 10 --retry 3 <url>
"""
try:
resp = requests.get(url, timeout=(5, 10), **kwargs)
resp.raise_for_status() # raise on 4xx/5xx
return resp.json()
except Timeout:
print(f"Request timed out: {url}")
except ConnectionError:
print(f"Cannot connect to: {url}")
except HTTPError as e:
status = e.response.status_code
if status == 401:
print("Authentication failed — check your token")
elif status == 403:
print("Permission denied")
elif status == 404:
print(f"Not found: {url}")
elif status == 429:
retry_after = e.response.headers.get("Retry-After", "unknown")
print(f"Rate limited. Retry after: {retry_after}s")
elif status >= 500:
print(f"Server error {status} — try again later")
else:
print(f"HTTP error {status}: {e}")
except RequestException as e:
print(f"Unexpected request error: {e}")
except ValueError:
print("Response is not valid JSON")
return NoneInstall requests
pip install requests — it's not in the standard library. For async, also pip install httpx. Pin versions in requirements.txt: requests==2.32.3
Identify the curl flags
Parse your curl command for: method (-X), headers (-H), body (-d or --data-raw), auth (-u), files (-F), and SSL flags (-k, --cacert).
Map to requests parameters
Use the reference table above. Key rules: -H → headers={}, -d with JSON → json={}, -d with form → data={}, -F → files={}, -u → auth=().
Add timeout and raise_for_status
All production code should have timeout=(connect, read) and call resp.raise_for_status() before accessing resp.json() to surface 4xx/5xx errors.
Use Session for multiple requests
If making multiple calls to the same API, use requests.Session() to share headers, cookies, and connection pooling. 30-50% faster for repeated calls.
Test with the same endpoint
Run both the original curl and your Python code against the same endpoint and compare responses. Use httpbin.org for testing — it echoes back your request details.
Test your conversion with httpbin.org