Watch at EaseHistorias. Respuestas. Perspectiva.

Tecnología · GUÍA PRÁCTICA

Python ZoneInfo: detecta horas inexistentes sin rechazar una repetición válida

Comparar directamente con la conversión a UTC puede rechazar las dos apariciones de una hora repetida. Comprueba primero la ida y vuelta de la hora local; después resuelve la ambigüedad y elige la clave del instante.

Matriz original que compara la ida y vuelta de una hora normal, las dos apariciones repetidas y una hora inexistente de Londres.
Ilustración técnica original: ambas apariciones repetidas existen; el hueco de primavera no supera la comprobación de ida y vuelta. Watch at Ease

Que un datetime tenga zona horaria no demuestra que esa lectura del reloj local haya existido. Python permite construir un datetime con ZoneInfo dentro del hueco que deja el adelanto de primavera. También puede ocurrir el error contrario: un atajo que compara el datetime directamente con su conversión a UTC puede declarar inexistente una hora repetida perfectamente válida.

Separa tres decisiones: si existe la lectura del reloj local, cuál de sus apariciones quiso indicar el usuario y cómo guardar o comparar el instante elegido. Esta comprobación conserva las dos apariciones válidas de una hora repetida y rechaza una hora ausente sin desplazarla silenciosamente.

1. Identifica la comprobación que produce falsos negativos

La guía de migración de pytz-deprecation-shim ofrece una pequeña función de existencia basada en dt.astimezone(timezone.utc) == dt. La receta cercana basada en dateutil es otro método. El atajo de igualdad falla para una hora repetida válida cuando cambiar fold modifica su desfase UTC: PEP 495 establece deliberadamente que esa comparación entre zonas devuelva False. Por tanto, False no demuestra que la hora esté en un hueco.

En una prueba limitada con CPython 3.12.14, las 01:30 del 2024-10-27 en Europe/London se convierten en 00:30 UTC con fold=0 y en 01:30 UTC con fold=1. Ambos son instantes reales. La comparación directa con cada conversión a UTC dio False en los dos casos. Úsalo como caso de regresión de la función, no como prueba de que ZoneInfo no pueda representar la hora repetida.

2. Compara el reloj local después de la ida y vuelta

Para un datetime dt cuyo tzinfo sea una instancia de ZoneInfo, exige primero un valor con zona efectiva: dt.tzinfo debe existir y dt.utcoffset() no debe ser None. Calcula back = dt.astimezone(timezone.utc).astimezone(dt.tzinfo). Después calcula exists = back.replace(tzinfo=None) == dt.replace(tzinfo=None). Se comparan deliberadamente la fecha y los campos del reloj local tras ir a UTC y volver; no se compara directamente el valor original con uno de otra zona.

Si exists es False, rechaza la entrada o aplica una política del producto documentada por separado. No sustituyas automáticamente dt por back y lo llames validación: estarías cambiando una entrada inexistente. Si la aplicación ya utiliza dateutil, sus funciones documentadas datetime_exists(dt) y datetime_ambiguous(dt) evitan mantener una implementación propia. Ejecuta los mismos casos contra la implementación que despliegues.

3. Resuelve la ambigüedad solo después de comprobar la existencia

Una vez que exists sea True, el valor ZoneInfo es ambiguo si dt.replace(fold=1-dt.fold).utcoffset() difiere de dt.utcoffset(). Pregunta, conserva o elige explícitamente la aparición anterior (fold=0) o posterior (fold=1). Asignar fold no es en sí una validación. Esta prueba determina si ocurre la lectura del reloj local; no valida estrictamente cualquier etiqueta fold recibida fuera de un intervalo repetido.

Si un registro anterior ya identifica el instante mediante UTC o un desfase, conserva esa información en vez de volver a adivinar a partir de una cadena local sin desfase. ZoneInfo asigna fold adecuadamente al convertir un instante existente. Para citas introducidas en hora local, conserva la clave de zona y la política de ambigüedad junto con el instante resultante. No fijes una corrección de una hora: la magnitud y las reglas de cada transición dependen de los datos de la zona elegida.

4. Conserva una matriz de cuatro casos de regresión

Con ZoneInfo('Europe/London'), la prueba revisó una hora normal, 2024-10-26 01:30; los dos valores fold de 2024-10-27 01:30; y la hora inexistente 2024-03-31 01:30. Los resultados de existencia por ida y vuelta fueron True, True, True y False. La ambigüedad, evaluada únicamente tras confirmar la existencia, dio False, True, True y False. El valor del hueco volvió desde UTC como las 02:30 locales: el reloj original no sobrevivió a la conversión.

Son casos históricos, no predicciones de todas las transiciones futuras. Registra la versión de Python, la clave de zona y el origen de los datos horarios utilizados por la aplicación, y añade casos para sus regiones reales de servicio. ZoneInfo lee los datos del sistema o, cuando hace falta, el paquete oficial tzdata. La falta de ese paquete o una clave de zona inválida son problemas de inicialización distintos; no son un hueco de horario estacional ni deben convertirse en un rechazo de la entrada del usuario.

5. Utiliza una clave de instante después de validar

Cuando la entrada exista y su aparición esté resuelta, usa dt.astimezone(timezone.utc) si la identidad de negocio corresponde a un instante. En la prueba, introducir los dos valores repetidos de Londres, con el mismo objeto ZoneInfo, en un conjunto produjo una sola clave local. Convertirlos antes a UTC produjo dos. La resta de los valores locales dio cero segundos; la resta de los valores UTC dio 3.600 segundos. Son semánticas de comparación diferentes, no opciones de almacenamiento equivalentes.

Esta última conversión sirve para ordenar eventos, eliminar duplicados por instante y calcular tiempo realmente transcurrido. Una programación recurrente en hora local puede necesitar deliberadamente la semántica del reloj, así que conserva su regla local original. Cambiar las claves ahora no reconstruye un evento ya sobrescrito por una colisión de claves locales; recupéralo de los registros de origen si se conservaron. Acepta la reparación solo cuando la hora normal, ambas apariciones repetidas y el hueco sigan sus rutas previstas.

Diagrama original que comprueba la existencia, elige una aparición repetida y finalmente crea la clave del instante UTC.
Ilustración técnica original: existencia, elección de aparición e identidad del instante son tres decisiones distintas. Watch at Ease