Python datetime
frequently trip me up when I use them, usually due to a combination of
- dense documentation that is hard to read
- unexpected default behaviour
I've made this post mostly so that I can reference it myself when I need to.
Essentially, if you are using the pip package pytz
for anything except looking up definitions & metadata of UTC offsets, then you could just be using the standard library! (and the officially-recommended dateutil
if you need a good parser)
One quick note is that python 3.9+ improve the behaiour of the .isoformat()
method so that it produces a string with the colon between the hours and minutes of the offset, e.g.
+0000 -> +00:00
This change means that the strings more closly meet the stricter RFC3339 standard for datetimes, and is much more usable by different systems in the wild. If you're frustrated with .isoformat()
, try upgrading to 3.9+ first!
This should be simple, right? Let's try to a) Get the current time in UTC and b) convert to ISO 8601
There is a method datetime.datetime.utcnow()
, it sounds perfect!
In [1]: from datetime import datetime
In [2]: datetime.utcnow().isoformat()
Out[2]: '2022-03-06T00:48:07.160164'
# It turns out that utcnow is _not_ timezone-aware
In [3]: datetime.utcnow()
Out[3]: datetime.datetime(2022, 3, 6, 0, 49, 0, 407065)
https://docs.python.org/3/library/datetime.html#datetime.datetime.now
Looking at the documentation for the datetime.now
method:
"This function is preferred over today() and utcnow()."
In [6]: from datetime import timezone
In [7]: datetime.now(tz=timezone.utc)
Out[7]: datetime.datetime(2022, 3, 6, 0, 51, 50, 160870, tzinfo=datetime.timezone.utc)
In [8]: datetime.now(tz=timezone.utc).isoformat()
Out[8]: '2022-03-06T00:51:54.887995+00:00'
Success! This format is (as stated in the intro) unexpected, and it's the existence of methods like utcnow
that make the datetime
module all the more confusing, especially for newcomers to python that are unaware of how confusing it is.
If all you need is to apply an offset, then you can do this without having to mess around with classes
n [9]: from datetime import timedelta
In [10]: datetime.now(tz=timezone(timedelta(hours=11))).isoformat()
Out[10]: '2022-03-06T11:54:45.133517+11:00'
In [11]: datetime.now(tz=timezone.utc).isoformat()
Out[11]: '2022-03-06T00:54:59.331494+00:00'
This can be done via the astimezone()
method, which of course is chained onto a datetime
object and modifies it
In [14]: datetime.now().astimezone()
Out[14]: datetime.datetime(2022, 3, 6, 11, 57, 28, 962640, tzinfo=datetime.timezone(datetime.timedelta(seconds=39600), 'AEDT'))
You can see that the timezone name is correctly, displaying the Daylight-Savings version of Australian Eastern Standard time
.astimezone()
converts a datetime in a certain timezone to another, converting all datetime values as it does so. This makes it extremely handy for converting datetimes to different timezones
Call the replace
method and set microseconds to 0
In [19]: datetime.now().replace(microsecond=0).astimezone(timezone.utc).isoformat()
Out[19]: '2022-03-06T01:02:55+00:00'