Python urllib.request: Native HTTP

Published on: August 19, 2026
Reading time: 4 minutes
Keyboard tiles spelling HTTP representing Python urllib.request requests

The urllib.request module opens URLs and sends HTTP requests using only the Python standard library. It provides urlopen(), configurable Request objects, and an extensible handler system for redirects, authentication, cookies, proxies, and HTTPS.

Higher-level clients such as Requests or HTTPX are more convenient for large applications, but urllib.request is valuable in portable scripts, installers, administration tools, and constrained environments. This guide covers GET, POST, JSON, bounded downloads, TLS, redirects, proxies, errors, retries, and protection against untrusted URLs.

A basic GET request

from urllib.request import urlopen

with urlopen("https://www.python.org/", timeout=10) as response:
    print(response.status)
    print(response.headers.get_content_type())
    data = response.read(4096)

The response is a context manager and exposes status, headers, and url. The body is bytes because the client cannot reliably determine the text encoding automatically.

charset = response.headers.get_content_charset() or "utf-8"
text = data.decode(charset, errors="replace")

The Python codecs guide explains the boundary between text and bytes.

Always set a timeout

Without a timeout, DNS resolution, connection establishment, TLS, or reads may block for a long time.

with urlopen(url, timeout=10) as response:
    body = response.read(1_000_000)

A timeout is not a response-size limit. A server can continuously send data while remaining active. Count bytes and stop when the configured maximum is exceeded.

Using Request

from urllib.request import Request, urlopen

request = Request(
    "https://api.example.com/items",
    headers={
        "Accept": "application/json",
        "User-Agent": "MyTool/1.0",
    },
    method="GET",
)

with urlopen(request, timeout=10) as response:
    body = response.read(500_000)

Identify automation honestly. Do not impersonate a browser to bypass access policies. Respect rate limits, terms, and robots rules where relevant.

Correct query strings

Use urlencode() rather than manual concatenation.

from urllib.parse import urlencode

params = urlencode({"q": "secure python", "page": 2})
url = f"https://example.com/search?{params}"

The urllib.parse guide covers quoting, URL decomposition, and security validation.

Form POST requests

from urllib.parse import urlencode
from urllib.request import Request, urlopen

form = urlencode({"name": "Ana", "active": "1"}).encode("ascii")
request = Request(
    "https://example.com/form",
    data=form,
    headers={"Content-Type": "application/x-www-form-urlencoded"},
    method="POST",
)

with urlopen(request, timeout=10) as response:
    result = response.read(100_000)

If data is supplied and no method is declared, POST becomes the default. Explicit methods make behavior easier to review.

Sending JSON

import json
from urllib.request import Request, urlopen

payload = json.dumps({"title": "Example"}).encode("utf-8")
request = Request(
    "https://api.example.com/items",
    data=payload,
    headers={
        "Content-Type": "application/json",
        "Accept": "application/json",
    },
    method="POST",
)

with urlopen(request, timeout=10) as response:
    raw = response.read(500_000)
    result = json.loads(raw.decode("utf-8"))

Validate Content-Type, status, and size before parsing. The REST API guide adds response validation, authentication, and retry strategies.

HTTPError and URLError

from urllib.error import HTTPError, URLError

try:
    with urlopen(request, timeout=10) as response:
        body = response.read(100_000)
except HTTPError as error:
    error_body = error.read(20_000)
    print(error.code, error.reason)
except URLError as error:
    print(f"Network failure: {error.reason}")
except TimeoutError:
    print("Request timed out")

HTTPError represents an HTTP error response and remains readable as a response object. URLError covers DNS, connection, TLS, and protocol failures.

Bounded downloads

from pathlib import Path

MAX_BYTES = 50 * 1024 * 1024

with urlopen(url, timeout=20) as response:
    declared = response.headers.get("Content-Length")
    if declared and int(declared) > MAX_BYTES:
        raise ValueError("Declared file is too large")

    total = 0
    with Path("download.tmp").open("wb") as output:
        while chunk := response.read(64 * 1024):
            total += len(chunk)
            if total > MAX_BYTES:
                raise ValueError("Download exceeded limit")
            output.write(chunk)

Write to a temporary file, validate its format or digest, and atomically move it into place. The hashlib guide shows SHA-256 verification.

Compressed responses

urllib.request does not transparently handle every content encoding. If you request gzip, inspect the header and limit compressed and expanded data.

import gzip

encoding = response.headers.get("Content-Encoding", "").lower()
raw = response.read(2_000_000)
data = gzip.decompress(raw) if encoding == "gzip" else raw

For large or untrusted content, use incremental decompression with an output cap. See the Python gzip guide.

TLS and private CAs

import ssl

context = ssl.create_default_context(cafile="company-ca.pem")
with urlopen(request, timeout=10, context=context) as response:
    body = response.read(100_000)

HTTPS certificate and hostname validation are secure by default. Do not disable them. Fix the trust chain instead. The Python ssl guide explains TLS contexts.

Redirect behavior

The default opener follows HTTP redirects. Inspect response.url to learn the final destination. Sensitive headers should not be forwarded to an unexpected host. Use add_unredirected_header() or a restrictive redirect handler for credentials.

Responses 301 and 302 may change POST to GET, matching common browser behavior. Codes 307 and 308 preserve the method. Review this policy for state-changing requests.

Disabling automatic redirects

from urllib.request import build_opener, HTTPRedirectHandler

class NoRedirect(HTTPRedirectHandler):
    def redirect_request(self, req, fp, code, msg, headers, newurl):
        return None

opener = build_opener(NoRedirect())

If redirects are allowed, cap their count and validate the scheme, hostname, port, and resolved IP at every hop.

Environment proxies

The default opener can read http_proxy, https_proxy, and system settings. Sensitive services should not depend on uncontrolled environment state.

from urllib.request import ProxyHandler, build_opener

opener = build_opener(ProxyHandler({}))

An explicit empty mapping disables auto-detected proxies. Configure approved proxies directly and keep credentials out of source code.

Basic and Digest authentication

HTTPBasicAuthHandler and HTTPDigestAuthHandler integrate credentials into an opener. Basic authentication only encodes credentials and therefore requires HTTPS. Scope each credential to the intended URI. Python 3.14 added SHA-256 support to Digest authentication.

SSRF protection

Never pass an attacker-provided URL directly to urlopen(). The library supports more than HTTPS, including file:, data:, and FTP. An application can accidentally read local files or contact internal services.

Parse the URL, allow only HTTPS, normalize the hostname, resolve DNS, block private, loopback, link-local, and cloud-metadata addresses, and repeat validation after each redirect. Account for DNS rebinding and differences between validated and connected addresses.

Safe retries

The module does not provide a complete retry policy. Retry only transient failures and idempotent operations, using exponential backoff and a maximum attempt count. A POST may have succeeded even when its response was lost. Use an idempotency key when the API supports one.

Common mistakes

Common problems include missing timeouts, unlimited reads, assuming an encoding, disabling TLS verification, following redirects without destination checks, leaking Authorization headers, accepting arbitrary schemes, inheriting untrusted proxy variables, and blindly retrying POST.

Best practices

Create explicit Request objects, set timeouts, cap response and decompression sizes, validate status and media type, use secure HTTPS contexts, control redirects and proxies, and close responses with with. For complex applications, prefer a maintained HTTP client with connection pooling and richer policies.

Conclusion

urllib.request is a capable dependency-free HTTP client. It works well for controlled scripts when timeouts, limits, TLS, redirects, and external URLs are handled explicitly. Its handler architecture is powerful, but safe usage requires understanding each stage.

Read the official urllib.request documentation and RFC 9110 for HTTP semantics.

Share:

Facebook
WhatsApp
Twitter
LinkedIn

Article content

    Related articles

    Ethernet cables connected to a network switch representing Python socketserver services
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python socketserver: Build Servers

    Learn Python socketserver to build TCP and UDP servers with handlers, concurrency, framing, limits, timeouts, and graceful shutdown.

    Ler mais

    Tempo de leitura: 5 minutos
    19/08/2026
    Illuminated server room representing secure TLS connections with Python ssl
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python ssl: Secure TLS Connections

    Learn Python ssl to build TLS clients and servers, validate certificates and hostnames, configure CA trust, minimum versions, and mTLS.

    Ler mais

    Tempo de leitura: 5 minutos
    19/08/2026
    Account verification screen representing message authentication with Python HMAC
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python HMAC: Authenticate Messages

    Learn Python HMAC to sign and verify webhooks, files, and service messages with SHA-256, safe keys, canonical data, and replay

    Ler mais

    Tempo de leitura: 5 minutos
    19/08/2026
    Fingerprint scanner representing digest verification with Python hashlib
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python hashlib: Secure Hashes

    Learn Python hashlib for SHA-256, file verification, BLAKE2, key derivation, password hashing decisions, and safer digest handling.

    Ler mais

    Tempo de leitura: 5 minutos
    19/08/2026
    Projected binary code representing Python binascii conversions
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python binascii: Binary and ASCII

    Learn Python binascii to convert hexadecimal, Base64 and quoted-printable, calculate CRC values, and validate binary data safely.

    Ler mais

    Tempo de leitura: 5 minutos
    18/08/2026
    A developer typing code on a laptop with a Python book beside in an office.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python codecs: Master Encodings

    Learn Python codecs to inspect encodings, handle errors and BOMs, process incremental streams, and migrate codecs.open to open.

    Ler mais

    Tempo de leitura: 6 minutos
    18/08/2026