Ir al contenido principal
Tecnología

Manejo de zonas horarias en Python - zoneinfo vs pytz explicado

Naive vs Aware - La primera distinción que debes dominar

Los objetos datetime de Python vienen en dos variantes: naive (sin atributo tzinfo) y aware (con tzinfo establecido). datetime.now() devuelve un datetime naive por defecto, que silenciosamente adopta la zona horaria local del servidor al ser serializado o comparado. Un proyecto desarrollado localmente en JST y desplegado en servidores UTC puede producir fácilmente errores de desfase de nueve horas que ningún test detecta porque ambos lados asumieron lo incorrecto.

La regla profesional es convertir las marcas de tiempo de entrada a aware lo antes posible, realizar todos los cálculos internos con aware, y serializar de vuelta a cadenas solo en los límites. Mezclar datetimes naive y aware en operaciones aritméticas lanza TypeError, así que añadir alias de tipo que distingan AwareDatetime de datetime en mypy o pyright proporciona una protección estática sólida.

zoneinfo - La respuesta de la biblioteca estándar en Python 3.9+

PEP 615 introdujo el módulo zoneinfo en Python 3.9, proporcionando acceso directo a la base de datos de zonas horarias IANA sin dependencias de terceros. zoneinfo.ZoneInfo(«Asia/Tokyo») devuelve una instancia de tzinfo que puedes pasar directamente a datetime, eliminando toda una categoría de peculiaridades específicas de pytz. Eliminar un paquete de terceros también simplifica la gestión de dependencias y la auditoría de la cadena de suministro.

Por defecto, zoneinfo lee los datos de zona horaria del sistema operativo. Linux y macOS incluyen /usr/share/zoneinfo poblado, pero Windows carece de datos IANA por defecto. El remedio estándar es pip install tzdata, que proporciona un paquete Python al que zoneinfo recurre como alternativa. Las imágenes Docker basadas en Alpine, frecuentemente elegidas por su pequeño tamaño, también carecen de tzdata por defecto y deben configurarse explícitamente.

pytz - La opción heredada con peculiaridades específicas

pytz ha sido la opción de facto para zonas horarias IANA desde principios de la década de 2000. Funciona en todas las versiones de Python aún en uso, pero tiene una API inusual. No puedes pasar una zona horaria pytz directamente al constructor de datetime; hacerlo inicializa el datetime con un desfase histórico de Local Mean Time (LMT) que difiere varios minutos del estándar moderno. Para Tokio, este error de LMT produce un desfase de 9 horas y 19 minutos en lugar de las 9 horas esperadas.

El patrón correcto en pytz es usar tz.localize(naive_dt) para datetimes naive o dt.astimezone(tz) para los que ya son aware. Al migrar a zoneinfo, las llamadas a tz.normalize() dispersas por los proyectos con pytz pueden eliminarse por completo, ya que la aritmética de zoneinfo produce el resultado correcto sin normalización manual.

zoneinfo frente a pytz - origen, conversión y migración
Aspectozoneinfopytz
De dónde vieneBiblioteca estándar. Introducida en Python 3.9 como PEP 615Paquete de terceros. Opción de facto desde principios de la década de 2000 y casi la única antes de Python 3.9
Convertir un datetime naive en awareBasta pasar zoneinfo.ZoneInfo(«Asia/Tokyo») directamente a datetimeRequiere tz.localize(naive_dt), y dt.astimezone(tz) para los valores que ya son aware
Pasar la zona directamente como tzinfoFunciona tal como se esperaInicializa con un desfase histórico de Local Mean Time (LMT), de 9 horas y 19 minutos para Tokio en lugar de las 9 horas esperadas
Normalizar después de la aritméticaNo hace falta. La aritmética normal de datetime da el resultado correctoRequiere tz.normalize(), que no existe en zoneinfo
Lugar que ocupa hoyOpción por defecto para el código nuevo en Python 3.9 o posteriorEl código existente puede coexistir detrás de tzinfo y migrarse un límite a la vez

zoneinfo lee los datos de zona horaria del sistema operativo por defecto, así que en Windows y en imágenes Docker basadas en Alpine hace falta pip install tzdata como paso aparte.

El atributo fold - Resolviendo la ambigüedad del horario de verano

Python 3.6 añadió el atributo fold a datetime para resolver un problema de larga data: cuando el horario de verano termina, la misma hora del reloj ocurre dos veces. El 1 de noviembre de 2026 a la 1:30 AM en la hora del Este de EE.UU., el momento es ambiguo. fold=0 selecciona la primera ocurrencia (EDT, UTC-4) y fold=1 selecciona la segunda (EST, UTC-5). Sin esta distinción, los programas no pueden representar ambos momentos sin ambigüedad.

El caso opuesto, cuando el horario de verano comienza y una hora del reloj salta hacia adelante, deja un vacío que no existe en el reloj. fold también se aplica a este vacío, aunque la PEP 495 define su significado de forma deliberadamente inversa al caso ambiguo. zoneinfo no lanza ninguna excepción y aplica el desplazamiento vigente antes de la transición, de modo que un viaje de ida y vuelta por UTC aterriza en el instante de una hora más tarde. Las aplicaciones que programan alarmas o temporizaciones de tareas deben manejar ambas situaciones explícitamente. Los tests de frontera deben incluir tanto el salto de primavera como la superposición de otoño.

Mantener tzdata actualizado - Consideraciones operativas

La base de datos de zonas horarias IANA recibe unas pocas actualizaciones por año, reflejando cambios de política como la abolición del horario de verano o ajustes de hora estándar. En 2026, varios países continúan debatiendo sus políticas de horario de verano, lo que significa que un sistema con tzdata desactualizado puede calcular horas futuras incorrectas. Los despliegues en contenedores son particularmente susceptibles, porque la versión de tzdata de la imagen base se fija al momento de construcción y solo se actualiza al reconstruir la imagen.

En entornos gestionados como AWS Lambda o Cloud Run, no puedes fijar directamente la versión de tzdata incluida ni decidir cuándo cambia. Para cargas de trabajo críticas que calculan marcas de tiempo futuras, instala el paquete tzdata vía pip y asigna una cadena vacía a la variable de entorno PYTHONTZPATH, de modo que la base del sistema quede fuera de la ruta de búsqueda y prevalezca la copia instalada con pip. Si necesitas cambiar esa ruta durante la ejecución, llama a zoneinfo.reset_tzpath() y combínalo con ZoneInfo.clear_cache(), porque restablecer la ruta no invalida los objetos que ya están en la caché.

Recomendaciones prácticas

Para proyectos nuevos en Python 3.9 o posterior, zoneinfo es la opción correcta por defecto. En proyectos existentes que mezclan pytz y zoneinfo, ambos pueden coexistir detrás de tzinfo sin refactorización inmediata. Migra un límite a la vez, comenzando por el ingreso y egreso de la API, y los cambios se propagan gradualmente por el sistema sin reescrituras masivas.

Igualmente importante es la política de mantener el estado interno en UTC. Realizar aritmética en Asia/Tokyo o America/Los_Angeles invita a sorpresas relacionadas con el horario de verano. Convierte en el límite de entrada, calcula en UTC, y convierte de vuelta solo al mostrar al usuario o al persistir en una columna cuya semántica requiere hora local. El TIMESTAMP WITH TIME ZONE de PostgreSQL siempre almacena UTC internamente, convirtiéndolo en la contraparte natural de este enfoque.

XB!LINE

¿Te resultó útil este artículo?

Artículos Relacionados