Quickie Dev #11: About time… and Python

Do you know how to represent and manipulate time-related variables in Python?

This article is also available on Medium.

Photo by Ocean Ng on Unsplash

Most programming languages have various data types: strings, ints, floats, arrays… Some also have some “complex” types; for example, the dates and datetimes.

Dates are an essential component in lots of applications: be it a simple calendar, a flight-booking website, a dating app… all of those use dates at one time or another!

It’s so important that plenty of databases have a specific “date type” (even if some actually store it as a string via normalised conversions). And Python has some built-in support for dates and datetimes in its datetime package.

Disclaimer: this tutorial shows snippets of code that are valid for Python 3. Some are also ok in Python 2, but for example the timezone manipulation isn’t possible as is in Python 2!

“Date” versus “datetime”, and the timezone issue

“Date” or “datetime”?

When you work with dates, a big question is whether you should store just a date, or an actual “datetime”.

In the first case, you only have the day, the month and the year. In the second case, you also get the hour, minute, second and millisecond detail. In Python, the datetime package differentiates between the date and the datetime object and clearly shows that the second one contains way more data!

>>> import datetime
>>> datetime.date.fromtimestamp(0)
datetime.date(1970, 1, 1)
>>> datetime.datetime.fromtimestamp(0)
datetime.datetime(1970, 1, 1, 1, 0)

Here, I’ve printed the exact same moment in time, the “time zero” in computer science (which corresponds to January 1st, 1970 at 00:00 UTC) as a date and as a datetime. The date object only contains the year, month and day info, but the datetime also provides us with the hour and the minute.

Note: the display is shortened to the minute level because we don’t have seconds or milliseconds for this datetime; we’ll see in upcoming examples that a datetime object actually contains the data for those lower levels too.

Timezones and the UTC reference

We immediately see something very important: those datetime objects inherently contain the timezone. This means that this exact method will not create the same object depending on where you’re located when you run it! I’m in Paris (UTC +2h), so I get a result for the “time zero” that is “shifted” by one hour:

>>> import datetime
>>> datetime.datetime.fromtimestamp(0)
datetime.datetime(1970, 1, 1, 1, 0)
>>> datetime.datetime.fromtimestamp(0).isoformat()

Hopefully, we’ve established a normalised time reference over the years, the UTC-time (a coordinated universal time) that helps us get deterministic and reliable results. So if you don’t care about the current timezone and prefer to always get the same object, make sure to use the “utc” functions of the datetime package:

>>> import datetime
>>> datetime.datetime.utcfromtimestamp(0)
datetime.datetime(1970, 1, 1, 0, 0)
>>> datetime.datetime.utcfromtimestamp(0).isoformat()

But! Although using this technique is quick and easy, it creates a naive datetime object, meaning a variable that is not aware it has been “forced” to the UTC timezone. This may not be an issue depending on the context… however if you want to do some computation with this object later on, you might run into some problems where this time is treated as local time instead of UTC-time.

To avoid this, it’s better to create a datetime object and explicitly pass it the UTC timezone:

>>> from datetime import datetime, timezone
>>> datetime.fromtimestamp(0, timezone.utc)
datetime.datetime(1970, 1, 1, 0, 0, tzinfo=datetime.timezone.utc)
>>> datetime.fromtimestamp(0, timezone.utc).isoformat()

Of course, you can also pass in your own timezone (not just the UTC one) to get an object that knows it’s in a given local timezone:

>>> from datetime import datetime, timezone, timedelta
>>> d = datetime.fromtimestamp(0, timezone(timedelta(seconds=14400)))
>>> d
datetime.datetime(1970, 1, 1, 4, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=14400)))
>>> d.isoformat()

No matter if it’s the UTC timezone or your local timezone, you now have an aware datetime that contains its specific timezone. It’s important to note that, if you don’t use aware objects, you’ll probably have issues with the daylight-saving time info whenever you start making some calculations.

So if you want more robust time arithmetic, always include the timezones or use a dedicated lib that is specifically made for timezone-dependent computations, like the pytz package!

Snippet 1: Getting the current date (with or without time details)

Python makes it easy to get info on the current datetime, using the now() function:

>>> import datetime
>>> datetime.datetime.now()
datetime.datetime(2021, 8, 4, 15, 9, 37, 917634)

If you only want the current date (i.e. the day without the hour/min/second detail), you can use the today() function instead:

>>> datetime.date.today()
datetime.date(2021, 8, 4)

You see that the first function creates a Python datetime object while the second one creates a date object. Once again, be careful: those are computed with your current timezone in mind unless you explicitly pass another one:

>>> from datetime import datetime
>>> datetime.now()
datetime.datetime(2021, 8, 4, 15, 50, 51, 920016)
>>> datetime.now(timezone.utc)
datetime.datetime(2021, 8, 4, 13, 50, 57, 582092, tzinfo=datetime.timezone.utc)

By the way: you also have the utcnow() equivalent to quickly get the naive UTC-compliant current datetime object!

Snippet 2: Computing time differences

Remember that the datetime package is pretty powerful on its own, but as stated in the Python docs:

While date and time arithmetic is supported, the focus of the implementation is on efficient attribute extraction for output formatting and manipulation.

As we’ve seen before, you always have to be very careful with timezones when you compute something with your date/datetime objects.

In other words, this package is really great for creating, parsing and formatting dates or datetimes, but it’s not the best to compute time deltas or complex time operations – it requires you to tell it what to do in very precise details, otherwise things might go haywire.

You can still compute simple things, like a time difference in seconds:

>>> d1 = datetime.datetime(2021, 8, 4, 15, 19, 0, 0)
>>> d2 = datetime.datetime(2021, 8, 4, 15, 19, 30, 1000)
>>> d2 - d1
datetime.timedelta(0, 30, 1000)
>>> (d2 - d1).total_seconds()

In this case, Python creates a new type of object, the timedelta (that can be written in a pretty-form with the total_seconds() method) – that represents the difference between two datetimes.

Note that you can also do it with dates:

>>> D1 = datetime.date(2021, 8, 1)
>>> D2 = datetime.date(2021, 8, 4)
>>> D2 - D1
>>> (D2 - D1).days
>>> (D2 - D1).seconds

And that you can re-use a timedelta to update a datetime (or date) instance by a given offset:

>>> import datetime
>>> d1 = datetime.datetime(2021, 8, 4, 15, 19, 0, 0)
>>> delta = datetime.timedelta(0, 30, 1000)
>>> d1 + delta
datetime.datetime(2021, 8, 4, 15, 19, 30, 1000)

But when you want to work on some high-level timedelta arithmetic, it can be more interesting to use the timedelta pip package, because their Timedelta object contains more info than the native Python built-in timedelta (this example is copied from the lib’s README):

>>> import timedelta
>>> td = timedelta.Timedelta(days=2, hours=2)
# init from datetime.timedelta
>>> td = timedelta.Timedelta(datetime1 - datetime2)
>>> td = timedelta.Timedelta(days=2, hours=2)
>>> td.total.seconds
>>> td.total.minutes
>>> td.total.hours
>>> td.total.days

Snippet 3: Parsing a string into a date…

Another very common operation when working with time-related variables is to parse strings into their matching date or datetime object equivalent. This usually relies on some standards so that everyone writes the same moment with the same string format, no matter what original programming language they use.

The most famous standard is the ISO 8601. It relies on the Gregorian calendar and it handle timezone offsets. It’s what you get when you call the isoformat() method of a datetime object in Python (as shown in previous examples):

>>> import datetime
>>> d = datetime.datetime(2021, 8, 4, 15, 19, 0, 0)
>>> d.isoformat()

This format is great because it’s easy to read and it precisely defines time stamps so that you can’t confuse two different moments. It can also integrate the timezone info if there is one:

>>> import datetime
>>> d = datetime.datetime(2021, 8, 4, 15, 19, 0, 0, tzinfo=datetime.timezone.utc)
>>> d.isoformat()

But of course, they’re a plenty more formats you can use… and perhaps the dates are even stored with a completely custom format in your database!

To parse any string into a datetime (or a date), you can use the Python strptime() function:

>>> from datetime import datetime
>>> datetime.strptime('04/08/2021 16:26:07', '%d/%m/%Y %H:%M:%S')
datetime.datetime(2021, 8, 4, 16, 26, 7)

You can see the full list of time format codes here (scroll down in the page).

Snippet 4: … and formatting a date into a string!

On the other hand, if you have some datetime object and you want to print it in a very specific format (for example to show it on your website page), you have the strftime() method:

>>> from datetime import datetime
>>> d = datetime.now()
>>> d.strftime('%d/%m/%Y %H:%M:%S')
'04/08/2021 16:32:34'

If you want to get the ISO format directly, you have the isoformat() function; or you can simply print the datetime object, because the str function uses this format by default for datetime variables:

>>> from datetime import datetime
>>> d = datetime.now()
>>> print(d)
2021-08-04 16:37:53.789147


Manipulating time-related variables is pretty easy in Python, but you have to be careful about timezones to properly compute time differences (i.e. timedeltas). Various packages can help with timezone and timedeltas calculation like pytz and timedelta.

What about you: do you have any tips or snippets for Python time stuff you’d like to share? Feel free to react in the comments! 😉

Leave a Reply

Your email address will not be published. Required fields are marked *