How to Handle Timezone Differences in Python

Article Overview: In a previous post, we discussed the datetime module in Python and used it to iterate through a range of dates. Today, let’s learn how to manage timezones in Python using the datetime module and a third-party package called dateutil.  I aim to choose a range of world cities and print the local time in each of those cities. I will also print my local time and UTC, or Coordinated Universal Time. If you’ve not used the datetime module before, I suggest a quick review of this post before continuing.

The Python datetime module and dateutil package

The Python datetime module dramatically eases the coding complexities when dealing with timezones and leap years. The three most common classes of the datetime module are:

datetime.date()

This class uses the Gregorian calendar, which started in 1582 following a slight adjustment to the Julian calendar. However, datetime.date() assumes the Gregorian calendar extends infinitely into the past and the future, and it takes the attributes, year, month, and day.

datetime.time()

This class is independent of any particular day and assumes that a day consists of 24 x 60 x 60 seconds and ignores leap seconds. It accepts attributes, hour, minute, second, microsecond, and time zone information, abbreviated to tzinfo.

datetime.datetime()

This class is a combination of the two classes above and utilises the same attributes of both.

Today we’ll be using only the datetime() class from the datetime module.

However, we will introduce some greater functionality with a package called dateutil, a third-party package that adds power and utility to the standard datetime module in Python.  If you don’t have dateutil installed, you can do so with the following command:

pip install python-dateutil

Dateutil has several modules; however, today, we’ll use the tz module, allowing timezone implementation by using information from the datetime.tzinfo() class. If you wish to know more about the dateutil package, you can find it here.

Are your datetimes aware or naive?

When we speak of someone being naive in English, we usually mean they lack sophistication, are simple, or innocent. In Python, we distinguish between dates and times as naive or aware, meaning that a naive date or time instance does not contain timezone information, whilst an aware one does.

The Python documentation states that an aware date or time instance is a specific moment in time not open to interpretation, and it can locate itself in relation to other aware date or time objects.  If I did arithmetic with aware dates or times from different timezones, Python would deliver the correct time interval. If I tried it with a naive object, it would return the wrong information.

For this reason, we use datetime objects which contain an attribute called tzinfo, or timezone information, that creates awareness. Tzinfo captures information about the offset from UTC, the timezone names, and whether daylight saving time is in force.

Let’s write some code!

We’ll begin with importing the datetime module, and then the tz module from the dateutil package. As we code, I’ll introduce, use, and explain a few classes found within the tz module. Then we’ll find the current UTC, or time at zero degrees longitude.

from datetime import datetime as dt
from dateutil.tz import *

print(dt.now(tzlocal()))

# Result
# 2021-05-13 16:51:04.250732+01:00

In this code, we imported the datetime class from the datetime module and we gave it an alias of dt just to save me writing the class out longhand each time. We then used the asterisk notation to import all classes of the tz module which forms part of the dateutil package.

I called the now() method which forms part of the datetime module. The now() method returns the current date and time of the computer system during the execution of the now statement. However, we passed to the now method the method tzlocal() which is in the tz module and it returns the timezone information, or tzinfo. This gives access to the timezone and daylight saving information needed to make the datetime object aware. You can see the returned value is 16:51 on the afternoon of 13 May 2021, and the timezone is +1 hour from UTC. Now we’ll see what the time is at zero degrees longitude by calling for the current UTC.

from datetime import datetime as dt
from dateutil.tz import *

print(dt.now(tzlocal()), '\n')
print(dt.now(tzutc()))

# Result
# 2021-05-13 17:01:16.626317+01:00
# 2021-05-13 16:01:16.627316+00:00

Here we’ve introduced another method from the dateutil tz module, which is called tzutc(). As you’d expect, it returns an aware datetime instance in UTC of datetime.now(). So you see my time in London, which is 17:01 GMT Summer Time, and the time in UTC, which is 16:01 UTC.

Success – but not that readable

We’ve achieved a result, but it’s not that user friendly. I’m going to format the output from these two lines of code to give the timezone, and we’ll then format the output to be more readable.

from datetime import datetime as dt
from dateutil.tz import *

local = dt.now(tzlocal()).tzname()
print('London: ', dt.now(tzlocal()), local, '\n')

utc = dt.now(tzutc()).tzname()
print('UTC: ', '\t', dt.now(tzutc()), utc)

# Result
# London:  	2021-05-13 17:22:29.135941+01:00 GMT Summer Time
# UTC:  	2021-05-13 16:22:29.135941+00:00 UTC

There are a few things happening in this last bit of code. Let’s unpick it. I’ve introduced a new method from the dateutils tz module, called tzname(). This method returns the timezone name from the datetime instances for my time and UTC time, and I’ve passed it to two variables, local and utc respectively. Then in the print line, I’ve simply called the variable after the datetime instance to print out the timezone info. Of course, I’ve put in the name of the city or zone upfront as a string to enhance readability.

So readability is improving, but we can do more. Let’s format the output of the datetime instance to be more human friendly.

from datetime import datetime as dt
from dateutil.tz import *

local = dt.now(tzlocal()).tzname()
lon = dt.now(tzlocal())
print('London: ', lon.strftime('%A %d %b %Y  %H:%M hrs'), local, '\n')

utc = dt.now(tzutc()).tzname()
base = dt.now(tzutc())
print('UTC: ', '\t', base.strftime('%A %d %b %Y  %H:%M hrs'), utc)

# Result
# London:  	Thursday 13 May 2021  17:38 hrs GMT Summer Time
# UTC:  	Thursday 13 May 2021  16:38 hrs UTC

Now that’s a vast improvement. In this code, I introduced the strftime() method from the datetime module. This method does a string format on the datetime instance, hence the name.

I created a datetime instance for each location and passed it to variables lon and base, respectively. I then called the datetime instance using strftime() and used specific codes in a string to format the data to be returned. For the complete list of codes, go to this site. To explain the ones we’ve used, here’s a list:

%AWeekday as locale’s full name.Thursday
%dDay of the month as a zero-padded decimal number.13
%BMonth as locale’s full name.May
%YYear with century as a decimal number.2021
%HHour (24-hour clock) as a zero-padded decimal number.16
%-MMinute as a decimal number. (Platform specific)38

Now we have readable format giving all the information we need, we can add in those other cities in which we wish to know the time. Here’s the final bit of code with some reconfiguration and some new methods.

from datetime import datetime as dt
from dateutil.tz import *

utc = dt.now(tzutc()).tzname()
base = dt.now(tzutc())
print('UTC: ', '\t\t\t\t', base.strftime('%A %d %b %Y  %H:%M hrs'), utc, '\n')

print('East of UTC', '\n')

local = dt.now(tzlocal()).tzname()
lon = dt.now(tzlocal())
print('London: ', '\t\t\t', lon.strftime('%A %d %b %Y  %H:%M hrs'), local)

jburg = dt.now(tzoffset('SAST', 7200))
sa = dt.tzname(jburg)
print('Johannesburg: ', '\t\t', jburg.strftime('%A %d %b %Y  %H:%M hrs'), sa)

tokyo = dt.now(tzoffset('JST', 32400))
jp = dt.tzname(tokyo)
print('Tokyo: ', '\t\t\t', tokyo.strftime('%A %d %b %Y  %H:%M hrs'), jp)

kiri = dt.now(tzoffset('LINT', 50400))
ci = dt.tzname(kiri)
print('Kiribati: ', '\t\t\t', kiri.strftime('%A %d %b %Y  %H:%M hrs'), ci, '\n')

print('West of UTC', '\n')

wash_dc = dt.now(tzoffset('EDT', -14400))
us = dt.tzname(wash_dc)
print('Panama: ', '\t\t\t', wash_dc.strftime('%A %d %b %Y  %H:%M hrs'), us)

pana = dt.now(tzoffset('PAB', -18000))
pan = dt.tzname(pana)
print('Washington DC: ', '\t', pana.strftime('%A %d %b %Y  %H:%M hrs'), pan)

ckt = dt.now(tzoffset('CKT', -36000))
rar = dt.tzname(ckt)
print('Rarotonga: ', '\t\t', ckt.strftime('%A %d %b %Y  %H:%M hrs'), rar)

alo = dt.now(tzoffset('NUT', -39600))
nut = dt.tzname(alo)
print('Niue: ', '\t\t\t\t', alo.strftime('%A %d %b %Y  %H:%M hrs'), nut)

Output:

# Result

UTC:  			Friday 14 May 2021  10:34 hrs UTC

East of UTC

London:  		Friday 14 May 2021  11:34 hrs GMT Summer Time
Johannesburg:  	Friday 14 May 2021  12:34 hrs SAST
Tokyo:  		Friday 14 May 2021  19:34 hrs JST
Kiribati:  		Saturday 15 May 2021  00:34 hrs LINT

West of UTC

Panama:  		Friday 14 May 2021  06:34 hrs EDT
Washington DC:  	Friday 14 May 2021  05:34 hrs PAB
Rarotonga:  		Friday 14 May 2021  00:34 hrs CKT
Niue:  			Thursday 13 May 2021  23:34 hrs NUT

In this code, I’ve introduced another method of the tz class, which is tzoffset(). With tzoffset() you pass two attributes, the name of the timezone and the offset in seconds from UTC. Note that for timezones west of UTC, the offset number is positive; it’s negative for those East of UTC.

I find the world clock website to be useful for time, date, and abbreviation information.

In Summary

In this article, we introduced the datetime module in Python and the third-party package dateutils. We aimed to output a list of times in chosen world cities compared to UTC and our local time.

We learned about the datetime.datetime() class in Python, and the tz class in dateutils. We also looked at naive and aware datetime instances.

Classes we utilised are:

datetime.now()Returns the current date and time of the computer system during the execution of the now statement.
tz.local()Returns the timezone information or tzinfo.
tz.utc()Returns an aware datetime instance in UTC
tz.name()Returns the timezone name from a datetime instance
tz.offset()Accepts two attributes; timezone name and its offset in seconds from UTC
strftime()Does a string format on the datetime instance from entered codes

Finally, we produced a list of chosen cities worldwide and printed formatted dates and times, with their timezone name appended.

I trust this article has been helpful to you. Thanks for reading.