57

How do you convert a timezone-aware datetime object to the equivalent non-timezone-aware datetime for the local timezone?

My particular application uses Django (although, this is in reality a generic Python question):

import iso8601

....

date_str="2010-10-30T17:21:12Z"

....

d = iso8601.parse_date(date_str)

foo = app.models.FooModel(the_date=d)
foo.save()

This causes Django to throw an error:

raise ValueError("MySQL backend does not support timezone-aware datetimes.")

What I need is:

d = iso8601.parse_date(date_str)
local_d = SOME_FUNCTION(d)
foo = app.models.FooModel(the_date=local_d)

What would SOME_FUNCTION be?

kes
  • 5,983
  • 8
  • 41
  • 69
  • related: [How to convert a python utc datetime to a local datetime using only python standard library?](http://stackoverflow.com/q/4563272/4279) – jfs Oct 06 '14 at 03:06

5 Answers5

91

In general, to convert an arbitrary timezone-aware datetime to a naive (local) datetime, I'd use the pytz module and astimezone to convert to local time, and replace to make the datetime naive:

In [76]: import pytz

In [77]: est=pytz.timezone('US/Eastern')

In [78]: d.astimezone(est)
Out[78]: datetime.datetime(2010, 10, 30, 13, 21, 12, tzinfo=<DstTzInfo 'US/Eastern' EDT-1 day, 20:00:00 DST>)

In [79]: d.astimezone(est).replace(tzinfo=None)
Out[79]: datetime.datetime(2010, 10, 30, 13, 21, 12)

But since your particular datetime seems to be in the UTC timezone, you could do this instead:

In [65]: d
Out[65]: datetime.datetime(2010, 10, 30, 17, 21, 12, tzinfo=tzutc())

In [66]: import datetime

In [67]: import calendar

In [68]: datetime.datetime.fromtimestamp(calendar.timegm(d.timetuple()))
Out[68]: datetime.datetime(2010, 10, 30, 13, 21, 12)

By the way, you might be better off storing the datetimes as naive UTC datetimes instead of naive local datetimes. That way, your data is local-time agnostic, and you only convert to local-time or any other timezone when necessary. Sort of analogous to working in unicode as much as possible, and encoding only when necessary.

So if you agree that storing the datetimes in naive UTC is the best way, then all you'd need to do is define:

local_d = d.replace(tzinfo=None)
unutbu
  • 842,883
  • 184
  • 1,785
  • 1,677
  • Yes, I did ultimately decide to store the dates as naive UTC. You've answered both questions I had about that (stripping tzinfo and converting timezone), so thanks! – kes Mar 27 '11 at 23:46
  • `fromtimestamp()` may fail for dates from the past (if UTC offset was different at the time in the local timezone) if it doesn't use the tz database to find UTC offset (It does on Ubuntu, I'm not sure about Windows). – jfs Oct 06 '14 at 03:21
74

In recent versions of Django (at least 1.4.1):

from django.utils.timezone import localtime

result = localtime(some_time_object)
user9876
  • 10,954
  • 6
  • 44
  • 66
  • 4
    `localtime()` may use `settings.TIME_ZONE` that may be different from the local timezone e.g., if you haven't set it then it is `'America/Chicago'` by default. If `localtime()` does not use `settings.TIME_ZONE` then it uses [broken (according to its documentation) `LocalTimezone()`](https://github.com/django/django/blob/master/django/utils/timezone.py#L81) – jfs Oct 06 '14 at 03:18
3

A portable robust solution should use the tz database. To get local timezone as pytz tzinfo object, use tzlocal module:

#!/usr/bin/env python
import iso8601
import tzlocal # $ pip install tzlocal

local_timezone = tzlocal.get_localzone()
aware_dt = iso8601.parse_date("2010-10-30T17:21:12Z") # some aware datetime object
naive_local_dt = aware_dt.astimezone(local_timezone).replace(tzinfo=None)

Note: it might be tempting to use something like:

#!/usr/bin/env python3
# ...
naive_local_dt = aware_dt.astimezone().replace(tzinfo=None)

but it may fail if the local timezone has a variable utc offset but python does not use a historical timezone database on a given platform.

jfs
  • 399,953
  • 195
  • 994
  • 1,670
1

Using python-dateutil you can parse the date in iso-8561 format with dateutil.parsrser.parse() that will give you an aware datetime in UTC/Zulu timezone.

Using .astimezone() you can convert it to an aware datetime in another timezone.

Using .replace(tzinfo=None) will convert the aware datetime into a naive datetime.

from datetime import datetime
from dateutil import parser as datetime_parser
from dateutil.tz import tzutc,gettz

aware = datetime_parser.parse('2015-05-20T19:51:35.998931Z').astimezone(gettz("CET"))
naive = aware.replace(tzinfo=None)

In general the best idea is to convert all dates to UTC and store them that way, and convert them back to local as needed. I use aware.astimezone(tzutc()).replace(tzinfo=None) to make sure is in UTC and convert to naive.

RubenLaguna
  • 21,435
  • 13
  • 113
  • 151
  • [`dateutil`-based code may fail for ambiguous local times](https://github.com/dateutil/dateutil/issues/112) – jfs Sep 30 '15 at 00:32
  • See that the whole issue of [PEP-0495 Local Time Dissambiguation](https://www.python.org/dev/peps/pep-0495/) is a bit hairy in general. After reading [issue #57](https://github.com/dateutil/dateutil/issues/57) I understand that `tzlocal` is smarter and tries to figure out your real system timezone settings to dissambiguate, which probably is what most of the people would like. – RubenLaguna Sep 30 '15 at 06:24
  • 1
    It is not about how smart they are: both `tzlocal.get_localzone()` and `dateutil.tz.tzlocal()` may load their data from the **same** tzdata file. The bugs are due to an inherit limitation in `dateutil` approach to how timezone-aware datetime objects are interpreted (this approach requires PEP 495 to disambiguate even unambiguous conversions such as utc -> local). `pytz` works today (`get_localzone()` returns `pytz` tzinfo objects). – jfs Sep 30 '15 at 06:45
  • `ValueError: astimezone() cannot be applied to a naive datetime` – Cerin Jul 27 '18 at 19:45
1

I use this helper function all the time.

from datetime import datetime
import pytz

def tz_convert(t: datetime, tz=pytz.utc):
    '''
    Convert a timestamp to the target timezone.

    If the timestamp is naive, the timezone is set to the target timezone.
    '''
    if not t.tzinfo:
        tc = t.replace(tzinfo=tz)
    else:
        tc = t.astimezone(tz)

    return tc

Demo

# tz-aware timestamp
>>> t = datetime.now(tz=pytz.utc)
>>> t.isoformat()
'2022-09-15T08:24:38.093312+00:00'
>>> tc = tz_convert(t, pytz.timezone('est'))
>>> tc.isoformat()
'2022-09-15T03:24:38.093312-05:00'

# tz-naive timestamp
>>> t = datetime.now()
>>> t.isoformat()
'2022-09-15T10:22:41.464200'
>>> tc = tz_convert(t, pytz.timezone('est'))
>>> tc.isoformat()
'2022-09-15T10:22:41.464200-05:00'
timgeb
  • 76,762
  • 20
  • 123
  • 145
  • Note that you can pass `None` as the `tz` argument to convert tz-aware timestamps to the local timezone. – timgeb Sep 15 '22 at 09:11