ChatGPT解决这个技术问题 Extra ChatGPT

How to make a timezone aware datetime object

What I need to do

I have a timezone-unaware datetime object, to which I need to add a time zone in order to be able to compare it with other timezone-aware datetime objects. I do not want to convert my entire application to timezone unaware for this one legacy case.

What I've Tried

First, to demonstrate the problem:

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

First, I tried astimezone:

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

It's not terribly surprising this failed, since it's actually trying to do a conversion. Replace seemed like a better choice (as per How do I get a value of datetime.today() in Python that is "timezone aware"?):

>>> 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
>>> 

But as you can see, replace seems to set the tzinfo, but not make the object aware. I'm getting ready to fall back to doctoring the input string to have a timezone before parsing it (I'm using dateutil for parsing, if that matters), but that seems incredibly kludgy.

Also, I've tried this in both Python 2.6 and Python 2.7, with the same results.

Context

I am writing a parser for some data files. There is an old format I need to support where the date string does not have a timezone indicator. I've already fixed the data source, but I still need to support the legacy data format. A one time conversion of the legacy data is not an option for various business BS reasons. While in general, I do not like the idea of hard-coding a default timezone, in this case it seems like the best option. I know with reasonable confidence that all the legacy data in question is in UTC, so I'm prepared to accept the risk of defaulting to that in this case.

unaware.replace() would return None if it were modifying unaware object inplace. The REPL shows that .replace() returns a new datetime object here.
What I needed when I came here: import datetime; datetime.datetime.now(datetime.timezone.utc)
@MartinThoma I would use the named tz arg to be more readable: datetime.datetime.now(tz=datetime.timezone.utc)
astimezone() can now (starting with 3.6) be called on a naive object, and its parameter can (starting with 3.3) be omitted, so the solution is as simple as unaware.astimezone()

C
Catskul

In general, to make a naive datetime timezone-aware, use the localize method:

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

For the UTC timezone, it is not really necessary to use localize since there is no daylight savings time calculation to handle:

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

works. (.replace returns a new datetime; it does not modify unaware.)


Well, I feel silly. Replace returns a new datetime. It says that right there in the docs too, and I completely missed that. Thanks, that's exactly what I was looking for.
"Replace returns a new datetime." Yep. The hint that the REPL gives you is that it's showing you the returned value. :)
if the timezone is not UTC then don't use the constructor directly: aware = datetime(..., tz), use .localize() instead.
It is worth mentioning that local time may be ambiguous. tz.localize(..., is_dst=None) asserts that it is not .
putz has a bug that sets Amsterdam timezone to + 20 minutes to UTC, some archaic timezone from 1937. You had one job pytz.
Z
Zoe stands with Ukraine

All of these examples use an external module, but you can achieve the same result using just the datetime module, as also presented in this SO answer:

from datetime import datetime, timezone

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

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

Fewer dependencies and no pytz issues.

NOTE: If you wish to use this with python3 and python2, you can use this as well for the timezone import (hardcoded for 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()

Very good answer for preventing the pytz issues, I'm glad I scrolled down a bit! Didn't want to tackle with pytz on my remote servers indeed :)
Note that from datetime import timezone works in py3 but not py2.7.
You should note that dt.replace(tzinfo=timezone.utc) returns a new datetime, it does not modify dt in place. (I will edit to show this).
How might you, instead of using timezone.utc, provide a different timezone as a string (eg "America/Chicago")?
@bumpkin better late than never, i guess: tz = pytz.timezone('America/Chicago')
W
Wouter

I wrote this Python 2 script in 2011, but never checked if it works on Python 3.

I had moved from dt_aware to dt_unaware:

dt_unaware = dt_aware.replace(tzinfo=None)

and dt_unware to dt_aware:

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

you could use localtz.localize(dt_unware, is_dst=None) to raise an exception if dt_unware represents non-existing or ambiguous local time (note: there were no such issue in the previous revision of your answer where localtz was UTC because UTC has no DST transitions
@J.F. Sebastian , first comment applied
I appreciate you showing both directions of the conversion.
@Sérgio and when you put that argument in .replace and get "TypeError: replace() got an unexpected keyword argument 'tzinfo'"? What can be done for this problem?
C
Community

I use this statement in Django to convert an unaware time to an aware:

from django.utils import timezone

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

I do like this solution (+1), but it is dependent on Django, which is not what they were looking for (-1). =)
You don't actually the second argument. The default argument (None) will mean the local timezone is used implicitly. Same with the DST (which is the third argument_
If you want to convert to UTC: dt_aware = timezone.make_aware(dt_unaware, timezone.utc)
H
HoldOffHunger

Python 3.9 adds the zoneinfo module so now only the standard library is needed!

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

Attach a timezone:

>>> 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'

Attach the system's local timezone:

>>> 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'

Subsequently it is properly converted to other timezones:

>>> 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'

Wikipedia list of available time zones

Windows has no system time zone database, so here an extra package is needed:

pip install tzdata  

There is a backport to allow use of zoneinfo in Python 3.6 to 3.8:

pip install backports.zoneinfo

Then:

from backports.zoneinfo import ZoneInfo

on Windows, you also need to pip install tzdata
@MrFuppes Thanks for the tip! I'll test this tomorrow and it to my answer. Do you know what the situation is on Macs?
@xjcl You need pip install tzdata on any platform where the operating system doesn't provide a time zone database. Macs should work out of the box. It will not hurt to install tzdata unconditionally (since the system data is prioritized over tzdata) if your application needs time zone data.
@xjcl tzdata is effectively part of the standard library (we call it a "first party package"), you just need to pip install it because it has a much faster release cadence than CPython.
I wanted to mention, it's very easy to add this to requirements.txt conditionally for Windows only: tzdata; sys_platform == "win32" (from: stackoverflow.com/a/35614580/705296)
p
paolov

I agree with the previous answers, and is fine if you are ok to start in UTC. But I think it is also a common scenario for people to work with a tz aware value that has a datetime that has a non UTC local timezone.

If you were to just go by name, one would probably infer replace() will be applicable and produce the right datetime aware object. This is not the case.

the replace( tzinfo=... ) seems to be random in its behaviour. It is therefore useless. Do not use this!

localize is the correct function to use. Example:

localdatetime_aware = tz.localize(datetime_nonaware)

Or a more complete example:

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

gives me a timezone aware datetime value of the current local time:

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

This needs more upvotes, trying to do replace(tzinfo=...) on a timezone other than UTC will foul up your datetime. I got -07:53 instead of -08:00 for instance. See stackoverflow.com/a/13994611/1224827
Can you give a reproducible example of replace(tzinfo=...) having unexpected behavior?
Thanks a lot. I wasted a lot of time trying to use replace() but it didn't work.
D
Daniel

Use dateutil.tz.tzlocal() to get the timezone in your usage of datetime.datetime.now() and 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())

Note that datetime.astimezone will first convert your datetime object to UTC then into the timezone, which is the same as calling datetime.replace with the original timezone information being None.


If you want to make it UTC: .replace(tzinfo=dateutil.tz.UTC)
One import less and just: .replace(tzinfo=datetime.timezone.utc)
C
Community

This codifies @Sérgio and @unutbu's answers. It will "just work" with either a pytz.timezone object or an IANA Time Zone string.

def make_tz_aware(dt, tz='UTC', is_dst=None):
    """Add timezone information to a datetime object, only if it is naive."""
    tz = dt.tzinfo or tz
    try:
        tz = pytz.timezone(tz)
    except AttributeError:
        pass
    return tz.localize(dt, is_dst=is_dst) 

This seems like what datetime.localize() (or .inform() or .awarify()) should do, accept both strings and timezone objects for the tz argument and default to UTC if no time zone is specified.


Thanks, this helped me "brand" a raw datetime object as "UTC" without the system first assuming it to be local time and then recalculating the values!
H
Harry Moreno

for those that just want to make a timezone aware datetime

import datetime

datetime.datetime(2019, 12, 7, tzinfo=datetime.timezone.utc)

for those that want a datetime with a non utc timezone starting in python 3.9 stdlib

import datetime
from zoneinfo import ZoneInfo

datetime.datetime(2019, 12, 7, tzinfo=ZoneInfo("America/Los_Angeles")) 

How is this different to the main answer?
I don't care to use the localize function. This answer is more succinct for those trying to solve their problem quickly (what I wish was the accepted answer).
The localize function is just there to test the assert method right? It's not actually required? aware = datetime.datetime(2011, 8, 15, 8, 15, 12, 0, pytz.UTC) is the same as you've written they have just named the parameter no?
the assert is there to demonstrate how to change a datetime between timezone aware and unaware. I actually provided the keyword argument for clarity. you can omit the keyword and rely on positional arguments if you prefer. kwargs are less error prone though.
tzinfo named parameter is not mentioned in the accepted answer.
i
ivanleoncz

Yet another way of having a datetime object NOT naive:

>>> from datetime import datetime, timezone
>>> datetime.now(timezone.utc)
datetime.datetime(2021, 5, 1, 22, 51, 16, 219942, tzinfo=datetime.timezone.utc)

i
ilmatte

quite new to Python and I encountered the same issue. I find this solution quite simple and for me it works fine (Python 3.6):

unaware=parser.parse("2020-05-01 0:00:00")
aware=unaware.replace(tzinfo=tz.tzlocal()).astimezone(tz.tzlocal())

l
leenremm

Here is a simple solution to minimize changes to your code:

from datetime import datetime
import pytz

start_utc = datetime.utcnow()
print ("Time (UTC): %s" % start_utc.strftime("%d-%m-%Y %H:%M:%S"))

Time (UTC): 09-01-2021 03:49:03

tz = pytz.timezone('Africa/Cairo')
start_tz = datetime.now().astimezone(tz)
print ("Time (RSA): %s" % start_tz.strftime("%d-%m-%Y %H:%M:%S"))

Time (RSA): 09-01-2021 05:49:03


just be aware that datetime.utcnow() does not return a timezone aware datetime (in contrast to its name)
T
Turtles Are Cute

In the format of unutbu's answer; I made a utility module that handles things like this, with more intuitive syntax. Can be installed with pip.

import datetime
import saturn

unaware = datetime.datetime(2011, 8, 15, 8, 15, 12, 0)
now_aware = saturn.fix_naive(unaware)

now_aware_madrid = saturn.fix_naive(unaware, 'Europe/Madrid')

b
borchvm

Changing between timezones

import pytz
from datetime import datetime

other_tz = pytz.timezone('Europe/Madrid')

# From random aware datetime...
aware_datetime = datetime.utcnow().astimezone(other_tz)
>> 2020-05-21 08:28:26.984948+02:00

# 1. Change aware datetime to UTC and remove tzinfo to obtain an unaware datetime
unaware_datetime = aware_datetime.astimezone(pytz.UTC).replace(tzinfo=None)
>> 2020-05-21 06:28:26.984948

# 2. Set tzinfo to UTC directly on an unaware datetime to obtain an utc aware datetime
aware_datetime_utc = unaware_datetime.replace(tzinfo=pytz.UTC)
>> 2020-05-21 06:28:26.984948+00:00

# 3. Convert the aware utc datetime into another timezone
reconverted_aware_datetime = aware_datetime_utc.astimezone(other_tz)
>> 2020-05-21 08:28:26.984948+02:00

# Initial Aware Datetime and Reconverted Aware Datetime are equal
print(aware_datetime1 == aware_datetime2)
>> True

B
Behrooz Hosseini

Above all mentioned approaches, when it is a Unix timestamp . This is a very simple solution using pandas.

import pandas as pd
unix_timestamp = 1513393355
pst_tz = pd.Timestamp(unix_timestamp, unit='s', tz='US/Pacific')
utc_tz = pd.Timestamp(unix_timestamp , unit='s', tz='UTC')