[go: up one dir, main page]

datetime arithmetic in Python PREMIUM

Trey Hunner smiling in a t-shirt against a yellow wall
Trey Hunner
7 min. read Python 3.10—3.14
Tags

Need to work with dates and times in your application? Whether you're building a scheduling tool, tracking time intervals, or calculating deadlines, Python's datetime and timedelta objects are the tools to use.

Whenever you find yourself working with datetime objects, it's worth spending a few minutes learning about the various arithmetic operations that Python's datetime and timedelta objects support.

Periods of time

First, what is a timedelta object?

A datetime object represents a moment in time, but a timedelta object represents an amount of time.

So while a datetime object might represent the final second of 1999:

>>> from datetime import datetime
>>> y2k_eve = datetime(1999, 12, 31, 23, 59, 59)

A timedelta object could represent 5 minutes, 3 hours, or 7 days:

>>> from datetime import timedelta
>>> five_minutes = timedelta(minutes=5)
>>> three_hours = timedelta(hours=3)
>>> seven_days = timedelta(days=7)

Note that timedelta objects only really store one unit: seconds.

There's no way to represent a number of months or years with a timedelta object because months and years don't have a fixed length. We can only truly represent a second, minute, hour, or day (assuming 24 hour days, 60 minute hours, and 60 second minutes).

What's the purpose of a timedelta object? 🤔

Arithmetic with datetime objects!

We'll get to date/time arithmetic in a moment, but first let's talk about comparing datetime objects.

Date comparisons

Python's datetime objects can be compared with the < and > operators:

>>> from datetime import datetime, timedelta, timezone
>>> sputnik_launch = datetime(1957, 10, 4, 19, 28, 34, tzinfo=timezone.utc)
>>> moon_landing = datetime(1969, 7, 20, 20, 17, tzinfo=timezone.utc)
>>> sputnik_launch < moon_landing
True

One datetime object is "less than" another if it represents an earlier moment in time.

So the first Internet message is less than the Apollo 11 moon landing because it happened earlier:

>>> pdt = timezone(timedelta(hours=-7))  # Pacific Daylight Time timezone
>>> first_internet_message = datetime(1969, 10, 29, 22, 30, 0, tzinfo=pdt)
>>> first_internet_message < moon_landing
False

That's pretty handy!

Shifting dates and times

What if you want to see whether a given datetime object represents a future time?

>>> party_date = datetime(2100, 12, 31)

Well, you could compare it to the current time:

>>> datetime.now() < party_date
True

But what if you want to check whether a given datetime object represents a time within the past week? For this, you'll need to "shift" one of these datetime objects.

This is what timedelta objects are for:

>>> party_date < datetime.now() - timedelta(weeks=1)
False

That False result means that party_date is not within the past week.

Python's datetime objects can be added and subtracted with a timedelta objects to shift from one moment in time to another:

>>> now = datetime.now()
>>> three_days_hence = now + timedelta(days=3)
>>> two_days_ago = now - timedelta(days=2)

Subtracting two datetime objects

What happens if you try to add two datetime objects together?

>>> from datetime import datetime, timedelta, timezone
>>> pst = timezone(timedelta(hours=-8))  # Pacific Standard Time timezone
>>> first_flight = datetime(1903, 12, 17, 10, 35, 0, tzinfo=pst)
>>> sputnik_launch = datetime(1957, 10, 4, 19, 28, 34, tzinfo=timezone.utc)
>>> first_flight + sputnik_launch

We get an error:

>>> first_flight + sputnik_launch
Traceback (most recent call last):
  File "<python-input-0>", line 1, in <module>
    first_flight + sputnik_launch
    ~~~~~~~~~~~~~^~~~~~~~~~~~~~~~
TypeError: unsupported operand type(s) for +: 'datetime.datetime' and 'datetime.datetime'

We can add a datetime to a timedelta, but we can't add a datetime to a datetime.

This makes sense. What would a moment in time added to another moment in time even represent?

What about subtracting two datetime objects? Is that possible?

>>> sputnik_launch - first_flight

It is!

>>> sputnik_launch - first_flight
datetime.timedelta(days=19650, seconds=3214)

The difference between two moments in time is an amount of time, which is represented by a timedelta object.

So we could ask whether the elapsed time between two datetime objects was less than a Uranus year by adding a datetime to a timedelta:

>>> uranus_year = timedelta(days=30_687)  # 30,687 Earth days
>>> sputnik_launch < first_flight + uranus_year
True

Or we could subtract the two datetime objects and see whether the returned timedelta represents less than one Uranus orbit:

>>> sputnik_launch - first_flight < uranus_year
True

Either approach works and which makes the most sense will depend on the question you're asking and your personal preference.

Arithmetic between timedelta objects

We've just seen that we can subtract datetime objects from one another to get a timedelta. This works for the datetime module's date objects as well.

For example we could subtract dates to find the time between the first flight and the first commercial jet flight:

>>> from datetime import date
>>> first_flight = date(1903, 12, 17)
>>> commercial_jet_flight = date(1952, 5, 2)
>>> flying_to_jets = commercial_jet_flight - first_flight
>>> print(flying_to_jets)
17669 days, 0:00:00

Or we could find the time between the invention of the can and the can opener:

>>> can_patent = date(1810, 8, 25)
>>> can_opener_patent = date(1858, 1, 5)
>>> can_to_opener = can_opener_patent - can_patent
>>> print(can_to_opener)
17300 days, 0:00:00

And since we can compare one timedelta with another, we can also verify that the time between cans and can openers was shorter than the time between flying and jets:

>>> can_to_opener < flying_to_jets
True

What would happen if add two timedelta objects together? Is that possible?

>>> can_to_opener + flying_to_jets

It is!

>>> can_to_opener + flying_to_jets
datetime.timedelta(days=34969)

We can add and subtract timedelta objects with each other to get new ones:

>>> flying_to_jets - can_to_opener
datetime.timedelta(days=369)

We can even multiply or divide a timedelta object by a number to scale it up or down:

>>> uranus_year = timedelta(days=30_687)  # 30,687 Earth days
>>> uranus_year * 10
datetime.timedelta(days=306870)

Or we can divide a timedelta object by another timedelta object. This can be handy for finding out approximately how many years a timedelta represents:

>>> uranus_year / timedelta(days=365.25)
84.0164271047228
>>> flying_to_jets / timedelta(days=365.25)
48.375085557837096
>>> can_to_opener / timedelta(days=365.25)
47.36481861738535

It's even possible to check how many times a timedelta can be evenly divided by another:

>>> can_to_opener // timedelta(7)  # weeks with no can opener
2471

Or how much time would remain if one timedelta divided another:

>>> can_to_opener % timedelta(days=7)
datetime.timedelta(days=3)
>>> weeks, remaining = divmod(can_to_opener, timedelta(days=7))
>>> weeks, remaining
(2471, datetime.timedelta(days=3))
>>> print(f"{weeks} weeks and {remaining.days} days")
2471 weeks and 3 days

Aside: arithmetic with timezones

You may have noticed that some of the above datetime objects included timezone information (tzinfo).

A datetime object with timezone information is called aware (timezone-aware) and a datetime object that lacks timezone information in called naive (timezone-naive).

As we saw above, datetime objects can be compared across timezones:

>>> from datetime import datetime, timedelta, timezone
>>> pdt = timezone(timedelta(hours=-7))  # Pacific Daylight Time timezone
>>> first_internet_message = datetime(1969, 10, 29, 22, 30, 0, tzinfo=pdt)
>>> moon_landing = datetime(1969, 7, 20, 20, 17, tzinfo=timezone.utc)

The timezone offset will be taken into account during the comparison.

The timezones shown above all have a fixed offset from UTC time. Many timezones observe Daylight Saving Time, which means their offset changes twice per year.

For handling timezones that observe Daylight Saving Time, Python has a zoneinfo module (which relies on a pip-installable tzdata package). When using zoneinfo timezones, the offset of a datetime object will automatically adjust appropriately:

>>> from zoneinfo import ZoneInfo
>>> nyc_nye = datetime(1999, 12, 31, 23, 59, 59, tzinfo=ZoneInfo("America/New_York"))
>>> print(nyc_nye)
1999-12-31 23:59:59-05:00
>>> print(nyc_nye + timedelta(days=180))
2000-06-28 23:59:59-04:00

Timezones, the datetime.timezone function, Python's zoneinfo module, and the legacy pytz project are outside the scope of this article. I may write move about those another time.

Advanced datetime arithmetic

Python's datetime and timedelta objects support a lot of arithmetic, but they do have their limitations.

Each timedelta stores its data as a single number that represents seconds and/or fractions of a second. Units that are a constant multiple of seconds can work well in the land of timedelta objects, namely minutes, hours, days, and weeks. That is, assuming minutes of 60 seconds, hours of 60 minutes, days of 24 hours, and weeks of 7 days.

Time units that are not a constant multiple of seconds can't be represented by timedelta objects. There's no way to represent a month or a year with a timedelta because these units have no fixed length (i.e. years have either 365 or 366 days).

To perform arithmetic with these variable-length units, you can use the third-party python-dateutil library. The dateutil library includes a relativedelta class that can shift dates in some pretty sophisticated ways.

For example, we could identify the 10th anniversary of the first flight:

>>> from datetime import datetime, timezone
>>> from dateutil.relativedelta import relativedelta
>>> pst = timezone(timedelta(hours=-8))
>>> first_flight = datetime(1903, 12, 17, 10, 35, 0, tzinfo=pst)
>>> print(first_flight + relativedelta(years=10))
1913-12-17 10:35:00-08:00

Or the 6 month anniversary:

>>> print(first_flight + relativedelta(months=6))
1904-06-17 10:35:00-08:00

Or we could find the second Monday after the Apollo 11 moon landing:

>>> from dateutil.relativedelta import relativedelta, MO
>>> second_monday_after = moon_landing + relativedelta(weekday=MO(2))
>>> second_monday_after
datetime.datetime(1969, 7, 28, 20, 17, tzinfo=datetime.timezone.utc)
>>> print(second_monday_after.date())
1969-07-28

For most applications I write that involve time arithmetic, I start with the datetime module from the Python standard library but I usually end up finding a need to install python-dateutil, either for relativedelta or one of its other advanced time-related features.

Use arithmetic with Python's datetime and timedelta objects

If you can imagine arithmetic that datetime and timedelta objects should support, they probably already do support it.

The addition, subtraction, multiplication, division, and modulo operators have all been overloaded to support pretty much all sensible operations (with constant time durations, that is).

The next time you need to ask a question of your datetime objects, don't forget about datetime arithmetic!

Remember to handle time zones carefully, and consider using third-party libraries like python-dateutil for more advanced needs, such as dealing with months, years, and daylight saving time transitions.

And if you find yourself needing to format and parse dates and times, see converting a string to a datetime and my strptime/strftime tool.

Also see the official Python documentation for more on Python's datetime module.

This is a free preview of a premium article. You have 2 previews remaining.