Casi todo el mundo instala requests y casi nadie se pregunta si hace falta. Para una petición suelta, un guion o un programa que no quieres llenar de dependencias, urllib ya viene con Python y basta.
También te puede interesar
Una función que vale para todo
import json, urllib.request, urllib.parse, urllib.error
def pedir(url, datos=None, cabeceras=None, timeout=20):
cuerpo = json.dumps(datos).encode() if datos is not None else None
cab = {"User-Agent": "mi-programa/1.0", "Accept": "application/json"}
if datos is not None:
cab["Content-Type"] = "application/json"
cab.update(cabeceras or {})
req = urllib.request.Request(url, data=cuerpo, headers=cab,
method="POST" if datos is not None else "GET")
with urllib.request.urlopen(req, timeout=timeout) as r:
return r.status, r.headers.get("Content-Type"), r.read().decode()
Con eso están cubiertos el GET y el POST. Lo que decide el método es si hay cuerpo, y el data tiene que ir en bytes: de ahí el .encode().
GET
q = urllib.parse.urlencode({"lenguaje": "python", "version": "3.14"})
est, tipo, cuerpo = pedir(f"https://httpbin.org/get?{q}")
salidaestado : 200
tipo : application/json
args : {'lenguaje': 'python', 'version': '3.14'}
urlencode no es opcional: se encarga de los espacios, los acentos y los caracteres raros. Pegar los parámetros a mano funciona hasta el primer valor con un & dentro.
POST con JSON
salidaestado : 200
eco : {'lenguaje': 'python', 'nombre': 'decodigo'}
Un 404 sí lanza excepción
try:
pedir("https://httpbin.org/status/404")
except urllib.error.HTTPError as e:
print(f"HTTPError: {e.code} {e.reason}")
except urllib.error.URLError as e:
print(f"URLError: {e.reason}")
salidaHTTPError: 404 NOT FOUND
HttpClient devuelve el 404 como una respuesta normal y te obliga a mirar el código. En Python, cualquier estado 4xx o 5xx lanza HTTPError, y hay que capturarlo o el programa se cae. El HTTPError además es una respuesta: tiene .code, .reason, .headers y se puede leer su cuerpo, que es donde suele venir el mensaje de error de la API.Y el orden de los except importa: HTTPError hereda de URLError, así que el concreto va primero. URLError es lo que sale cuando ni siquiera hubo respuesta: no hay red, no resuelve el nombre, venció el plazo.
Descargar sin llenar la memoria
import shutil
req = urllib.request.Request(url, headers={"User-Agent": "mi-programa/1.0"})
with urllib.request.urlopen(req, timeout=20) as r, open("descarga.bin", "wb") as f:
shutil.copyfileobj(r, f)
salidadescargados 50000 bytes
copyfileobj copia por trozos. Con r.read() cargarías el archivo entero en memoria, que con un vídeo o un volcado de base de datos no es opción.
¿Y requests?
salidano está: urllib viene con Python, requests hay que instalarlo
| urllib | requests / httpx | |
|---|---|---|
| Instalación | ninguna | pip install |
| Un GET sencillo | 4 líneas | 1 línea |
| JSON | a mano con json |
.json() |
| Sesiones y cookies | a mano | de serie |
| Reintentos, HTTP/2, asíncrono | no | sí (httpx) |
La regla práctica: si el programa ya tiene dependencias y hace más de dos peticiones, usa httpx o requests. Si es un guion suelto que quieres poder copiar a cualquier máquina con Python y que funcione, urllib.
Todo el código se ejecutó con Python 3.14 en un contenedor limpio antes de publicar esta página; las salidas están copiadas de esa ejecución.