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

1

Basic GET Request

ItemcurlPython requests
Simple GETcurl https://api.example.com/usersrequests.get("https://api.example.com/users")
Verbosecurl -v https://api.example.comresp = requests.get(url) print(resp.status_code, resp.headers)
Save to filecurl -o output.json https://...with open("output.json","wb") as f: f.write(resp.content)
Follow redirectscurl -L https://...requests.get(url, allow_redirects=True) # default
HEAD requestcurl -I https://...requests.head(url)
Silent outputcurl -s https://...# requests is silent by default
pythonGET with query params
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 timing
2

POST with JSON Body

data= sends form encoding

❌ Bad
# 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

✅ Good
# 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", ...}
pythonPOST, PUT, PATCH, DELETE
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, timeout
3

Headers and Auth

pythonCustom Headers + Bearer Token
# 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"})
4

POST Form Data and File Upload

pythonForm 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")),
])
5

Session — Persistent Cookies and Headers

pythonSession for Multiple Requests
# 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()
6

Timeout, Retry, SSL

pythonTimeouts, Retries, 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"))
7

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

pythonhttpx async — equivalent to curl in parallel
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)
8

Quick curl Flag Reference

Itemcurl flagrequests equivalent
Method-X POST / -X PUTrequests.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:passauth=("user", "pass")
Bearer token-H "Authorization: Bearer ..."headers={"Authorization": "Bearer ..."}
Skip SSL-k / --insecureverify=False
Timeout--max-time 30timeout=30
Follow redirects-L (default on)allow_redirects=True (default)
Verbose-vprint(resp.status_code, resp.headers)
Cookies-b "key=val"cookies={"key": "val"}
Proxy--proxy http://proxy:8080proxies={"http": "http://proxy:8080"}
9

Complete Error Handling Pattern

pythonProduction-ready request with full error handling
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 None
1

Install 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

2

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).

3

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=().

4

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.

5

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.

6

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

httpbin.org is a free HTTP testing service that echoes back exactly what it received — headers, body, method, query params. Use it to verify your Python code sends the same request as your curl command: requests.post("https://httpbin.org/post", json={...}) will show you exactly what the server received.

Frequently Asked Questions