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:
- Import objects from the
datetime
library using thefrom datetime import ...
syntax. Thedatetime
datatype will only be placed in the local namespace if you usefrom datetime import datetime
to work with it. - Always use
dt
to refer to thedatetime
library. For example, instead ofimport datetime
, you would useimport datetime as dt
. This way,dt
will always refer to the library, anddatetime
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, usedatetime.today()
. - Supply keyword arguments to the constructor, for
year
,month
, andday
, with additional options forhour
,minute
,second
, andmicrosecond
. - Use a POSIX
timestamp
, by usingdatetime.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 datetime
s aren't a problem unless you're only comparing them to other naive datetime
s. 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 datetime
s to datetime
s with timezone information. If two datetime
s 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 argumenttzinfo
: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.