Master Python's datetime type

Learn how to work with date and time values using Python's datetime library, and how to avoid some of the gotchas and pitfalls of the datetime datatype.

Master Python's datetime datatype
Thinkstock

Python's datetime library, part of its standard library, provides datatypes and methods for working with dates and times. Dates and times are slippery, inconsistent things, however, and libraries for working with them can only smooth things over so much.

In this article, we'll explore how to use Python's datetime library, its datatypes, and its methods. You'll learn how to make the most of these features while steering clear of their traps and complexities in Python.

datetime objects and namespacing

The name datetime refers to both the datetime library and to one of the Python datatypes. The library is namespaced as just datetime. But the datatype for date-time objects is namespaced as datetime.datetime.

If you just enter import datetime into a Python program, you're only importing the datetime library. If you want to work with the datetime datatype to create dates and times, then you need to refer to datetime.datetime or use from datetime import datetime. This will retrieve the datatype object rather than the library.

There are two ways to reduce confusion when working with the datetime library and datatype:

  1. Import objects from the datetime library using the from datetime import ... syntax. The datetime datatype will only be placed in the local namespace if you use from datetime import datetime to work with it.
  2. Always use dt to refer to the datetime library. For example, instead of import datetime, you would use import datetime as dt. This way, dt will always refer to the library, and datetime will always refer to the datatype.

For the sake of clarity, we'll take the second option for our examples, and use datetime to refer to the datatype and dt as an alias for the datetime library.

The datetime datatype

The datetime datatype is how Python represents dates and times in an object. When you create an instance of datetime, you can instantiate it in one of several ways:

  • Use the current date and time with datetime.now(). For just the date, use datetime.today().
  • Supply keyword arguments to the constructor, for year, month, and day, with additional options for hour, minute, second, and microsecond.
  • Use a POSIX timestamp, by using datetime.fromtimestamp(timestamp). (More on this soon.)
  • Supply a date/time string in a stated format with datetime.strptime(). (More on this soon.)

All of these return a new datetime object, with the date and time information encoded into it as properties. For instance, if you created a new datetime object named now, you could get the year by inspecting now.year.

Timezones and naive datetimes

Any datetime that has real-world relevance should have a timezone associated with it. But in all the above examples for creating a datetime, the resulting object is what's called a naive datetime—one with no timezone information attached to it.

Naive datetimes aren't a problem unless you're only comparing them to other naive datetimes. For instance, if you take a datetime object periodically as part of a benchmarking operation, and you only compare those objects to each other, it isn't an issue.

The problem is when you try to compare naive datetimes to datetimes with timezone information. If two datetimes have timezone information, it's not hard to adjust them and determine their relationship. But if one is naive, there's no way to compare them intelligibly.

To that end, when you create datetime objects, get in the habit of adding a timezone. You can always use UTC time as a fallback, like so:

  • For dt.datetime.now(), you can pass in a timezone as an argument: dt.datetime.now(dt.timezone.utc).
  • For dt.datetime, you can pass a timezone as the keyword argument tzinfo: dt.datetime(year=2023, month=4, day=1, tzinfo=dt.timezone.utc).

If you have an existing naive datetime and you want to assign a timezone to it, you can use the datetime.astimezone() method. This lets you pass in a timezone as an argument, and get back a new datetime with a timezone added. The original datetime information is assumed to be UTC time.

If you want to find out what timezone (if any) is associated with a datetime object, inspect its .tzinfo property. None means you are looking at a naive datetime.

Time differences and timedelta objects

datetime objects describe a point in time. If you want an object that refers to a span of time, or the difference between two points in time, you need a different kind of object: a timedelta.

timedelta objects are what you get when you take two datetime objects and subtract one from the other to get the difference. If you have a datetime from now and a datetime from an hour ago, the resulting timedelta will represent that one-hour difference. If you create a third datetime value and add that timedelta to it, you'll get a new datetime object offset by that much time.

Here's a simplified example:


import time
import datetime as dt
now = dt.datetime.now()
time.sleep(10)
# ten seconds later ... 
now_2 = dt.datetime.now()
ten_seconds_delta = now_2 - now

now and now_2 were taken 10 seconds apart. Get the difference between the two, and we have a timedelta object of 10 seconds.

You can also create timedelta objects manually, by passing arguments to the constructor that describe the differences. Here's an example using a negative delta value:


import datetime as dt
now = dt.datetime.now()
minus_one_hour = dt.timedelta(hours=-1)
one_hour_ago = now + minus_one_hour

An important thing to understand about timedelta objects is that they might seem to be missing certain units. They contain time information (seconds, minutes, hours) and date information in the form of days. But they do not contain weeks, months, or years.

This isn't a bug, but a deliberate omission. Date values beyond counting days have inconsistent measures, so there's no point in having them be part of a timedelta. For instance, it doesn't make sense to talk about a delta of "one month" if you don't know which months are in question, since not all months have the same number of days. (The same goes for years.) So if you want to talk about what happened on the same day of the last month, say the 30th, you need to make sure the last month had that day.

Converting from and to POSIX timestamps

A common format for time in computing is the POSIX timestamp, or the number of seconds (expressed as a float) since January 1, 1970. datetime objects can be created from a timestamp with datetime.fromtimestamp(), which takes in the timestamp as an argument and an optional timezone. You can also yield a timestamp from a datetime object with datetime.timestamp().

Be warned that attempts to get a timestamp from a time object that's outside the range of valid times for a timestamp will raise an OverflowError exception. In other words, if you're processing historical dates, don't use POSIX timestamps to store or serialize them. Use the ISO 8601 string format, described in the next section.

Converting date and time strings

If you want to serialize a datetime object to a string, the safest and most portable way to do it is with datetime.isoformat(). This returns a string-formatted version of the datetime object in the ISO 8601 format; e.g., '2023-04-11T12:03:17.328474'. This string can then be converted back into a datetime object with datetime.fromisoformat().

For custom date/time formatting, datetime objects offer datetime.strftime() to print a datetime object using custom formatting, and datetime.strptime() to parse date/time strings with custom formatting. The format codes for strftime and strptime follow the C89 standard, with a few custom extensions depending on the platform.

Again, as a general rule, don't use a custom date format for serializing dates to strings and back agan. The ISO format described above is easier to work with and automatically consistent.

Copyright © 2023 IDG Communications, Inc.