How to Make a Timezone Aware Datetime Object

How to make a timezone aware datetime object

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

How to convert a timezone aware string to datetime in Python without dateutil?

As of Python 3.7, datetime.datetime.fromisoformat() can handle your format:

>>> import datetime
>>> datetime.datetime.fromisoformat('2012-11-01T04:16:13-04:00')
datetime.datetime(2012, 11, 1, 4, 16, 13, tzinfo=datetime.timezone(datetime.timedelta(days=-1, seconds=72000)))

In older Python versions you can't, not without a whole lot of painstaking manual timezone defining.

Python does not include a timezone database, because it would be outdated too quickly. Instead, Python relies on external libraries, which can have a far faster release cycle, to provide properly configured timezones for you.

As a side-effect, this means that timezone parsing also needs to be an external library. If dateutil is too heavy-weight for you, use iso8601 instead, it'll parse your specific format just fine:

>>> import iso8601
>>> iso8601.parse_date('2012-11-01T04:16:13-04:00')
datetime.datetime(2012, 11, 1, 4, 16, 13, tzinfo=<FixedOffset '-04:00'>)

iso8601 is a whopping 4KB small. Compare that tot python-dateutil's 148KB.

As of Python 3.2 Python can handle simple offset-based timezones, and %z will parse -hhmm and +hhmm timezone offsets in a timestamp. That means that for a ISO 8601 timestamp you'd have to remove the : in the timezone:

>>> from datetime import datetime
>>> iso_ts = '2012-11-01T04:16:13-04:00'
>>> datetime.strptime(''.join(iso_ts.rsplit(':', 1)), '%Y-%m-%dT%H:%M:%S%z')
datetime.datetime(2012, 11, 1, 4, 16, 13, tzinfo=datetime.timezone(datetime.timedelta(-1, 72000)))

The lack of proper ISO 8601 parsing is being tracked in Python issue 15873.

How to add a timezone to a datetime object?

Regarding pytz, note that there is zoneinfo in the standard lib. No need for a third party library for time zone handling with Python >= 3.9. Example usage.

Then, if your input represents wall time in some time zone, you can just localize. If the input represents UTC, you can set the tzinfo to UTC a bit more easily and then convert to local time using astimezone.

from datetime import datetime, timezone
import pytz

s = '20200901-01u30m30s'
local_tz = 'Europe/Amsterdam'

# if s represents local time, just localize:
dtobj_tz = pytz.timezone(local_tz).localize(datetime.strptime(s, '%Y%m%d-%Hu%Mm%Ss'))
# datetime.datetime(2020, 9, 1, 1, 30, 30, tzinfo=<DstTzInfo 'Europe/Amsterdam' CEST+2:00:00 DST>)

# if s represents UTC, set it directly:
dtobj_utc = datetime.strptime(s, '%Y%m%d-%Hu%Mm%Ss').replace(tzinfo=timezone.utc)
# ...and convert to desired tz:
dtobj_tz = dtobj_utc.astimezone(pytz.timezone(local_tz))
# datetime.datetime(2020, 9, 1, 3, 30, 30, tzinfo=<DstTzInfo 'Europe/Amsterdam' CEST+2:00:00 DST>)

How do I get a value of datetime.today() in Python that is timezone aware ?

In the standard library, there is no cross-platform way to create aware timezones without creating your own timezone class. (Edit: Python 3.9 introduces zoneinfo in the standard library which does provide this functionality.)

On Windows, there's win32timezone.utcnow(), but that's part of pywin32. I would rather suggest to use the pytz library, which has a constantly updated database of most timezones.

Working with local timezones can be very tricky (see "Further reading" links below), so you may rather want to use UTC throughout your application, especially for arithmetic operations like calculating the difference between two time points.

You can get the current date/time like so:

import pytz
from datetime import datetime
datetime.utcnow().replace(tzinfo=pytz.utc)

Mind that datetime.today() and datetime.now() return the local time, not the UTC time, so applying .replace(tzinfo=pytz.utc) to them would not be correct.

Another nice way to do it is:

datetime.now(pytz.utc)

which is a bit shorter and does the same.


Further reading/watching why to prefer UTC in many cases:

  • pytz documentation
  • What Every Developer Should Know About Time – development hints for many real-life use cases
  • The Problem with Time & Timezones - Computerphile – funny, eye-opening explanation about the complexity of working with timezones (video)

In python, how do I create a timezone aware datetime from a date and time?

The trick is to first combine the naive time and the date into a naive datetime. This naive datetime can then be converted to an aware datetime.

The conversion can be done using the third party package pytz (using, in this case, the 'Europe/London' timezone):

import datetime
import pytz

naive_time = datetime.time(0, 30)
date = datetime.date(2016, 12, 25)
naive_datetime = datetime.datetime.combine(date, naive_time)

timezone = pytz.timezone('Europe/London')
aware_datetime = timezone.localize(naive_datetime)

If you're doing it in Django, and want to use the current timezone (as configured in Django), you can replace the final two lines with a call to make_aware:

from django.utils import timezone

aware_datetime = timezone.make_aware(naive_datetime)

Two ways to create timezone aware datetime objects (Django). Seven minutes difference?

This happens on Django 3.2 and lower, which rely on the pytz library. In Django 4 (unless you enable to setting to use the deprecated library), the output of the two examples you give is identical.

In Django 3.2 and below, the variance arises because the localised time is built in two different ways. When using make_aware, it is done by calling the localize() method on the pytz timezone instance. In the second version, it's done by passing a tzinfo object directly to the datetime constructor.

The difference between the two is well illustrated in this blog post:

The biggest mistake people make with pytz is simply attaching its time zones to the constructor, since that is the standard way to add a time zone to a datetime in Python. If you try and do that, the best case scenario is that you'll get something obviously absurd:

import pytz
from datetime import datetime

NYC = pytz.timezone('America/New_York')
dt = datetime(2018, 2, 14, 12, tzinfo=NYC)
print(dt)
# 2018-02-14 12:00:00-04:56

Why is the time offset -04:56 and not -05:00? Because that was the local solar mean time in New York before standardized time zones were adopted, and is thus the first entry in the America/New_York time zone. Why did pytz return that? Because unlike the standard library's model of lazily-computed time zone information, pytz takes an eager calculation approach.

Whenever you construct an aware datetime from a naive one, you need to call the localize function on it:

dt = NYC.localize(datetime(2018, 2, 14, 12))
print(dt)
# 2018-02-14 12:00:00-05:00

Exactly the same thing is happening with your Europe/Berlin example. pytz is eagerly fetching the first entry in its database, which is a pre-1983 solar time, which was 53 minutes and 28 seconds ahead of Greenwich Mean Time (GMT). This is obviously inappropriate given the date - but the tzinfo isn't aware of the date you are using unless you pass it to localize().

This is the difference between your two approaches. Using make_aware correctly calls localize() on the object. Assigning the tzinfo directly to the datetime object, however, doesn't, and results in pytz using the (wrong) time zone information because it was simply the first entry for that zone in its database.

The pytz documentation obliquely refers to this as well:

This library only supports two ways of building a localized time. The first is to use the localize() method provided by the pytz library. This is used to localize a naive datetime (datetime with no timezone information)... The second way of building a localized time is by converting an existing localized time using the standard astimezone() method... Unfortunately using the tzinfo argument of the standard datetime constructors ‘’does not work’’ with pytz for many timezones.

It is actually because of these and several other bugs in the pytz implementation that Django dropped it in favour of Python's built-in zoneinfo module.

More from that blog post:

At the time of its creation, pytz was cleverly designed to optimize for performance and correctness, but with the changes introduced by PEP 495 and the performance improvements to dateutil, the reasons to use it are dwindling.
... The biggest reason to use dateutil over pytz is the fact that dateutil uses the standard interface and pytz doesn't, and as a result it is very easy to use pytz incorrectly.

Passing a pytz tzinfo object directly to a datetime constructor is incorrect. You must call localize() on the tzinfo class, passing it the date. The correct way to initialise the datetime in your second example is:

> berlin = get_current_timezone()
> berlin.localize(datetime.datetime(1999, 1, 1, 0, 0, 0))
datetime.datetime(1999, 1, 1, 0, 0, tzinfo=<DstTzInfo 'Europe/Berlin' CET+1:00:00 STD>)

... which matches what make_aware produces.

How to add timezone into a naive datetime instance in python

Use tz.localize(d) to localize the instance. From the documentation:

The first is to use the localize() method provided by the pytz library. This is used to localize a naive datetime (datetime with no timezone information):

>>> loc_dt = eastern.localize(datetime(2002, 10, 27, 6, 0, 0))
>>> print(loc_dt.strftime(fmt))
2002-10-27 06:00:00 EST-0500

If you don't use tz.localize(), but use datetime.replace(), chances are that a historical offset is used instead; tz.localize() will pick the right offset in effect for the given date. The US Eastern timezone DST start and end dates have changed over time, for example.

When you try to localize a datetime value that is ambiguous because it straddles the transition period from summer to winter time or vice-versa, the timezone will be consulted to see if the resulting datetime object should have .dst() return True or False. You can override the default for the timezone with the is_dst keyword argument for .localize():

dt = tz.localize(naive, is_dst=True)

or even switch off the choice altogether by setting is_dst=None. In that case, or in the rare cases there is no default set for a timezone, an ambiguous datetime value would lead to a AmbiguousTimeError exception being raised. The is_dst flag is only consulted for datetime values that are ambiguous and is ignored otherwise.

To go back the other way, turn a timezone-aware object back to a naive object, use .replace(tzinfo=None):

naivedt = awaredt.replace(tzinfo=None)

Python - given a timezone-aware datetime object how do I get a timezone-naive UTC datetime object?

# First convert back to UTC
utc_time = pacific_now.astimezone(pytz.utc)

# Remove the timezone attribute to make it timezone-naive
utc_time = utc_time.replace(tzinfo=None)

Get UTC timestamp from time zone aware datetime object in Python

Use datetime.astimezone to get the same datetime in UTC (or any other timezone).

dt = datetime.datetime(year, month, day, hour, minute, tzinfo=pytz.timezone('US/Pacific'))
dt_utc = dt.astimezone(pytz.timezone('UTC'))


Related Topics



Leave a reply



Submit