Session, reintentos y archivos con Requests [Python]
Cuando tu código hace muchas peticiones, conviene ser eficiente y resistente a fallos puntuales. Aquí ves cómo con sesiones, reintentos y transferencia de archivos.
Por qué importa la eficiencia
Cada llamada suelta a requests.get() abre una conexión nueva: resuelve el DNS, establece la conexión TCP y negocia TLS antes de enviar un solo byte útil.
Con una petición aislada apenas se nota, pero si consultas una API cien veces, ese trabajo repetido se acumula. Las sesiones, los reintentos y el manejo correcto de archivos hacen que tu código sea más rápido y más tolerante a los fallos de red.
Paso 7: Session para reutilizar conexiones y cabeceras
Si haces varias peticiones al mismo servidor, usa requests.Session(): reutiliza la conexión TCP (más rápido), conserva las cookies y comparte cabeceras y autenticación entre todas las llamadas.
import requests
with requests.Session() as s:
# Se aplican a todas las peticiones de la sesión
s.headers.update({"User-Agent": "agusbot-tutorial/1.0"})
s.auth = ("agus", "secreto")
# El servidor guarda una cookie...
s.get("https://httpbin.org/cookies/set?sesion=abc123", timeout=10)
# ...y la sesión la reenvía sola
r = s.get("https://httpbin.org/cookies", timeout=10)
print(r.json()) # {'cookies': {'sesion': 'abc123'}}
El bloque with cierra las conexiones al terminar. Una limitación a recordar: la sesión no tiene un timeout global, así que sigues pasándolo en cada llamada (o lo fijas con un adaptador, como en el paso siguiente).
Paso 8: Reintentos automáticos con Retry
Las redes fallan a veces: un 503 puntual o un límite de peticiones (429) no deberían romper tu script. requests delega los reintentos en urllib3, y se configuran una sola vez en la sesión.
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
class TimeoutAdapter(HTTPAdapter):
"""Adaptador que aplica un timeout por defecto a toda la sesión."""
def __init__(self, *args, timeout=10, **kwargs):
self.timeout = timeout
super().__init__(*args, **kwargs)
def send(self, request, **kwargs):
kwargs["timeout"] = kwargs.get("timeout") or self.timeout
return super().send(request, **kwargs)
def crear_sesion():
reintentos = Retry(
total=3, # máximo de reintentos
backoff_factor=1, # espera creciente entre intentos
status_forcelist=[429, 500, 502, 503, 504],
allowed_methods=["GET", "HEAD", "OPTIONS"],
)
adaptador = TimeoutAdapter(max_retries=reintentos, timeout=10)
s = requests.Session()
s.mount("https://", adaptador)
s.mount("http://", adaptador)
return s
sesion = crear_sesion()
r = sesion.get("https://httpbin.org/status/200")
print(r.status_code)
Por defecto solo se reintentan métodos idempotentes (GET, HEAD, OPTIONS…). No añadas POST a allowed_methods a menos que tu API garantice que repetir la petición no duplica datos, por ejemplo un pago o un pedido.
Cómo funciona la espera entre reintentos
Con backoff_factor la espera crece de forma exponencial en cada intento fallido, para no saturar más a un servidor que ya está en apuros. El primer reintento suele ser inmediato y los siguientes esperan cada vez más: con un factor de 1, aproximadamente 2 y luego 4 segundos, aunque el cálculo exacto depende de la versión de urllib3. Además, si el servidor responde con la cabecera Retry-After (típico de los códigos 429 y 503), Retry la respeta por defecto. Elige total y backoff_factor pensando en cuánto tiempo puede esperar realmente tu programa: tres reintentos con factor 1 ya suman varios segundos de espera.
Paso 9: Descargar y subir archivos
Para descargar archivos grandes usa stream=True: requests no carga todo en memoria, sino que lo lee por bloques.
import requests
url = "https://httpbin.org/bytes/102400" # 100 KB de datos de prueba
with requests.get(url, stream=True, timeout=10) as r:
r.raise_for_status()
with open("descarga.bin", "wb") as f:
for bloque in r.iter_content(chunk_size=8192):
f.write(bloque)
print("Descarga completa")
Para subir un archivo usa files=; requests arma la petición multipart/form-data por ti:
import requests
with open("reporte.txt", "rb") as archivo: # abrir en modo binario
r = requests.post(
"https://httpbin.org/post",
files={"archivo": archivo},
timeout=30,
)
print(r.status_code)
print(list(r.json()["files"])) # ['archivo']
Detalles que conviene conocer sobre archivos
- Elige bien el
chunk_size. Bloques de 8 KB a 1 MB funcionan bien; uno muy pequeño hace muchas iteraciones y uno enorme consume memoria innecesaria. - Usa siempre
with. Constream=Truela conexión no se libera hasta que consumes todo el contenido o cierras la respuesta; el bloquewithlo garantiza aunque ocurra un error. - Abre los archivos a subir en modo binario (
"rb"). Si los abres en modo texto, un archivo con caracteres especiales puede corromperse o fallar. - Personaliza el nombre y el tipo. En
files=puedes pasar una tupla(nombre, objeto_archivo, tipo_mime)cuando el servidor exige un nombre o unContent-Typeconcreto. - Comprueba el resultado. Tras una descarga, compara el tamaño del archivo con la cabecera
Content-Lengthsi el servidor la envía, para detectar descargas cortadas.