How to convert a UTC datetime to a local datetime using only standard library?
Asked Answered
G

14

337

I have a python datetime instance that was created using datetime.utcnow() and persisted in database.

For display, I would like to convert the datetime instance retrieved from the database to local datetime using the default local timezone (i.e., as if the datetime was created using datetime.now()).

How can I convert the UTC datetime to a local datetime using only python standard library (e.g., no pytz dependency)?

It seems one solution would be to use datetime.astimezone(tz), but how would you get the default local timezone?

Golter answered 30/12, 2010 at 14:14 Comment(3)
In which format was the time persisted to the database? If it is a standards format it may be that you don't need to do any conversion.Grandfather
This answer shows a simple way of using pytz.Ostend
Can not find an answer without recourse to pytz. Feel silly.Topazolite
G
6

I think I figured it out: computes number of seconds since epoch, then converts to a local timzeone using time.localtime, and then converts the time struct back into a datetime...

EPOCH_DATETIME = datetime.datetime(1970,1,1)
SECONDS_PER_DAY = 24*60*60

def utc_to_local_datetime( utc_datetime ):
    delta = utc_datetime - EPOCH_DATETIME
    utc_epoch = SECONDS_PER_DAY * delta.days + delta.seconds
    time_struct = time.localtime( utc_epoch )
    dt_args = time_struct[:6] + (delta.microseconds,)
    return datetime.datetime( *dt_args )

It applies the summer/winter DST correctly:

>>> utc_to_local_datetime( datetime.datetime(2010, 6, 6, 17, 29, 7, 730000) )
datetime.datetime(2010, 6, 6, 19, 29, 7, 730000)
>>> utc_to_local_datetime( datetime.datetime(2010, 12, 6, 17, 29, 7, 730000) )
datetime.datetime(2010, 12, 6, 18, 29, 7, 730000)
Golter answered 30/12, 2010 at 15:2 Comment(7)
Ugh. But that should work on Unix, at least. For Windows it can be incorrect if your daylight saving rules have changed since the date converted. Why don't you want to use a library?Posset
I only need utc/local conversion so dragging such a library seem to be overkill. Since I'm the only user of the application I can live with some limitations. It also avoid managing the dependency related issues (support Python 3?, Windows? quality...) which would be more costly than working on my small script.Golter
Well. if you need Python3, then you are out of luck. But otherwise researching and making your own solution is overkill compared to using a library.Posset
I just fell what I wanted was in the reach of the standard library. I do use libraries. But in this case, I wouldn't have bothered if there was no solution and just displayed UTC time. I'm also using Python 3 on this project...Golter
+1 (to offset the downvote: it might be valid requirement to seek stdlib-only solutions) with the caveat @Lennart mentioned. Given that dateutil might fail on both Unix and Windows. btw, you could extract utctotimestamp(utc_dt) -> (seconds, microseconds) from your code for clarity, see Python 2.6-3.x implementationCooper
An pytz (and dateutil) works on Python 3 since some time now, so the Python3/Windows/Quality issues are not a problem anymore.Posset
+1 also. I use a piece of software that uses PythonAXScript as an extension language. While I CAN use another library and do sometimes, it is very cumbersome for our team when transferring the Python script to another machine for use with said software. Standard library approaches wherever possible make much more sense for my use, even with little caveats. Thanks for this!!!Semidiurnal
C
447

In Python 3.3+:

from datetime import datetime, timezone

def utc_to_local(utc_dt):
    return utc_dt.replace(tzinfo=timezone.utc).astimezone(tz=None)

In Python 2/3:

import calendar
from datetime import datetime, timedelta

def utc_to_local(utc_dt):
    # get integer timestamp to avoid precision lost
    timestamp = calendar.timegm(utc_dt.timetuple())
    local_dt = datetime.fromtimestamp(timestamp)
    assert utc_dt.resolution >= timedelta(microseconds=1)
    return local_dt.replace(microsecond=utc_dt.microsecond)

Using pytz (both Python 2/3):

import pytz

local_tz = pytz.timezone('Europe/Moscow') # use your local timezone name here
# NOTE: pytz.reference.LocalTimezone() would produce wrong result here

## You could use `tzlocal` module to get local timezone on Unix and Win32
# from tzlocal import get_localzone # $ pip install tzlocal

# # get local timezone    
# local_tz = get_localzone()

def utc_to_local(utc_dt):
    local_dt = utc_dt.replace(tzinfo=pytz.utc).astimezone(local_tz)
    return local_tz.normalize(local_dt) # .normalize might be unnecessary

Example

def aslocaltimestr(utc_dt):
    return utc_to_local(utc_dt).strftime('%Y-%m-%d %H:%M:%S.%f %Z%z')

print(aslocaltimestr(datetime(2010,  6, 6, 17, 29, 7, 730000)))
print(aslocaltimestr(datetime(2010, 12, 6, 17, 29, 7, 730000)))
print(aslocaltimestr(datetime.utcnow()))

Output

Python 3.3
2010-06-06 21:29:07.730000 MSD+0400
2010-12-06 20:29:07.730000 MSK+0300
2012-11-08 14:19:50.093745 MSK+0400
Python 2
2010-06-06 21:29:07.730000 
2010-12-06 20:29:07.730000 
2012-11-08 14:19:50.093911 
pytz
2010-06-06 21:29:07.730000 MSD+0400
2010-12-06 20:29:07.730000 MSK+0300
2012-11-08 14:19:50.146917 MSK+0400

Note: it takes into account DST and the recent change of utc offset for MSK timezone.

I don't know whether non-pytz solutions work on Windows.

Cooper answered 8/11, 2012 at 10:22 Comment(11)
what does 'normalize' do?Whydah
@avi: in general: pytz: Why is normalize needed when converting between timezones?. Note: It is not necessary with the current implementation of .normalize() because the source timezone for .astimezone() is UTC.Cooper
I get TypeError: replace() takes no keyword arguments when trying the pytz solution.Monacid
@Monacid the code works as is, as the output in the answer demonstrates. Create complete minimal code example that leads to the TypeError, mention your Python, pytz versions that you use and post it as a separate Stack Overflow question.Cooper
Quick tip for the Python 3.3+ solution. To go the opposite direction (Native to UTC), this works: local_dt.replace(tzinfo=None).astimezone(tz=timezone.utc)Sibelius
@Sibelius local (naive) -> utc direction is more complicated (there may be ambiguous or even non-existing local times. Also, some timezones may have a different utc offset in the past -> the conversion may require a historical timezone database such as the one provided by the pytz module (and even if a library has access to the tz db, it has to use it correctly. Look at my many comments for common errors that can occur during the conversion.Cooper
@Sibelius here's a robust solution to a simpler problem Parsing of Ordered Timestamps in Local Time (to UTC) While Observing Daylight Saving TimeCooper
.astimezone() does not seem to work for dates that are far in the future. For a datetime object in 2045, it throws an OverflowError: OverflowError: timestamp out of range for platform time_tStifling
@Stifling DT.datetime(2045, 8, 18, tzinfo=DT.timezone.utc).astimezone(tz=None) works for me (/usr/bin/python3 -mplatform Linux-4.15.0-36-generic-x86_64-with-Ubuntu-18.04-bionic )Cooper
Does your answer take daylight saving into consideration?Bawdyhouse
@Bawdyhouse yes, DST in the answer stands for "Daylight Saving Time"Cooper
P
95

Since Python 3.9 you can use the zoneinfo module.

First lets get that time with utcnow():

>>> from datetime import datetime
>>> database_time = datetime.utcnow()
>>> database_time
datetime.datetime(2021, 9, 24, 4, 18, 27, 706532)

Then create the time zones:

>>> from zoneinfo import ZoneInfo
>>> utc = ZoneInfo('UTC')
>>> localtz = ZoneInfo('localtime')

Then convert. To convert between timezones, the datetime must know what timezone it is in, then we just use astimezone():

>>> utctime = database_time.replace(tzinfo=utc)
>>> localtime = utctime.astimezone(localtz)
>>> localtime
datetime.datetime(2021, 9, 24, 6, 18, 27, 706532, tzinfo=zoneinfo.ZoneInfo(key='localtime'))

For Python 3.6 to 3.8 you need the backports.zoneinfo module:

>>> try:
>>>     from zoneinfo import ZoneInfo
>>> except ImportError:
>>>     from backports.zoneinfo import ZoneInfo

The rest is the same.

For versions earlier than that need pytz or dateutil. datutil works similar to zoneinfo:

>>> from dateutil import tz
>>> utc = tz.gettz('UTC')
>>> localtz = tz.tzlocal()

The Conversion:
>>> utctime = now.replace(tzinfo=UTC)
>>> localtime = utctime.astimezone(localtz)
>>> localtime
datetime.datetime(2010, 12, 30, 15, 51, 22, 114668, tzinfo=tzlocal())

pytz has a different interface which is a result of Python's time zone handling not handling ambigous times:

>>> import pytz
>>> utc = pytz.timezone('UTC')
# There is no local timezone support, you need to know your timezone
>>> localtz = pytz.timezone('Europe/Paris')

>>> utctime = utc.localize(database_time)
>>> localtime = localtz.normalize(utctime.astimezone(localtz))
>>> localtime
Posset answered 30/12, 2010 at 14:42 Comment(12)
Thanks, I'll keep that at hand if I ever need a full solution. Does dateutil support Python 3.1 (says Python 2.3+ but it is suspicious)?Golter
dateutil might fail with the .astimezone() codeCooper
pytz indeed handles DST changeovers better.Posset
Update: pytz and dateutil both support Python 3 nowadays.Posset
@J.F.Sebastian: dateutil can not distinguish between the two 1:30 times that happen during a DST changeover. If you need to be able to distinguish between those two times, the workaround it to use pytz, which can handle it.Posset
@J.F.Sebastian: That is, the workaround it pytz, unless the obvious one works: Keep all datetimes in UTC, and only convert for display. But that's what you should do always anyway, even with pytz.Posset
@LennartRegebro: I would provided pytz-based solution myself if there were easy portable way to get local timezone name (that you need to create corresponding pytz timezone object). Here's an example for linux. "keep all datetimes in UTC" is a sound advice but it is irrelevant to the questionCooper
@LennartRegebro Is ">>>" in python block common? I'm used to reading output as comments after the code, you have the other way around, which makes since i.e. python REPL. Also, could you add update to refer 3.9+ users to the 3.9 answer? Yours is WAY HIGHER, but out-of-date now that 3.9 added TZ.Escalade
@Escalade Yes, it's common. This is the way it looks in the interpreter.Posset
Not true anymore docs.python.org/3/library/zoneinfo.htmlBertiebertila
Why not use a simple datetime.now(timezone.utc) and avoid the confusing utcnow plus the additional replace step? Also, ZoneInfo('localtime') is platform-specific.Aisne
datetime.utcnow() is dangerous because it returns a naive datetime. datetime.now(timezone.utc) is much better.Louis
B
27

Python 3.9 adds the zoneinfo module so now it can be done as follows (stdlib only):

from zoneinfo import ZoneInfo
from datetime import datetime

utc_unaware = datetime(2020, 10, 31, 12)  # loaded from database
utc_aware = utc_unaware.replace(tzinfo=ZoneInfo('UTC'))  # make aware
local_aware = utc_aware.astimezone(ZoneInfo('localtime'))  # convert

Central Europe is 1 or 2 hours ahead of UTC, so local_aware is:

datetime.datetime(2020, 10, 31, 13, 0, tzinfo=backports.zoneinfo.ZoneInfo(key='localtime'))

as str:

2020-10-31 13:00:00+01:00

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

pip install tzdata  

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

sudo pip install backports.zoneinfo

Then:

from backports.zoneinfo import ZoneInfo
Bushhammer answered 1/6, 2020 at 23:53 Comment(6)
you don't even need zoneinfo here - see first part of jfs' answer (the Python3.3+ part) ;-)Aisne
@MrFuppes I think ZoneInfo('localtime') is a lot clearer than tz=None (after all, one might expect tz=None to remove the timezone or return UTC rather than localtime).Bushhammer
@MrFuppes Plus the question might involve OP having a website and wanting to show users timestamps in their local time. For that you would have to convert to the user's explicit timezone rather than localtime.Bushhammer
Your note on Win10 might be true in the sense that Python 3.9 doesn't use it, but Win10 has include IANA ICU data since Windows 10 1703 (released in 2017), but it improved in 1903. According to Raymond Chen they recommend apps use it over Windows Registry TZ data now. devblogs.microsoft.com/oldnewthing/20210527-00/?p=105255Escalade
Good answer, I hope this can rise to the top one day.Bertiebertila
@Bushhammer After installing tzdata, I get an error on Windows. It is complaining about ZoneInfo('localtime') . The error is: zoneinfo._common.ZoneInfoNotFoundError: No time zone found with key localtime.Intendment
T
17

You can't do it with standard library. Using pytz module you can convert any naive/aware datetime object to any other time zone. Lets see some examples using Python 3.

Naive objects created through class method utcnow()

To convert a naive object to any other time zone, first you have to convert it into aware datetime object. You can use the replace method for converting a naive datetime object to an aware datetime object. Then to convert an aware datetime object to any other timezone you can use astimezone method.

The variable pytz.all_timezones gives you the list of all available time zones in pytz module.

import datetime,pytz

dtobj1=datetime.datetime.utcnow()   #utcnow class method
print(dtobj1)

dtobj3=dtobj1.replace(tzinfo=pytz.UTC) #replace method

dtobj_hongkong=dtobj3.astimezone(pytz.timezone("Asia/Hong_Kong")) #astimezone method
print(dtobj_hongkong)

Naive objects created through class method now()

Because now method returns current date and time, so you have to make the datetime object timezone aware first. The localize function converts a naive datetime object into a timezone-aware datetime object. Then you can use the astimezone method to convert it into another timezone.

dtobj2=datetime.datetime.now()

mytimezone=pytz.timezone("Europe/Vienna") #my current timezone
dtobj4=mytimezone.localize(dtobj2)        #localize function

dtobj_hongkong=dtobj4.astimezone(pytz.timezone("Asia/Hong_Kong")) #astimezone method
print(dtobj_hongkong)
Twobyfour answered 12/10, 2016 at 13:48 Comment(0)
A
8

Building on Alexei's comment. This should work for DST too.

import time
import datetime

def utc_to_local(dt):
    if time.localtime().tm_isdst:
        return dt - datetime.timedelta(seconds = time.altzone)
    else:
        return dt - datetime.timedelta(seconds = time.timezone)
Angevin answered 3/2, 2018 at 22:43 Comment(0)
G
6

I think I figured it out: computes number of seconds since epoch, then converts to a local timzeone using time.localtime, and then converts the time struct back into a datetime...

EPOCH_DATETIME = datetime.datetime(1970,1,1)
SECONDS_PER_DAY = 24*60*60

def utc_to_local_datetime( utc_datetime ):
    delta = utc_datetime - EPOCH_DATETIME
    utc_epoch = SECONDS_PER_DAY * delta.days + delta.seconds
    time_struct = time.localtime( utc_epoch )
    dt_args = time_struct[:6] + (delta.microseconds,)
    return datetime.datetime( *dt_args )

It applies the summer/winter DST correctly:

>>> utc_to_local_datetime( datetime.datetime(2010, 6, 6, 17, 29, 7, 730000) )
datetime.datetime(2010, 6, 6, 19, 29, 7, 730000)
>>> utc_to_local_datetime( datetime.datetime(2010, 12, 6, 17, 29, 7, 730000) )
datetime.datetime(2010, 12, 6, 18, 29, 7, 730000)
Golter answered 30/12, 2010 at 15:2 Comment(7)
Ugh. But that should work on Unix, at least. For Windows it can be incorrect if your daylight saving rules have changed since the date converted. Why don't you want to use a library?Posset
I only need utc/local conversion so dragging such a library seem to be overkill. Since I'm the only user of the application I can live with some limitations. It also avoid managing the dependency related issues (support Python 3?, Windows? quality...) which would be more costly than working on my small script.Golter
Well. if you need Python3, then you are out of luck. But otherwise researching and making your own solution is overkill compared to using a library.Posset
I just fell what I wanted was in the reach of the standard library. I do use libraries. But in this case, I wouldn't have bothered if there was no solution and just displayed UTC time. I'm also using Python 3 on this project...Golter
+1 (to offset the downvote: it might be valid requirement to seek stdlib-only solutions) with the caveat @Lennart mentioned. Given that dateutil might fail on both Unix and Windows. btw, you could extract utctotimestamp(utc_dt) -> (seconds, microseconds) from your code for clarity, see Python 2.6-3.x implementationCooper
An pytz (and dateutil) works on Python 3 since some time now, so the Python3/Windows/Quality issues are not a problem anymore.Posset
+1 also. I use a piece of software that uses PythonAXScript as an extension language. While I CAN use another library and do sometimes, it is very cumbersome for our team when transferring the Python script to another machine for use with said software. Standard library approaches wherever possible make much more sense for my use, even with little caveats. Thanks for this!!!Semidiurnal
S
4

The standard Python library does not come with any tzinfo implementations at all. I've always considered this a surprising shortcoming of the datetime module.

The documentation for the tzinfo class does come with some useful examples. Look for the large code block at the end of the section.

Sumy answered 30/12, 2010 at 17:29 Comment(3)
My guess about the like of core implementation is because the timezone database needs to be updated regularly as some country don't have fixed rules to determine DST periods. If I'm not mistaken, the JVM for example needs to be updated to get the last timezone database...Golter
@Nitro Zark, at least they should have had one for UTC. The os module could have provided one for local time based on operating system functions.Sumy
I was checking the list of new features in 3.2 and they added the UTC timezone in 3.2 (docs.python.org/dev/whatsnew/3.2.html#datetime). There does not seem to be a local timezone though...Golter
G
3

Use time.timezone, it gives an integer in "seconds west of UTC".

For example:

from datetime import datetime, timedelta, timezone
import time

# make datetime from timestamp, thus no timezone info is attached
now = datetime.fromtimestamp(time.time())

# make local timezone with time.timezone
local_tz = timezone(timedelta(seconds=-time.timezone))

# attach different timezones as you wish
utc_time = now.astimezone(timezone.utc)
local_time = now.astimezone(local_tz)

print(utc_time.isoformat(timespec='seconds')) 
print(local_time.isoformat(timespec='seconds'))

On my PC (Python 3.7.3), it gives:

2021-05-07T12:50:46+00:00
2021-05-07T20:50:46+08:00

Pretty simple and uses only standard libraries~

Glasper answered 7/5, 2021 at 12:57 Comment(0)
H
1

The easiest way I have found is to get the time offset of where you are, then subtract that from the hour.

def format_time(ts,offset):
    if not ts.hour >= offset:
        ts = ts.replace(day=ts.day-1)
        ts = ts.replace(hour=ts.hour-offset)
    else:
        ts = ts.replace(hour=ts.hour-offset)
    return ts

This works for me, in Python 3.5.2.

Hernandez answered 13/12, 2016 at 21:50 Comment(0)
C
0

A simple (but maybe flawed) way that works in Python 2 and 3:

import time
import datetime

def utc_to_local(dt):
    return dt - datetime.timedelta(seconds = time.timezone)

Its advantage is that it's trivial to write an inverse function

Chamois answered 9/3, 2016 at 12:15 Comment(1)
This gives the wrong result, as time.timezone doesn't take into consideration daylight savings.Shandrashandrydan
K
0

Here is another way to change timezone in datetime format (I know I wasted my energy on this but I didn't see this page so I don't know how) without min. and sec. cause I don't need it for my project:

def change_time_zone(year, month, day, hour):
      hour = hour + 7 #<-- difference
      if hour >= 24:
        difference = hour - 24
        hour = difference
        day += 1
        long_months = [1, 3, 5, 7, 8, 10, 12]
        short_months = [4, 6, 9, 11]
        if month in short_months:
          if day >= 30:
            day = 1
            month += 1
            if month > 12:
              year += 1
        elif month in long_months:
          if day >= 31:
            day = 1
            month += 1
            if month > 12:
              year += 1
        elif month == 2:
          if not year%4==0:
            if day >= 29:
              day = 1
              month += 1
              if month > 12:
                year += 1
          else:
            if day >= 28:
              day = 1
              month += 1
              if month > 12:
                year += 1
      return datetime(int(year), int(month), int(day), int(hour), 00)
Kwh answered 28/9, 2018 at 20:34 Comment(1)
Use timedelta to switch between timezones. All you need is the offset in hours between timezones. Don't have to fiddle with boundaries for all 6 elements of a datetime object. timedelta handles leap years, leap centuries, etc., too, with ease. You must 'from datetime import timedelta'. Then if the offset is a variable in hours: timeout = timein + timedelta(hours = offset), where timein and timeout are datetime objects. e.g. timein + timedelta(hours = -8) converts from GMT to PST.Lyallpur
M
0

for a specific situation:
 input utc datetime string. // usually from log
 output locale datetime string.


def utc_to_locale(utc_str):
    # from utc to locale
    d1=datetime.fromisoformat(utc_str+'-00:00')
    return d1.astimezone().strftime('%F %T.%f')[:-3]

tests:

>>> utc_to_locale('2022-02-14 00:49:06')
'2022-02-14 08:49:06.000'
>>> utc_to_locale('2022-02-14 00:49:06.123')
'2022-02-14 08:49:06.123'
>>> utc_to_locale('2022-02-14T00:49:06.123')
'2022-02-14 08:49:06.123'
Moseley answered 14/2, 2022 at 17:51 Comment(0)
M
-1

This is a terrible way to do it but it avoids creating a definition. It fulfills the requirement to stick with the basic Python3 library.

# Adjust from UST to Eastern Standard Time (dynamic)
# df.my_localtime should already be in datetime format, so just in case
df['my_localtime'] = pd.to_datetime.df['my_localtime']

df['my_localtime'] = df['my_localtime'].dt.tz_localize('UTC').dt.tz_convert('America/New_York').astype(str)
df['my_localtime'] = pd.to_datetime(df.my_localtime.str[:-6])
Microgram answered 26/3, 2019 at 16:44 Comment(1)
While this is interesting and I am going to use this method it is not using just the stdlib. It is using Pandas to do the conversions pandas.pydata.org/pandas-docs/version/0.25/reference/api/…Cerebration
L
-1

Use timedelta to switch between timezones. All you need is the offset in hours between timezones. Don't have to fiddle with boundaries for all 6 elements of a datetime object. timedelta handles leap years, leap centuries, etc., too, with ease. You must first

from datetime import datetime, timedelta

Then if offset is the timezone delta in hours:

timeout = timein + timedelta(hours = offset)

where timein and timeout are datetime objects. e.g.

timein + timedelta(hours = -8)

converts from GMT to PST.

So, how to determine offset? Here is a simple function provided you only have a few possibilities for conversion without using datetime objects that are timezone "aware" which some other answers nicely do. A bit manual, but sometimes clarity is best.

def change_timezone(timein, timezone, timezone_out):
    '''
    changes timezone between predefined timezone offsets to GMT
    timein - datetime object
    timezone - 'PST', 'PDT', 'GMT' (can add more as needed)
    timezone_out - 'PST', 'PDT', 'GMT' (can add more as needed)
    ''' 
    # simple table lookup        
    tz_offset =  {'PST': {'GMT': 8, 'PDT': 1, 'PST': 0}, \
                  'GMT': {'PST': -8, 'PDT': -7, 'GMT': 0}, \
                  'PDT': {'GMT': 7, 'PST': -1, 'PDT': 0}}
    try:
        offset = tz_offset[timezone][timezone_out]
    except:
        msg = 'Input timezone=' + timezone + ' OR output time zone=' + \
            timezone_out + ' not recognized'
        raise DateTimeError(msg)

    return timein + timedelta(hours = offset)

After looking at the numerous answers and playing around with the tightest code I can think of (for now) it seems best that all applications, where time is important and mixed timezones must be accounted for, should make a real effort to make all datetime objects "aware". Then it would seem the simplest answer is:

timeout = timein.astimezone(pytz.timezone("GMT"))

to convert to GMT for example. Of course, to convert to/from any other timezone you wish, local or otherwise, just use the appropriate timezone string that pytz understands (from pytz.all_timezones). Daylight savings time is then also taken into account.

Lyallpur answered 16/12, 2019 at 20:4 Comment(2)
This is /not/ a good idea. DST does not shift at the same date for the whole world. Use datetime functions to do this.Mouthwatering
@gabe - Understand that the table is not a good idea, although it may suit someone covering only US timezones. As I said at the end, the best way is to ALWAYS make datetime objects "aware" so that you can easily use the datetime functions.Lyallpur

© 2022 - 2024 — McMap. All rights reserved.