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