El módulo urllib.request permite abrir URLs y realizar solicitudes HTTP utilizando únicamente la biblioteca estándar. Proporciona urlopen(), objetos Request configurables y una arquitectura de handlers para redirects, autenticación, cookies, proxies y HTTPS.
Clientes superiores como Requests o HTTPX son más cómodos en aplicaciones grandes, pero urllib.request es útil en scripts portables, instaladores, herramientas administrativas y entornos sin dependencias externas. Esta guía cubre GET, POST, JSON, descargas limitadas, TLS, redirects, proxies, errores, retries y protección frente a URLs no confiables.
Primera solicitud GET
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)
La respuesta funciona como context manager y expone status, headers y url. El cuerpo son bytes porque el cliente no puede determinar automáticamente el encoding correcto.
charset = response.headers.get_content_charset() or "utf-8"
text = data.decode(charset, errors="replace")
La guía de codecs en Python explica la frontera entre texto y bytes.
Usa siempre timeout
Sin timeout, DNS, conexión, TLS o lectura pueden bloquear el programa durante mucho tiempo.
with urlopen(url, timeout=10) as response:
body = response.read(1_000_000)
El timeout no limita el tamaño. Un servidor puede continuar enviando datos mientras la conexión sigue activa. Cuenta los bytes y detén la lectura al superar el máximo configurado.
Objeto Request
from urllib.request import Request, urlopen
request = Request(
"https://api.example.com/items",
headers={
"Accept": "application/json",
"User-Agent": "MiHerramienta/1.0",
},
method="GET",
)
with urlopen(request, timeout=10) as response:
body = response.read(500_000)
Identifica la automatización honestamente. No suplantes un navegador para evitar políticas. Respeta límites, términos y robots cuando corresponda.
Query strings correctas
from urllib.parse import urlencode
params = urlencode({"q": "python seguro", "page": 2})
url = f"https://example.com/search?{params}"
La guía de urllib.parse en Python cubre quoting, análisis y validación de URLs.
POST de formulario
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)
Si se proporciona data sin método, POST se convierte en el valor por defecto. Declararlo facilita la revisión.
Enviar JSON
import json
from urllib.request import Request, urlopen
payload = json.dumps({"title": "Ejemplo"}).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"))
Valida Content-Type, estado y tamaño antes de procesar. La guía para integrar APIs con Python complementa autenticación y errores.
HTTPError y 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"Fallo de red: {error.reason}")
except TimeoutError:
print("Tiempo agotado")
HTTPError representa una respuesta de error y todavía permite leer un cuerpo limitado. URLError cubre DNS, conexión, TLS y protocolos.
Descargas con límite
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("El archivo declarado es demasiado grande")
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("La descarga superó el límite")
output.write(chunk)
Escribe en un temporal, valida formato o digest y mueve de forma atómica. La guía de hashlib en Python muestra verificación SHA-256.
Contenido comprimido
urllib.request no descomprime automáticamente todos los encodings. Si solicitas gzip, inspecciona la cabecera y limita entrada y salida.
import gzip
encoding = response.headers.get("Content-Encoding", "").lower()
raw = response.read(2_000_000)
data = gzip.decompress(raw) if encoding == "gzip" else raw
Para contenido grande o externo usa descompresión incremental con máximo. Consulta la guía de gzip en Python.
TLS y CA privada
import ssl
context = ssl.create_default_context(cafile="empresa-ca.pem")
with urlopen(request, timeout=10, context=context) as response:
body = response.read(100_000)
HTTPS valida certificado y hostname de forma segura. No desactives la comprobación. La guía de ssl en Python explica los contextos TLS.
Redirects
El opener por defecto sigue redirects. Revisa response.url para conocer el destino final. Los headers sensibles no deben enviarse a dominios inesperados. Usa add_unredirected_header() o un handler restrictivo.
Los códigos 301 y 302 pueden convertir POST en GET. Los códigos 307 y 308 conservan el método. Revisa esta política en operaciones que modifican estado.
Desactivar redirects automáticos
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())
Si permites redirects, limita la cantidad y valida esquema, hostname, puerto e IP resuelta en cada salto.
Proxies del entorno
El opener puede leer http_proxy, https_proxy y configuración del sistema. Un servicio sensible no debería depender de un entorno no controlado.
from urllib.request import ProxyHandler, build_opener
opener = build_opener(ProxyHandler({}))
Un diccionario vacío desactiva proxies detectados. Configura proxies aprobados explícitamente y mantén credenciales fuera del código.
Autenticación Basic y Digest
HTTPBasicAuthHandler y HTTPDigestAuthHandler integran credenciales. Basic solo codifica usuario y contraseña y requiere HTTPS. Limita cada credencial al URI correcto. Python 3.14 añadió SHA-256 a Digest.
Protección contra SSRF
No pases una URL del usuario directamente a urlopen(). La biblioteca acepta esquemas como file:, data: y FTP, lo que puede leer archivos locales o acceder a servicios internos.
Permite solo HTTPS, normaliza hostname, resuelve DNS, bloquea direcciones privadas, loopback, link-local y metadata de nube, y repite la validación después de cada redirect. Considera DNS rebinding.
Retries seguros
El módulo no incluye una política completa. Repite solo errores transitorios y métodos idempotentes, con backoff y máximo. Un POST puede haberse procesado aunque la respuesta se perdiera. Usa idempotency key cuando exista.
Errores frecuentes
Los fallos comunes son omitir timeout, leer sin límite, asumir encoding, desactivar TLS, seguir redirects sin validar destino, filtrar Authorization, aceptar cualquier esquema, heredar proxies no confiables y repetir POST ciegamente.
Buenas prácticas
Crea Request explícito, define timeout, limita respuesta y descompresión, valida estado y tipo, usa HTTPS seguro, controla redirects y proxies y cierra con with. Para aplicaciones complejas, prefiere un cliente mantenido con pooling y políticas más completas.
Conclusión
urllib.request es un cliente HTTP funcional sin dependencias. Es adecuado para scripts controlados si timeouts, límites, TLS, redirects y URLs externas se gestionan explícitamente. Su arquitectura de handlers es potente, pero exige comprender cada etapa.
Consulta la documentación oficial de urllib.request y el RFC 9110 sobre semántica HTTP.







