Billing cycles, reports and schedules are defined in months and weeks, which timedelta can't express.
timedelta stops at days and weeks, because months have different lengths. "Add one month" needs a rule. The usual one: keep the day, but clamp it to the last day of the target month (Jan 31 + 1 month = Feb 28 or 29).
The calendar module helps:
calendar.monthrange(year, month)returns(weekday of the 1st, number of days).calendar.isleap(year),calendar.monthcalendar(...), and text/HTML calendars.
Other common recipes:
- First of the month:
d.replace(day=1). Last day: first of next month minus one day. - Date ranges:
[start + timedelta(days=i) for i in range((end - start).days + 1)]. - Start of the week (Monday):
d - timedelta(days=d.weekday()). - ISO weeks:
d.isocalendar()returns(year, week, weekday). The ISO year can differ from the calendar year near New Year: 2024-12-30 belongs to ISO week 1 of 2025. Format with%G-W%V, not%Y-W%V.
For complex rules (every second Tuesday, business days with holidays), dateutil.relativedelta/rrule or pandas offsets are worth the dependency. Keep the policy explicit in your code and tests, since stakeholders often disagree on what "a month later" means.
Example
import calendar
from datetime import date, timedelta
def add_months(d, n):
y, m = divmod(d.month - 1 + n, 12)
year, month = d.year + y, m + 1
last = calendar.monthrange(year, month)[1]
return d.replace(year=year, month=month, day=min(d.day, last))
print(add_months(date(2025, 1, 31), 1)) # 2025-02-28
print(add_months(date(2024, 1, 31), 1)) # 2024-02-29
print(add_months(date(2025, 11, 15), 3)) # 2026-02-15
d = date(2025, 3, 19)
next_first = add_months(d.replace(day=1), 1)
print(next_first - timedelta(days=1)) # 2025-03-31
print(d - timedelta(days=d.weekday())) # 2025-03-17 (Monday)
nye = date(2024, 12, 30)
print(nye.isocalendar()[:2]) # (2025, 1)
print(nye.strftime("%Y-W%V"), nye.strftime("%G-W%V")) # 2024-W01 2025-W01Remember
- Month arithmetic needs an explicit rule, usually clamping to month end
calendar.monthrangegives the number of days in a month- ISO week years differ from calendar years; format with
%G-W%V
Watch out for
- Labelling ISO weeks with
%Y, which mislabels the last days of December or first days of January.