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 (sets Content-Type: application/json automatically). Use data= 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 the proxies= parameter — applies to any HTTP method including POST.
  • For SOCKS5 proxies, install requests[socks] and use the socks5h:// scheme (not socks5://) to ensure DNS resolves at the proxy, preventing DNS leaks.[2]
  • As of version 2.32.5 (2026), requests supports only HTTP/1.1 natively. For HTTP/2 or HTTP/3, switch to httpx.[3]
  • Always call response.raise_for_status() or check response.status_coderequests does 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())
💡 Always open files in binary mode ("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

Featurerequestshttpx
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 forSimple, synchronous scripts and scrapingConcurrent 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.

Try Nstproxy for Free →

FAQ

Q: What is the difference between json= and data= in requests.post()?

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.

Q: Does requests.post() raise an exception for 404 or 500 responses?

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.

Q: How do I use a proxy with requests.post()?

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.

Q: Why use socks5h:// instead of socks5:// for SOCKS5 proxies?

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.

Q: When should I use httpx instead of requests for POST requests?

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