curl vs Python requests — Full Comparison with Side-by-Side Examples
curl and Python requests are both HTTP clients — but built for very different contexts. curl is a command-line tool perfect for testing, scripting, and one-off API calls. Python requests is a library for production code, automation, and complex HTTP workflows. This guide shows every common pattern side by side with complete working examples.
curl
perfect for testing and one-off API calls
requests
perfect for production Python code
1:1
mapping exists for almost every curl flag
Session
requests.Session() handles cookies like curl -c
When to Use Each
| Item | Use curl When... | Use Python requests When... |
|---|---|---|
| Primary use | Testing an API endpoint quickly from terminal | Making HTTP calls in production Python code |
| Sharing | Sharing a reproducible API call with a teammate | Handling responses, parsing JSON, proper error handling |
| Automation | Shell scripts and CI/CD pipelines (bash) | Complex multi-step API workflows with Python logic |
| Debugging | Seeing exactly what is being sent with -v | Retry logic, session management, connection pooling |
| Quick use | One-liner from the command line | Any Python program that needs HTTP calls |
| Performance | High-throughput download/upload scripts | Async HTTP with httpx for concurrent requests |
Basic GET Request
# curl — simplest form:
curl https://api.example.com/users
# curl with verbose output (shows headers):
curl -v https://api.example.com/users
# Python requests — simple GET:
import requests
response = requests.get('https://api.example.com/users')
print(response.status_code) # 200
print(response.json()) # parsed JSON dict/list
print(response.headers) # response headers
print(response.elapsed) # request timing
# curl with query params:
curl "https://api.example.com/users?page=2&limit=10"
# Python (auto-encodes to URL):
response = requests.get(
'https://api.example.com/users',
params={'page': 2, 'limit': 10}
# → GET /users?page=2&limit=10
)
# Always check for errors:
response.raise_for_status() # raises HTTPError for 4xx/5xx
data = response.json()POST with JSON Body
data= sends form encoding (wrong for JSON APIs)
# curl sends JSON correctly:
curl -X POST https://api.example.com/users \
-H "Content-Type: application/json" \
-d '{"name":"Alice","email":"alice@example.com"}'
# ❌ WRONG Python equivalent — sends form data, not JSON!
requests.post(url, data={"name": "Alice", "email": "alice@example.com"})
# Content-Type: application/x-www-form-urlencoded ← wrong!json= matches curl -d with JSON Content-Type
# ✅ CORRECT — use json= parameter (matches curl -d with JSON)
response = requests.post(
'https://api.example.com/users',
json={"name": "Alice", "email": "alice@example.com"}
)
# requests automatically sets: Content-Type: application/json
# requests automatically serializes the dict to JSON string
print(response.status_code) # 201
print(response.json()) # {"id": 42, "name": "Alice", ...}Headers and Authentication
# Custom headers:
# curl:
curl -H "Authorization: Bearer mytoken" \
-H "X-Custom-Header: value" \
-H "Accept: application/json" \
https://api.example.com/protected
# Python requests:
response = requests.get(
'https://api.example.com/protected',
headers={
'Authorization': 'Bearer mytoken',
'X-Custom-Header': 'value',
'Accept': 'application/json',
}
)
# Basic auth:
# curl:
curl -u username:password https://api.example.com
# Python:
response = requests.get(url, auth=('username', 'password'))
# Digest auth:
from requests.auth import HTTPDigestAuth
response = requests.get(url, auth=HTTPDigestAuth('user', 'pass'))
# API key in header:
# curl -H "X-API-Key: your_key" https://api.example.com
response = requests.get(url, headers={'X-API-Key': 'your_key'})File Upload
# curl single file upload:
curl -X POST -F "file=@photo.jpg" -F "user_id=123" \
https://api.example.com/upload
# Python equivalent:
with open('photo.jpg', 'rb') as f:
response = requests.post(
'https://api.example.com/upload',
files={'file': f},
data={'user_id': '123'}
)
# With custom filename and content type:
response = requests.post(
url,
files={'file': ('custom_name.jpg', open('photo.jpg','rb'), 'image/jpeg')}
)
# Multiple files:
response = requests.post(url, files=[
('images', ('photo1.jpg', open('photo1.jpg', 'rb'), 'image/jpeg')),
('images', ('photo2.jpg', open('photo2.jpg', 'rb'), 'image/jpeg')),
])Session / Cookie Handling
# curl maintains cookies with -c (write) and -b (read):
curl -c cookies.txt https://api.example.com/login \
-X POST -d "user=alice&pass=secret"
# Use saved cookies in subsequent requests:
curl -b cookies.txt https://api.example.com/dashboard
# Python requests.Session() handles cookies automatically:
import requests
session = requests.Session()
# Set default headers for ALL session requests:
session.headers.update({
'Authorization': 'Bearer mytoken',
'User-Agent': 'MyApp/1.0',
})
# Login (Set-Cookie headers stored automatically):
session.post('https://api.example.com/login',
json={'user': 'alice', 'pass': 'secret'})
# Cookies and headers automatically included in subsequent requests:
dashboard = session.get('https://api.example.com/dashboard')
profile = session.get('https://api.example.com/me')
# Session also reuses TCP connections (connection pooling) → 30-50% fasterTimeout and SSL
# curl timeouts and SSL:
curl --connect-timeout 5 --max-time 30 https://api.example.com
curl -k https://api.example.com # skip SSL verification
curl --cacert /path/to/ca.pem https://api.example.com
# Python requests equivalents:
# Separate connect timeout and read timeout:
response = requests.get(url, timeout=(5, 30)) # (connect_sec, read_sec)
response = requests.get(url, timeout=30) # applies to both
# Skip SSL (dev only — never in production):
import urllib3
urllib3.disable_warnings()
response = requests.get(url, verify=False)
# Custom CA bundle:
response = requests.get(url, verify='/path/to/ca.pem')
# Client certificate:
# curl --cert client.crt --key client.key https://api.example.com
response = requests.get(url, cert=('client.crt', 'client.key'))Full Flag Reference
| Item | curl flag | Python requests equivalent |
|---|---|---|
| -X POST | HTTP method | requests.post() / requests.request("POST", url) |
| -H "K: V" | Header | headers={"K": "V"} |
| -d "json" | JSON body | json={...} (use json=, not data=!) |
| -F "key=val" | Form data | data={"key": "val"} |
| -F "f=@file" | File upload | files={"f": open("file","rb")} |
| -u user:pass | Basic auth | auth=("user", "pass") |
| -b "k=v" | Cookie | cookies={"k": "v"} |
| -k | Skip SSL verification | verify=False |
| --max-time 30 | Total timeout (seconds) | timeout=30 |
| --connect-timeout 5 | Connect timeout only | timeout=(5, 30) — (connect, read) |
| -L | Follow redirects | allow_redirects=True (True by default) |
| -o file.json | Save to file | with open('f.json','wb') as f: f.write(resp.content) |
| --proxy http://p:8080 | HTTP proxy | proxies={"http": "http://p:8080"} |
| -I | HEAD request | requests.head(url) |
Debug with httpbin.org
Start with curl for discovery
Use curl -v to explore an API endpoint. It shows exactly what headers and body the server expects and returns. Much faster to iterate than Python code.
Copy curl to Python
Once you have a working curl command, use our curl-to-Python converter (or curlconverter.com) to get the Python requests equivalent automatically.
Add error handling
Call response.raise_for_status() before response.json(). Wrap in try/except for requests.exceptions.RequestException to handle network errors.
Use Session for multiple calls
If making 2+ calls to the same API, use requests.Session() for connection pooling, shared headers, and automatic cookie management. 30-50% faster.
Add timeout to all calls
Always set timeout=(5, 30) or similar. Never make a production API call without a timeout — a slow server will hang your program indefinitely.
Consider httpx for async
If you're using FastAPI, asyncio, or making many concurrent requests, switch to httpx. The API is nearly identical to requests but supports async/await.