15
\$\begingroup\$

I am looking for a cleaner, more pythonic way to get a list of the name of the months starting with the current month and ending 12 months later.

For example, it's December so my list should be

['Dec', 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov']

Here is how I'm currently doing it:

from datetime import datetime

currentMonth = datetime.now().month

if (currentMonth == 1):
    theList = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
elif (currentMonth == 2):
    theList = ['Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec', 'Jan']
elif (currentMonth == 3):
    theList = ['Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec', 'Jan', 'Feb']
elif (currentMonth == 4):
    theList = ['Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec', 'Jan', 'Feb', 'Mar']
elif (currentMonth == 5):
    theList = ['May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec', 'Jan', 'Feb', 'Mar', 'Apr']
elif (currentMonth == 6):
    theList = ['Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec', 'Jan', 'Feb', 'Mar', 'Apr', 'May']
elif (currentMonth == 7):
    theList = ['Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec', 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun']
elif (currentMonth == 8):
    theList = ['Aug', 'Sep', 'Oct', 'Nov', 'Dec', 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul']
elif (currentMonth == 9):
    theList = ['Sep', 'Oct', 'Nov', 'Dec', 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug']
elif (currentMonth == 10):
    theList = ['Oct', 'Nov', 'Dec', 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep']
elif (currentMonth == 11):
    theList = ['Nov', 'Dec', 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct']
elif (currentMonth == 12):
    theList = ['Dec', 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov']

print(theList)

Ideas?

\$\endgroup\$

6 Answers 6

23
\$\begingroup\$

Instead of hard-coding month names - delegate all the work to respective libraries.

I would suggest 2 approaches:

  • calendar.month_abbr

    from datetime import datetime
    from calendar import month_abbr
    
    def get_forward_month_list():
        month = datetime.now().month   # current month number
        return [month_abbr[(month % 12 + i) or month] for i in range(12)]
    
  • dateutil.relativedelta.relativedelta

    from datetime import datetime, timedelta
    from dateutil.relativedelta import relativedelta
    
    def get_forward_month_list():
        now = datetime.now()
        return [(now + relativedelta(months=i)).strftime('%b') for i in range(12)]
    

Both approaches will return the expected/needed list of month names:

print(get_forward_month_list())
['Dec', 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov']
\$\endgroup\$
10
  • 2
    \$\begingroup\$ Awesome. I thought there was a way to do it utilizing a library. Thank you. \$\endgroup\$ Dec 9, 2019 at 18:26
  • 11
    \$\begingroup\$ To avoid modulos and ors and complicated expressions for skipping the empty [0] I would use return month_abbr[month:] + month_abbr[1:month] instead \$\endgroup\$
    – njzk2
    Dec 10, 2019 at 3:23
  • 2
    \$\begingroup\$ @Christopher said "I thought there was a way to do it utilizing a library." The only thing the library did was provide the month_abbr list. It's the same as your first definition of theList, so, in terms of coding, all the library did was save a few characters of typing. — Thinking at a higher level though, libraries guarantee getting the same abbreviations as everyone else, and that should the world decide to make changes to the official abbreviation list (e.g. add a new month) your code will handle it with no changes on your part. And if a user prefers French, that's what they'll see. \$\endgroup\$ Dec 10, 2019 at 14:35
  • 1
    \$\begingroup\$ @njzk2 Absolutely. I've been staring at (month % 12 + i) or month for far too long. I still don't get why the naive solution (month + i) % 12 wouldn't work? What am I missing? \$\endgroup\$
    – JollyJoker
    Dec 10, 2019 at 15:22
  • 1
    \$\begingroup\$ @njzk2 — Another good reason for not using the %12 method is that it makes an assumption about the number of elements in a list obtained from a library. In this case, I doubt a thirteenth month will be added anytime soon, but as a general principle, it's not good to make assumptions about what something else provides. Even if the library were something that the program's author maintains, it's still not good to use hard-wired numbers. Changing the library would require changing this code. 0, 1, and many are the only good numbers to use when programming. \$\endgroup\$ Dec 10, 2019 at 16:39
9
\$\begingroup\$

First of call, theList is not a good name for that variable. You can be much more specific in your case! I'd suggest months.

Also, there is no need to have all of those lists hard-coded in your script. Use a single instance of that list, and then create modified versions as you need them.

Edit: I was absolutely sure that there has to be a Python library that has the names already covered, but failed to find it. RomanPerekhrest beat me here and correctly identified calendar.month_abbr as the way to go.

A list comprehension like below could to the trick:

months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
current_month = datetime.now().month - 1
print([months[(current_month + i) % 12] for i in range(12)])

Or you can use slicing to get it even more comfortable:

months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
current_month = datetime.now().month - 1
print(months[current_month:]+months[:current_month])

If performance ever should become a concern, you could create a look-up using a dictionary beforehand, so that the computation does not have to be repeated:

MONTHS_TO_COME = {i+1: months[i:]+months[:i] for i in range(12)}

# ... other code ...

month = datetime.now().month
print(MONTHS_TO_COME[month])

Oh, and maybe have a look at the "official" Style Guide for Python Code, aka PEP 8, for some hints on idiomatic formatting commonly found in Python code (e.g. lower_case_with_underscores in variable names).

\$\endgroup\$
8
\$\begingroup\$

Shamelessly ripping off @RomanPerekhrest 's answer. You can't rotate a list in Python but you can a deque, and the collections module always needs more love:

from datetime import datetime
from calendar import month_abbr
from collections import deque

def get_forward_months():
    months = deque(month_abbr[1:])
    months.rotate(1-datetime.now().month)
    return months
\$\endgroup\$
2
  • 1
    \$\begingroup\$ I don't think you need the list comprehension. months = deque(month_abbr[1:]) should work, shouldn't it? \$\endgroup\$
    – Matt M.
    Dec 10, 2019 at 14:46
  • \$\begingroup\$ @MattM. Yes, even better, thanks. \$\endgroup\$
    – richardb
    Dec 10, 2019 at 15:31
3
\$\begingroup\$

I'd recommend using the built-in strftime rather than defining your own list of strings:

import datetime

date = datetime.date.today()
months = []
for _ in range (12):
    months.append(date.strftime("%b"))
    date = date.replace(month=(date.month % 12 + 1))

Because someone will always complain when you can't find a way to do a loop like this as a list comprehension, here's a way to do it in a single line of code with a cheesy timedelta (this doesn't work for arbitrarily long sequences because 30 days isn't exactly one month, but for your use case the rounding errors won't add up enough to matter):

months = [(datetime.date.today().replace(day=15) + datetime.timedelta(days=30*n)).strftime("%b") for n in range(12)]

although to make this readable I think you'd want to break it up a little:

months = [
    (datetime.date.today().replace(day=15) + 
     datetime.timedelta(days=30*n)).strftime("%b") 
     for n in range(12)
]

IMO the for loop version is cleaner. :)

\$\endgroup\$
2
\$\begingroup\$

You can work with a single list theList = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']

What you want to do is to start with a certain index and then add 12 elements to a list while you have index%12. So you can do something like

from datetime import datetime

theList = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
month = datetime.now().month
newList = list()
for i in range(12):
    newList.append(theList[(month-1+i)%12])

print(newList)
\$\endgroup\$
0
2
\$\begingroup\$

My solution is similar to some of the others here. However, it doesn't use loops, math, conditional logic, or hard-coded month names. That should make it more resistant to bugs.

import calendar
from datetime import datetime

monthList = lambda month: \
    calendar.month_abbr[month:] + \
    calendar.month_abbr[1:month]

currentMonth = datetime.now().month

print(monthList(currentMonth))

The output:

['Oct', 'Nov', 'Dec', 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep']

This uses Python's calendar module, because it includes a month_abbr list with all the month name abbreviations. It has an empty string at element 0 and the month names are in elements 1–12.

['', 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']

The simplest solution is to use slices of that list, taking into account that the elements needed start with index 1.

What this solution does is take a slice of month_abbr starting with the current month to the end of the list (calendar.month_abbr[month:]) and appends a slice of month_abbr starting with the first month up to but not including the current month (calendar.month_abbr[1:month]).

Localization

An additional benefit to using the calendar.month_abbr list is that it is localized. If the locale in the program's environment changes to one with a different language, then the month abbreviation list automatically contains the names of months in that language. For example, updating the above code to switch locales, the following code will print the month list in the language of the default locale (English in this example), then print them in German and Hebrew (as an example of non-Latin script).

import calendar
import locale
from datetime import datetime

monthList = lambda month: \
    calendar.month_abbr[month:] + \
    calendar.month_abbr[1:month]

currentMonth = datetime.now().month

print(locale.getlocale(),
      monthList(currentMonth))

locale.setlocale(locale.LC_ALL, 'de_DE')
print(locale.getlocale(),
      monthList(currentMonth))

locale.setlocale(locale.LC_ALL, 'he_IL')
print(locale.getlocale(),
      monthList(currentMonth))

The output:

('en_US', 'UTF-8') ['Oct', 'Nov', 'Dec', 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep']
('de_DE', 'ISO8859-1') ['Okt', 'Nov', 'Dez', 'Jan', 'Feb', 'Mär', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep']
('he_IL', 'ISO8859-8') ['אוק', 'נוב', 'דצמ', 'ינו', 'פבר', 'מרץ', 'אפר', 'מאי', 'יונ', 'יול', 'אוג', 'ספט']
\$\endgroup\$
3
  • \$\begingroup\$ Good answer, but it would be better if your observations preceded your alternate code. \$\endgroup\$
    – pacmaninbw
    Oct 7, 2020 at 14:54
  • \$\begingroup\$ Thanks! How would it be better? Do you mean I should put the explanation of using and slicingcalendar.month_abbr before the code? I had originally written it that way. Then I thought the impatient TL;DR crowd (of which I'm a member) might like to see the solution and output as the very first thing, so I rearranged it. I could be persuaded to arrange it back again, especially if your comment gets a lot of votes. \$\endgroup\$ Oct 7, 2020 at 15:31
  • \$\begingroup\$ Your initial version would have been better, since this sight is about the original posters code and not about alternate solutions. \$\endgroup\$
    – pacmaninbw
    Oct 7, 2020 at 16:43

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.