Python Requests Headers: Set, Reuse, and Inspect Them (2026)
Learn how to set headers with Python Requests, reuse them safely with sessions, inspect prepared requests, and troubleshoot overrides.

Direct answer: pass a dictionary to headers= for one request, put stable defaults on requests.Session().headers when several calls share them, and inspect response.request.headers to see the prepared headers Requests sent. Use response.headers for headers returned by the server. Set an explicit timeout on every network call.
This guide covers Requests 2.34.2, the release documented by the Python Requests project in 2026. Requests officially supports Python 3.10+ and also runs on PyPy. The examples use ordinary HTTP APIs, but the same rules apply when you call internal services, webhooks, or a website screenshot endpoint.
1. Set headers on one request
For a single call, create a mapping and pass it to headers. Header names are case-insensitive. Values should be strings, bytestrings, or Unicode-compatible values.
import requests
url = "https://api.example.com/items"
headers = {
"Accept": "application/json",
"User-Agent": "inventory-client/1.0",
}
response = requests.get(url, headers=headers, timeout=(3.05, 20))
response.raise_for_status()
items = response.json()
print(items)
The Requests Quickstart recommends passing a dictionary to headers=. Requests carries your custom names into the final request, subject to authentication, redirect, proxy, and body-preparation rules described later.
Common request headers
| Header | Typical purpose | Example |
|---|---|---|
Accept |
Response formats your client can read | application/json |
Content-Type |
Format of the request body | application/json |
User-Agent |
Identifies the client | inventory-client/1.0 |
Authorization |
Credentials or bearer token | Bearer … |
X-Request-ID |
Trace identifier for one operation | abc-123 |
When you send JSON, prefer json=payload; Requests serializes the object and sets the appropriate content type. If you use data= with a pre-encoded string, set Content-Type yourself.
payload = {"name": "Ada", "role": "admin"}
response = requests.post(
"https://api.example.com/users",
json=payload,
headers={"Accept": "application/json"},
timeout=20,
)
response.raise_for_status()
2. Reuse defaults with requests.Session
Use a Session when multiple calls share headers, cookies, or a connection pool. Session-level and per-request settings are combined: defaults live on session.headers, while a call can override them with its own headers= mapping.
import requests
session = requests.Session()
session.headers.update({
"Accept": "application/json",
"User-Agent": "inventory-client/1.0",
})
first = session.get("https://api.example.com/items", timeout=20)
first.raise_for_status()
second = session.get(
"https://api.example.com/items/42",
headers={"X-Request-ID": "abc-123"},
timeout=20,
)
second.raise_for_status()
Sessions persist cookies and use HTTP keep-alive and connection pooling automatically through urllib3. The Session documentation explains this reusable client state.
Override or remove a default for one call
session.headers.update({"Accept": "application/json"})
response = session.get(
"https://api.example.com/raw",
headers={"Accept": "application/octet-stream"},
timeout=20,
)
response.raise_for_status()
Keep short-lived bearer tokens and endpoint-specific content types out of a Session shared by unrelated hosts. If you need to remove a default for one request, set that key to None in the per-request mapping; Requests omits the key while preparing the request.
session.headers.update({"X-Experimental": "enabled"})
response = session.get(
"https://api.example.com/items",
headers={"X-Experimental": None},
timeout=20,
)
3. Inspect what Requests sent
A response has two different header mappings. response.request.headers belongs to the outgoing PreparedRequest. response.headers belongs to the server response.

response = session.get("https://api.example.com/items", timeout=20)
sent_headers = dict(response.request.headers)
received_headers = dict(response.headers)
print("sent:", sent_headers)
print("received:", received_headers)
The Advanced Usage guide documents response.request as the PreparedRequest used for the call. Header lookup is case-insensitive, so response.request.headers["user-agent"] and ["User-Agent"] address the same value.
Redact secrets before logging. Do not print or persist bearer tokens, cookies, API keys, proxy credentials, or signed URLs in normal application logs.
Inspect before sending with PreparedRequest
When a value is missing or unexpectedly changed, prepare the request through the Session and inspect it before transmission.
from requests import Request, Session
session = Session()
session.headers.update({"Accept": "application/json"})
request = Request(
"GET",
"https://api.example.com/items",
headers={"X-Debug": "1"},
)
prepared = session.prepare_request(request)
print(dict(prepared.headers))
print(prepared.method, prepared.url)
response = session.send(prepared, timeout=20)
response.raise_for_status()
The API reference describes PreparedRequest as the fully mutable object containing the exact request data that will be sent. Preparing through a Session applies session defaults before inspection.
4. Header precedence and surprising overrides
A dictionary passed to headers= is not always the final authority. Requests applies other configuration while preparing and sending:
- Authentication: credentials from
.netrccan replace anAuthorizationvalue, and theauth=parameter has higher precedence. - Redirects: Requests removes
Authorizationwhen a redirect moves to another host. - Proxy credentials: credentials embedded in a proxy URL can replace
Proxy-Authorization. - Body length: Requests may calculate or replace
Content-Lengthwhen it can determine the body size. - Session merging: per-request values override Session defaults for that call.
If an authorization header disappears, or a content length differs from the value you supplied, inspect the prepared request after all these steps. Avoid forcing transport-managed headers unless you have a specific protocol requirement.
5. Authentication patterns that stay maintainable
Bearer token for one request
import os
import requests
token = os.environ["API_TOKEN"]
response = requests.get(
"https://api.example.com/items",
headers={
"Accept": "application/json",
"Authorization": f"Bearer {token}",
},
timeout=20,
)
response.raise_for_status()
Bearer token on a dedicated Session
session = requests.Session()
session.headers.update({
"Accept": "application/json",
"Authorization": f"Bearer {token}",
})
response = session.get("https://api.example.com/items", timeout=20)
Use a dedicated Session per credential scope. This limits accidental credential reuse if your program talks to several services.
6. Timeouts, retries, and reliability
Requests has no default timeout. A call can wait indefinitely if a server stops responding, so set one explicitly. A tuple separates connection and read limits:
response = requests.get(
"https://api.example.com/items",
headers={"Accept": "application/json"},
timeout=(3.05, 20),
)
The Advanced Usage documentation recommends attaching a timeout to requests to external servers. Catch timeout and connection exceptions at your application boundary, and retry only idempotent operations unless the API provides an idempotency key.
import requests
try:
response = requests.get(
"https://api.example.com/items",
timeout=(3.05, 20),
)
response.raise_for_status()
except requests.Timeout:
print("The connection or response took too long")
except requests.HTTPError as exc:
print(f"The server returned an HTTP error: {exc}")
except requests.RequestException as exc:
print(f"The request failed: {exc}")
7. Performance and cost considerations
A Session avoids repeatedly creating transport state and enables connection reuse. For a small script, the difference is usually less important than correct timeouts and error handling; for a worker making many calls to the same host, reuse a Session throughout the worker’s lifetime.
Keep headers small and stable. Large cookies or tracing metadata increase request size. Do not add a unique header value to Session defaults when it should be generated per operation; pass it on that request instead.
Requests itself does not charge per request. Your API provider may charge for calls, bandwidth, or authentication. If the request is a screenshot job, provider billing rules apply to the screenshot service rather than to Requests.
8. Troubleshooting checklist
| Symptom | Likely cause | Fix |
|---|---|---|
| Server says a header is missing | Typo, wrong scope, or a later preparation rule | Print a redacted dict(response.request.headers); for pre-send debugging inspect prepared.headers. |
Authorization is different |
auth=, .netrc, or a cross-host redirect |
Remove competing credentials, disable redirects while diagnosing, and inspect the prepared request. |
| Content type is rejected | Using data= without matching Content-Type |
Use json=payload for JSON, or set the correct content type for encoded data. |
| Request hangs | No timeout was supplied | Set timeout=(connect_seconds, read_seconds) on every call. |
| Session header leaks to another API | One Session is shared across unrelated hosts | Use separate Sessions or pass credentials per request. |
| Header casing looks different | Case-insensitive mapping normalizes display casing | Compare names case-insensitively; behavior is unchanged. |
| Proxy authentication fails | Proxy URL credentials override your supplied value | Inspect proxy configuration and the prepared request; correct the proxy credentials. |
9. A complete reusable client
import os
from typing import Any
import requests
class InventoryClient:
def __init__(self, base_url: str, token: str) -> None:
self.session = requests.Session()
self.base_url = base_url.rstrip("/")
self.session.headers.update({
"Accept": "application/json",
"User-Agent": "inventory-client/1.0",
"Authorization": f"Bearer {token}",
})
def get_item(self, item_id: str, request_id: str | None = None) -> Any:
headers = {"X-Request-ID": request_id} if request_id else None
response = self.session.get(
f"{self.base_url}/items/{item_id}",
headers=headers,
timeout=(3.05, 20),
)
response.raise_for_status()
return response.json()
def debug_last(self, response: requests.Response) -> dict[str, dict[str, str]]:
sent = dict(response.request.headers)
for secret in ("authorization", "cookie", "proxy-authorization"):
for key in list(sent):
if key.lower() == secret:
sent[key] = "[redacted]"
return {"sent": sent, "received": dict(response.headers)}
client = InventoryClient(
"https://api.example.com",
os.environ["API_TOKEN"],
)
print(client.get_item("42"))
10. Or skip the browser setup: ScreenshotNeo
If your Python client ultimately needs a clean image or PDF of a web page, you can call ScreenshotNeo directly with Requests. It accepts custom headers, cookies, user agents, Authorization values, wait conditions, CSS and JavaScript, device settings, full-page capture, element selectors, PDF options, caching, signed links, asynchronous jobs, bulk capture, and more. See the ScreenshotNeo API documentation for the complete option list.

import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
timeout=90,
)
open("shot.webp", "wb").write(r.content)
print(r.headers.get("X-Page-Verdict"), r.headers.get("X-Billed"))
Equivalent cURL:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
Equivalent Node.js:
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
ScreenshotNeo removes cookie and consent banners, newsletter popups, and chat widgets before capture. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the result with X-Page-Verdict and X-Billed headers. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. The Free plan includes 1,000 screenshots per month without a card; paid plans start at $5 for 3,000 screenshots. Create a free ScreenshotNeo account.
11. Frequently asked questions
Are header names case-sensitive?
No. Requests uses a case-insensitive header mapping. Servers should also treat HTTP field names case-insensitively.
Should I use a dictionary or a Session?
Use headers= for one call or an endpoint-specific value. Use Session.headers for stable defaults shared by several calls to the same service.
Why does response.headers not show my request header?
It contains response headers from the server. Inspect response.request.headers for the outgoing request.
Can I force an exact header set?
Prepare a request with session.prepare_request() and inspect or modify the resulting PreparedRequest. Authentication, redirects, proxies, and body handling can still affect what is appropriate to send.
Where should secrets go?
Load them from environment variables or a secret manager, scope them to a dedicated Session or request, and redact them before logging prepared headers.


