Q&A library Python

What happens when I add a month to the 31st of January?

Python Advanced dateoffsetdatetimemonth-end

Short answer

Adding a month to a date that does not exist in the target month clamps to the last valid day, so 31 January becomes 28 February. Use MonthEnd if you actually mean the end of the month.

The short version

Adding a month to the 31st of January gives the 28th of February, because the 31st does not exist there and pandas clamps to the last valid day. That is sensible, but it is not reversible, and it is not the same as "the end of the month". Use MonthEnd when you mean the month's last day.

The clamping behaviour

import pandas as pd

pd.Timestamp("2021-01-31") + pd.DateOffset(months=1)   # 2021-02-28

There is no 31st of February, so the result is clamped to the 28th. Every date library must do something here, and clamping is the least surprising choice.

It is not reversible

This is the catch that causes bugs.

d = pd.Timestamp("2021-01-31")
back = d + pd.DateOffset(months=1) - pd.DateOffset(months=1)
# 2021-01-28, not 2021-01-31

Adding a month and subtracting it again does not return you to where you started. Information (the original day of the month) was lost in the clamp. So a loop that repeatedly adds and subtracts months will drift, and a schedule built by adding months one at a time will slowly move away from month end.

Building a monthly schedule correctly

Do not add one month repeatedly. Add n months to the original date each time, so the clamp always applies to the same starting day.

start = pd.Timestamp("2021-01-31")
dates = [start + pd.DateOffset(months=n) for n in range(4)]
# 31 Jan, 28 Feb, 31 Mar, 30 Apr

Each date is derived from the original, so March correctly returns to the 31st rather than being stuck on the 28th.

When you mean the end of the month

If your intent is "the last day of the following month", say so with the anchored offset.

pd.Timestamp("2021-01-15") + pd.offsets.MonthEnd(1)   # 2021-01-31
pd.Timestamp("2021-01-31") + pd.offsets.MonthEnd(1)   # 2021-02-28

MonthEnd moves to the end of the month, which is a different operation from adding a calendar month. Note that from the 15th, one MonthEnd lands on the same month's end, which surprises people expecting it to jump a month. That is what "anchored" means: it moves to the next boundary, not a fixed distance.

Leap years too

pd.Timestamp("2020-02-29") + pd.DateOffset(years=1)   # 2021-02-28

The same clamping applies. An anniversary of the 29th of February simply does not exist in most years, and the library must choose. Knowing it chooses the 28th means you will not be surprised by an off-by-one in an annual calculation.

The general rule

Calendar arithmetic is full of rules that cannot all be satisfied at once, so every library makes compromises. When months or years are involved, do not assume the obvious result. Write a small test with the awkward dates (the 31st, the 29th of February, a date around a clock change) and confirm your code does what you intend. Those three cases catch nearly every date bug.

Need this solved on your own data?

The library is free and always will be. When the problem is bigger than an answer, we take on the work.

Start a project