Grammatical List Join in Python [duplicate]
Asked Answered
O

6

28

What's the most pythonic way of joining a list so that there are commas between each item, except for the last which uses "and"?

["foo"] --> "foo"
["foo","bar"] --> "foo and bar"
["foo","bar","baz"] --> "foo, bar and baz"
["foo","bar","baz","bah"] --> "foo, bar, baz and bah"
Oread answered 7/11, 2013 at 14:52 Comment(2)
who cares about an Oxford comma?Pilothouse
This question and answers do not use the Oxford comma. For a question and answers that use the Oxford comma, see this question instead.Anile
S
33

This expression does it:

print ", ".join(data[:-2] + [" and ".join(data[-2:])])

As seen here:

>>> data
    ['foo', 'bar', 'baaz', 'bah']
>>> while data:
...     print ", ".join(data[:-2] + [" and ".join(data[-2:])])
...     data.pop()
...
foo, bar, baaz and bah
foo, bar and baaz
foo and bar
foo
Sawyers answered 7/11, 2013 at 15:6 Comment(4)
why the 'e for e in' syntax? i don't even think that's actually necessary.Kamala
Yep; you're right. Was playing around with something else, but what I ended up with didn't need the generator comprehension part. Fixed.Sawyers
+1 for joining the last two with "and" prior to the general join with ",".Sifuentes
Accepted this answer because it is concise and works without assumptions.Oread
G
15

Try this, it takes into consideration the edge cases and uses format(), to show another possible solution:

def my_join(lst):
    if not lst:
        return ""
    elif len(lst) == 1:
        return str(lst[0])
    return "{} and {}".format(", ".join(lst[:-1]), lst[-1])

Works as expected:

 my_join([])
=> ""
 my_join(["x"])
=> "x"
 my_join(["x", "y"])
=> "x and y"
 my_join(["x", "y", "z"])
=> "x, y and z"
Gravamen answered 7/11, 2013 at 15:4 Comment(4)
Not so "smart" as other answers, and therefore the most "pythonic" solution. The only point, I'd use format in the second branch too, so that the fun always returns a string.Crespi
@thg435 I prefer readability over "smartness" :) For the second branch, a simple str() will doCankered
+1 for not trying to be clever like the others... It's very frustrating, running across code like the currently most-upvoted answer only to discover later on it has a bug not immediately noticed due to the clevernessToque
+1 for function with test casesDunois
L
5

The fix based on the comment led to this fun way. It assumes no commas occur in the string entries of the list to be joined (which would be problematic anyway, so is a reasonable assumption.)

def special_join(my_list):
    return ", ".join(my_list)[::-1].replace(",", "dna ", 1)[::-1]


In [50]: def special_join(my_list):
        return ", ".join(my_list)[::-1].replace(",", "dna ", 1)[::-1]
   ....:

In [51]: special_join(["foo", "bar", "baz", "bah"])
Out[51]: 'foo, bar, baz and bah'

In [52]: special_join(["foo"])
Out[52]: 'foo'

In [53]: special_join(["foo", "bar"])
Out[53]: 'foo and bar'
Lodestone answered 7/11, 2013 at 14:56 Comment(2)
special_join(["foo"]) returns ' and foo'...Cow
@chepner: "To describe something as clever is not considered a compliment in the Python culture." ;)Crespi
I
1

Already good answers available. This one works in all test cases and is slightly different than some others.

def grammar_join(words):
    return reduce(lambda x, y: x and x + ' and ' + y or y,
                 (', '.join(words[:-1]), words[-1])) if words else ''

tests = ([], ['a'], ['a', 'b'], ['a', 'b', 'c'])
for test in tests:                                 
    print grammar_join(test)

a
a and b
a, b and c
Ignorant answered 7/11, 2013 at 15:56 Comment(0)
K
-1

just special-case the last one. something like this:

'%s and %s'%(', '.join(mylist[:-1]),mylist[-1])

there's probably not going to be any more concise method.

this will fail in the zero case too.

Kamala answered 7/11, 2013 at 14:57 Comment(1)
Even this is too concise, as it assumes 2 or more items.Sifuentes
H
-1

In case you need a solution where negative indexing isn't supported (i.e. Django QuerySet)

def oxford_join(string_list):
    if len(string_list) < 1:
        text = ''
    elif len(string_list) == 1:
        text = string_list[0]
    elif len(string_list) == 2:
        text = ' and '.join(string_list)
    else:
        text = ', '.join(string_list)
        text = '{parts[0]}, and {parts[2]}'.format(parts=text.rpartition(', '))  # oxford comma
    return text

oxford_join(['Apples', 'Oranges', 'Mangoes'])
Hyaloplasm answered 22/9, 2015 at 4:51 Comment(1)
Breaks if items in your list contain commas.Howsoever

© 2022 - 2024 — McMap. All rights reserved.