Cómo hacer que un objeto de fecha y hora sea consciente (no ingenuo)

11 minutos de lectura

Avatar de usuario de Mark Tozzi
Marcos Tozzi

Lo que necesito hacer

Tengo un objeto de fecha y hora que no reconoce la zona horaria, al que necesito agregar una zona horaria para poder compararlo con otros objetos de fecha y hora que reconocen la zona horaria. No quiero convertir toda mi aplicación a la zona horaria sin saberlo para este caso heredado.

lo que he probado

Primero, para demostrar el problema:

Python 2.6.1 (r261:67515, Jun 24 2010, 21:47:49) 
[GCC 4.2.1 (Apple Inc. build 5646)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import datetime
>>> import pytz
>>> unaware = datetime.datetime(2011,8,15,8,15,12,0)
>>> unaware
datetime.datetime(2011, 8, 15, 8, 15, 12)
>>> aware = datetime.datetime(2011,8,15,8,15,12,0,pytz.UTC)
>>> aware
datetime.datetime(2011, 8, 15, 8, 15, 12, tzinfo=<UTC>)
>>> aware == unaware
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: can't compare offset-naive and offset-aware datetimes

Primero, probé una zona horaria:

>>> unaware.astimezone(pytz.UTC)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: astimezone() cannot be applied to a naive datetime
>>>

No es terriblemente sorprendente que haya fallado, ya que en realidad está tratando de hacer una conversión. Reemplazar parecía una mejor opción (según ¿Cómo obtengo un valor de datetime.today() en Python que es “consciente de la zona horaria”?):

>>> unaware.replace(tzinfo=pytz.UTC)
datetime.datetime(2011, 8, 15, 8, 15, 12, tzinfo=<UTC>)
>>> unaware == aware
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: can't compare offset-naive and offset-aware datetimes
>>> 

Pero como puede ver, replace parece establecer el tzinfo, pero no hace que el objeto sea consciente. Me estoy preparando para volver a manipular la cadena de entrada para tener una zona horaria antes de analizarla (estoy usando dateutil para analizar, si eso importa), pero eso parece increíblemente torpe.

Además, probé esto tanto en Python 2.6 como en Python 2.7, con los mismos resultados.

Contexto

Estoy escribiendo un analizador para algunos archivos de datos. Hay un formato antiguo que debo admitir donde la cadena de fecha no tiene un indicador de zona horaria. Ya arreglé la fuente de datos, pero aún necesito admitir el formato de datos heredado. Una conversión de una sola vez de los datos heredados no es una opción por varias razones comerciales de BS. Si bien, en general, no me gusta la idea de codificar una zona horaria predeterminada, en este caso parece ser la mejor opción. Sé con razonable confianza que todos los datos heredados en cuestión están en UTC, por lo que estoy preparado para aceptar el riesgo de no hacerlo en este caso.

  • unaware.replace() volvería None si estuviera modificando unaware objeto en su lugar. El REPL muestra que .replace() devuelve un nuevo datetime objeto aquí.

    – jfs

    15 de agosto de 2011 a las 16:40

  • Lo que necesitaba cuando vine aquí: import datetime; datetime.datetime.now(datetime.timezone.utc)

    – Martín Tomas

    16 de enero de 2018 a las 9:32

  • @MartinThoma usaría el nombre tz arg para que sea más legible: datetime.datetime.now(tz=datetime.timezone.utc)

    – Asclepio

    14 de octubre de 2019 a las 2:45

  • astimezone() ahora (comenzando con 3.6) puede llamarse en un objeto ingenuo, y su parámetro puede (comenzando con 3.3) omitirse, por lo que la solución es tan simple como unaware.astimezone()

    – vashekcz

    25 de febrero de 2021 a las 21:49


  • Encontré el truco: Europa/París, Berlín, CET, … tienen errores completos en pytz y estuve en un lío de inestabilidad durante meses debido a eso. ¡Reemplacé todo eso por dateutil.tz.gettz(…) y ahora mi código es estable y funciona! Mi consejo: ¡abandona por completo a pytz!

    – hervé-guerin

    29/09/2022 a las 10:35

avatar de usuario de unutbu
unutbu

En general, para hacer que una fecha y hora ingenua reconozca la zona horaria, use el método de localización:

import datetime
import pytz

unaware = datetime.datetime(2011, 8, 15, 8, 15, 12, 0)
aware = datetime.datetime(2011, 8, 15, 8, 15, 12, 0, pytz.UTC)

now_aware = pytz.utc.localize(unaware)
assert aware == now_aware

Para la zona horaria UTC, no es realmente necesario usar localize ya que no hay cálculo de horario de verano para manejar:

now_aware = unaware.replace(tzinfo=pytz.UTC)

obras. (.replace devuelve una nueva fecha y hora; no modifica unaware.)

  • Bueno, me siento tonto. Reemplazar devuelve una nueva fecha y hora. También dice eso allí mismo en los documentos, y me lo perdí por completo. Gracias, eso es exactamente lo que estaba buscando.

    –Mark Tozzi

    15 de agosto de 2011 a las 14:24

  • “Reemplazar devuelve una nueva fecha y hora”. Sí. La pista que le da REPL es que le muestra el valor devuelto. 🙂

    – Karl Knechtel

    15 de agosto de 2011 a las 14:33

  • si la zona horaria no es UTC, entonces no use el constructor directamente: aware = datetime(..., tz)usar .localize() en cambio.

    – jfs

    8 de marzo de 2014 a las 21:07

  • Vale la pena mencionar que la hora local puede ser ambigua. tz.localize(..., is_dst=None) afirma que no lo es.

    – jfs

    08/03/2014 a las 21:10

  • putz tiene un error que establece la zona horaria de Ámsterdam a + 20 minutos de UTC, una zona horaria arcaica de 1937. Tenías un trabajo pytz.

    – Borís

    18 de febrero de 2020 a las 16:09

avatar de usuario de kang
kang

Todos estos ejemplos usan un módulo externo, pero puede lograr el mismo resultado usando solo el módulo de fecha y hora, como también se presenta en esta respuesta SO:

from datetime import datetime, timezone

dt = datetime.now()
dt = dt.replace(tzinfo=timezone.utc)

print(dt.isoformat())
# '2017-01-12T22:11:31+00:00'

Menos dependencias y sin problemas de pytz.

NOTA: Si desea usar esto con python3 y python2, también puede usarlo para la importación de la zona horaria (codificada para UTC):

try:
    from datetime import timezone
    utc = timezone.utc
except ImportError:
    #Hi there python2 user
    class UTC(tzinfo):
        def utcoffset(self, dt):
            return timedelta(0)
        def tzname(self, dt):
            return "UTC"
        def dst(self, dt):
            return timedelta(0)
    utc = UTC()

  • Muy buena respuesta para prevenir el pytz ¡Me alegro de haberme desplazado un poco hacia abajo! No quería abordar con pytz en mis servidores remotos de hecho 🙂

    – Tregoreg

    1 de febrero de 2017 a las 20:20

  • Tenga en cuenta que from datetime import timezone funciona en py3 pero no en py2.7.

    – 7yl4r

    20 de febrero de 2017 a las 16:44

  • Debes tener en cuenta que dt.replace(tzinfo=timezone.utc) devuelve una nueva fecha y hora, no modifica dt en su lugar. (Voy a editar para mostrar esto).

    – Blairg23

    28 de marzo de 2017 a las 0:58

  • ¿Cómo podría, en lugar de usar timezone.utc, proporcionar una zona horaria diferente como una cadena (por ejemplo, “América/Chicago”)?

    – pueblerino

    26/09/2017 a las 18:36

  • @bumpkin más vale tarde que nunca, supongo: tz = pytz.timezone('America/Chicago')

    – Florián

    13 de febrero de 2019 a las 11:40

Avatar de usuario de Sérgio
sergio

Escribí este script de Python 2 en 2011, pero nunca verifiqué si funciona en Python 3.

Me había mudado de dt_aware a dt_unaware:

dt_unaware = dt_aware.replace(tzinfo=None)

y dt_unware a dt_aware:

from pytz import timezone
localtz = timezone('Europe/Lisbon')
dt_aware = localtz.localize(dt_unware)

  • podrías usar localtz.localize(dt_unware, is_dst=None) para generar una excepción si dt_unware representa la hora local inexistente o ambigua (nota: no hubo tal problema en la revisión anterior de su respuesta donde localtz era UTC porque UTC no tiene transiciones de horario de verano

    – jfs

    15 mayo 2014 a las 19:55


  • @JF Sebastian, primer comentario aplicado

    – Sérgio

    15 mayo 2014 a las 20:59

  • Le agradezco que muestre ambas direcciones de la conversión.

    – Cristiano largo

    24 de junio de 2016 a las 1:08

  • @Sérgio y cuando pones ese argumento en .replace y obtienes “TypeError: replace() obtuvo un argumento de palabra clave inesperado ‘tzinfo'”? ¿Qué se puede hacer para este problema?

    – Gustavo Rottgering

    22 de junio de 2020 a las 19:36

Avatar de usuario de Googol
Gúgol

Uso esta declaración en Django para convertir un tiempo inconsciente en uno consciente:

from django.utils import timezone

dt_aware = timezone.make_aware(dt_unaware, timezone.get_current_timezone())

avatar de usuario de xjcl
xjcl

Python 3.9 agrega el zoneinfo módulo ¡así que ahora solo se necesita la biblioteca estándar!

from zoneinfo import ZoneInfo
from datetime import datetime
unaware = datetime(2020, 10, 31, 12)

Adjunte una zona horaria:

>>> unaware.replace(tzinfo=ZoneInfo('Asia/Tokyo'))
datetime.datetime(2020, 10, 31, 12, 0, tzinfo=zoneinfo.ZoneInfo(key='Asia/Tokyo'))
>>> str(_)
'2020-10-31 12:00:00+09:00'

Adjunte la zona horaria local del sistema:

>>> unaware.replace(tzinfo=ZoneInfo('localtime'))
datetime.datetime(2020, 10, 31, 12, 0, tzinfo=zoneinfo.ZoneInfo(key='localtime'))
>>> str(_)
'2020-10-31 12:00:00+01:00'

Posteriormente se convierte correctamente a otras zonas horarias:

>>> unaware.replace(tzinfo=ZoneInfo('localtime')).astimezone(ZoneInfo('Asia/Tokyo'))
datetime.datetime(2020, 10, 31, 20, 0, tzinfo=backports.zoneinfo.ZoneInfo(key='Asia/Tokyo'))
>>> str(_)
'2020-10-31 20:00:00+09:00'

Lista de Wikipedia de zonas horarias disponibles


ventanas no tiene base de datos de zona horaria del sistema, por lo que aquí se necesita un paquete adicional:

pip install tzdata  

Hay un backport para permitir el uso de zoneinfo en Pitón 3.6 a 3.8:

pip install backports.zoneinfo

Entonces:

from backports.zoneinfo import ZoneInfo

  • en Windows, también necesita pip install tzdata

    – FObersteiner

    11 de junio de 2020 a las 17:19

  • @MrFuppes ¡Gracias por el consejo! Voy a probar esto mañana y a mi respuesta. ¿Sabes cuál es la situación en Mac?

    – xjcl

    11 de junio de 2020 a las 22:32

  • @xjcl Necesitas pip install tzdata en cualquier plataforma donde el sistema operativo no proporcione una base de datos de zonas horarias. Las Mac deberían funcionar fuera de la caja. No dolerá instalar tzdata incondicionalmente (ya que los datos del sistema tienen prioridad sobre tzdata) si su aplicación necesita datos de zona horaria.

    – Pablo

    21 de septiembre de 2020 a las 19:29

  • @xjcl tzdata es efectivamente parte de la biblioteca estándar (lo llamamos “paquete propio”), solo necesita instalarlo por pip porque tiene una cadencia de lanzamiento mucho más rápida que CPython.

    – Pablo

    27 de septiembre de 2020 a las 0:18

  • Quería mencionar que es muy fácil agregar esto a requirements.txt condicionalmente solo para Windows: tzdata; sys_platform == "win32" (de: stackoverflow.com/a/35614580/705296)

    –Joe Sadoski

    31 de mayo de 2022 a las 14:37

Estoy de acuerdo con las respuestas anteriores, y está bien si está bien para comenzar en UTC. Pero creo que también es un escenario común para las personas trabajar con un valor consciente de tz que tiene un datetime que tiene una zona horaria local no UTC.

Si solo tuviera que ir por nombre, uno probablemente inferiría que replace() será aplicable y producirá el objeto consciente de fecha y hora correcto. Este no es el caso.

el replace( tzinfo=… ) parece ser aleatorio en su comportamiento. Por lo tanto, es inútil. ¡No uses esto!

localizar es la función correcta a utilizar. Ejemplo:

localdatetime_aware = tz.localize(datetime_nonaware)

O un ejemplo más completo:

import pytz
from datetime import datetime
pytz.timezone('Australia/Melbourne').localize(datetime.now())

me da un valor de fecha y hora consciente de la zona horaria de la hora local actual:

datetime.datetime(2017, 11, 3, 7, 44, 51, 908574, tzinfo=<DstTzInfo 'Australia/Melbourne' AEDT+11:00:00 DST>)

  • en Windows, también necesita pip install tzdata

    – FObersteiner

    11 de junio de 2020 a las 17:19

  • @MrFuppes ¡Gracias por el consejo! Voy a probar esto mañana y a mi respuesta. ¿Sabes cuál es la situación en Mac?

    – xjcl

    11 de junio de 2020 a las 22:32

  • @xjcl Necesitas pip install tzdata en cualquier plataforma donde el sistema operativo no proporcione una base de datos de zonas horarias. Las Mac deberían funcionar fuera de la caja. No dolerá instalar tzdata incondicionalmente (ya que los datos del sistema tienen prioridad sobre tzdata) si su aplicación necesita datos de zona horaria.

    – Pablo

    21 de septiembre de 2020 a las 19:29

  • @xjcl tzdata es efectivamente parte de la biblioteca estándar (lo llamamos “paquete propio”), solo necesita instalarlo por pip porque tiene una cadencia de lanzamiento mucho más rápida que CPython.

    – Pablo

    27 de septiembre de 2020 a las 0:18

  • Quería mencionar que es muy fácil agregar esto a requirements.txt condicionalmente solo para Windows: tzdata; sys_platform == "win32" (de: stackoverflow.com/a/35614580/705296)

    –Joe Sadoski

    31 de mayo de 2022 a las 14:37

Avatar de usuario de Daniel
Daniel

Usar dateutil.tz.tzlocal() para obtener la zona horaria en su uso de datetime.datetime.now() y datetime.datetime.astimezone():

from datetime import datetime
from dateutil import tz

unlocalisedDatetime = datetime.now()

localisedDatetime1 = datetime.now(tz = tz.tzlocal())
localisedDatetime2 = datetime(2017, 6, 24, 12, 24, 36, tz.tzlocal())
localisedDatetime3 = unlocalisedDatetime.astimezone(tz = tz.tzlocal())
localisedDatetime4 = unlocalisedDatetime.replace(tzinfo = tz.tzlocal())

Tenga en cuenta que datetime.astimezone primero convertirá su datetime objeto a UTC y luego a la zona horaria, que es lo mismo que llamar datetime.replace siendo la información original de la zona horaria None.

  • Si quieres que sea UTC: .replace(tzinfo=dateutil.tz.UTC)

    – Martín Tomas

    19 de agosto de 2019 a las 6:08

  • Una importación menos y justa: .replace(tzinfo=datetime.timezone.utc)

    – kubanczyk

    9 de abril de 2020 a las 15:16

¿Ha sido útil esta solución?