En esta página

HTTP y requests

12 min lectura TextoCap. 5 — Python para web

¿Qué es HTTP y por qué importa?

HTTP (HyperText Transfer Protocol) es el protocolo que utilizan los navegadores, aplicaciones móviles y servicios backend para comunicarse. En Python, consumir APIs HTTP es una de las tareas más frecuentes: desde obtener datos de servicios externos hasta integrar sistemas.

Los conceptos clave que debes conocer antes de continuar:

  • Métodos HTTP: GET (obtener datos), POST (crear), PUT/PATCH (actualizar), DELETE (eliminar)
  • URL: la dirección del recurso (https://api.ejemplo.com/usuarios/42)
  • Headers: metadatos de la petición/respuesta (Content-Type, Authorization, Accept)
  • Body: el cuerpo de la petición (para POST/PUT), generalmente en formato JSON
  • Status codes: código numérico que indica el resultado (200 OK, 404 Not Found, 500 Internal Server Error)

Instalando requests

pip install requests

requests es la librería HTTP de Python más popular. Su lema es "HTTP para humanos" — tiene una API mucho más sencilla que el módulo urllib incluido en la biblioteca estándar.

GET — Obtener datos

import requests

# Petición GET simple
respuesta = requests.get("https://jsonplaceholder.typicode.com/posts/1")

# Información de la respuesta
print(respuesta.status_code)   # 200
print(respuesta.headers["Content-Type"])  # application/json; charset=utf-8
print(respuesta.encoding)      # utf-8
print(respuesta.url)           # URL final (después de redirecciones)

# Cuerpo de la respuesta
datos = respuesta.json()       # Parsea JSON automáticamente
print(datos["title"])

texto = respuesta.text          # Como string
bytes_raw = respuesta.content   # Como bytes (para imágenes, PDFs, etc.)

# GET con parámetros de consulta (query params)
params = {
    "userId": 1,
    "_limit": 5,
    "_sort": "id",
    "_order": "desc"
}

respuesta = requests.get(
    "https://jsonplaceholder.typicode.com/posts",
    params=params
)
# URL resultante: .../posts?userId=1&_limit=5&_sort=id&_order=desc
print(f"URL: {respuesta.url}")
posts = respuesta.json()
print(f"Posts obtenidos: {len(posts)}")

POST — Enviar datos

import requests
import json

# POST con JSON (el más común en APIs modernas)
nuevo_post = {
    "title": "Mi primer post desde Python",
    "body": "Contenido del artículo creado con requests",
    "userId": 1
}

respuesta = requests.post(
    "https://jsonplaceholder.typicode.com/posts",
    json=nuevo_post,  # Serializa automáticamente y pone Content-Type: application/json
    headers={"Authorization": "Bearer mi-token-secreto"}
)

print(respuesta.status_code)  # 201 Created
post_creado = respuesta.json()
print(f"ID asignado: {post_creado['id']}")

# POST con datos de formulario (form-data)
respuesta_form = requests.post(
    "https://httpbin.org/post",
    data={"usuario": "ana", "contraseña": "secreto123"}
    # Content-Type: application/x-www-form-urlencoded
)

# POST con archivo multipart
with open("imagen.png", "rb") as img:
    respuesta_archivo = requests.post(
        "https://httpbin.org/post",
        files={"archivo": ("imagen.png", img, "image/png")},
        data={"descripcion": "Mi foto de perfil"}
    )

PUT, PATCH y DELETE

# PUT — reemplazar completamente un recurso
respuesta_put = requests.put(
    "https://jsonplaceholder.typicode.com/posts/1",
    json={"id": 1, "title": "Título actualizado", "body": "Contenido nuevo", "userId": 1}
)
print(f"PUT: {respuesta_put.status_code}")  # 200

# PATCH — actualización parcial
respuesta_patch = requests.patch(
    "https://jsonplaceholder.typicode.com/posts/1",
    json={"title": "Solo el título cambia"}
)
print(f"PATCH: {respuesta_patch.status_code}")  # 200

# DELETE
respuesta_delete = requests.delete("https://jsonplaceholder.typicode.com/posts/1")
print(f"DELETE: {respuesta_delete.status_code}")  # 200

Manejo robusto de errores

import requests
from requests.exceptions import (
    RequestException,
    Timeout,
    ConnectionError,
    HTTPError,
    TooManyRedirects
)

def hacer_peticion_segura(url: str, **kwargs) -> dict | None:
    """
    Realiza una petición HTTP con manejo completo de errores.
    Devuelve los datos JSON o None si hay error.
    """
    try:
        respuesta = requests.get(url, timeout=(5, 30), **kwargs)

        # raise_for_status() lanza HTTPError para códigos 4xx y 5xx
        respuesta.raise_for_status()

        return respuesta.json()

    except Timeout:
        print("⚠️  La petición excedió el tiempo de espera")
    except ConnectionError:
        print("⚠️  No se pudo conectar al servidor")
    except TooManyRedirects:
        print("⚠️  Demasiadas redirecciones")
    except HTTPError as e:
        codigo = e.response.status_code
        if codigo == 400:
            print(f"❌ Petición inválida (400): {e.response.json()}")
        elif codigo == 401:
            print("❌ No autorizado (401): revisa las credenciales")
        elif codigo == 403:
            print("❌ Prohibido (403): sin permisos")
        elif codigo == 404:
            print(f"❌ Recurso no encontrado (404): {url}")
        elif codigo == 429:
            print("❌ Límite de peticiones excedido (429)")
        elif codigo >= 500:
            print(f"❌ Error del servidor ({codigo})")
        else:
            print(f"❌ Error HTTP {codigo}: {e}")
    except RequestException as e:
        print(f"❌ Error de red inesperado: {e}")
    except ValueError:
        print("❌ La respuesta no es JSON válido")

    return None

Session — Reutilizar conexiones y configuración

import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

def crear_sesion(reintentos: int = 3) -> requests.Session:
    """Crea una sesión configurada con reintentos automáticos."""
    sesion = requests.Session()

    # Configurar reintentos automáticos
    estrategia_reintento = Retry(
        total=reintentos,
        backoff_factor=1,  # Espera: 1s, 2s, 4s
        status_forcelist=[429, 500, 502, 503, 504],
    )
    adaptador = HTTPAdapter(max_retries=estrategia_reintento)
    sesion.mount("https://", adaptador)
    sesion.mount("http://", adaptador)

    # Headers comunes para todas las peticiones de esta sesión
    sesion.headers.update({
        "Authorization": "Bearer mi-token-de-api",
        "Accept": "application/json",
        "User-Agent": "MiApp/1.0 Python/3.14"
    })

    return sesion

# Usar la sesión (más eficiente que requests.get() individual)
with crear_sesion() as sesion:
    usuarios = sesion.get("https://api.ejemplo.com/usuarios").json()
    pedidos = sesion.get("https://api.ejemplo.com/pedidos").json()
    # La conexión TCP se reutiliza entre peticiones

httpx — La alternativa moderna con soporte async

pip install httpx
import httpx

# API síncrona (casi idéntica a requests)
with httpx.Client(timeout=10.0, base_url="https://jsonplaceholder.typicode.com") as cliente:
    respuesta = cliente.get("/users/1")
    respuesta.raise_for_status()
    usuario = respuesta.json()
    print(f"Usuario: {usuario['name']}")


# API asíncrona (ventaja principal de httpx)
import asyncio

async def obtener_multiples_usuarios(ids: list[int]) -> list[dict]:
    """Obtiene múltiples usuarios en paralelo con async."""
    async with httpx.AsyncClient(
        base_url="https://jsonplaceholder.typicode.com",
        timeout=10.0
    ) as cliente:
        # Crear todas las tareas y ejecutarlas concurrentemente
        tareas = [cliente.get(f"/users/{uid}") for uid in ids]
        respuestas = await asyncio.gather(*tareas)

        usuarios = []
        for resp in respuestas:
            resp.raise_for_status()
            usuarios.append(resp.json())
        return usuarios


async def main() -> None:
    ids = [1, 2, 3, 4, 5]
    usuarios = await obtener_multiples_usuarios(ids)
    for u in usuarios:
        print(f"  {u['id']:2}. {u['name']:25}{u['email']}")

# Ejecutar la función asíncrona
asyncio.run(main())

Ejemplo práctico: cliente para una API pública

import requests
from dataclasses import dataclass

@dataclass
class Post:
    id: int
    user_id: int
    title: str
    body: str

class ClienteJSONPlaceholder:
    """Cliente para la API de prueba JSONPlaceholder."""

    BASE_URL = "https://jsonplaceholder.typicode.com"

    def __init__(self) -> None:
        self._sesion = requests.Session()
        self._sesion.headers["Accept"] = "application/json"

    def obtener_posts(self, user_id: int | None = None) -> list[Post]:
        """Obtiene posts, opcionalmente filtrados por usuario."""
        params = {"userId": user_id} if user_id else {}
        respuesta = self._sesion.get(f"{self.BASE_URL}/posts", params=params, timeout=10)
        respuesta.raise_for_status()
        return [Post(
            id=p["id"],
            user_id=p["userId"],
            title=p["title"],
            body=p["body"]
        ) for p in respuesta.json()]

    def crear_post(self, user_id: int, title: str, body: str) -> Post:
        """Crea un nuevo post."""
        respuesta = self._sesion.post(
            f"{self.BASE_URL}/posts",
            json={"userId": user_id, "title": title, "body": body},
            timeout=10
        )
        respuesta.raise_for_status()
        datos = respuesta.json()
        return Post(id=datos["id"], user_id=datos["userId"],
                    title=datos["title"], body=datos["body"])

    def __enter__(self) -> "ClienteJSONPlaceholder":
        return self

    def __exit__(self, *args) -> None:
        self._sesion.close()


# Usar el cliente
with ClienteJSONPlaceholder() as cliente:
    posts = cliente.obtener_posts(user_id=1)
    print(f"Posts del usuario 1: {len(posts)}")
    for post in posts[:3]:
        print(f"  [{post.id}] {post.title[:50]}...")

    nuevo = cliente.crear_post(1, "Mi Post de Prueba", "Contenido del post")
    print(f"\nPost creado con ID: {nuevo.id}")

Resumen

requests es la librería estándar de facto para HTTP en Python: simple, legible y robusta. Usa siempre timeout, raise_for_status() y Session para código de producción. Para aplicaciones asíncronas, httpx ofrece casi la misma API con soporte nativo para async/await. En la próxima lección construiremos una API completa con FastAPI.

Usa Session para múltiples peticiones al mismo servidor
requests.Session() reutiliza la conexión TCP subyacente (keep-alive), lo que es significativamente más eficiente cuando haces múltiples peticiones al mismo servidor. También permite configurar headers y autenticación una sola vez.
Siempre especifica timeout en requests
Por defecto, requests espera indefinidamente. Siempre pasa timeout=(connect_timeout, read_timeout) o un valor simple en segundos. Sin timeout, tu programa puede bloquearse para siempre si el servidor no responde.
httpx es la alternativa moderna y asíncrona a requests
httpx ofrece una API casi idéntica a requests pero también soporta async/await (HTTP/2, HTTP/3). Para aplicaciones asíncronas con FastAPI o asyncio, usa httpx.AsyncClient en lugar de requests.