"If my time is 16:44, what is that in UTC time?" is what I asked myself last week.
"Do we use UTC dates or do we store timezones into the database? And by the way, if I call datetime.now()
, am I getting the correct time or should I adjust it with a timezone suffix?".
This time-business is so delicate. I never had much trouble with date-times, until last week, when I had to create a humble Celery task that needs to be a master of time, that needs to understand how all the dates on all these objects relate to one another. And suddenly I find myself appalled by all datetimes with no timezone info. What a grievous mistake it was to allow programmers to forget about timezones, to just call now()
and hope for the best. How is my super-duper, time-lord of a Celery task supposed to lord the time if a single measly timezone-lacking datetime knocks it out of its balance?
So I had to read a few docs and a few standards to really, truly understand what to do with missing timezones and whether the datetime I am creating is really and truly the exact datetime I want and not one that is 1h before or ahead (my timezone is +01:00, which is very easy to mistake for +00:00).
What is UTC time
UTC is a global time standard. It defined how time will be measured and coordinated between everybody. But UTC is also used as a successor of GMT (=Greenwich Mean Time) to describe the time at the 0° meridian.
In code, we usually denote it with +00:00 or with a Z. The letter Z is used for historical reasons because Z was the name of the timezone using this time. This time is also sometimes called the Zulu time since NATO's phonetic alphabet for Z is "Zulu".
The daylight saving time (=DST) does not affect UTC. DST simply modifies the ±[hh]:[mm]-part of the date-time: Europe's CTE (=Central European Time) is usually described as UTC+1
, but during the summer, when DST kicks in, it simply becomes UTC+2
.
What is the ISO format / ISO 8601 format
This ISO standard defines how to format dates and date-times without confusion and misunderstandings.
Date format:
- 2020-10-26
Date and time formats:
- 2020-10-26T08:15:30+02:00
- 2020-10-26T08:15:30Z
- 20201026T081530Z
- 2020-10-26T08:15:30.456+02:00 <- with microseconds
If the timezone is omitted, the datetime is usually interpreted to be recording the locale time. But of course, this is up to every programming team to define for themselves.
The T is also often omitted. The ISO standard does allow for this, but only if the 2 parties communicating with such dates have agreed to allow it. However, there is no mention of substituting T with a space. But I think we all see that our community has decided that it can correctly understand a datetime with a space and is actively using it everywhere.
What is UNIX time / Epoch time / POSIX time
Unix time is an integer, it's the number of seconds passed since 1st January 1970 UTC - a capriciously chosen date. This date is called the Epoch.
UNIX time excludes all leap seconds. It pretends that every day has exactly 24 * 60 * 60 (=86 400) seconds.
Timezones and UTC offsets
UTC offsets are the ±[hh]:[mm] parts of date-time formats. They are a multiple of 15 minutes and they describe the difference between UTC and the locale time.
Python in the world of datetime
Python differentiates between naive and aware datetimes. The first one has no timezone, while the second does have a timezone. Each datetime
object has an optional attribute tzinfo
, which can hold timezone information.
How to get the current time
My current time is 2020-10-26T12:30:10Z
, my timezone is +1. Here I get my locale time:
In : from datetime import datetime
In : datetime.now() # produces a naive datetime
Out: datetime.datetime(2020, 10, 26, 13, 30, 10, 933315)
In : datetime.utcnow() # produces a naive datetime
Out: datetime.datetime(2020, 10, 26, 12, 30, 10, 933315)
Both above functions create a naive datetime.
How to get the current time with timezone
In : from datetime import datetime, timezone
In : datetime.now(tz=timezone.utc)
Out: datetime.datetime(2020, 10, 26, 12, 30, 10, 933315, tzinfo=datetime.timezone.utc)
How to create an exact datetime
Here I create the 31st Dec 2020, at midnight UTC time:
In : from datetime import datetime, timezone
In : datetime(2020, 12, 31, 0, 0, 0, tzinfo=timezone.utc)
Out: datetime.datetime(2020, 12, 31, 0, 0, 0, tzinfo=datetime.timezone.utc)
And here I create the 31st Dec 2020, at midnight in the timezone +03:00:
In : from datetime import datetime, timedelta, timezone
In : datetime(2020, 12, 31, 0, 0, 0, tzinfo=timezone(timedelta(minutes=3 * 60)))
Out: datetime.datetime(2020, 12, 31, 0, 0, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=3600))
Timezones are set with timedelta
to describe the UTC offset.
How to get the UNIX time of any date / how to turn UNIX time to date
In Python, this format is called "timestamp". And it is not an integer, but a float, so that microseconds can be represented.
In : from datetime import datetime, timezone
# turn a date into a timestamp:
In : d = datetime(2020, 10, ...)
In : d.timestamp()
Out: 1603711810.933315 # the num of seconds since Epoch
# turn the timestamp into a naive date
In : datetime.fromtimestamp(1603711810.933315)
Out: datetime.datetime(2020, 10, 26, 11, 30, 10, 933315)
# turn the timestamp into an aware date
In : datetime.fromtimestamp(1603711810.933315, tz=timezone.utc)
Out: datetime.datetime(2020, 10, 26, 11, 30, 10, 933315, tzinfo=datetime.timezone.utc)
# turn the timestamp into an aware date at UTC+3
In : datetime.fromtimestamp(1603711810.933315, tz=timezone(timedelta(minutes=3 * 60)))
Out: datetime.datetime(2020, 10, 26, 14, 30, 10, 933315, tzinfo=datetime.timezone(datetime.timedelta(seconds=10800)))
The POSIX time is always the time at UTC+0, which means that when we call fromtimestamp
with the timezone parameter we define only what the timezone of the result will be, not the timezone of the input value. When we passed in UTC+0
, it produced the time 11:30
, when we passed in UTC+3
it produced 14:30
.
How to parse a string into a datetime
Use the datetime.fromisoformat
for this, BUT: not all ISO 8601 strings can be parsed. Only strings of this format can be parsed:
YYYY-MM-DD[*HH[:MM[:SS[.fff[fff]]]][+HH:MM[:SS[.ffffff]]]]
Using Z
as the timezone is not supported.
In : datetime.fromisoformat("2020-10-26T12:30+02:00")
Out: datetime.datetime(2020, 10, 26, 12, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200)))
In : datetime.fromisoformat("2020-10-26 12:30:10.998")
Out: datetime.datetime(2020, 10, 26, 12, 30, 10, 998000)
How to add or remove days/months/years to a datetime
One way is to use timedelta
:
In : d = datetime.fromisoformat("2020-10-26 12:30:10.555+01:00")
Out: datetime.datetime(2020, 10, 26, 12, 30, 10, 555000, tzinfo=datetime.timezone(datetime.timedelta(seconds=3600)))
# add 1 day
In : d + timedelta(days=1)
Out: datetime.datetime(2020, 10, 27, 12, 30, 10, 555000, tzinfo=datetime.timezone(datetime.timedelta(seconds=3600)))
# add 4 hours
In : d + timedelta(minutes=4 * 60)
Out: datetime.datetime(2020, 10, 26, 16, 30, 10, 555000, tzinfo=datetime.timezone(datetime.timedelta(seconds=3600)))
# subtract a year (given that this is not a leap year)
In : d + timedelta(minutes=365)
Out: datetime.datetime(2019, 10, 27, 12, 30, 10, 555000, tzinfo=datetime.timezone(datetime.timedelta(seconds=3600)))
The other is to replace parts of the date, but this can trigger ValueError
s:
In : d = datetime.fromisoformat("2020-02-29 00:00:00+00:00")
Out: datetime.datetime(2020, 2, 29, 0, 0, tzinfo=datetime.timezone.utc)
# this date 20 years ago:
In : d.replace(year=2000)
Out: datetime.datetime(2000, 2, 29, 0, 0, tzinfo=datetime.timezone.utc)
# change year to 2019, which didn't have Feb 29th:
In: d.replace(year=2019)
---------------------------------------------------------------------------
ValueError
----> 1 d.replace(year=2019)
ValueError: day is out of range for month
The replace
function can be used to replace any part of the datetime: minutes, hours, days, timezone, ...:
In : d = datetime.fromisoformat("2020-02-29 00:00:00+00:00")
Out: datetime.datetime(2020, 2, 29, 0, 0, tzinfo=datetime.timezone.utc)
In : d.replace(year=2019, day=28, tzinfo=timezone(timedelta(minutes=-8 * 60)))
Out: datetime.datetime(2019, 2, 28, 0, 0, tzinfo=datetime.timezone(datetime.timedelta(days=-1, seconds=57600)))
Top comments (0)