Welcome, guest | Sign In | My Account | Store | Cart

Adding or subtracting a month to a Python datetime.date or datetime.datetime is a little bit of a pain. Here is the code I use for that. These functions return the same datetime type as given. They preserve time of day data (if that is at all important to you).

See also:

Python, 39 lines
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
def add_one_month(t):
    """Return a `datetime.date` or `datetime.datetime` (as given) that is
    one month earlier.
    
    Note that the resultant day of the month might change if the following
    month has fewer days:
    
        >>> add_one_month(datetime.date(2010, 1, 31))
        datetime.date(2010, 2, 28)
    """
    import datetime
    one_day = datetime.timedelta(days=1)
    one_month_later = t + one_day
    while one_month_later.month == t.month:  # advance to start of next month
        one_month_later += one_day
    target_month = one_month_later.month
    while one_month_later.day < t.day:  # advance to appropriate day
        one_month_later += one_day
        if one_month_later.month != target_month:  # gone too far
            one_month_later -= one_day
            break
    return one_month_later

def subtract_one_month(t):
    """Return a `datetime.date` or `datetime.datetime` (as given) that is
    one month later.
    
    Note that the resultant day of the month might change if the following
    month has fewer days:
    
        >>> subtract_one_month(datetime.date(2010, 3, 31))
        datetime.date(2010, 2, 28)
    """
    import datetime
    one_day = datetime.timedelta(days=1)
    one_month_earlier = t - one_day
    while one_month_earlier.month == t.month or one_month_earlier.day > t.day:
        one_month_earlier -= one_day
    return one_month_earlier

8 comments

Andreas Balogh 13 years, 9 months ago  # | flag

There is a much simpler way. For finding the previous month you go to the first, then subtract one day:

from datetime import timedelta

def subtract_one_month(dt0):
    dt1
= dt0.replace(days=1)
    dt2
= dt1 - timedelta(days=1)
    dt3
= dt2.replace(days=1)
   
return dt3

For finding the next month's first you advance to the next month. By adding 32 days from the first of a month you will always reach the next month.

def add_one_month(dt0):
    dt1
= dt0.replace(days=1)
    dt2
= dt1 + timedelta(days=32)
    dt3
= dt2.replace(days=1)
   
return dt3
Trent Mick 13 years, 9 months ago  # | flag

@andreas: Yup, thanks. For some of my use cases, preserving the day of month was important.

jort.bloem 11 years, 9 months ago  # | flag

What about this as an alternative; the range of months is limited by date or datetime, both are supported, and all data (day, and hour/minute/second if it's a datetime) are maintained. The code is also much shorter, and negative numbers are supported for subtracting months.

Note: months may be positive, or negative, but must be an integer.

def addmonths(date,months): targetmonth=months+date.month try: date.replace(year=date.year+int(targetmonth/12),month=(targetmonth%12)) except: # There is an exception if the day of the month we're in does not exist in the target month # Go to the FIRST of the month AFTER, then go back one day. date.replace(year=date.year+int((targetmonth+1)/12),month=((targetmonth+1)%12),day=1) date+=datetime.timedelta(days=-1)

jort.bloem 11 years, 9 months ago  # | flag

Lets try that again:

# Note: months may be positive, or negative, but must be an integer.
def addmonths(date,months):
    targetmonth
=months+date.month
   
try:
        date
.replace(year=date.year+int(targetmonth/12),month=(targetmonth%12))
   
except:
       
# There is an exception if the day of the month we're in does not exist in the target month
       
# Go to the FIRST of the month AFTER, then go back one day.
        date
.replace(year=date.year+int((targetmonth+1)/12),month=((targetmonth+1)%12),day=1)
        date
+=datetime.timedelta(days=-1)
Richard Fairhurst 11 years, 8 months ago  # | flag

I think the code of jort.bloom is on the right track, but I believe errors will also always be generated when the target month is a multiple of 12, which returns a modulo remainder of 0. An error in the month will mess up the result. For example, addmonths(12/5/2011, 0) i.e., December 5, 2011 minus no months, will return 12/31/2011, not 12/5/2011, because an error will occur when month=(targetmonth%12) tries to set month=0, not month=12.

Also, when the input date is the last day of the month, often I favor using the end of the month of the offset date. So, while 7/31/2012 - 1 month = 6/30/2012 is good, 6/30/2012 - 1 month = 5/30/2012 is bad (I want 5/31/2012). A variable indicating that I want to favor the end of the month should be included so that I can specify if I want an end of the month date to offset to the closest matching day number in a month with more days (i.e, 30 in a 30 day month = 30 in a 31 day month) or to the closest end of the month day in a month with more days (30 in a 30 day month = 31 in a 31 day month).

Here is the way I would revise the code to deal with these issues:

# Note: months may be positive, or negative, but must be an integer. 
# If favorEoM (favor End of Month) is true and input date is the last day of the month then
# return an offset date that also falls on the last day of the month.
def addmonths(date,months,favorEoM):
   
try:
        targetdate
= date
        targetmonths
= months+targetdate.month
       
if targetmonths%12 = 0:
           
# Month must be between 1 and 12 so a modulo remainder of 0 = 12
            targetmonth
= 12
       
else:
            targetmonth
= targetmonths%12
       
if favorEoM == True:
           
# Favor matching an End of Month date to an End of Month offset date.
            testdate
= date+datetime.timedelta(days=1)
           
if testdate.day == 1:
               
# input date was a last day of month and end of month is favored, so
               
# go to the FIRST of the month AFTER, then go back one day.
                targetdate
.replace(year=targetdate.year+int((targetmonths+1)/12),month=(targetmonth%12+1),day=1)
                targetdate
+=datetime.timedelta(days=-1)
       
else:
           
# Do not favor matching an End of Month date to the offset End of Month.
            targetdate
.replace(year=dtargetdate.year+int(targetmonths/12),month=(targetmonth))
       
return targetdate
   
except:
       
# There is an exception if the day of the month we're in does not exist in the target month
       
# Go to the FIRST of the month AFTER, then go back one day.
        targetdate
.replace(year=targetdate.year+int((targetmonths+1)/12),month=(targetmonth%12+1),day=1)
        targetdate
+=datetime.timedelta(days=-1)
       
return targetdate
Richard Fairhurst 11 years, 8 months ago  # | flag

There was a missing else block in the code I posted. Here it is corrected.

# Note: months may be positive, or negative, but must be an integer.  
# If favorEoM (favor End of Month) is true and input date is the last day of the month then
# return an offset date that also falls on the last day of the month.
def addmonths(date,months,favorEoM):  
   
try:  
        targetdate
= date
        targetmonths
= months+targetdate.month  
       
if targetmonths%12 = 0:
           
# Month must be between 1 and 12 so a modulo remainder of 0 = 12
            targetmonth
= 12
       
else:
            targetmonth
= targetmonths%12
       
if favorEoM == True:
           
# Favor matching an End of Month date to an End of Month offset date.
            testdate
= date+datetime.timedelta(days=1)
           
if testdate.day == 1:
               
# input date was a last day of month and end of month is favored, so
               
# go to the FIRST of the month AFTER, then go back one day.
                targetdate
.replace(year=targetdate.year+int((targetmonths+1)/12),month=(targetmonth%12+1),day=1)
                targetdate
+=datetime.timedelta(days=-1)
           
else:
                targetdate
.replace(year=dtargetdate.year+int(targetmonths/12),month=(targetmonth))
       
else:
           
# Do not favor matching an End of Month date to the offset End of Month.
            targetdate
.replace(year=dtargetdate.year+int(targetmonths/12),month=(targetmonth))
       
return targetdate
   
except:
       
# There is an exception if the day of the month we're in does not exist in the target month
       
# Go to the FIRST of the month AFTER, then go back one day.
        targetdate
.replace(year=targetdate.year+int((targetmonths+1)/12),month=(targetmonth%12+1),day=1)
        targetdate
+=datetime.timedelta(days=-1)  
       
return targetdate
Richard Fairhurst 11 years, 8 months ago  # | flag

Oops. I caught a variable name spelling error. I wish I could edit my previous comments, but since I can't, here is the corrected code again:

# Note: months may be positive, or negative, but must be an integer.   
# If favorEoM (favor End of Month) is true and input date is the last day of the month then  
# return an offset date that also falls on the last day of the month.  
def addmonths(date,months,favorEoM):  
   
try:  
        targetdate
= date  
        targetmonths
= months+targetdate.month  
       
if targetmonths%12 = 0:  
           
# Month must be between 1 and 12 so a modulo remainder of 0 = 12  
            targetmonth
= 12  
       
else:  
            targetmonth
= targetmonths%12  
       
if favorEoM == True:  
           
# Favor matching an End of Month date to an End of Month offset date.  
            testdate
= date+datetime.timedelta(days=1)  
           
if testdate.day == 1:  
               
# input date was a last day of month and end of month is favored, so  
               
# go to the FIRST of the month AFTER, then go back one day.  
                targetdate
.replace(year=targetdate.year+int((targetmonths+1)/12),month=(targetmonth%12+1),day=1)  
                targetdate
+=datetime.timedelta(days=-1)  
           
else:
                targetdate
.replace(year=targetdate.year+int(targetmonths/12),month=(targetmonth))  
       
else:  
           
# Do not favor matching an End of Month date to the offset End of Month.  
            targetdate
.replace(year=targetdate.year+int(targetmonths/12),month=(targetmonth))  
       
return targetdate  
   
except:  
       
# There is an exception if the day of the month we're in does not exist in the target month  
       
# Go to the FIRST of the month AFTER, then go back one day.  
        targetdate
.replace(year=targetdate.year+int((targetmonths+1)/12),month=(targetmonth%12+1),day=1)  
        targetdate
+=datetime.timedelta(days=-1)  
       
return targetdate
Garel Alex 9 years, 7 months ago  # | flag

My own:

def add_month(date):
   
"""add one month to date, maybe falling to last day of month

    :param datetime.datetime date: the date

    ::
      >>> add_month(datetime(2014,1,31))
      datetime.datetime(2014, 2, 28, 0, 0)
      >>> add_month(datetime(2014,12,30))
      datetime.datetime(2015, 1, 30, 0, 0)
    """

   
# number of days this month
    month_days
= calendar.monthrange(date.year, date.month)[1]
    candidate
= date + timedelta(days=month_days)
   
# but maybe we are a month too far
   
if candidate.day != date.day:
       
# go to last day of next month,
       
# by getting one day before begin of candidate month
       
return candidate.replace(day=1) - timedelta(days=1)
   
else:
       
return candidate