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 2003. 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.
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. zoneinfo trata este tiempo omitido como si fuera el momento anterior, pero 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 entre 5 y 10 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 verificar directamente la versión de tzdata incluida. Para cargas de trabajo críticas que calculan marcas de tiempo futuras, el enfoque más seguro es instalar el paquete tzdata vía pip y configurar ZoneInfo para que lo prefiera mediante la variable de entorno TZPATH. Esto pone la versión de los datos de zona horaria bajo control explícito en lugar de ocultarla dentro del entorno de ejecución.
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.