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.
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
datetimelibrary using thefrom datetime import ...syntax. Thedatetimedatatype will only be placed in the local namespace if you usefrom datetime import datetimeto work with it. - Always use
dtto refer to thedatetimelibrary. For example, instead ofimport datetime, you would useimport datetime as dt. This way,dtwill always refer to the library, anddatetimewill 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 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 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.


