106

So I am trying to find a way to increment a datetime object by one month. However, it seems this is not so simple, according to this question.

I was hoping for something like:

import datetime as dt

now = dt.datetime.now()
later = now + dt.timedelta(months=1)

But that doesn't work. I was also hoping to be able to go to the same day (or the closest alternative) in the next month if possible. For example, a datetime object set at January 1st would increment to Feb 1st whereas a datetime object set at February 28th would increment to March 31st as opposed to March 28th or something.

To be clear, February 28th would (typically) map to March 31st because it is the last day of the month, and thus it should go to the last day of the month for the next month. Otherwise it would be a direct link: the increment should go to the day in the next month with the same numbered day.

Is there a simple way to do this in the current release of Python?

Community
  • 1
  • 1
d0rmLife
  • 4,112
  • 7
  • 24
  • 33
  • 3
    You'll need to specify the desired behaviour a bit more fully. Why should Feb 28th map to March 31st? What should Feb 27th map to? March 30th? What about Feb 14th? Feb 15th? IOW, what does "closest alternative" mean? It's not obvious. – Mark Dickinson Jan 28 '16 at 16:28
  • @SiHa You need to be more specific. I believe SO shuffles answers that are not accepted in an effort to diversify voting patterns. – d0rmLife Jan 28 '16 at 16:41
  • 1
    http://stackoverflow.com/a/4406260/2632856 – Kirubaharan J Jan 28 '16 at 16:42
  • @SiHa Now it's clear. Thanks. – d0rmLife Jan 28 '16 at 16:46
  • Since I didn't see this option below, when I'm just wanting to iterate over months and don't really care about the day, I just add 40 days and then replace the day with 1. `thisdate = datetime.datetime(2020, 1, 1) nextmonth = (thisdate + datetime.timedelta(days=40)).replace(day=1)` – aaron_python_dude Mar 09 '22 at 00:26

4 Answers4

210

Check out from dateutil.relativedelta import * for adding a specific amount of time to a date, you can continue to use timedelta for the simple stuff i.e.

import datetime
from dateutil.relativedelta import *
use_date = datetime.datetime.now()

use_date = use_date + datetime.timedelta(minutes=+10)
use_date = use_date + datetime.timedelta(hours=+1)
use_date = use_date + datetime.timedelta(days=+1)
use_date = use_date + datetime.timedelta(weeks=+1)

or you can start using relativedelta

use_date = use_date+relativedelta(months=+1)

use_date = use_date+relativedelta(years=+1)

for the last day of next month:

use_date = use_date+relativedelta(months=+1)
use_date = use_date+relativedelta(day=31)

Right now this will provide 29/02/2016

for the penultimate day of next month:

use_date = use_date+relativedelta(months=+1)
use_date = use_date+relativedelta(day=31)
use_date = use_date+relativedelta(days=-1)

last Friday of the next month:

use_date = use_date+relativedelta(months=+1, day=31, weekday=FR(-1))

2nd Tuesday of next month:

new_date = use_date+relativedelta(months=+1, day=1, weekday=TU(2))

As @mrroot5 points out dateutil's rrule functions can be applied, giving you an extra bang for your buck, if you require date occurences.
for example:
Calculating the last day of the month for 9 months from the last day of last month.
Then, calculate the 2nd Tuesday for each of those months.

from dateutil.relativedelta import *
from dateutil.rrule import *
from datetime import datetime
use_date = datetime(2020,11,21)

#Calculate the last day of last month
use_date = use_date+relativedelta(months=-1)
use_date = use_date+relativedelta(day=31)

#Generate a list of the last day for 9 months from the calculated date
x = list(rrule(freq=MONTHLY, count=9, dtstart=use_date, bymonthday=(-1,)))
print("Last day")
for ld in x:
    print(ld)

#Generate a list of the 2nd Tuesday in each of the next 9 months from the calculated date
print("\n2nd Tuesday")
x = list(rrule(freq=MONTHLY, count=9, dtstart=use_date, byweekday=TU(2)))
for tuesday in x:
    print(tuesday)

Last day
2020-10-31 00:00:00
2020-11-30 00:00:00
2020-12-31 00:00:00
2021-01-31 00:00:00
2021-02-28 00:00:00
2021-03-31 00:00:00
2021-04-30 00:00:00
2021-05-31 00:00:00
2021-06-30 00:00:00

2nd Tuesday
2020-11-10 00:00:00
2020-12-08 00:00:00
2021-01-12 00:00:00
2021-02-09 00:00:00
2021-03-09 00:00:00
2021-04-13 00:00:00
2021-05-11 00:00:00
2021-06-08 00:00:00
2021-07-13 00:00:00

rrule could be used to find the next date occurring on a particular day.
e.g. the next 1st of January occurring on a Monday (Given today is the 4th November 2021)

from dateutil.relativedelta import *
from dateutil.rrule import *
from datetime import *
year = rrule(YEARLY,dtstart=datetime.now(),bymonth=1,bymonthday=1,byweekday=MO)[0].year
year
2024

or the next 5 x 1st of January's occurring on a Monday

years = rrule(YEARLY,dtstart=datetime.now(),bymonth=1,bymonthday=1,byweekday=MO)[0:5]
for i in years:print(i.year)
... 
2024
2029
2035
2046
2052

The first Month next Year that starts on a Monday:

>>> month = rrule(YEARLY,dtstart=datetime.date(2023, 1, 1),bymonthday=1,byweekday=MO)[0]
>>> month.strftime('%Y-%m-%d : %B')
'2023-05-01 : May'

If you need the months that start on a Monday between 2 dates:

months = rrule(YEARLY,dtstart=datetime.date(2025, 1, 1),until=datetime.date(2030, 1, 1),bymonthday=1,byweekday=MO)
>>> for m in months:
...     print(m.strftime('%Y-%m-%d : %B'))
... 
2025-09-01 : September
2025-12-01 : December
2026-06-01 : June
2027-02-01 : February
2027-03-01 : March
2027-11-01 : November
2028-05-01 : May
2029-01-01 : January
2029-10-01 : October

This is by no means an exhaustive list of what is available. Documentation is available here: https://dateutil.readthedocs.org/en/latest/

Rolf of Saxony
  • 21,661
  • 5
  • 39
  • 60
13

Note: This answer shows how to achieve this using only the datetime and calendar standard library (stdlib) modules - which is what was explicitly asked for. The accepted answer shows how to better achieve this with one of the many dedicated non-stdlib libraries. If you can use non-stdlib libraries, by all means do so for these kinds of date/time manipulations!

How about this?

def add_one_month(orig_date):
    # advance year and month by one month
    new_year = orig_date.year
    new_month = orig_date.month + 1
    # note: in datetime.date, months go from 1 to 12
    if new_month > 12:
        new_year += 1
        new_month -= 12

    new_day = orig_date.day
    # while day is out of range for month, reduce by one
    while True:
        try:
            new_date = datetime.date(new_year, new_month, new_day)
        except ValueError as e:
            new_day -= 1
        else:
            break

    return new_date

EDIT:

Improved version which:

  1. keeps the time information if given a datetime.datetime object
  2. doesn't use try/catch, instead using calendar.monthrange from the calendar module in the stdlib:
import datetime
import calendar

def add_one_month(orig_date):
    # advance year and month by one month
    new_year = orig_date.year
    new_month = orig_date.month + 1
    # note: in datetime.date, months go from 1 to 12
    if new_month > 12:
        new_year += 1
        new_month -= 12

    last_day_of_month = calendar.monthrange(new_year, new_month)[1]
    new_day = min(orig_date.day, last_day_of_month)

    return orig_date.replace(year=new_year, month=new_month, day=new_day)
taleinat
  • 8,441
  • 1
  • 30
  • 44
  • Throwing an exception in a hot loop can *not* be good practice. – djechlin Jan 28 '16 at 16:41
  • 1
    How is that a hot loop? It will loop at most 3 times. Exceptions will be thrown relatively few times given random dates, about 0.016 (6 / 365.25) exceptions per call on average. If this becomes a performance bottleneck for someone, they can optimize as needed. – taleinat Jan 28 '16 at 16:46
  • I didn't say anything about performance, this is just not how exceptions are intended to be used or are used in software engineering. They should be for exceptions, not usual business logic. – djechlin Jan 28 '16 at 16:47
  • Doesn't this truncate time information? – djechlin Jan 28 '16 at 16:49
  • 2
    OK long story short rolling your own date code is ill advised. This might work well enough for some class project but you really should be using a well-supported, well-tested library when working with dates. So the answer to the OP's question is probably just "no" but I would expect there to be one or two standard libraries used for this pretty common problem (which I think are discussed in the linked question). – djechlin Jan 28 '16 at 16:50
  • Throws exception on leap seconds. – djechlin Jan 28 '16 at 16:59
  • @djechlin: Um, Python's `datetime` doesn't even support leap seconds in the first place. – Mark Dickinson Jan 28 '16 at 16:59
  • @MarkDickinson really? I've always thought that sort of thing was pretty important. – djechlin Jan 28 '16 at 17:01
  • Excellent man, this is fantatsic – Daniel May 05 '20 at 14:31
0

Question: Is there a simple way to do this in the current release of Python?

Answer: There is no simple (direct) way to do this in the current release of Python.

Reference: Please refer to docs.python.org/2/library/datetime.html, section 8.1.2. timedelta Objects. As we may understand from that, we cannot increment month directly since it is not a uniform time unit.

Plus: If you want first day -> first day and last day -> last day mapping you should handle that separately for different months.

erol yeniaras
  • 3,701
  • 2
  • 22
  • 40
  • 3
    This isn't a useful answer as there's really no reason to believe you're right and not just misinformed. – djechlin Jan 28 '16 at 16:45
  • Not necessarily. He does not ask "what is the simple way of doing that?" but "is there simple way of doing that?" and the answer is no. Maybe this answer will stop him spending his time for nothing and starting to implement a solution that would work for him, which saves him time and effort. – erol yeniaras Jan 28 '16 at 16:47
  • Citation needed. You are not a citation. – djechlin Jan 28 '16 at 16:51
  • Find a major library (dateutil!) that implements it, which implies it doesn't exist in the standard library. Cite a blog post by a leader in the industry. Show a formal request to whichever body controls Python for such a feature or find plans to support it directly in Python n+1. Show a feature of Python that this would be incompatible with. Show the documentation for the only place it would make sense to be. Etc.... – djechlin Jan 28 '16 at 16:57
  • Please have a look at: https://docs.python.org/2/library/datetime.html, section 8.1.2. timedelta Objects. And please consider that the OP asks for something like 'later = now + dt.timedelta(months=1)'. As you see, you cannot directly increment month since it is not uniform. BTW, someone already gave a full answer. – erol yeniaras Jan 28 '16 at 17:02
  • OK add that to your answer instead of telling me. – djechlin Jan 28 '16 at 17:05
  • @djechlin *Cite a blog post by a leader in the industry* - looking for proof by appeal to authority. Counter - [Clarke's first law](https://en.wikipedia.org/wiki/Clarke%27s_three_laws), an established eminent figure saying something "is impossible" is probably wrong. ;) – TessellatingHeckler Jan 28 '16 at 17:15
  • @TessellatingHeckler so everything else I listed is fine? great, thanks. also, a nit: proof != evidence. Eric Lippert believing something about C# is evidence that it is true. I don't exactly think that's a fallacy, but to each their own. – djechlin Jan 28 '16 at 17:24
  • @TessellatingHeckler ouch, to continue this discussion: physics != whether a programming language feature does not exist. And sometimes it's very possible to prove something is not possible. I trust you have the intellect to make the distinction ;) – djechlin Jan 28 '16 at 17:26
-6
>>> now
datetime.datetime(2016, 1, 28, 18, 26, 12, 980861)
>>> later = now.replace(month=now.month+1)
>>> later
datetime.datetime(2016, 2, 28, 18, 26, 12, 980861)

EDIT: Fails on

y = datetime.date(2016, 1, 31); y.replace(month=2) results in ValueError: day is out of range for month

Ther is no simple way to do it, but you can use your own function like answered below.