Python: Trabajo con APIs (requests)
Aprende a usar la librería requests de Python para hacer llamadas GET y POST, enviar cabeceras, parámetros de consulta, manejar respuestas JSON y gestionar errores.
La librería requests es la forma estándar de realizar llamadas HTTP desde Python. Envuelve el módulo de bajo nivel urllib de Python en una API limpia, de modo que obtener una página web o llamar a una API REST toma una sola línea en lugar de diez. Este capítulo cubre todo lo que necesitas: instalar requests, hacer llamadas GET y POST, enviar cabeceras y parámetros de consulta, trabajar con respuestas JSON, subir archivos, usar sesiones y manejar errores de forma robusta.
Instalación
requests no forma parte de la librería estándar, por lo que se instala con pip:
pip install requestsSi estás trabajando dentro de un entorno virtual (recomendado), actívalo primero para que el paquete esté limitado a tu proyecto. Tras la instalación, verifica que funciona:
import requests
print(requests.__version__) # e.g. 2.32.3Hacer una petición GET
requests.get() envía una petición HTTP GET y devuelve un objeto Response. Esta es la operación más común — se usa para obtener datos de APIs, páginas web y archivos.
import requests
response = requests.get("https://jsonplaceholder.typicode.com/todos/1")
print(response.status_code) # 200
print(response.url) # https://jsonplaceholder.typicode.com/todos/1
print(response.text) # raw response body as a stringSalida esperada:
200
https://jsonplaceholder.typicode.com/todos/1
{"userId": 1, "id": 1, "title": "delectus aut autem", "completed": false}Leer la respuesta como JSON
La mayoría de las APIs modernas devuelven JSON. Llama a .json() en la respuesta en lugar de analizar .text manualmente — llama a json.loads() por ti y devuelve un dict o list de Python.
import requests
response = requests.get("https://jsonplaceholder.typicode.com/todos/1")
data = response.json()
print(data["title"]) # delectus aut autem
print(data["completed"]) # FalseConsulta el capítulo de Python JSON para obtener más detalles sobre cómo se corresponden los objetos de Python con los tipos JSON.
Enviar parámetros de consulta
Los parámetros de consulta son los pares clave-valor que aparecen después del ? en una URL, como ?q=python&page=2. Pásalos como un dict al argumento params — requests los codifica en la URL y los añade automáticamente.
import requests
params = {
"q": "python requests",
"page": 1,
"per_page": 5,
}
response = requests.get("https://httpbin.org/get", params=params)
# requests builds the full URL for you
print(response.url)
# https://httpbin.org/get?q=python+requests&page=1&per_page=5Usa siempre params= en lugar de construir la URL a mano — maneja correctamente los caracteres especiales y la codificación.
Enviar cabeceras de petición
Las cabeceras transportan metadatos: tokens de autenticación, preferencias de tipo de contenido, claves de API y más. Pásalas como un dict a headers=:
import requests
headers = {
"Accept": "application/json",
"Authorization": "Bearer my-api-token",
"User-Agent": "MyApp/1.0",
}
response = requests.get("https://httpbin.org/headers", headers=headers)
print(response.json())Cabeceras comunes que enviarás:
| Cabecera | Propósito |
|---|---|
Authorization | Token de autenticación (Bearer, Basic, etc.) |
Content-Type | Formato del cuerpo de la petición (por ejemplo, application/json) |
Accept | Formato que quieres recibir del servidor |
User-Agent | Identifica tu cliente ante el servidor |
X-API-Key | Clave de API en una cabecera personalizada (varía según el servicio) |
Hacer una petición POST
requests.post() envía datos al servidor — se usa para crear recursos, enviar formularios o llamar a acciones.
Enviar JSON
Pasa un dict de Python a json=. La librería lo serializa y establece la cabecera Content-Type: application/json automáticamente:
import requests
payload = {
"title": "Buy groceries",
"completed": False,
"userId": 1,
}
response = requests.post(
"https://jsonplaceholder.typicode.com/todos",
json=payload,
)
print(response.status_code) # 201 Created
print(response.json())Salida esperada:
201
{'title': 'Buy groceries', 'completed': False, 'userId': 1, 'id': 201}Enviar datos de formulario
Algunas APIs o formularios HTML esperan datos de tipo application/x-www-form-urlencoded. Usa data= en lugar de json=:
import requests
form_data = {
"username": "alice",
"password": "secret",
}
response = requests.post("https://httpbin.org/post", data=form_data)
print(response.status_code)Enviar archivos (subida multipart)
Para subir un archivo, ábrelo en modo binario y pásalo mediante files=:
import requests
with open("report.pdf", "rb") as f:
response = requests.post(
"https://httpbin.org/post",
files={"file": f},
)
print(response.status_code)requests codifica la subida como multipart/form-data, que es lo que esperan la mayoría de los endpoints de carga de archivos.
Otros métodos HTTP
Las APIs REST usan distintos verbos HTTP para diferentes operaciones. requests proporciona una función por método:
import requests
base = "https://jsonplaceholder.typicode.com/todos/1"
# Update a resource (replace entirely)
response = requests.put(base, json={"title": "Updated", "completed": True, "userId": 1})
print(response.status_code) # 200
# Partial update
response = requests.patch(base, json={"completed": True})
print(response.status_code) # 200
# Delete a resource
response = requests.delete(base)
print(response.status_code) # 200Manejo de errores
Verificar códigos de estado
El código de estado HTTP te indica si la petición fue exitosa. Los grupos más importantes:
| Rango | Significado |
|---|---|
| 2xx | Éxito (200 OK, 201 Created, 204 No Content) |
| 3xx | Redirección (manejada automáticamente por requests) |
| 4xx | Error del cliente (400 Bad Request, 401 Unauthorized, 404 Not Found) |
| 5xx | Error del servidor (500 Internal Server Error, 503 Service Unavailable) |
raise_for_status()
Llamar a .raise_for_status() en una respuesta lanza una excepción HTTPError automáticamente si el código de estado es 4xx o 5xx. Esta es la forma más limpia de fallar rápido ante respuestas incorrectas:
import requests
response = requests.get("https://jsonplaceholder.typicode.com/todos/99999")
try:
response.raise_for_status()
data = response.json()
print(data)
except requests.exceptions.HTTPError as err:
print(f"HTTP error: {err}")Sin raise_for_status(), una respuesta 404 parece un éxito — tu código lee un cuerpo de error y lo procesa silenciosamente.
Manejo de errores de red
Los fallos a nivel de red (fallo de resolución DNS, conexión rechazada, tiempo de espera agotado) lanzan requests.exceptions.ConnectionError o requests.exceptions.Timeout. Captura ambos con la clase base requests.exceptions.RequestException:
import requests
try:
response = requests.get("https://api.example.com/data", timeout=5)
response.raise_for_status()
data = response.json()
except requests.exceptions.Timeout:
print("The request timed out — server took too long to respond.")
except requests.exceptions.ConnectionError:
print("Could not connect — check your network or the URL.")
except requests.exceptions.HTTPError as err:
print(f"HTTP error {response.status_code}: {err}")
except requests.exceptions.RequestException as err:
print(f"Unexpected error: {err}")Este patrón cubre toda la jerarquía de excepciones: tiempo de espera, conexión, error HTTP y la clase base que captura todo lo demás. Consulta Python try/except para una visión más detallada del manejo de excepciones en Python.
Establece siempre un tiempo de espera
Por defecto, requests esperará indefinidamente si el servidor no responde. Pasa siempre timeout= para evitar que los programas se queden colgados:
# timeout=(connect_timeout, read_timeout) in seconds
response = requests.get("https://api.example.com/data", timeout=(3, 10))La forma de tupla establece el tiempo de espera de conexión y el tiempo de espera de lectura por separado. Un tiempo de espera de lectura de 10 segundos significa "esperar hasta 10 segundos entre bytes una vez establecida la conexión".
Usar sesiones
Un objeto requests.Session persiste configuraciones — cabeceras, cookies, autenticación — a través de múltiples peticiones al mismo host. También reutiliza la conexión TCP subyacente (agrupación de conexiones), lo que es más rápido que crear una nueva conexión para cada llamada.
import requests
with requests.Session() as session:
# Set headers once — every request in this session will include them
session.headers.update({
"Authorization": "Bearer my-api-token",
"Accept": "application/json",
})
# All requests reuse the connection and headers
r1 = session.get("https://api.example.com/users")
r2 = session.get("https://api.example.com/posts")
r3 = session.post("https://api.example.com/todos", json={"title": "New"})
print(r1.status_code, r2.status_code, r3.status_code)Usa una sesión siempre que hagas más de una petición al mismo servidor.
Inspeccionar la respuesta
El objeto Response expone todo lo que el servidor devolvió:
import requests
response = requests.get("https://httpbin.org/get")
print(response.status_code) # 200
print(response.reason) # OK
print(response.headers) # dict of response headers
print(response.headers["Content-Type"]) # application/json
print(response.encoding) # utf-8
print(response.elapsed) # how long the request took
print(response.url) # final URL (after redirects)Para contenido binario (imágenes, PDFs), usa response.content (devuelve bytes) en lugar de response.text:
import requests
response = requests.get("https://httpbin.org/image/png")
with open("image.png", "wb") as f:
f.write(response.content)Autenticación
HTTP Basic Auth
Pasa una tupla (username, password) a auth=:
import requests
response = requests.get(
"https://httpbin.org/basic-auth/alice/secret",
auth=("alice", "secret"),
)
print(response.status_code) # 200Token Bearer (claves de API)
La mayoría de las APIs modernas usan un token bearer en la cabecera Authorization:
import requests
headers = {"Authorization": "Bearer eyJhbGciOiJIUzI1NiIs..."}
response = requests.get("https://api.example.com/me", headers=headers)Nunca escribas tokens directamente en los archivos fuente. Cárgalos desde variables de entorno o un gestor de secretos:
import os
import requests
token = os.environ["API_TOKEN"]
headers = {"Authorization": f"Bearer {token}"}
response = requests.get("https://api.example.com/me", headers=headers)Ejemplo real — GitHub API
Este ejemplo demuestra un patrón completo: sesión, autenticación bearer, paginación, manejo de errores y análisis de JSON:
import os
import requests
GITHUB_TOKEN = os.environ.get("GITHUB_TOKEN", "")
BASE_URL = "https://api.github.com"
with requests.Session() as session:
session.headers.update({
"Authorization": f"Bearer {GITHUB_TOKEN}",
"Accept": "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28",
})
try:
# Fetch the first page of public repos for a user
response = session.get(
f"{BASE_URL}/users/torvalds/repos",
params={"per_page": 5, "sort": "updated"},
timeout=10,
)
response.raise_for_status()
repos = response.json()
for repo in repos:
print(f"{repo['name']:40s} ★ {repo['stargazers_count']}")
except requests.exceptions.HTTPError as err:
print(f"GitHub API error: {err}")
except requests.exceptions.RequestException as err:
print(f"Network error: {err}")Este patrón — sesión con cabeceras compartidas, raise_for_status(), try/except delimitado — es el enfoque listo para producción para cualquier cliente de API que escribas.
Peticiones concurrentes con asyncio
Para programas que llaman a muchos endpoints a la vez, la librería sincrónica requests bloquea en cada llamada. Cambia a aiohttp (el equivalente asíncrono) y combínalo con el módulo asyncio de Python:
import asyncio
import aiohttp
async def fetch(session, url):
async with session.get(url) as response:
return await response.json()
async def main():
urls = [
"https://jsonplaceholder.typicode.com/todos/1",
"https://jsonplaceholder.typicode.com/todos/2",
"https://jsonplaceholder.typicode.com/todos/3",
]
async with aiohttp.ClientSession() as session:
results = await asyncio.gather(*[fetch(session, u) for u in urls])
for r in results:
print(r["title"])
asyncio.run(main())Consulta el capítulo de Python asyncio para entender cómo funciona async/await antes de adoptar este patrón.
Referencia rápida
| Tarea | Código |
|---|---|
| Petición GET | requests.get(url) |
| GET con parámetros | requests.get(url, params={"key": "val"}) |
| GET con cabeceras | requests.get(url, headers={"Authorization": "Bearer token"}) |
| POST con cuerpo JSON | requests.post(url, json={"key": "val"}) |
| POST con datos de formulario | requests.post(url, data={"key": "val"}) |
| Subir un archivo | requests.post(url, files={"file": open("f.pdf", "rb")}) |
| PUT / PATCH / DELETE | requests.put/patch/delete(url, json=...) |
| Verificar estado | response.status_code |
| Lanzar excepción en 4xx/5xx | response.raise_for_status() |
| Analizar cuerpo JSON | response.json() |
| Cuerpo de texto sin formato | response.text |
| Cuerpo en bytes sin formato | response.content |
| Establecer tiempo de espera | requests.get(url, timeout=5) |
| Reutilizar conexión | with requests.Session() as s: ... |
Capítulos relacionados
- Python pip — instala
requestsy gestiona las dependencias del proyecto - Python JSON — comprende la codificación JSON que impulsa la mayoría de las respuestas de API
- Python try/except — escribe un manejo robusto de excepciones en torno a las llamadas de red
- Python Virtual Environments — aísla las dependencias del proyecto
- Python asyncio — llamadas HTTP concurrentes sin bloqueo