POST Requests with Python: requests Library, JSON, Files & Proxies (2026)
The Python requests library has over 300 million monthly downloads and is the de facto standard for making HTTP requests in Python.[1] This guide covers every practical aspect of POST requests — from the simplest JSON body to file uploads, authentication, session management, proxy routing, and SOCKS5 configuration — with complete, runnable code examples throughout.
⚡ Key Takeaways
- Use
json=for JSON bodies (setsContent-Type: application/jsonautomatically). Usedata=for HTML form submissions (application/x-www-form-urlencoded). requests.Session()reuses TCP connections and persists headers, cookies, and proxy config — always use it for multi-request workflows.- Proxies are configured as a
{"http": "...", "https": "..."}dictionary passed to theproxies=parameter — applies to any HTTP method including POST. - For SOCKS5 proxies, install
requests[socks]and use thesocks5h://scheme (notsocks5://) to ensure DNS resolves at the proxy, preventing DNS leaks.[2] - As of version 2.32.5 (2026),
requestssupports only HTTP/1.1 natively. For HTTP/2 or HTTP/3, switch tohttpx.[3] - Always call
response.raise_for_status()or checkresponse.status_code—requestsdoes not raise exceptions for 4xx/5xx responses by default.
Install and Import
# Install pip install requests # For SOCKS5 proxy support, install the extras: pip install "requests[socks]" # Import import requests
Basic POST Request
The simplest POST sends a request with no body — or with raw string data — to a URL and reads the response:
import requests response = requests.post("https://httpbin.org/post") print(response.status_code) # 200 print(response.text) # raw response body as string print(response.json()) # parse JSON response to dict (raises if not JSON) print(response.headers) # response headers dict print(response.url) # final URL after any redirects print(response.elapsed) # time delta for the request
POST JSON Data (Most Common for APIs)
Pass a Python dict to the json= parameter — requests automatically serialises it to JSON and sets Content-Type: application/json:
import requests payload = { "username": "alice", "email": "alice@example.com", "score": 42, } response = requests.post( "https://httpbin.org/post", json=payload, # ← serialises dict + sets Content-Type automatically timeout=15, ) response.raise_for_status() # raises requests.HTTPError for 4xx/5xx data = response.json() print(data["json"]) # httpbin echoes back the parsed JSON body
json= vs data= — they are not interchangeable. json= serialises a dict and sends Content-Type: application/json. data= with a dict sends application/x-www-form-urlencoded (an HTML form submission). Passing a JSON string to data= sends the raw string with no Content-Type header set — a common source of silent API failures.[4]
POST Form Data (HTML Form Submission)
Use data= with a dict to submit data the same way an HTML form does:
import requests # dict → application/x-www-form-urlencoded (HTML form format) form_data = { "username": "alice", "password": "s3cr3t", } response = requests.post( "https://httpbin.org/post", data=form_data, timeout=15, ) response.raise_for_status() # httpbin echoes back the form fields under "form" print(response.json()["form"])
You can also pass a raw string or bytes to data= for custom body content. When data= receives a dict, requests encodes it automatically — no manual URL-encoding needed.
Uploading Files (multipart/form-data)
Use the files= parameter for file uploads. The value is a dict mapping field names to file tuples:
import requests # Simple file upload with open("report.csv", "rb") as f: response = requests.post( "https://httpbin.org/post", files={"file": f}, timeout=30, ) # Custom filename, content-type, and additional form fields with open("image.png", "rb") as img: response = requests.post( "https://httpbin.org/post", files={ "upload": ("custom_name.png", img, "image/png"), }, data={"description": "Profile photo"}, # form fields alongside the file timeout=60, ) response.raise_for_status() print(response.json())
"rb") when uploading. Opening in text mode ("r") produces encoding issues on some platforms and corrupts binary files.
Custom Request Headers
Pass a headers= dict to add or override request headers. Common uses: adding authentication tokens, setting User-Agent, or specifying Accept types:
import requests headers = { "Authorization": "Bearer eyJhbGciOiJIUzI1NiJ9...", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "Accept": "application/json", "X-Request-ID": "abc-123", } response = requests.post( "https://httpbin.org/post", json={"action": "update"}, headers=headers, timeout=15, ) response.raise_for_status() print(response.json()["headers"]) # httpbin echoes back headers it received
Authentication
HTTP Basic Auth
import requests response = requests.post( "https://httpbin.org/post", json={"data": "value"}, auth=("username", "password"), # → sets Authorization: Basic ... header timeout=15, )
Bearer Token
import requests token = "your-api-token-here" response = requests.post( "https://api.example.com/data", json={"key": "value"}, headers={"Authorization": f"Bearer {token}"}, timeout=15, )
Login Flow with Cookie Session
import requests # POST to login endpoint to get a session cookie response = requests.post( "https://example.com/login", data={"username": "alice", "password": "s3cr3t"}, timeout=15, ) cookies = response.cookies # httpbin echoes cookies: session_id, user_id, etc. for cookie in cookies: print(cookie.name, ":", cookie.value)
Sessions: Persistent Connections and State
A requests.Session object reuses TCP connections and persists headers, cookies, and proxies across multiple requests. Always use a session for multi-request workflows — it is faster and cleaner than calling requests.post() repeatedly with the same parameters:[1]
import requests with requests.Session() as session: # Set headers and proxies once — apply to every request in this session session.headers.update({ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)", "Accept": "application/json", }) session.proxies = { "http": "http://user:pass@gate.nstproxy.io:8080", "https": "http://user:pass@gate.nstproxy.io:8080", } # Login — session stores the returned cookie automatically login = session.post( "https://example.com/login", data={"username": "alice", "password": "s3cr3t"}, timeout=15, ) login.raise_for_status() # Subsequent requests automatically include the session cookie response = session.post( "https://example.com/api/orders", json={"item": "proxy-plan", "qty": 1}, timeout=15, ) response.raise_for_status() print(response.json())
Using Proxies with POST Requests
The proxies= parameter accepts a dict mapping protocols to proxy URLs. It applies to every HTTP method — GET, POST, PUT, and all others — identically:[5]
import requests # HTTP/HTTPS proxy — no authentication proxies = { "http": "http://proxy.example.com:8080", "https": "http://proxy.example.com:8080", } # HTTP/HTTPS proxy — with username and password proxies_auth = { "http": "http://username:password@gate.nstproxy.io:8080", "https": "http://username:password@gate.nstproxy.io:8080", } response = requests.post( "https://httpbin.org/post", json={"key": "value"}, proxies=proxies_auth, timeout=15, ) response.raise_for_status() # Verify proxy is active — should show proxy IP, not your real one check = requests.get("https://ipinfo.io/json", proxies=proxies_auth) print(check.json()["ip"])
Environment Variable Proxies
You can also set proxies via environment variables — requests automatically reads them without any code change:
# In your shell before running the script
export HTTP_PROXY="http://username:password@gate.nstproxy.io:8080"
export HTTPS_PROXY="http://username:password@gate.nstproxy.io:8080"
export NO_PROXY="localhost,127.0.0.1"
Rotating Proxies Across Multiple POST Requests
import requests, random PROXIES = [ "http://user:pass@gate.nstproxy.io:8080", "http://user:pass@gate.nstproxy.io:8081", "http://user:pass@gate.nstproxy.io:8082", ] def post_with_rotation(url, payload): proxy = random.choice(PROXIES) proxies = {"http": proxy, "https": proxy} try: resp = requests.post(url, json=payload, proxies=proxies, timeout=15) resp.raise_for_status() return resp.json() except requests.RequestException as e: print(f"Proxy {proxy} failed: {e}") return None
SOCKS5 Proxies
SOCKS5 support requires the optional socks extras. Use socks5h:// (not socks5://) to resolve DNS at the proxy server and prevent DNS leaks:[2]
pip install "requests[socks]"
import requests # socks5h:// — DNS resolves at proxy (recommended, prevents DNS leaks) # socks5:// — DNS resolves locally (your ISP sees the hostname lookup) proxies = { "http": "socks5h://username:password@proxy.example.com:1080", "https": "socks5h://username:password@proxy.example.com:1080", } response = requests.post( "https://httpbin.org/post", json={"action": "test"}, proxies=proxies, timeout=15, ) response.raise_for_status() print(response.json())
Error Handling
By default, requests does not raise exceptions for 4xx or 5xx HTTP responses — it returns the response object regardless. Handle errors explicitly:
import requests from requests.exceptions import ( HTTPError, ConnectionError, Timeout, TooManyRedirects, ProxyError ) def safe_post(url, payload, proxies=None, retries=3): for attempt in range(retries): try: response = requests.post( url, json=payload, proxies=proxies, timeout=15, ) response.raise_for_status() # raises HTTPError for 4xx/5xx return response.json() except ProxyError as e: print(f"Proxy connection failed (attempt {attempt+1}): {e}") except Timeout: print(f"Request timed out (attempt {attempt+1})") except HTTPError as e: print(f"HTTP {e.response.status_code}: {e}") if e.response.status_code in (400, 401, 403, 404): break # client errors — no point retrying except (ConnectionError, TooManyRedirects) as e: print(f"Connection error: {e}") return None
Async POST Requests: httpx
The requests library is synchronous only — one request blocks until the response arrives. For concurrent POST requests (submitting to many endpoints simultaneously), use httpx with asyncio. httpx also natively supports HTTP/2:[3]
pip install httpx[http2]
import asyncio import httpx async def post_async(client, url, payload, proxy): try: response = await client.post(url, json=payload, timeout=15) response.raise_for_status() return response.json() except httpx.HTTPError as e: print(f"Error: {e}") return None async def main(): proxy_url = "http://username:password@gate.nstproxy.io:8080" async with httpx.AsyncClient(proxy=proxy_url, http2=True) as client: tasks = [ post_async(client, "https://httpbin.org/post", {"id": i}, proxy_url) for i in range(10) ] results = await asyncio.gather(*tasks) return results asyncio.run(main())
Quick Comparison: requests vs httpx
| Feature | requests | httpx |
|---|---|---|
| HTTP/1.1 | ✅ | ✅ |
| HTTP/2 | ❌ (needs httpx or requests-h2) | ✅ (with [http2] extra) |
| Async/await | ❌ | ✅ |
| Sync API | ✅ | ✅ |
| SOCKS5 | ✅ (with [socks] extra) | ✅ (with httpx[socks]) |
| Best for | Simple, synchronous scripts and scraping | Concurrent requests, API clients, HTTP/2 targets |
Route Python POST Requests Through Clean Residential IPs
Nstproxy's 110M+ residential proxies integrate directly with requests and httpx via standard proxy URL format — HTTP, HTTPS, and SOCKS5 on the same credentials.
FAQ
json= accepts a Python dict, serialises it to a JSON string, and automatically sets Content-Type: application/json. data= with a dict submits it as an HTML form (application/x-www-form-urlencoded). Use json= for REST APIs that expect JSON; use data= for HTML form submissions or legacy APIs that expect form-encoded data.
No. By default, requests returns the response object for any status code without raising an exception. Call response.raise_for_status() immediately after the request to raise a requests.exceptions.HTTPError for any 4xx or 5xx response. Without this, a 500 error will be silently returned as a response object with no indication of failure unless you check response.status_code explicitly.
Pass a proxies dict to the request: requests.post(url, json=data, proxies={"http": "http://user:pass@host:port", "https": "http://user:pass@host:port"}). For session-based workflows, set session.proxies once and all subsequent requests in the session automatically use it. For SOCKS5 proxies, install requests[socks] and use socks5h:// as the protocol prefix.
The difference is where DNS resolution happens. socks5:// resolves hostnames locally on your machine before tunnelling through the proxy — your real DNS server (and ISP) sees every hostname you request. socks5h:// sends the hostname to the proxy for resolution, so your local DNS never sees the destination and there are no DNS leaks. Always use socks5h:// when privacy or leak prevention matters.
Use httpx when you need: (1) concurrent POST requests using async/await without threading; (2) HTTP/2 support for targets that benefit from multiplexing; (3) an API-compatible drop-in where you want async without changing much code. For synchronous scripts and straightforward scraping, requests remains simpler and has a larger ecosystem of tutorials and integrations.
Further Reading
Sources
- Decodo — Master Python Requests with Proxies (Nov 2025)
- Requests Official Documentation — Advanced Usage: SOCKS Proxies
- Bright Data — Master Python HTTP Requests: Advanced Guide 2026 (April 2026)
- IPRoyal — Python Requests Library (2026 Guide)
- GeeksforGeeks — Proxies with Python Requests Module (July 2025)
- ScrapingBee — How to Use a Proxy with Python Requests (Jan 2026)

