311

I've never had to convert time to and from UTC. Recently had a request to have my app be timezone aware, and I've been running myself in circles. Lots of information on converting local time to UTC, which I found fairly elementary (maybe I'm doing that wrong as well), but I can not find any information on easily converting the UTC time to the end-users timezone.

In a nutshell, and android app sends me (appengine app) data and within that data is a timestamp. To store that timestamp to utc time I am using:

datetime.utcfromtimestamp(timestamp)

That seems to be working. When my app stores the data, it is being store as 5 hours ahead (I am EST -5)

The data is being stored on appengine's BigTable, and when retrieved it comes out as a string like so:

"2011-01-21 02:37:21"

How do I convert this string to a DateTime in the users correct time zone?

Also, what is the recommended storage for a users timezone information? (How do you typically store tz info ie: "-5:00" or "EST" etc etc ?) I'm sure the answer to my first question might contain a parameter the answers the second.

martineau
  • 119,623
  • 25
  • 170
  • 301
MattoTodd
  • 14,467
  • 16
  • 59
  • 76
  • 2
    related: [How to convert a python utc datetime to a local datetime using only python standard library?](http://stackoverflow.com/a/13287083/4279) – jfs Jun 03 '13 at 14:42
  • [This answer](http://stackoverflow.com/a/18646797/2697658) shows how to solve this in a simple way. – juan Sep 06 '13 at 15:47

16 Answers16

574

If you don't want to provide your own tzinfo objects, check out the python-dateutil library. It provides tzinfo implementations on top of a zoneinfo (Olson) database such that you can refer to time zone rules by a somewhat canonical name.

from datetime import datetime
from dateutil import tz

# METHOD 1: Hardcode zones:
from_zone = tz.gettz('UTC')
to_zone = tz.gettz('America/New_York')

# METHOD 2: Auto-detect zones:
from_zone = tz.tzutc()
to_zone = tz.tzlocal()

# utc = datetime.utcnow()
utc = datetime.strptime('2011-01-21 02:37:21', '%Y-%m-%d %H:%M:%S')

# Tell the datetime object that it's in UTC time zone since 
# datetime objects are 'naive' by default
utc = utc.replace(tzinfo=from_zone)

# Convert time zone
central = utc.astimezone(to_zone)

Edit Expanded example to show strptime usage

Edit 2 Fixed API usage to show better entry point method

Edit 3 Included auto-detect methods for timezones (Yarin)

Joe Holloway
  • 28,320
  • 15
  • 82
  • 92
  • Thanks for pointing that out. The library points to this project (http://www.twinsun.com/tz/tz-link.htm) as the source for the timezones. Do you know where there is a list of the string possibilities that you can pass to gettz() ? – MattoTodd Jan 23 '11 at 03:36
  • Also, there's another library called `pytz` (http://pytz.sourceforge.net/) that provides the tz database as well. I personally prefer `python-dateutil` because of the other utility modules it provides, but if you don't see yourself using those modules `pytz` works too. – Joe Holloway Jan 23 '11 at 03:52
  • It looks like python-dateutil will work just fine. Great Solution. – MattoTodd Jan 23 '11 at 04:03
  • tried your code but its not really working, from_zone and to_zone are NoneTypes so utc.aztimezone(to_zone) which i guess should be 'astimezone' returns 'TypeError: astimezone() argument 1 must be datetime.tzinfo, not None' – aschmid00 May 31 '11 at 15:15
  • If `from_zone` and `to_zone` are `None`, then there may be an issue with the zoneinfo database on your system. What OS/distro are you using? Thanks for pointing out the typo, I've corrected it. – Joe Holloway May 31 '11 at 19:04
  • @Joe-Holloway: I'm getting the same `None` error as @aschmid00, too. I'm on the latest 10.6.7 build of Snow Leopard. Is there a way I can check to see which tz's are in my zoneinfo database? – Ben Kreeger Jun 03 '11 at 14:11
  • 1
    On my previous comment, I was able to import `dateutil.tz` and use `tz.tzutc()` and `tz.tzlocal()` as time zone objects I was looking for. It looks like the time zone database on my system is good (I checked in `/usr/share/zoneinfo`). Not sure what was up. – Ben Kreeger Jun 03 '11 at 14:18
  • 1
    @Benjamin You are on the right track. The `tz` module is the correct entry point to be using for this library. I've updated my answer to reflect this. The `dateutil.zoneinfo` module I was showing previously is used internally by the `tz` module as a fall back if it can't locate the system's zoneinfo DB. If you look inside the library you'll see that there's a zoneinfo DB tarball in the package that it uses if it can't find your system's DB. My old example was trying to hit that DB directly and I'm guessing you were having issues loading that private DB (isn't on the python path somehow?) – Joe Holloway Jun 03 '11 at 15:01
  • 2
    @MattoTodd: [utc -> local conversion is broken in `dateutil`](https://github.com/dateutil/dateutil/issues/112), use [`pytz` instead](http://stackoverflow.com/a/32904812/4279). – jfs Oct 02 '15 at 09:58
  • 2
    @J.F.Sebastian this is fixed in [#225](https://github.com/dateutil/dateutil/pull/225) – tim-phillips Apr 07 '16 at 00:51
  • @Rohmer the github issue is closed but I don't know whether the original test from #112 passes. Have you tried it with the updated `dateutil` version? – jfs Apr 07 '16 at 01:06
  • @J.F.Sebastian Hmm, I just looked at the last comment in #112 saying it was fixed, apologies if that was premature. Let me test. – tim-phillips Apr 07 '16 at 01:08
  • @J.F.Sebastian I'm getting correct tz data for PDT right now – tim-phillips Apr 07 '16 at 01:21
  • @Rohmer: you are right. [The test](https://gist.github.com/zed/3838828#file-test_utc_to_dst-py) passes on dateutil's master branch. Though the change is not released yet (the test fails on the current version: `python-dateutil-2.5.2`). – jfs Apr 07 '16 at 01:35
  • Is there a problem with just adding a fixed amount of time to convert time to another timezone – briankip Apr 21 '16 at 19:22
  • 2
    @briankip It depends on how sophisticated your application needs to be, but in general applications, yes, it's a problem. First of all depending on what time of year it is, the number of hours to add/subtract might change as some time zones honor DST and other don't. Secondly, time zone rules are arbitrarily changed over time so if you're working with a past date/time you need to apply the rules that were valid at that point in time, not present time. That's why it's best to use an API that leverages the Olson TZ database behind the scenes. – Joe Holloway Apr 22 '16 at 17:10
  • So, does it take into account DST or not? I'm a little bit confused. I'm EST (usually GMT-5) but with DST it's currently GMT-4. If I have a date in the past that is GMT-5 but another date that is today (which is GMT-4), I suppose this answer won't take it into account and use GMT-4 for both dates, right? Would there be an easy solution for that, since DST doesn't start or end at the same date every year? – dan Jul 19 '17 at 01:39
  • @dnLL Yes it will, if you're properly using UTC instead of a localized datetime. UTC is agnostic to DST hence a UTC datetime doesn't change when DST rules change in a particular time zone. It's a pure "point in time". It's when you "localize" from UTC to a particular time zone that you need to be aware of the DST rules and those rules are encoded in the tz database the various libraries use. The tz database is updated as geopolitical factors affect DST. It follows that it's important to keep your tz database current (regularly updating libraries, system packages, etc). – Joe Holloway Jul 19 '17 at 14:25
  • So, if I have a UTC time in February and another in July and that I apply the `tz.tzlocal()` to both, everything is *smart* enough to make it work? You say UTC doesn't change with DST, so if there is let's say exactly 6 months (about 4320h) between the 2 dates, when I apply the `tz.tzlocal()` there will still be 4320h between both dates even though if I was counting hour by hour it could be 4321 because we skipped an hour for DST? If it works that way, then the date in July is probably wrong (GMT-5 instead of GMT-4 with DST). Sorry if that's confusing, just trying to understand here. – dan Jul 19 '17 at 15:24
  • I mean, we literally skip an hour in March and get it back in November IIRC with DST. It has to be somewhere. – dan Jul 19 '17 at 15:26
  • Yep, I understand how DST works. Your problem is that you're thinking in local time, not UTC. You need to think in UTC time and view your time zone as a localized time derived from it (based on the tz rules that are/were in effect at the given UTC time). You can go from UTC to localized time without ambiguity, but you can't always go from a localized time to UTC without ambiguity. When DST "falls back" an hour you have an entire hour-long window of time that repeats itself in local time, but not in UTC. In UTC the forward-only aspect of time is preserved. That's why the recommendation... – Joe Holloway Jul 19 '17 at 15:50
  • 1
    Is typically to store UTC timestamps any time you need to convert between time zones (or just do it exclusively). In certain situations you may also need to know what timezone a piece of data was captured, so either store the timezone or the localized time alongside it, but having the UTC timestamp ensures you have a "point in time" that isn't ambiguous with respect to locality. – Joe Holloway Jul 19 '17 at 15:53
  • Armin does a good job explaining things in this blog post: http://lucumr.pocoo.org/2011/7/15/eppur-si-muove/ – Joe Holloway Jul 19 '17 at 15:58
  • 1
    for local time zone use `datetime.now().astimezone().tzinfo`, `tz.tzlocal()` causes crash for me – larsaars Jan 27 '21 at 23:49
  • If you're using Python >= 3.9 timezone info is now built in (https://docs.python.org/3/library/zoneinfo.html) – David Waterworth May 18 '23 at 00:50
77

Here's a resilient method that doesn't depend on any external libraries:

from datetime import datetime
import time

def datetime_from_utc_to_local(utc_datetime):
    now_timestamp = time.time()
    offset = datetime.fromtimestamp(now_timestamp) - datetime.utcfromtimestamp(now_timestamp)
    return utc_datetime + offset

This avoids the timing issues in DelboyJay's example. And the lesser timing issues in Erik van Oosten's amendment.

As an interesting footnote, the timezone offset computed above can differ from the following seemingly equivalent expression, probably due to daylight savings rule changes:

offset = datetime.fromtimestamp(0) - datetime.utcfromtimestamp(0) # NO!

Update: This snippet has the weakness of using the UTC offset of the present time, which may differ from the UTC offset of the input datetime. See comments on this answer for another solution.

To get around the different times, grab the epoch time from the time passed in. Here's what I do:

def utc2local(utc):
    epoch = time.mktime(utc.timetuple())
    offset = datetime.fromtimestamp(epoch) - datetime.utcfromtimestamp(epoch)
    return utc + offset
David Foster
  • 6,931
  • 4
  • 41
  • 42
  • 2
    This works well and requires nothing other than time and datetime. – Mike_K Nov 04 '13 at 23:45
  • 12
    @Mike_K: but it is incorrect. It doesn't support DST or timezones that had different utc offset in the past for other reasons. Full support without `pytz`-like db is impossible, see PEP-431. Though you can write stdlib-only solution that works in such cases on systems that already have historical timezone db e.g., Linux, OS X, see [my answer](http://stackoverflow.com/a/13287083/4279). – jfs Dec 14 '13 at 20:17
  • @J.F.Sebastian: You say this code doesn't work in general but also say it works on OS X and Linux. Do you mean this code doesn't work on Windows? – David Foster Dec 15 '13 at 21:37
  • 3
    @DavidFoster: your code may fail on Linux and OS X too. The difference between yours and [mine stdlib-only solutions](http://stackoverflow.com/a/13287083/4279) is that yours uses *current* offset that might be different from utc offset at `utc_datetime` time. – jfs Dec 15 '13 at 22:25
  • 2
    Good call. That's a very subtle difference. The UTC offset can change over time too. *sigh* – David Foster Dec 18 '13 at 05:36
  • This is right what was asked for. Should be prefered solution IMO. – dekingsey Dec 24 '20 at 04:27
  • It should not return `utc + offset`, since that would have the wrong `tzinfo`, so it should return this instead: `utc.astimezone(datetime.timezone(offset))` – paulie4 Apr 29 '21 at 19:23
38

See the datetime documentation on tzinfo objects. You have to implement the timezones you want to support yourself. The are examples at the bottom of the documentation.

Here's a simple example:

from datetime import datetime,tzinfo,timedelta

class Zone(tzinfo):
    def __init__(self,offset,isdst,name):
        self.offset = offset
        self.isdst = isdst
        self.name = name
    def utcoffset(self, dt):
        return timedelta(hours=self.offset) + self.dst(dt)
    def dst(self, dt):
            return timedelta(hours=1) if self.isdst else timedelta(0)
    def tzname(self,dt):
         return self.name

GMT = Zone(0,False,'GMT')
EST = Zone(-5,False,'EST')

print datetime.utcnow().strftime('%m/%d/%Y %H:%M:%S %Z')
print datetime.now(GMT).strftime('%m/%d/%Y %H:%M:%S %Z')
print datetime.now(EST).strftime('%m/%d/%Y %H:%M:%S %Z')

t = datetime.strptime('2011-01-21 02:37:21','%Y-%m-%d %H:%M:%S')
t = t.replace(tzinfo=GMT)
print t
print t.astimezone(EST)

Output

01/22/2011 21:52:09 
01/22/2011 21:52:09 GMT
01/22/2011 16:52:09 EST
2011-01-21 02:37:21+00:00
2011-01-20 21:37:21-05:00a
Mark Tolonen
  • 166,664
  • 26
  • 169
  • 251
  • Thanks for the accept! I updated it slightly to translate your specific example time. Parsing the time creates what the docs call a "naive" time. Use `replace` to set the timezone to GMT and make it timezone "aware", then use `astimezone` to convert to another timezone. – Mark Tolonen Jan 22 '11 at 21:54
  • 1
    Sorry for swapping for Joe's answer. Technically your answer explains exactly how to do it with raw python (which is good to know), but the library he suggested gets me to a solution much quicker. – MattoTodd Jan 23 '11 at 04:03
  • 5
    Doesn't look like it automatically handles daylight savings time. – Gringo Suave Mar 22 '12 at 18:19
  • @GringoSuave: it wasn't meant to. The rules for that are complicated and change often. – Mark Tolonen Mar 22 '12 at 21:41
28

If you want to get the correct result even for the time that corresponds to an ambiguous local time (e.g., during a DST transition) and/or the local utc offset is different at different times in your local time zone then use pytz timezones:

#!/usr/bin/env python
from datetime import datetime
import pytz    # $ pip install pytz
import tzlocal # $ pip install tzlocal

local_timezone = tzlocal.get_localzone() # get pytz tzinfo
utc_time = datetime.strptime("2011-01-21 02:37:21", "%Y-%m-%d %H:%M:%S")
local_time = utc_time.replace(tzinfo=pytz.utc).astimezone(local_timezone)
jfs
  • 399,953
  • 195
  • 994
  • 1,670
23

This answer should be helpful if you don't want to use any other modules besides datetime.

datetime.utcfromtimestamp(timestamp) returns a naive datetime object (not an aware one). Aware ones are timezone aware, and naive are not. You want an aware one if you want to convert between timezones (e.g. between UTC and local time).

If you aren't the one instantiating the date to start with, but you can still create a naive datetime object in UTC time, you might want to try this Python 3.x code to convert it:

import datetime

d=datetime.datetime.strptime("2011-01-21 02:37:21", "%Y-%m-%d %H:%M:%S") #Get your naive datetime object
d=d.replace(tzinfo=datetime.timezone.utc) #Convert it to an aware datetime object in UTC time.
d=d.astimezone() #Convert it to your local timezone (still aware)
print(d.strftime("%d %b %Y (%I:%M:%S:%f %p) %Z")) #Print it with a directive of choice

Be careful not to mistakenly assume that if your timezone is currently MDT that daylight savings doesn't work with the above code since it prints MST. You'll note that if you change the month to August, it'll print MDT.

Another easy way to get an aware datetime object (also in Python 3.x) is to create it with a timezone specified to start with. Here's an example, using UTC:

import datetime, sys

aware_utc_dt_obj=datetime.datetime.now(datetime.timezone.utc) #create an aware datetime object
dt_obj_local=aware_utc_dt_obj.astimezone() #convert it to local time

#The following section is just code for a directive I made that I liked.
if sys.platform=="win32":
    directive="%#d %b %Y (%#I:%M:%S:%f %p) %Z"
else:
    directive="%-d %b %Y (%-I:%M:%S:%f %p) %Z"

print(dt_obj_local.strftime(directive))

If you use Python 2.x, you'll probably have to subclass datetime.tzinfo and use that to help you create an aware datetime object, since datetime.timezone doesn't exist in Python 2.x.

Brōtsyorfuzthrāx
  • 4,387
  • 4
  • 34
  • 56
12

If using Django, you can use the timezone.localtime method:

from django.utils import timezone
date 
# datetime.datetime(2014, 8, 1, 20, 15, 0, 513000, tzinfo=<UTC>)

timezone.localtime(date)
# datetime.datetime(2014, 8, 1, 16, 15, 0, 513000, tzinfo=<DstTzInfo 'America/New_York' EDT-1 day, 20:00:00 DST>)
funnydman
  • 9,083
  • 4
  • 40
  • 55
frmdstryr
  • 20,142
  • 3
  • 38
  • 32
6

The following worked for me in a Cloud environment for US west:

import datetime
import pytz

#set the timezone
tzInfo = pytz.timezone('America/Los_Angeles')
dt = datetime.datetime.now(tz=tzInfo)
print(dt)
Jie
  • 1,107
  • 1
  • 14
  • 18
  • Thank you, this was the best solution for me. However there is a minor typo: dt = datetime.datetime.now(tz=tzInfo) should be dt = datetime.now(tz=tzInfo) – Jeremy Morgan Feb 18 '23 at 19:08
5

Consolidating the answer from franksands into a convenient method.

import calendar
import datetime

def to_local_datetime(utc_dt):
    """
    convert from utc datetime to a locally aware datetime according to the host timezone

    :param utc_dt: utc datetime
    :return: local timezone datetime
    """
    return datetime.datetime.fromtimestamp(calendar.timegm(utc_dt.timetuple()))
Martlark
  • 14,208
  • 13
  • 83
  • 99
5

Short and simple:

from datetime import datetime

t = "2011-01-21 02:37:21"
datetime.fromisoformat(t) + (datetime.now() - datetime.utcnow())
Eric McLachlan
  • 3,132
  • 2
  • 25
  • 37
4

You can use arrow

from datetime import datetime
import arrow

now = datetime.utcnow()

print(arrow.get(now).to('local').format())
# '2018-04-04 15:59:24+02:00'

you can feed arrow.get() with anything. timestamp, iso string etc

d21d3q
  • 365
  • 2
  • 10
  • convert a timestamp to human readable format: from datetime import datetime; dt_obj = datetime.fromtimestamp(1652139647); print(arrow.get(dt_obj).to('local').format()) # we can use our manual timezone instead of "local". ex: "Asia/Tehran" – ali reza May 10 '22 at 05:25
3

This worked for me:

from django.utils import timezone
from datetime import timedelta,datetime

ist_time = timezone.now() + timedelta(hours=5,minutes=30)

#second method

ist_time = datetime.now() + timedelta(hours=5,minutes=30)
ZygD
  • 22,092
  • 39
  • 79
  • 102
2

You can use calendar.timegm to convert your time to seconds since Unix epoch and time.localtime to convert back:

import calendar
import time

time_tuple = time.strptime("2011-01-21 02:37:21", "%Y-%m-%d %H:%M:%S")
t = calendar.timegm(time_tuple)

print time.ctime(t)

Gives Fri Jan 21 05:37:21 2011 (because I'm in UTC+03:00 timezone).

myaut
  • 11,174
  • 2
  • 30
  • 62
2
import datetime

def utc_str_to_local_str(utc_str: str, utc_format: str, local_format: str):
    """
    :param utc_str: UTC time string
    :param utc_format: format of UTC time string
    :param local_format: format of local time string
    :return: local time string
    """
    temp1 = datetime.datetime.strptime(utc_str, utc_format)
    temp2 = temp1.replace(tzinfo=datetime.timezone.utc)
    local_time = temp2.astimezone()
    return local_time.strftime(local_format)

utc_tz_example_str = '2018-10-17T00:00:00.111Z'
utc_fmt = '%Y-%m-%dT%H:%M:%S.%fZ'
local_fmt = '%Y-%m-%dT%H:%M:%S+08:00'

# call my function here
local_tz_str = utc_str_to_local_str(utc_tz_example_str, utc_fmt, local_fmt)
print(local_tz_str)   # 2018-10-17T08:00:00+08:00

When I input utc_tz_example_str = 2018-10-17T00:00:00.111Z, (UTC +00:00)
then I will get local_tz_str = 2018-10-17T08:00:00+08:00 (My target timezone +08:00)

parameter utc_format is a format determined by your specific utc_tz_example_str.
parameter local_fmt is the final desired format.

In my case, my desired format is %Y-%m-%dT%H:%M:%S+08:00 ( +08:00 timezone). You should construct the format you want.

Ray Cao
  • 71
  • 6
  • 1
    You should give a better description of your problem in plain text, where you explain the rest of users what are you trying to achieve and which is your problem – m33n Oct 17 '18 at 10:59
1

I traditionally defer this to the frontend -- send times from the backend as timestamps or some other datetime format in UTC, then let the client figure out the timezone offset and render this data in the proper timezone.

For a webapp, this is pretty easy to do in javascript -- you can figure out the browser's timezone offset pretty easily using builtin methods and then render the data from the backend properly.

Matt Billenstein
  • 678
  • 1
  • 4
  • 7
  • 3
    This works well in a lot of cases where you're simply localizing for display. Occasionally, however, you'll need to perform some business logic based on the user's time zone and you'll want to be able to do the conversion the application server tier. – Joe Holloway Jan 23 '11 at 01:32
1

From the answer here, you can use the time module to convert from utc to the local time set in your computer:

utc_time = time.strptime("2018-12-13T10:32:00.000", "%Y-%m-%dT%H:%M:%S.%f")
utc_seconds = calendar.timegm(utc_time)
local_time = time.localtime(utc_seconds)
franksands
  • 1,748
  • 16
  • 21
0

Here is a quick and dirty version that uses the local systems settings to work out the time difference. NOTE: This will not work if you need to convert to a timezone that your current system is not running in. I have tested this with UK settings under BST timezone

from datetime import datetime
def ConvertP4DateTimeToLocal(timestampValue):
   assert isinstance(timestampValue, int)

   # get the UTC time from the timestamp integer value.
   d = datetime.utcfromtimestamp( timestampValue )

   # calculate time difference from utcnow and the local system time reported by OS
   offset = datetime.now() - datetime.utcnow()

   # Add offset to UTC time and return it
   return d + offset
DelboyJay
  • 2,799
  • 1
  • 13
  • 18
  • it should be `datetime.fromtimestamp(ts)`: posix timestamp (float seconds) -> datetime object in local time (it works well if OS remembers past utc offsets for local timezone i.e., on Unix but not on Windows for dates in the past)). Otherwise pytz could be used. – jfs Jun 03 '13 at 14:33
  • May I suggest to do the following: `offset = datetime.utcnow().replace(minute=0, second=0, microsecond=0) - datetime.now().replace(minute=0, second=0, microsecond=0)` I got weird microsecond differences without it. – Erik van Oosten Sep 26 '13 at 09:21
  • @ErikvanOosten: have you tried `datetime.fromtimestamp(ts)` instead of the answer? – jfs Dec 14 '13 at 20:22