Be Hai Nguyen
Posted on February 11, 2023
Understanding the relationship between local date time and UTC date time. We will look at the following issues -- ⓵ local time, time zone, UTC offset, local date time and UTC date time; ⓶ assign UTC time zone to MySQL and PostgreSQL UTC date time values which are stored with no time zone information.
Python: local date time and UTC date time. |
I've done some studies on this subject, and have written some investigative code. I will just start off at the point most relevant to what we are going to look at.
We will be using only the Python standard date time library datetime — Basic date and time types, no third party packages involved.
❶ First, let's have a look at local time, time zone, UTC offset, local date time and UTC date time.
import time
local_time = time.localtime()
print("Time Zone: ", time.tzname)
print("Time Zone: ", time.strftime("%Z", local_time))
print("Date and Time Zone: ", time.strftime("%Y-%m-%d %H:%M:%S %Z", local_time) )
print("UTC Offset: ", time.strftime("%z", local_time))
Output:
Time Zone: ('AUS Eastern Standard Time', 'AUS Eastern Summer Time')
Time Zone: AUS Eastern Summer Time
Date and Time Zone: 2023-02-10 15:19:18 AUS Eastern Summer Time
UTC Offset: +1100
The most important piece of information is UTC Offset: +1100
, the first two (2) digits, positive 11
, is the number of hours, the second two (2) digits, 00
, is the number of minutes. I am in the beautiful state of Victoria, Australia; and at the time of this writing, we are 11 (eleven) hours ahead of the UTC date time. Of course, depending on where we are, this UTC offset figure could be a negative, which would indicate that we are behind the UTC date time.
Let's look at this 11 (eleven) hours ahead of the UTC date time:
from datetime import datetime, timezone
local_datetime = datetime.now()
utc_datetime = datetime.now(timezone.utc)
local_iso_str = datetime.strftime(local_datetime, "%Y-%m-%dT%H:%M:%S.%f")[:-3]
utc_iso_str = datetime.strftime(utc_datetime, "%Y-%m-%dT%H:%M:%S.%f")[:-3]
print(f"local dt: {local_iso_str}, tzname: {local_datetime.tzname()}")
print(f" utc dt: {utc_iso_str}, tzname: {utc_datetime.tzname()}")
print("\n")
print(f"local dt: {local_datetime.isoformat()}")
print(f" utc dt: {utc_datetime.isoformat()}")
Output:
local dt: 2023-02-10T15:46:08.407, tzname: None
utc dt: 2023-02-10T04:46:08.407, tzname: UTC
local dt: 2023-02-10T15:46:08.407281
utc dt: 2023-02-10T04:46:08.407281+00:00
We can see that my local date time is 11 hours ahead of UTC. The UTC offset for UTC date time is 00:00
-- which is understandable.
The time zone name for the local date time is None
, and UTC
for UTC date time. These are in conformance with datetime.tzname(). However, the first time I wrote this code, I was expecting either AUS Eastern Standard Time
or AUS Eastern Summer Time
for the local date time! 😂 This leads to datetime.astimezone(tz=None).
Add the following 5 (five) lines to the end of the last example:
print("\n")
utc_to_local_datetime = utc_datetime.astimezone()
utc_2_local_iso_str = datetime.strftime(utc_to_local_datetime, "%Y-%m-%dT%H:%M:%S.%f")[:-3]
print( f"utc to local dt: {utc_2_local_iso_str}, tzname: {utc_to_local_datetime.tzname()}" )
print( f"utc to local dt: {utc_to_local_datetime.isoformat()}" )
The last two (2) output lines are from the new code:
local dt: 2023-02-10T16:24:40.089, tzname: None
utc dt: 2023-02-10T05:24:40.089, tzname: UTC
local dt: 2023-02-10T16:24:40.089415
utc dt: 2023-02-10T05:24:40.089415+00:00
utc to local dt: 2023-02-10T16:24:40.089, tzname: AUS Eastern Summer Time
utc to local dt: 2023-02-10T16:24:40.089415+11:00
We can see that datetime.astimezone(tz=None) converts a UTC date time into local date time correctly, and the converted value now also has local time zone name; and furthermore, it still retains the original UTC offset value.
We can also calculate the UTC offset from our local date time:
from datetime import datetime, timezone
local_now = datetime.now()
utc_now = local_now.astimezone(timezone.utc)
local_as_utc = local_now.replace(tzinfo=timezone.utc)
print( f"{local_now}, tzname: {local_now.tzname()}" )
print( f"{utc_now}, tzname: {utc_now.tzname()}" )
print( f"{local_as_utc}, tzname: {local_as_utc.tzname()}" )
print( f"{local_as_utc - utc_now}" )
The last output line, 11:00:00
, is the UTC offset:
2023-02-11 10:08:22.023929, tzname: None
2023-02-10 23:08:22.023929+00:00, tzname: UTC
2023-02-11 10:08:22.023929+00:00, tzname: UTC
11:00:00
❷ Consider cases where date time data are UTC date time, but stored with no time zone information. The expiry
column of the following MySQL table:
CREATE TABLE `sessions` (
`id` int NOT NULL AUTO_INCREMENT,
`session_id` varchar(255) DEFAULT NULL,
`data` blob,
`expiry` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `session_id` (`session_id`)
) ENGINE=InnoDB AUTO_INCREMENT=51 DEFAULT CHARSET=utf8mb3;
If you are familiar with the Python Flask Web Development Framework, you might recognise that the above sessions
table is the server-side session table implemented by the package Flask-Session.
And a PostgreSQL equivalence:
CREATE TABLE IF NOT EXISTS ompdev1.sessions
(
id integer NOT NULL GENERATED BY DEFAULT AS IDENTITY
( CYCLE INCREMENT 1 START 1 MINVALUE 1 MAXVALUE
2147483647 CACHE 1 ),
session_id character varying(255) COLLATE
pg_catalog."default",
data bytea,
expiry timestamp without time zone,
CONSTRAINT sessions_pkey PRIMARY KEY (id),
CONSTRAINT sessions_session_id_key UNIQUE (session_id)
)
Values for sessions.expiry
are stored as:
● MySQL: 2023-02-09 05:00:45
, 2023-02-09 03:58:36
, etc.
● PostgreSQL: 2023-01-07 11:18:32.442136
, 2023-02-06 21:33:06.190584
, etc.
We know these are UTC date time: because that's how they are in the Flask-Session package code, when checking if a particular server session has expired, the package code also uses UTC date time comparison.
Suppose we want to check when a particular abandoned session has expired in our own local date time. How do we do the conversion?
Let's have a look at two (2) methods which will help to accomplish the conversion.
⓵ Method datetime.timetuple() breaks a date time value into individual components, and returns class time.struct_time, which we can access as a tuple:
time.struct_time((d.year, d.month, d.day,
d.hour, d.minute, d.second,
d.weekday(), yday, dst))
In the following example, we get the local date time, call the above method, then write out individual elements:
from datetime import datetime
local_datetime = datetime.now()
print(f"local dt: {local_datetime.isoformat()}")
dt_tuple = local_datetime.timetuple()
print("\n")
print(dt_tuple[0], dt_tuple[1], dt_tuple[2])
print(dt_tuple[3], dt_tuple[4], dt_tuple[5])
print(dt_tuple[6], dt_tuple[7], dt_tuple[8])
local dt: 2023-02-10T23:34:00.062678
2023 2 10
23 34 0
4 41 -1
We're interested in the first 6 (six) elements, which are year
, month
, day
, hour
, minute
and second
.
⓶ Next, the datetime constructor reads:
class datetime.datetime(year, month, day, hour=0, minute=0,
second=0, microsecond=0, tzinfo=None, *, fold=0)
What that means is, if we have year
, month
, day
, hour
, minute
, second
, microsecond
and time zone
, we can create a time zone aware date time. We'll ignore microsecond
, and default it to 0
from here onward.
Let's pick one of the sessions.expiry
value from above, 2023-02-06 21:33:06.190584
, and see how this constructor works with the following example:
from datetime import datetime, timezone
utc_datetime = datetime(2023, 2, 6, 21, 33, 6, 0, timezone.utc)
utc_iso_str = datetime.strftime(utc_datetime, "%Y-%m-%dT%H:%M:%S.%f")[:-3]
utc_2_local_iso_str = datetime.strftime(utc_datetime.astimezone(), "%Y-%m-%dT%H:%M:%S.%f")[:-3]
print( " utc dt: ", utc_iso_str, "tzname: ", utc_datetime.tzname() )
print( "utc to local dt: ", utc_2_local_iso_str, "tzname: ", utc_datetime.astimezone().tzname() )
utc dt: 2023-02-06T21:33:06.000 tzname: UTC
utc to local dt: 2023-02-07T08:33:06.000 tzname: AUS Eastern Summer Time
In short, converting sessions.expiry
date time to UTC date time, or more precisely, assigning UTC time zone to the sessions.expiry
values, there are two (2) steps involved:
- Use datetime.timetuple() to break a
sessions.expiry
value into individual components. - Call datetime constructor with these components and
timezone.utc
to create a UTC date time.
I am not sure if this is the most effective way of doing this, please keep a look out for a better approach.
I have tried this successfully with MySQL and PostgreSQL:
✿✿✿
I hope you find the information in this post useful and helpful. Thank you for reading and stay safe as always.
Posted on February 11, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.