Python Web Scraping

Set a User-Agent in Python Requests

By the Scrappey Research Team

Set a User-Agent in Python Requests — conceptual illustration
On this page

To set a User-Agent in Python requests, pass a headers dictionary with a "User-Agent" key to the request, or set it once on a Session so every call reuses it. The User-Agent is the HTTP header that identifies the client software making the request. By default requests sends something like python-requests/2.32.3, which trivially marks the traffic as a script, so most public sites and anti-bot services reject or throttle it. Replacing it with a current browser string is the first and cheapest fix, but as you will see below, the header alone does not make your traffic look like a browser.

Quick facts

Per-requestrequests.get(url, headers={"User-Agent": "..."})
Per-sessionsession.headers.update({"User-Agent": "..."})
Default UApython-requests/<version> — recognised and blocked
Better than UA aloneFull header set + matching Accept/Accept-Language order
Hard limitCannot change TLS/JA4 fingerprint — needs curl_cffi

Set it on one request or on a Session

There are two ways to set the User-Agent, and the choice comes down to how many requests you are making. For a single call, pass a headers dict:

import requests

headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
                         "AppleWebKit/537.36 (KHTML, like Gecko) "
                         "Chrome/126.0.0.0 Safari/537.36"}
r = requests.get("https://httpbin.org/user-agent", headers=headers)
print(r.json())   # {"user-agent": "Mozilla/5.0 ..."}

For anything more than one request, use a Session. Setting the header once on session.headers applies it to every request the session makes, and the session also persists cookies and reuses TCP connections, which is both faster and more realistic:

session = requests.Session()
session.headers.update({"User-Agent": "Mozilla/5.0 ... Chrome/126.0.0.0 Safari/537.36"})

session.get("https://example.com/")          # warm up, collect cookies
data = session.get("https://example.com/api")  # same UA + cookies reused

Two practical notes. Header keys are case-insensitive in requests, so "User-Agent" and "user-agent" behave the same. And a per-request headers dict is merged on top of the session headers, so you can override the UA for a single call without changing the session default.

Why the default UA gets blocked — and why one header is not enough

The reason the default fails is that python-requests/2.32.3 is an honest self-identification: no human browser ever sends it, so a single string match flags the request. Swapping in a real Chrome or Firefox string clears that specific check and is enough for many simple, unprotected sites. See what a User-Agent is for the full anatomy of the string.

The catch is that real browsers do not send only a User-Agent. They send a consistent set of headers in a specific order: Accept, Accept-Language, Accept-Encoding, Sec-Ch-Ua client hints, Sec-Fetch-* metadata, and so on. A request that carries a Chrome User-Agent but omits the headers a real Chrome sends is internally inconsistent, and servers may treat a self-identified Chrome client differently from one whose headers do not match. So the right move is to mirror a full browser header set, not just the one field:

browser_headers = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
                  "(KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36",
    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,"
              "image/webp,*/*;q=0.8",
    "Accept-Language": "en-US,en;q=0.9",
    "Accept-Encoding": "gzip, deflate, br",
    "Sec-Ch-Ua": '"Chromium";v="126", "Google Chrome";v="126", "Not.A/Brand";v="24"',
    "Sec-Ch-Ua-Mobile": "?0",
    "Sec-Ch-Ua-Platform": '"Windows"',
    "Sec-Fetch-Dest": "document",
    "Sec-Fetch-Mode": "navigate",
    "Sec-Fetch-Site": "none",
    "Upgrade-Insecure-Requests": "1",
}

Keep the values internally consistent: a UA claiming Windows should pair with Sec-Ch-Ua-Platform: "Windows", and the Chrome version in the UA should match the version in Sec-Ch-Ua. Internally inconsistent header sets are an obvious mismatch, so keep the platform and version values aligned with the User-Agent.

Rotating UAs, and the wall you hit with TLS fingerprinting

If you send many requests, rotating the User-Agent lets you test or represent several browser/OS combinations rather than a single fixed one. Use a small pool of current strings, since outdated browser versions can cause sites to serve different markup and, ideally, swap the whole header set together so the UA and client hints always agree:

import random

UA_POOL = [
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
    "(KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36",
    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 "
    "(KHTML, like Gecko) Version/17.4 Safari/605.1.15",
]
session.headers["User-Agent"] = random.choice(UA_POOL)

Here is the limit that surprises people. You can perfect every header and still get a 403 error, because anti-bot systems also read the TLS fingerprint — a JA3/JA4 hash computed from the encrypted handshake that happens before any header is sent. Python's requests negotiates TLS through OpenSSL with a fixed cipher list, producing one well-known signature that no browser emits. The HTTP headers identify Chrome while the TLS handshake is the one Python's requests produces, so the two describe different clients even though your code never sees the handshake directly.

Plain requests cannot change its TLS fingerprint. To make the handshake match the browser in your User-Agent, switch to curl_cffi, which wraps curl-impersonate and replicates a browser's exact cipher list, extension order, and HTTP/2 settings with a single impersonate="chrome" argument. When even that is not enough because the page runs JavaScript challenges, a managed web data API like Scrappey handles the headers, TLS fingerprint, proxies, and browser rendering in one call.

Code example

python
import requests

# A full browser-like header set, not just the User-Agent. Mirroring the
# headers a real Chrome sends keeps the values internally consistent.
browser_headers = {
    "User-Agent": (
        "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
        "(KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36"
    ),
    "Accept": (
        "text/html,application/xhtml+xml,application/xml;q=0.9,"
        "image/avif,image/webp,*/*;q=0.8"
    ),
    "Accept-Language": "en-US,en;q=0.9",
    "Accept-Encoding": "gzip, deflate, br",
    "Sec-Ch-Ua-Platform": '"Windows"',
    "Sec-Fetch-Mode": "navigate",
}

# Set it once on a Session so every request reuses the same UA + cookies.
session = requests.Session()
session.headers.update(browser_headers)

r = session.get("https://httpbin.org/headers", timeout=15)
print(r.json()["headers"]["User-Agent"])

# If you still get a 403, the block is on the TLS fingerprint, not the
# headers. Plain requests cannot change that -- switch to curl_cffi:
#
#   from curl_cffi import requests as cffi
#   r = cffi.get("https://example.com", impersonate="chrome")
#
# curl_cffi sends a Chrome-identical TLS handshake AND browser headers.

Related terms

Concept map

How Set a User-Agent in Python Requests connects

The terms most directly tied to this one. Hover a node to see its neighbours, click to preview, drag to rearrange.

0 terms · 0 connections
You are here · Python Web Scraping
Building map…

Frequently asked questions

How do I set a User-Agent for every request in Python requests?

Create a Session and call session.headers.update({"User-Agent": "..."}). Every request made through that session then sends the header automatically, and the session also keeps cookies and reuses connections across calls. This is cleaner than passing a headers dict to each individual request, and it makes your traffic more consistent.

What is the default User-Agent in Python requests?

By default requests sends a User-Agent like python-requests/2.32.3, where the number is the installed library version. No real browser ever sends that string, so it is an immediate giveaway that the request comes from a script, which is why many sites and anti-bot services block or throttle it on sight.

Is setting the User-Agent enough to avoid being blocked?

Often no. Replacing the default UA clears the simplest check and works on many unprotected sites, but real browsers send a full, consistent set of headers, and protected sites also read the TLS fingerprint from the handshake. Plain requests always produces the same non-browser TLS signature, so a perfect User-Agent paired with a Python handshake is still detectable.

How do I make my TLS fingerprint match a real browser in Python?

The standard requests library cannot change its TLS fingerprint because it uses OpenSSL with a fixed cipher list. Use curl_cffi instead, which wraps curl-impersonate and reproduces a browser's exact cipher suite, extension order, and HTTP/2 settings when you pass impersonate="chrome". For pages that also require running JavaScript, route the request through a managed scraping API.

Last updated: 2026-06-16 · Facts last verified: 2026-06-16