18
\$\begingroup\$

I am new to programming and currently learning Python using "Automate the Boring" by Al Sweigart. I've just completed the "Comma Code" task in Chapter 4 and would like some feedback on my solution. The task is:

Say you have a list value like this:

spam = ['apples', 'bananas', 'tofu', 'cats']

Write a function that takes a list value as an argument and returns a string with all the items separated by a comma and space, with and inserted before the last item. For example, passing the previous spam list to the function would return 'apples, bananas, tofu, and cats'. But your function should be able to work with any list value passed to it.

I tried to come up with something that mostly sticks to what you learn by this point in the book, but I did use .join() which is from a few chapters later. I also tried to make sure my code works with lists containing strings and/or integers, and for the output to be correct independent of list size (no comma when the list has two items, etc.).

your_list = ['item1', 'item2', 3, 'item4', 'item5', 'item6', 7, 'item8', 9]

def list_concatenator(your_list):
    items = len(your_list)
    if items == 0:
        return 'Your list is empty'

    elif items == 1:
        return "'" + str(your_list[0]) + "'"

    elif items == 2:
        return "'" + str(your_list[0]) + ' and ' + str(your_list[1]) + "'"

    else:      
        your_list_split1 = (', '.join((map(str,your_list[:-1]))))
        return "'" + your_list_split1 + ', and ' + str(your_list[-1]) + "'"

list_concatenator(your_list)

I realize these exercises have been answered before, I'm just looking for some feedback on my solution.

\$\endgroup\$
  • 5
    \$\begingroup\$ For a two-element list like ['apples', 'bananas'], your code skips the comma and returns 'apples and bananas' rather than 'apples, and bananas' like the spec seems to ask for. Is that intentional? (Also, I suspect you're not supposed to actually wrap the output string in quotes; that's just how the book writes string literals. But I might be mistaken.) \$\endgroup\$ – Ilmari Karonen May 30 at 12:02
  • \$\begingroup\$ @IlmariKaronen - OP is actually trying to implement this. \$\endgroup\$ – Justin May 30 at 15:37
  • 2
    \$\begingroup\$ Don’t signal an error by returning an error message, throw an exception instead. The calling code will know better how exactly to notify the user of the problem. \$\endgroup\$ – Roman Odaisky May 31 at 0:07
  • 5
    \$\begingroup\$ @IlmariKaronen That is almost certainly a failure in the specification. Such formatting would be obviously incorrect punctuation. The description doesn't contain any specific instructions on the case of a two item list and provides only an example of four items. Note that the description doesn't describe what to do in the case of empty or single item lists, either. The OP has extended the specification to handle edge cases that were left unspecified. Another valid approach would be to just throw an error for less than three items. \$\endgroup\$ – jpmc26 May 31 at 10:37
  • 2
    \$\begingroup\$ Don't return 'Your list is empty', throw an error! \$\endgroup\$ – Tvde1 Jun 2 at 10:23
9
\$\begingroup\$

You’ve got too many ()’s in this statement:

your_list_split1 = (', '.join((map(str,your_list[:-1]))))

You don’t need the parenthesis around the entire expression, nor for the argument inside of the join(...) function. You should also add a space after the comma, to be PEP-8 compliant:

your_list_split1 = ', '.join(map(str, your_list[:-1]))

I’m not sure if it was a requirement of the question or not, but generally with lists, you do not include a comma between the second last item and the word “and”. If that comma is not required by the assignment, then if you remove it, the items == 2 case is unnecessary, as the final else clause will properly handle 2 items. Edit: Or since the comma appears to be required, if it is acceptable for 2 items ('item1, and item2'), keep the comma in the else: and the items == 2 case is still unnecessary.

If you are learning Python 3, you should probably jump right to Python 3.7, and take advantage of f-strings, where you can put formatting codes for variables/expressions directly into the strings:

else:
    return f"'{your_list_split1}, and {your_list[-1]}'"

Don’t intermix code and function definitions.

def list_concatenator(your_list):
    # ... etc ...

your_list = [ ... ]
list_concatenator(your_list)

Avoid using global variable names as function argument names. In longer functions, the reader might get confused between whether a global variable is being referenced or not.


As it presently stands, your code does some work, gets a result, and then does nothing with it. You probably want to assign the returned value to a variable, or print it, or both.


You are calling str(...) alot throughout your function, to ensure that the item you are referencing in your list is a string which can be concatenated with other strings. You should instead just do it once, at the top of the function:

def list_concatenator(your_list):
    your_list = list(map(str, your_list))
    # remainder of the code, without the need for any str( ) calls.
\$\endgroup\$
  • 7
    \$\begingroup\$ The Oxford comma is required in the assignment, as shown in the example output (whether we like it or not...). \$\endgroup\$ – Graipher May 30 at 15:18
  • 2
    \$\begingroup\$ And your_list = map(str, your_list) can be a problem, depending on what you want to do with it afterwards, since it is a generator-like object. So no [-1] to get the last element, no falsiness if it is empty, only being able to iterate over it once, etc. All of which you can remove by wrapping it with list(), of course. \$\endgroup\$ – Graipher May 30 at 15:22
  • 2
    \$\begingroup\$ generally with lists, you do not include a comma between the second last item and the word “and”. According to en.wikipedia.org/wiki/Serial_comma, "Opinions among writers and editors differ on whether to use the serial comma, and usage also differs somewhat between regional varieties of English. Generally (with few exceptions), British English does not make use of this comma, while on the other hand it is common and even mandatory in American English.[6] ... This practice is controversial and is known as the serial comma or Oxford comma. ..." \$\endgroup\$ – Solomon Ucko May 30 at 18:00
  • 3
    \$\begingroup\$ @Justin Actually that is the OP’s addition to the problem; it is not mentioned in the chapter 4 problem text (which is now included in the question post verbatim). \$\endgroup\$ – AJNeufeld May 30 at 20:41
  • 2
    \$\begingroup\$ @AJNeufeld - Yes, according to OP's stated question (with the addition), I meant. \$\endgroup\$ – Justin May 30 at 20:45
13
\$\begingroup\$

Your code is well-built and covers everything stated in the task, but could be made much shorter by using the .format() function.

str.format() is one of the string formatting methods in Python 3, which allows multiple substitutions and value formatting. This method lets us concatenate elements within a string through positional formatting.

Your code can, therefore, be made shorter and more concise this way. Here is what your code might look like -

def list_concatenator(your_list):

     if not your_list:
         return "Your list is empty!"

     elif len(your_list) == 1:
         return str(your_list[0])

     elif len(your_list) == 2:
         return '{0} and {1}'.format(your_list[0], your_list[1])

     else:
         body = ", ".join(map(str, your_list[:-1]))
         return '{0}, and {1}'.format(body, your_list[-1])

I have used {0} and {1} so that positional arguments can be placed in the order you want them to be in, though it is not necessary to do so.

OR

Another way to format in Python 3 is to use f-strings.

The idea behind f-strings is to make string interpolation simpler. To create an f-string, prefix the string with the letter “ f ”. The string itself can be formatted in much the same way that you would with str.format(). f-strings provide a concise and convenient way to embed python expressions inside string literals for formatting.

Here's how your code would look like with f-strings (much shorter) -

def list_concetenator(your_list):

    if not your_list:
        return "Your list is empty!"

    elif len(your_list) == 1:
        return str(your_list[0])

    elif len(your_list) == 2:
        return f'{your_list[0]} and {your_list[1]}'

    else:
        body = ", ".join(map(str, a_list[:-1]))
        return f'{body}, and {your_list[-1]}'

Here are some example outputs -

your_list = ['item1', 'item2', 3, 'item4', 'item5', 'item6', 7, 'item8', 9]

print(list_concatenator(['item1', 'item2', 3, 'item4', 'item5', 'item6', 7, 'item8', 9]))
>>> item1, item2, 3, item4, item5, item6, 7, item8, and 9

print(list_concatenator(['item1', 'item2', 3]))
>>> item1, item2, and 3

print(list_concatenator(['item1', 3]))
>>> item1 and 3

print(list_concatenator(['item1']))
>>> item1 

print(list_concatenator([]))
>>> Your list is empty!

The program also takes care of the Oxford comma, where lists with two items do not have commas (e.g. apples and bananas) but lists with more than two items are separated with commas (apples, bananas, and cakes). Here are some useful links that help us understand what Oxford commas really are and when they should be used -

  1. https://www.grammarly.com/blog/comma-before-and/

  2. https://en.wikipedia.org/wiki/Serial_comma

Overall, I believe formatting was the main problem in your code.


EDIT -

In the comments section above, @Ilmari Karonen correctly mentions that the usage of " ' " in the book is the book's way of writing string literals. Therefore, I have edited my answer to make this change (i.e. remove unnecessary quotes). But if you require these quotes, then you could always wrap the string in double quotes, like this - f"'{your_list[0]} and {your_list[1]}'" or this - "'{0} and {1}'".format(your_list[0], your_list[1]).

Another let-down in your code is the use of inconsistent parentheses here,

(', '.join((map(str,your_list[:-1]))))

which could just be written as -

', '.join(map(str,your_list[:-1])).

To remain concise and immaculate in writing programs, I suggest you have a look at PEP 8, which is Python's official style guide.

Hope this helps!

\$\endgroup\$
  • 1
    \$\begingroup\$ forgot to comment earlier and say thanks, I did find your answer very useful too. \$\endgroup\$ – benb Jun 1 at 12:31
  • \$\begingroup\$ @benb - I'm glad my answer helped you. Just wondering why you unaccepted my answer :( \$\endgroup\$ – Justin Jun 3 at 13:04
  • \$\begingroup\$ Sorry about that, there's nothing wrong with your answer. I've actually started using f-strings because of your advice, thank you. It was difficult to pick ONE answer, and I decided on AJNeufelds because other beginners reading this would probably benefit from the extra things his answer covers (e.g., not to use global variable names as function names etc.). \$\endgroup\$ – benb Jun 4 at 4:08
  • 1
    \$\begingroup\$ @benb - No worries. Just so know, I've also provided a link to PEP 8, which is Python's official style guide. It should help you with formatting and style in every possible way. \$\endgroup\$ – Justin Jun 4 at 4:13
9
\$\begingroup\$
  1. If you need to add '' then you should do this outside of the function. This can be fairly easy knowing the output is always a string.

    >>> print(repr(list_concatenator('a')))
    'a'
    

    By default the IDLE prompt does this automatically.

    >>> list_concatenator('a')
    'a'
    
  2. Rather than using len(your_list) == 0 you can use not your_list.

    if not your_list:
        return 'Your list is empty'
    
  3. You can use str.join to join the rest of your lists.

def list_concatenator(your_list, comma=', ', conjunction=' and '):
    if not your_list:
        return 'Your list is empty'

    your_list = [str(i) for i in your_list]
    if len(your_list) > 1:
       your_list[:-1] = [comma.join(your_list[:-1])]
    return conjunction.join(your_list)

>>> list_concatenator(['item1', 'item2', 3, 'item4', 'item5', 'item6', 7, 'item8', 9])
'item1, item2, 3, item4, item5, item6, 7, item8 and 9'
>>> list_concatenator('abcde', conjunction=' or ')
'a, b, c, d or e'
>>> list_concatenator('abcde', conjunction=', or ')
'a, b, c, d, or e'
>>> list_concatenator('ab')
'a and b'
>>> list_concatenator('a')
'a'

It should be noted that the challenge says to seperate each value with a ', ', and so you could simplify this by mutating the last value with the conjunction, and just use one str.join.

def list_concatenator(your_list, comma=', ', conjunction='and'):
    if not your_list:
        return 'Your list is empty'

    your_list = [str(i) for i in your_list]
    if len(your_list) > 1:
        your_list[-1] = conjunction + ' ' + your_list[-1]
    return comma.join(your_list)

>>> list_concatenator(['item1', 'item2', 3, 'item4', 'item5', 'item6', 7, 'item8', 9])
'item1, item2, 3, item4, item5, item6, 7, item8, and 9'
>>> list_concatenator('abcde', conjunction='or')
'a, b, c, d, or e'
>>> list_concatenator('ab')
'a, and b'
>>> list_concatenator('a')
'a'
\$\endgroup\$
  • 1
    \$\begingroup\$ Posting on behalf of a_faulty_star. a_faulty_star: P.S: I have noticed that the outputs in Peilonrayz's answer have single quotes for every element of the input list, which is not the desired output. As I don't have 150 reputation, I can't comment :( But if someone who has 150 reputation notices this, please inform them! Thanks! \$\endgroup\$ – benb May 31 at 8:18
  • 1
    \$\begingroup\$ @benb It was in your original code. You should be able to see how to remove them if you need to. \$\endgroup\$ – Peilonrayz May 31 at 8:22
  • 2
    \$\begingroup\$ In my original code the output looked like 'item1, item2' etc., with the whole string having ' ' around it. What a_faulty_star is pointing out is in the above code the output is 'item1', 'item2' two etc., with each item having it's own quotes. As you said, you can fix it by taking out the double quotes "" in your_list = [f"'{i}'" for i in your_list]. \$\endgroup\$ – benb May 31 at 8:41
  • 2
    \$\begingroup\$ Ah, I misread your ' and ' as "' and '". I've changed it now. \$\endgroup\$ – Peilonrayz May 31 at 9:01
5
\$\begingroup\$

I used the following function for achieving the task. I am new here, so please feel free to comment anything you feel is wrong!

In the function, firstly, I check if the given list is empty or not.

If it is, then I return an empty string.

Otherwise, I first store the length of string in a variable. Then, I use list comprehension to convert all the elements in the list to string format, because some of them may be non-string and cause an error while using join function. Then, I join the strings in the list, separated by a comma and space, by using the join function on all elements of the list except the last. Then, I add an 'and' if the list has three or more elements. I have added the Oxford comma and a space before 'and' because the question has mentioned it.

Here is the function:

>>> def list_concatenator(your_list):
    if not your_list:
        return ''
    your_list = [str(i) for i in your_list]
    p = len(your_list)
    return ', '.join(your_list[:-1]) + (',' if p>2 else '') + (' and ' if p>1 else '') + your_list[-1]

Here are some outputs:

>>> li = []
>>> print(list_concatenator(li))


>>> li2 = ['apples']
>>> print(list_concatenator(li2))
apples

>>> li3 = ['apples', 'bananas']
>>> print(list_concatenator(li3))
apples and bananas

>>> li4 = ['apples', 'bananas', 'tofu', 'cats']
>>> print(list_concatenator(li4))
apples, bananas, tofu, and cats

>>> li5 = ['item1', 'item2', 3, 'item4', 'item5', 'item6', 7, 'item8', 9]
>>> print(list_concatenator(li5))
item1, item2, 3, item4, item5, item6, 7, item8, and 9

P.S: I have noticed that the outputs in Peilonrayz's answer have single quotes for every element of the input list, which is not the desired output. As I don't have 150 reputation, I can't comment :( But if someone who has 150 reputation notices this, please inform them! Thanks!

\$\endgroup\$
  • 2
    \$\begingroup\$ I think this is a great solution. I will leave @AJNeufeld comment as the answer as I think it provides a lot of useful information for a beginner, but your solution seems to do everything required while also being very concise. Thanks. \$\endgroup\$ – benb May 31 at 8:21
  • 1
    \$\begingroup\$ I have left the code to work as close to the original code as possible. For ease of programming I changed from the oxford comma to what the spec defines. But the function works the same otherwise. \$\endgroup\$ – Peilonrayz May 31 at 8:25
  • 1
    \$\begingroup\$ @a_faulty_star - You have not implemented the Oxford comma in your answer. An Oxford comma is where lists with two items do not have commas but lists with more than two items are separated with commas. Here is the link - en.wikipedia.org/wiki/Serial_comma. Therefore, if you are using the Oxford comma, then your third output should be - apples and bananas. \$\endgroup\$ – Justin May 31 at 12:38
  • 3
    \$\begingroup\$ @a_faulty_star - No worries. Your answer is pretty close to a code review and you have already received 3 upvotes, which is amazing for a first attempt. As benb said, your solution is very concise, so it did help him. Kudos for writing your first answer on Code Review! \$\endgroup\$ – Justin May 31 at 13:21
  • 2
    \$\begingroup\$ Thank you @Justin. You and benb are too kind :') \$\endgroup\$ – a_faulty_star May 31 at 13:55
3
\$\begingroup\$

Here are a few mostly stylistic points.

    def list_concatenator(your_list):

"list_concatenator" is a poor choice for a function name. I'd rather expect "concatenator" to be a class that provides concatenation. A function name is, more commonly, a verb, that tells what the function does (e.g. sort) or a noun describing what the function returns (e.g. sqrt). Besides, "concatenate a list" is a very vague description of the task at hand. Therefore I'd suggest to name your function like "join_with_serial_comma" or "oxford_comma_concat" or even "oxfordize" ;)

"your_list" is bad too (who's this "you" exactly?). It's just some list, so name it "some_list" or "a_list" or "lst". Moreover, since you're only using len and [] in your code, there's no reason to restrict your function to lists specifically, just make it accept any sequence and name your argument like "sequence" or "seq"

    items = len(your_list)

Again, poor variable naming. items is what the list contains (cf. dict.items), what you mean here is length, size, number_of_items and so on.

    if items == 0:
        return 'Your list is empty'

An empty list is an exceptional situation for your function, and you can't and shouldn't return any value in this case. Much better option would be to raise an exception (ValueError would be a good choice), better yet, just remove this condition, the rest of the code is going to break anyways if the argument is empty (or not a sequence at all, for that matter). Ask forgiveness, not permission.

    elif items == 1:
        return "'" + str(your_list[0]) + "'"

If the previous condition ends with return or raise, there's no need for elif, just if would be more readable.

    elif items == 2:
        return "'" + str(your_list[0]) + ' and ' + str(your_list[1]) + "'"

As said in other posts, format or f-strings are usually more readable than concatenation.

    else:
        your_list_split1 = (', '.join((map(str,your_list[:-1]))))
        return "'" + your_list_split1 + ', and ' + str(your_list[-1]) + "'"

Yet again, your_list_split1 is a weird variable name, and doesn't reflect what the variable actually contains.

Since you always return from other branches, you can remove else here and reduce the indentation.

Comprehensions are usually more readable in python than map:

    head = ', '.join(str(item) for item in sequence[:-1])

A more "pythonic" alternative to -1 indexing would be an unpacking assignment:

    *head, tail = seq
    head_joined = ', '.join(str(p) for p in head)
    return f'{head_joined}, and {tail}'

Hope this helps.

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

Slicing the list would allow you to get all except the last element. Testing the list length will indicate what needs to go before the final element.

if len(your_list) > 0:
    if len(your_list) > 2: ander = ','       # if passive comma needed
    if len(your_list) > 1: ander += ' and '
    ', '.join(your_list[0:-1]) + ander + thelist[-1]
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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