Traer datos de internet, en unas pocas líneas
La mayoría de programas Python reales eventualmente necesitan hablar con algo por red — una API REST, un servicio de meteorología, un endpoint de GitHub, una descarga. La librería estándar puede hacer esto (vía urllib), pero la herramienta de facto en el mundo Python es una librería de terceros llamada requests. La API es tan mucho más amigable que vale la pena el único pip install.
pip install requests
Si no estás ya trabajando dentro de un entorno virtual, configura uno primero — mantiene esta instalación dentro del alcance de tu proyecto.
Una primera petición GET
Tres líneas: llama a get, comprueba el código de estado, mira el body. response.text es el body de la respuesta como string. Truncado aquí porque el JSON completo es largo.
Códigos de estado que vale la pena reconocer a nivel de chuleta:
- 200 — OK, todo funcionó.
- 201 — Creado (respuesta común a POST).
- 301 / 302 — redirects;
requestslos sigue automáticamente por defecto. - 400 — bad request; algo de lo que enviaste estaba mal.
- 401 / 403 — no autenticado / no autorizado.
- 404 — el recurso no existe.
- 429 — rate limited; baja el ritmo.
- 500 — error del servidor.
Parsear una respuesta JSON
Cuando el endpoint devuelve JSON, llama a .json() en la respuesta — parsea el body y te da un dict (o lista):
Por debajo, .json() es lo mismo que json.loads(response.text) — solo un atajo para el caso común.
Enviar parámetros de query
No pegues manualmente ?key=value&... a la URL. Pasa un dict a params=:
requests maneja la codificación de URL por ti — espacios, caracteres especiales, Unicode, todo funciona con seguridad.
La URL real que salió está disponible en response.url, útil para depurar.
Peticiones POST con cuerpos JSON
Para enviar datos — crear recursos, enviar formularios, llamar endpoints mutantes — usa requests.post:
import requests
payload = {
"title": "Docs update",
"body": "Added HTTP requests page.",
"labels": ["docs"],
}
response = requests.post(
"https://api.example.com/issues",
json=payload,
headers={"Authorization": "Bearer YOUR_TOKEN"},
)
print(response.status_code)
print(response.json())
El argumento json= hace dos cosas: serializa payload a JSON y pone Content-Type: application/json. Podrías hacer ambas cosas a mano con data=json.dumps(payload) y una cabecera explícita, pero json= es el atajo idiomático.
Para datos con codificación de formulario (el tipo que envía un formulario HTML clásico), usa data= en su lugar:
requests.post("https://example.com/login", data={"user": "rosa", "password": "..."})
Cabeceras
Pasa cualquier cabecera personalizada como un dict:
import requests
response = requests.get(
"https://api.example.com/profile",
headers={
"Authorization": "Bearer abc123",
"User-Agent": "my-tool/1.0",
},
)
La mayoría de APIs quieren una cabecera Authorization para autenticación. El esquema exacto (Bearer, Basic, Token) está en su documentación.
Los timeouts no son opcionales
Por defecto, requests esperará para siempre una respuesta. En un programa real, eso convierte un servidor inestable en un script colgado. Pasa siempre un timeout:
import requests
try:
response = requests.get("https://api.example.com/slow", timeout=5)
except requests.Timeout:
print("Server took too long.")
El número son segundos. timeout=5 significa "ríndete si no tenemos respuesta en 5 segundos". Puedes pasar una tupla (connect_timeout, read_timeout) para más control.
Manejo de errores
Pueden pasar dos tipos de problemas:
- Errores a nivel HTTP (4xx, 5xx) — el servidor respondió, pero la respuesta es un error.
response.status_codete lo dice. - Problemas a nivel de red — timeouts, fallos de DNS, hosts inalcanzables. Estos lanzan excepciones.
El patrón idiomático combina ambos:
raise_for_status() es un no-op para respuestas 2xx y un lanzador de excepción en otro caso. RequestException es la clase base para cada error que requests lanza — un solo catch para "cualquier cosa que fuera mal hablando con este endpoint".
Descargar un archivo
Para descargas binarias grandes, hacer streaming de la respuesta para que no se quede en memoria:
import requests
url = "https://example.com/large.zip"
with requests.get(url, stream=True, timeout=30) as r:
r.raise_for_status()
with open("large.zip", "wb") as f:
for chunk in r.iter_content(chunk_size=8192):
f.write(chunk)
stream=True le dice a requests que no precargue el body. iter_content(chunk_size=...) produce el body un trozo a la vez, que escribes directo a disco.
Sesiones: reutilizar conexiones y defaults
Si vas a hacer varias peticiones al mismo servicio, usa una Session. Reutiliza la conexión TCP subyacente (más rápido) y te deja establecer defaults una vez:
import requests
session = requests.Session()
session.headers.update({"Authorization": "Bearer abc123"})
# Cada petición a través de esta sesión lleva la cabecera.
a = session.get("https://api.example.com/users/1")
b = session.get("https://api.example.com/users/2")
c = session.post("https://api.example.com/users", json={"name": "Rosa"})
Para scripts que golpean la misma API docenas de veces, una sesión es una aceleración significativa.
Un ejemplo realista: pequeño cliente de GitHub
Traer la última release de un repo:
import requests
def latest_release(owner, repo):
url = f"https://api.github.com/repos/{owner}/{repo}/releases/latest"
response = requests.get(url, timeout=10)
response.raise_for_status()
data = response.json()
return {
"tag": data["tag_name"],
"name": data["name"],
"published": data["published_at"],
"url": data["html_url"],
}
release = latest_release("python", "cpython")
print(release)
Menos de quince líneas: construye la URL, pídela, comprueba errores, extrae los campos que te importan. Esa es la forma de la mayoría de clientes de API que escribirás.
¿Y qué hay de urllib?
El urllib.request de la librería estándar puede hacer todo lo que requests puede — en más líneas y con peor ergonomía. Si absolutamente no puedes añadir una dependencia, está ahí:
import json
import urllib.request
with urllib.request.urlopen("https://api.github.com/repos/python/cpython") as r:
data = json.loads(r.read().decode("utf-8"))
print(data["name"])
Para cualquier cosa más allá de un script rápido, requests (o httpx si necesitas async) vale la pena la instalación.
Unos cuantos hábitos
- Pon siempre un timeout. Sin excepciones.
- Usa
raise_for_status()en scripts que esperan éxito — convierte una respuesta mala en una excepción ruidosa. - Pasa dicts a
params=yjson=, no strings construidos a mano. - Envuelve llamadas relacionadas en una
Sessioncuando estés golpeando la misma API muchas veces. - Loguea
response.status_codeyresponse.textcuando depures — el body normalmente te dice exactamente qué le disgustó al servidor.
Siguiente: fechas y horas
Con requests en tu caja de herramientas, puedes hablar con cualquier API web moderna, descargar archivos y construir pequeñas integraciones entre servicios. Combinado con las páginas de JSON y CSV anteriores, ahora tienes el bucle completo "trae datos, léelos, haz algo con ellos, escríbelos de vuelta" — la forma de una enorme cantidad de scripts reales de Python. La mayor parte de esos datos llevan un timestamp, y la siguiente página cubre cómo representa Python fechas, horas y las trampas de timezone que vale la pena evitar.
Preguntas frecuentes
¿Cómo hago una petición HTTP en Python?
Instala la librería requests con pip install requests, luego llama a requests.get(url) para un GET o requests.post(url, json=...) para un POST. El objeto response tiene .status_code, .text, .json() y .headers. Ejemplo: r = requests.get('https://api.example.com/users/1').
¿Debería usar requests o urllib?
requests para cualquier cosa que escribirías a mano — su API es drásticamente más amigable. urllib está incorporado y está bien cuando añadir una dependencia es imposible, pero requiere más código para cosas como cuerpos JSON y sesiones. Para producción, muchos equipos también usan httpx (una librería compatible con requests con soporte async).
¿Cómo envío JSON en una petición POST en Python?
Pasa json={'key': 'value'} a requests.post(...). requests serializa el dict a JSON y pone Content-Type: application/json por ti. No pases data= y json= a la vez — elige uno.
¿Cómo manejo errores con la librería requests?
Comprueba response.status_code (200 significa éxito), o llama a response.raise_for_status() para lanzar una excepción con 4xx/5xx. Los problemas a nivel de red (timeouts, fallos de DNS) lanzan subclases de requests.RequestException — atrapa esa para cubrir ambos.