lists: list comprehensions


List Comprehensions are a simple way to make a list from an iterable with one line

requirements

  • I open a terminal to run makePythonTdd.sh with list_comprehensions as the name of the project

    ./makePythonTdd.sh list_comprehensions
    

    on Windows without Windows Subsystem Linux use makePythonTdd.ps1

    ./makePythonTdd.ps1 list_comprehensions
    

    it makes the folders and files that are needed, installs packages, runs the first test, and the terminal shows AssertionError

    E       AssertionError: True is not false
    
    tests/test_list_comprehensions.py:7: AssertionError
    
  • I hold ctrl (windows/linux) or option (mac) on the keyboard and use the mouse to click on tests/test_list_comprehensions.py:7 to open it in the editor

  • then I change True to False to make the test pass

    self.assertFalse(False)
    
  • I change the name of the class to match the CapWords format

    class TestListComprehensions(unittest.TestCase):
    

test_making_a_list_w_a_for_loop

red: make it fail

I change test_failure

 1import unittest
 2
 3
 4class TestListComprehensions(unittest.TestCase):
 5
 6  def test_making_a_list_w_a_for_loop(self):
 7      a_list = []
 8      iterable = range(0, 4)
 9
10      for item in iterable:
11          a_list.append(item)
12
13      self.assertEqual(a_list, [])

the terminal shows AssertionError

AssertionError: Lists differ: [0, 1, 2, 3] != []
  • a_list = [] makes an empty list and gives it a name

  • iterable = range(0, 4) makes a range object that goes from the first given number to the second given number minus 1, in this case it goes from 0 to 3

  • for item in iterable: goes over every item in the range object

  • a_list.append(item) gets called every time the for loop runs

green: make it pass

I change the expectation to match the values in the terminal

self.assertEqual(a_list, [0, 1, 2, 3])

the test passes. The list now has the items from the range object after the for loop runs

refactor: make it better

  • I add another assertion to show that I can do the same thing with the list constructor

    self.assertEqual(a_list, [0, 1, 2, 3])
    self.assertEqual(a_list, list())
    

    the terminal shows AssertionError

    AssertionError: Lists differ: [0, 1, 2, 3] != []
    

    I add the iterable to the call

    self.assertEqual(a_list, list(iterable))
    

    the test passes. Why use a for loop when I can use the list constructor to do the same thing with less characters? Sometimes one is better than the other, I show this before the end of the chapter

  • I add another assertion to practice writing a for loop

    self.assertEqual(a_list, list(iterable))
    self.assertEqual(
        src.list_comprehensions.a_for_loop(iterable),
        [0, 1, 2, 3]
    )
    

    the terminal shows NameError

    NameError: name 'src' is not defined
    
  • I add it to the list of Exceptions encountered in test_list_comprehensions.py

    # Exceptions Encountered
    # AssertionError
    # NameError
    
  • I add an import statement

    import src.list_comprehensions
    import unittest
    

    the terminal shows AttributeError

    AttributeError: module 'src.list_comprehensions' has no attribute 'a_for_loop'
    
  • I add it to the list of Exceptions encountered in test_list_comprehensions.py

    # Exceptions Encountered
    # AssertionError
    # NameError
    # AttributeError
    
  • I add a function to list_comprehensions.py

    def a_for_loop():
        return None
    

    the terminal shows TypeError

    TypeError: a_for_loop() takes 0 positional arguments but 1 was given
    

    I add the argument

    def a_for_loop(a_container):
        return None
    

    the terminal shows AssertionError

    AssertionError: None != [0, 1, 2, 3]
    

    I change the return statement

    def a_for_loop(a_container):
        return [0, 1, 2, 3]
    

    the test passes

  • this solution does not change, the test fails when I change the values in the range object

    iterable = range(0, 5)
    

    the terminal shows AssertionError

    AssertionError: Lists differ: [0, 1, 2, 3, 4] != [0, 1, 2, 3]
    
  • I undo the change then import the random module to add randomness to the test. I need a better solution

    import random
    import src.list_comprehensions
    import unittest
    
  • I change the second value given to the range object

    iterable = range(0, random.randint(2, 1000))
    

    the terminal shows AssertionError

    AssertionError: Lists differ: [0, 1, 2, 3, ...] != [0, 1, 2, 3]
    

    this range object now goes from 0 to anywhere between 1 and 999. The values change every time the test runs

  • I change the expectation in the first assertion

    self.assertEqual(a_list, list(iterable))
    self.assertEqual(a_list, list(iterable))
    

    the test passes. I remove the line because it is a duplicate

  • I change the expectation of the second assertion

    self.assertEqual(a_list, list(iterable))
    self.assertEqual(
        src.list_comprehensions.a_for_loop(iterable),
        a_list
    )
    

    the terminal shows AssertionError

    AssertionError: Lists differ: [0, 1, 2, 3] != [0, 1, 2, 3, 4, ...]
    

    I change the function

    def a_for_loop(a_container):
        result = []
        for stuff in a_container:
            result.append(stuff)
        return result
        return [0, 1, 2, 3]
    

    the test passes and I remove the second return statement


test_making_a_list_w_extend

I can use the extend method to make a list from an iterable

red: make it fail

I add a new test

def test_making_a_list_w_a_for_loop(self):
    ...

def test_making_a_list_w_extend(self):
    a_list = []
    iterable = range(0, random.randint(2, 1000))
    self.assertIsNone(a_list.extend())

the terminal shows TypeError

TypeError: list.extend() takes exactly one argument (0 given)

green: make it pass

I add the iterable

self.assertIsNone(a_list.extend(iterable))

the terminal shows green again

refactor: make it better

  • I add another assertion to see what changed in the list

    self.assertIsNone(a_list.extend(iterable))
    self.assertEqual(a_list, list())
    

    the terminal shows AssertionError

    AssertionError: Lists differ: [0, 1, 2, 3, ...] != []
    

    I change the expectation

    self.assertEqual(a_list, list(iterable))
    

    the test passes. extend uses less lines than the for loop but is not yet better than the list constructor

  • I made the same variables twice, one for the empty list and one for the iterable, I add them to the setUp method to remove duplication and change the tests to use the new class attributes

    class TestListComprehensions(unittest.TestCase):
    
        def setUp(self):
            self.a_list = []
            self.iterable = range(0, random.randint(2, 1000))
    
        def test_making_a_list_w_a_for_loop(self):
            for item in self.iterable:
                self.a_list.append(item)
    
            self.assertEqual(self.a_list, list(self.iterable))
            self.assertEqual(
                src.list_comprehensions.a_for_loop(self.iterable),
                self.a_list
            )
    
        def test_making_a_list_w_extend(self):
            self.assertIsNone(self.a_list.extend(self.iterable))
            self.assertEqual(self.a_list, list(self.iterable))
    

    the terminal still shows green


test_making_a_list_w_a_list_comprehension

I can use a list comprehension to make a list from an iterable

red: make it fail

I add a failing test

def test_making_a_list_w_extend(self):
    ...

def test_making_a_list_w_a_list_comprehension(self):
    self.assertEqual(
        list(self.iterable),
        []
    )

the terminal shows AssertionError

AssertionError: Lists differ: [0, 1, 2, 3, ...] != []

green: make it pass

The list comprehension is like the for loop without the append. I add one as the expectation

self.assertEqual(
    list(self.iterable),
    [item for item in self.iterable]
)

the test passes

refactor: make it better

  • I add another assertion for practice

    self.assertEqual(
        list(self.iterable),
        [item for item in self.iterable]
    )
    self.assertEqual(
        src.list_comprehensions.a_list_comprehension(self.iterable),
        [item for item in self.iterable]
    )
    

    the terminal shows AttributeError

    AttributeError: module 'src.list_comprehensions' has no attribute 'a_list_comprehension'
    

    I add the function

    def a_for_loop(a_container):
        result = []
        for stuff in a_container:
            result.append(stuff)
        return result
    
    
    def a_list_comprehension(a_collection):
        return [element for element in a_collection]
    

    the test passes

  • I made 2 functions that do the same thing - one that uses a for loop and another that uses a list comprehension

    result = []
    for stuff in a_container:
        result.append(stuff)
    

    and

    [element for element in a_collection]
    

    the difference between them is that in the first case I have to

    with the list comprehension, I do all the steps in one line, but none of the other ways are better than using the list constructor, yet.


test_making_a_list_w_conditions

What if I had to make a list from an iterable based on a condition?

red: make it fail

I add a failing test

def test_making_a_list_w_conditions(self):
    even_numbers = []
    for item in self.iterable:
        if item % 2 == 0:
            even_numbers.append(item)

    self.assertEqual(
        even_numbers,
        list(self.iterable)
    )

the terminal shows AssertionError

AssertionError: Lists differ: [0, 2, 4, 6, 8, ...] != [0, 1, 2, 3, 4, 5, 6, 7, 8...]
  • if item % 2 == 0: checks if the item in iterable leaves a remainder of 0 when divided by 2

  • % is the modulo operator, which divides the number on the left by the number on the right and returns a remainder, there’s a test for it in test_the_modulo_operation

green: make it pass

How can I make the even_numbers list with the constructor without changing the iterable? Since I can make the list with a for loop, I can do it with a list comprehension. I change the expectation

self.assertEqual(
    even_numbers,
    [item for item in self.iterable]
)

the terminal still shows AssertionError

AssertionError: Lists differ: [0, 2, 4, 6, 8, ...] != [0, 1, 2, 3, 4, 5, 6, 7, 8...]

I add the condition

self.assertEqual(
    even_numbers,
    [item for item in self.iterable if item % 2 == 0]
)

the test passes. This is a case where a list comprehension or a for loop is better than using the list constructor

refactor: make it better

  • I add another assertion for practice

    self.assertEqual(
        even_numbers,
        [item for item in self.iterable if item % 2 == 0]
    )
    self.assertEqual(
        src.list_comprehensions.get_even_numbers(self.iterable),
        [item for item in self.iterable if item % 2 == 0]
    )
    

    the terminal shows AttributeError

    AttributeError: module 'src.list_comprehensions' has no attribute 'get_even_numbers'
    

    I add a function to list_comprehensions.py

    def a_list_comprehension(a_collection):
        ...
    
    
    def get_even_numbers(numbers):
        return [number for number in iterable if number % 2 == 0]
    

    the test passes

  • I wrote the same condition in the test 3 times. If I want to change it, I have to make the same change everywhere I wrote it. Let us say the new condition is that the number should be divisible by 3

    def test_making_a_list_w_conditions(self):
        even_numbers = []
        for item in self.iterable:
            if item % 3 == 0:
                even_numbers.append(item)
    

    the terminal shows AssertionError

    AssertionError: Lists differ: [0, 3, 6, 9, ...] != [0, 2, 4, 6, ...]
    

    I change the condition in the list comprehension of the first assertion

    self.assertEqual(
        even_numbers,
        [item for item in self.iterable if item % 3 == 0]
    )
    

    the terminal shows green. I change the condition in the list comprehension of the second assertion

    self.assertEqual(
        src.list_comprehensions.get_even_numbers(self.iterable),
        [item for item in self.iterable if item % 3 == 0]
    )
    

    the terminal shows AssertionError

    AssertionError: Lists differ: [0, 2, 4, 6, ...] != [0, 3, 6, 9, ...]
    

    I change the condition in the solution

    def get_even_numbers(numbers):
        return [number for number in iterable if number % 3 == 0]
    

    the test passes

  • I add a function to remove the duplication

    import unittest
    
    
    def condition(number):
        return number % 3 == 0
    

    then change the condition in the test to call the new function

    def test_making_a_list_w_conditions(self):
        even_numbers = []
        for item in self.iterable:
            # if item % 3 == 0:
            if condition(item):
                even_numbers.append(item)
    

    the terminal still shows green. I remove the commented line and do the same thing in the first assertion

    self.assertEqual(
        even_numbers,
        # [item for item in self.iterable if item % 3 == 0]
        [item for item in self.iterable if condition(item)]
    )
    

    still green. I do it again with the next one

    self.assertEqual(
        even_numbers,
        [item for item in self.iterable if condition(item)]
    )
    self.assertEqual(
        src.list_comprehensions.get_even_numbers(self.iterable),
        # [item for item in self.iterable if item % 3 == 0]
        [item for item in self.iterable if condition(item)]
    )
    

    the terminal still shows green. I do NOT recommend using condition as a name for a function because it is general, it does not tell what it does. I use it to show that I think of a list comprehension as [item for item in iterable if condition]. I change the condition in the function

    def condition(number):
        return number % 2 == 0
    

    the terminal shows AssertionError

    AssertionError: Lists differ: [0, 3, 6, 9, ...] != [0, 2, 4, 6, ...]
    

    I change the condition in get_even_numbers

    def get_even_numbers(numbers):
        return [number for number in numbers if number % 2 == 0]
    

    the test passes. Yes, adding the function adds extra lines, and it makes managing the code easier since I now only have to make a change in one place in the test when I need

  • I add a new empty list to test another condition

    def test_making_a_list_w_conditions(self):
        even_numbers, odd_numbers = [], []
        for item in self.iterable:
            ...
    

    even_numbers, odd_numbers = [], [] makes 2 empty lists and names them

  • I add an else clause in the for loop

    for item in self.iterable:
        if condition(item):
            even_numbers.append(item)
        else:
            odd_numbers.append(item)
    

    I add an assertion

    self.assertEqual(
        src.list_comprehensions.get_even_numbers(self.iterable),
        [item for item in self.iterable if condition(item)]
    )
    self.assertEqual(
        odd_numbers,
        [item for item in self.iterable]
    )
    

    the terminal shows AssertionError

    AssertionError: Lists differ: [1, 3, 5, 7, ...] != [0, 1, 2, 3, 4, 5, 6, 7, 8, ...]
    

    I add the condition to the assertion

    self.assertEqual(
        odd_numbers,
        [item for item in self.iterable if not condition(item)]
    )
    

    the test passes. I add another assertion

    self.assertEqual(
        odd_numbers,
        [item for item in self.iterable if not condition(item)]
    )
    self.assertEqual(
        src.list_comprehensions.get_odd_numbers(self.iterable),
        [item for item in self.iterable if not condition(item)]
    )
    

    the terminal shows AttributeError

    AttributeError: module 'src.list_comprehensions' has no attribute 'get_odd_numbers'. Did you mean: 'get_even_numbers'?
    

    I add the function

    def get_even_numbers(numbers):
        ...
    
    
    def get_odd_numbers(numbers):
        return [number for number in numbers if number % 2 != 0]
    

    the test passes

  • I add a function for the condition to remove duplication and use a more descriptive name then call it in get_even_numbers

    def a_list_comprehension(a_collection):
        ...
    
    
    def is_even(number):
        return number % 2 == 0
    
    
    def get_even_numbers(numbers):
        return [number for number in numbers if is_even(number)]
        return [number for number in numbers if number % 2 == 0]
    

    the test is still passing. I remove the second return statement then call the new function in get_odd_numbers

    def get_even_numbers(numbers):
        return [number for number in numbers if is_even(number)]
    
    
    def get_odd_numbers(numbers):
        return [number for number in numbers if not is_even(number)]
        return [number for number in numbers if number % 2 != 0]
    

    the terminal still shows green, I remove the second return statement


test_making_a_list_w_processes

red: make it fail

I add a test to show I can do other operations in a list comprehension not just append

def test_making_a_list_w_conditions(self):
    ...

def test_making_a_list_w_processes(self):
    square_club = []
    for item in self.iterable:
        square_club.append(item*item)

    self.assertEqual(
        square_club,
        [item for item in self.iterable]
    )

the terminal shows AssertionError

AssertionError: Lists differ: [0, 1, 4, 9, ...] != [0, 1, 2, 3, ...]

green: make it pass

I add the calculation to the list comprehension

self.assertEqual(
    square_club,
    [item*item for item in self.iterable]
)

the test passes

refactor: make it better

  • I add another assertion

    self.assertEqual(
        square_club,
        [item*item for item in self.iterable]
    )
    self.assertEqual(
        src.list_comprehensions.square(self.iterable),
        [item*item for item in self.iterable]
    )
    

    the terminal shows AttributeError

    AttributeError: module 'src.list_comprehensions' has no attribute 'square'
    

    I add the function

    def get_odd_numbers(numbers):
        ...
    
    
    def square(numbers):
        return [number**2 for number in numbers]
    

    the test passes. x**y is how to write x raised to the power of y

    \[x ^ y\]
  • I add a function for the calculation I did 3 times in this test

    import unittest
    
    
    def process(number):
        return number ** 2
    

    I call it in the test

    def test_making_a_list_w_processes(self):
        square_club = []
        for item in self.iterable:
            # square_club.append(item*item)
            square_club.append(process(item))
    
        self.assertEqual(
            square_club,
            # [item*item for item in self.iterable]
            [process(item) for item in self.iterable]
    
        self.assertEqual(
            src.list_comprehensions.square(self.iterable),
            # [item*item for item in self.iterable]
            [process(item) for item in self.iterable]
        )
    

    the terminal still shows green. I remove the commented lines. I do NOT recommend using process as a name for a function because it is general, it does not tell what it does. I use it to show that I think of a list comprehension as [process(item) for item in iterable]


test_making_a_list_w_processes_and_conditions

I can use both processes and conditions in a list comprehension

red: make it fail

I add a failing test

def test_making_a_list_w_processes(self):
    ...

def test_making_a_list_w_processes_and_conditions(self):
    even_squares, odd_squares = [], []
    for item in self.iterable:
        if condition(item):
            even_numbers.append(process(item))
        else:
            odd_numbers.append(process(item))

    self.assertEqual(
        even_squares,
        [item for item in self.iterable]
    )

the terminal shows AssertionError

AssertionError: Lists differ: [0, 4, 16, 36, ...] != [0, 1, 2, 3, 4, ...]

green: make it pass

I add a call to condition

  self.assertEqual(
      even_squares,
      [item for item in self.iterable if condition(item)]
  )

the terminal shows AssertionError

AssertionError: Lists differ: [0, 4, 16, 36, ...] != [0, 2, 4, 6, ...]

I add a call to process

self.assertEqual(
    even_squares,
    [process(item) for item in self.iterable if condition(item)]
)

the test passes

refactor: make it better

I add another assertion

self.assertEqual(
    odd_squares,
    [item for item in self.iterable]
)

the terminal shows AssertionError

AssertionError: Lists differ: [1, 9, 25, 49, ...] != [0, 1, 2, 3, 4, 5, 6, 7, ...]

I add a call to condition

self.assertEqual(
    odd_squares,
    [item for item in self.iterable if not condition(item)]
)

the terminal shows AssertionError

AssertionError: Lists differ: [1, 9, 25, 49, ...] != [1, 3, 5, 7, ...]

I add a call to process

self.assertEqual(
    odd_squares,
    [process(item) for item in self.iterable if not condition(item)]
)

the test passes


review

The tests show I can make a list from an iterable with

I can use functions and conditions with list comprehensions to make a list with one line. I think of it as [process(item) for item in iterable if condition/NOT condition]

I can also do this with dictionaries, it is called a dict comprehension and the syntax is any mix of the following

{a_process(key): another_process(value) for key/value in iterable if condition/NOT condition}

Would you like to test dictionaries?


data structures: list comprehensions: tests and solutions