835

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.

5
  • 2
    unaware.replace() would return None if it were modifying unaware object inplace. The REPL shows that .replace() returns a new datetime object here.
    – jfs
    Commented Aug 15, 2011 at 16:40
  • 3
    What I needed when I came here: import datetime; datetime.datetime.now(datetime.timezone.utc) Commented Jan 16, 2018 at 9:32
  • 4
    @MartinThoma I would use the named tz arg to be more readable: datetime.datetime.now(tz=datetime.timezone.utc)
    – Asclepius
    Commented Oct 14, 2019 at 2:45
  • 1
    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()
    – vashekcz
    Commented Feb 25, 2021 at 21:49
  • 3
    I found the trick : Europe/Paris, Berlin, CET, ... are completety bugged in pytz and I was in a mess of instability for months because of that. I replaced all that by dateutil.tz.gettz(...) and now my code is stable and works ! My advice : abandon completely pytz ! Commented Sep 29, 2022 at 10:35

16 Answers 16

876

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

15
  • 15
    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.
    – Mark Tozzi
    Commented Aug 15, 2011 at 14:24
  • 2
    "Replace returns a new datetime." Yep. The hint that the REPL gives you is that it's showing you the returned value. :) Commented Aug 15, 2011 at 14:33
  • 6
    if the timezone is not UTC then don't use the constructor directly: aware = datetime(..., tz), use .localize() instead.
    – jfs
    Commented Mar 8, 2014 at 21:07
  • 2
    It is worth mentioning that local time may be ambiguous. tz.localize(..., is_dst=None) asserts that it is not .
    – jfs
    Commented Mar 8, 2014 at 21:10
  • 13
    putz has a bug that sets Amsterdam timezone to + 20 minutes to UTC, some archaic timezone from 1937. You had one job pytz.
    – Boris
    Commented Feb 18, 2020 at 16:09
438

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()
7
  • 27
    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 :)
    – Tregoreg
    Commented Feb 1, 2017 at 20:20
  • 4
    How might you, instead of using timezone.utc, provide a different timezone as a string (eg "America/Chicago")?
    – bumpkin
    Commented Sep 26, 2017 at 18:36
  • i think this answer is wrong: timezones change over time! eg. "Europe/Vienna" once hat an offset of "01:05", so setting it with replace() would offset the datetime by that, and not the current offset of "01:00". resulting in weirdness like >>> tz.normalize(datetime.strptime('2019-02-12', '%Y-%m-%d').replace(tzinfo=tz)).isoformat() '2019-02-12T23:55:00+01:00' better use localize: >>> tz.localize(datetime.datetime.strptime('2019-02-12', '%Y-%m-%d')).isoformat() '2019-02-12T00:00:00+01:00'
    – Florian
    Commented Feb 13, 2019 at 11:38
  • 6
    @bumpkin better late than never, i guess: tz = pytz.timezone('America/Chicago')
    – Florian
    Commented Feb 13, 2019 at 11:40
  • @bumpkin I couldn't find a way to do it without using pytz.
    – JoeyC
    Commented Aug 15, 2019 at 21:07
120

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)
4
  • 4
    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
    – jfs
    Commented May 15, 2014 at 19:55
  • @J.F. Sebastian , first comment applied
    – Sérgio
    Commented May 15, 2014 at 20:59
  • 4
    I appreciate you showing both directions of the conversion. Commented Jun 24, 2016 at 1:08
  • @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? Commented Jun 22, 2020 at 19:36
64

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
6
  • 5
    on Windows, you also need to pip install tzdata Commented Jun 11, 2020 at 17:19
  • @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
    Commented Jun 11, 2020 at 22:32
  • @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.
    – Paul
    Commented Sep 21, 2020 at 19:29
  • 1
    @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.
    – Paul
    Commented Sep 27, 2020 at 0:18
  • 1
    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) Commented May 31, 2022 at 14:37
55

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())
3
  • 4
    I do like this solution (+1), but it is dependent on Django, which is not what they were looking for (-1). =)
    – mkoistinen
    Commented Aug 28, 2015 at 17:54
  • 7
    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_
    – Oli
    Commented Sep 12, 2016 at 11:24
  • If you want to convert to UTC: dt_aware = timezone.make_aware(dt_unaware, timezone.utc) Commented Aug 12, 2021 at 19:15
28

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>)
5
  • 8
    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
    – Blairg23
    Commented Jul 27, 2017 at 0:09
  • 2
    Can you give a reproducible example of replace(tzinfo=...) having unexpected behavior?
    – xjcl
    Commented Jun 1, 2020 at 23:13
  • Thanks a lot. I wasted a lot of time trying to use replace() but it didn't work.
    – FrackeR011
    Commented Nov 12, 2021 at 10:47
  • Can anyone explaim <DstTzInfo 'Australia/Melbourne' AEDT+11:00:00 DST> to me? What does it mean? Commented Sep 5, 2022 at 3:53
  • 1
    @SorryformybadEnglish i think that's the repr() of a timezone object, giving different representations. the text name ('Australia/Melbourne'), the code (AEDT - Australian Eastern Daylight Time), the offset from UTC (+11:00:00), and i guess if daylight saving time is enabled (DST) Commented Apr 11, 2023 at 21:20
14

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.

2
  • 1
    If you want to make it UTC: .replace(tzinfo=dateutil.tz.UTC) Commented Aug 19, 2019 at 6:08
  • 2
    One import less and just: .replace(tzinfo=datetime.timezone.utc)
    – kubanczyk
    Commented Apr 9, 2020 at 15:16
11

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.

1
  • 1
    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!
    – Nikhil VJ
    Commented Nov 3, 2019 at 12:34
11

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")) 
10
  • How is this different to the main answer?
    – Jack
    Commented Jul 28, 2020 at 10:56
  • 1
    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). Commented Jul 28, 2020 at 18:12
  • 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?
    – Jack
    Commented Jul 29, 2020 at 8:39
  • 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. Commented Jul 29, 2020 at 16:46
  • 2
    tzinfo named parameter is not mentioned in the accepted answer.
    – Aaron
    Commented Sep 9, 2020 at 14:20
6

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)
1
  • 1
    Thanks, this should be much higher than the other answers :) Commented Oct 2, 2023 at 9:49
2

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())
2

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
1

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

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

1
  • 2
    just be aware that datetime.utcnow() does not return a timezone aware datetime (in contrast to its name) Commented Oct 3, 2021 at 20:35
0
  • As per the documentation datetime.utcnow:

    • Warning: Because naive datetime objects are treated by many datetime methods as local times, it is preferred to use aware datetimes to represent times in UTC. As such, the recommended way to create an object representing the current time in UTC is by calling datetime.now(timezone.utc).

    • This option is covered in other answers, but the document citation is not.
  • As per the documentation datetime.utcfromtimestamp

  • Tested in python 3.11.2

from datetime import datetime, timezone
import time  # for timestamp
import pytz  # for aware comparison

now = datetime.now(tz=timezone.utc)
aware = datetime(now.year, now.month, now.day, now.hour, now.minute, now.second, now.microsecond, tzinfo=pytz.UTC)

print(now == aware)
[out]: True

fts = datetime.fromtimestamp(time.time(), tz=timezone.utc)
aware = datetime(fts.year, fts.month, fts.day, fts.hour, fts.minute, fts.second, fts.microsecond, tzinfo=pytz.UTC)

print(fts == aware)
[out]: True
-4

Above all mentioned approaches, when it is a Unix timestamp, there 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')
0

Not the answer you're looking for? Browse other questions tagged or ask your own question.