Lab 3: Lists

  • Due: Wednesday 09/23 @ 11:59pm
  • Points: 1
  • Download: lab03.zip

Attendance

You need to submit the lab problems in addition to attending to get credit for lab. Students in mega lab only need to submit the lab problems. Preceptor interviews are highly recommended but not required for lab submissions.

If you are in regular lab, your TA will come around during lab to check you in. If you didn't attend for a good reason (such as being sick), fill out this form (within 2 weeks of your lab): attendance form.

Getting Started Videos

These videos may provide some helpful direction for tackling the coding problems on this assignment.

To see these videos, you should be logged into your berkeley.edu email.

Required Questions

Review

Lists

A list is a data structure that can hold an ordered collection of items. These items, known as elements, can be of any data type, including numbers, strings, or even other lists. A comma-separated list of expressions in square brackets creates a list:

>>> list_of_values = [2, 1, 3, True, 3]
>>> nested_list = [2, [1, 3], [True, [3]]]

Each position in a list has an index, with the left-most element indexed 0.

>>> list_of_values[0]
2
>>> nested_list[1]
[1, 3]

A negative index counts from the end, with the right-most element indexed -1.

>>> nested_list[-1]
[True, [3]]

Adding lists creates a longer list containing the elements of the added lists.

>>> [1, 2] + [3] + [4, 5]
[1, 2, 3, 4, 5]
List Comprehensions

A list comprehension describes the elements in a list and evaluates to a new list containing those elements.

There are two forms:

[<expression> for <element> in <sequence>]
[<expression> for <element> in <sequence> if <conditional>]

Here's an example that starts with [1, 2, 3, 4], picks out the even elements 2 and 4 using if i % 2 == 0, then squares each of these using i*i. The purpose of for i is to give a name to each element in [1, 2, 3, 4].

>>> [i*i for i in [1, 2, 3, 4] if i % 2 == 0]
[4, 16]

This list comprehension evaluates to a list of:

  • The value of i*i
  • For each element i in the sequence [1, 2, 3, 4]
  • For which i % 2 == 0

In other words, this list comprehension will create a new list that contains the square of every even element of the original list [1, 2, 3, 4].

We can also rewrite a list comprehension as an equivalent for statement, such as for the example above:

>>> result = []
>>> for i in [1, 2, 3, 4]:
...     if i % 2 == 0:
...         result = result + [i*i]
>>> result
[4, 16]
List Slicing

To create a copy of part or all of a list, we can use list slicing. The syntax to slice a list lst is: lst[<start index>:<end index>:<step size>].

This expression evaluates to a new list containing the elements of lst:

  • Starting at and including the element at <start index>.
  • Up to but not including the element at <end index>.
  • With <step size> as the difference between indices of elements to include.

If the start, end, or step size are not explicitly specified, Python has default values for them. A negative step size indicates that we are stepping backwards through a list when including elements.

    >>> lst = [6, 5, 4, 3, 2, 1, 0]
    >>> lst[:3]     # Start index defaults to 0
    [6, 5, 4]
    >>> lst[3:]     # End index defaults to len(lst)
    [3, 2, 1, 0]
    >>> lst[:]      # Creates a copy of the list
    [6, 5, 4, 3, 2, 1, 0]
    >>> lst[:] == lst
    True
    >>> lst[:] is lst
    False
    >>> lst[::-1]   # Make a reversed copy of the entire list
    [0, 1, 2, 3, 4, 5, 6]
    >>> lst[::2]    # Skip every other; step size defaults to 1 otherwise
    [6, 4, 2, 0]

Slicing never causes an IndexError, even when the indices are out of range or the list is empty. This is different from indexing with a single number, which does error. This difference is because a slice always evaluates to a list, while indexing evaluates to an element of a list, where if no element exists at that position, there is no valid value to return. This is why Python raises an error for invalid indexing, while slicing just returns an empty list.

Again, if a slice has nothing to include, the result is an empty list. Assume for the following doctests, lst carries through from the above doctests.

    >>> lst[10:]        # Start index is past the end of the list
    []
    >>> empty = []
    >>> # indexing an empty list is an error
    >>> empty[0]        # You'll get the following error or some similar variation
    Traceback (most recent call last):
      ...
    IndexError: list index out of range
    >>> empty[:]        # Slicing an empty list is not
    []
    >>> empty[1:]
    []
    >>> empty[:3]
    []
    >>> empty[::-1]
    []
    >>> empty[:] is empty   # Still creates a new list
    False
For Statements

A for statement executes code for each element of a sequence, such as a list or range. Each time the code is executed, the name right after for is bound to a different element of the sequence.

for <name> in <expression>:
    <suite>

First, <expression> is evaluated. It must evaluate to a sequence. Then, for each element in the sequence in order,

  1. <name> is bound to the element.
  2. <suite> is executed.

Here is an example:

for x in [-1, 4, 2, 0, 5]:
    print("Current elem:", x)

This would display the following:

Current elem: -1
Current elem: 4
Current elem: 2
Current elem: 0
Current elem: 5
Ranges

A range is a data structure that holds integer sequences. A range can be created by:

  • range(stop) contains 0, 1, ..., stop - 1
  • range(start, stop) contains start, start + 1, ..., stop - 1

Notice how the range function doesn't include the stop value; it generates numbers up to, but not including, the stop value.

For example:

>>> for i in range(3):
...     print(i)
...
0
1
2

While ranges and lists are both sequences, a range object is different from a list. A range can be converted to a list by calling list():

>>> range(3, 6)
range(3, 6)
>>> list(range(3, 6))  # list() converts the range object to a list
[3, 4, 5]
>>> list(range(5))
[0, 1, 2, 3, 4]
>>> list(range(1, 6))
[1, 2, 3, 4, 5]
Type Hints

Type hints can appear in assignment and def statements (and a few other places) to indicate the types of value that a variable should have or that functions should return.

An example without type hints:

x = 4

def pair(y, z):
    return [y, z]

The same example with type hints that x, y, and z are integers and the pair function returns a list of integers:

x: int = 4

def pair(y: int, z: int) -> list[int]:
    return [y, z]

Code behaves identically with or without type hints.

Automatic Type Checking: VS Code can be configured to annotate locations where a variable has been assigned a value that does not have the expected type. To enable type checking, open the VS Code settings, which you can do by pressing the Command and , keys on a Mac, or the Control and , keys simultaneously on a Windows machine. Type type checking in the search bar, which should pull up the Type Checking options. Use the dropdown menu to change type checking from the default of off to basic.

If these type checking options did not appear, you may need to install the Pylance extension. Open the Extensions view by holding Shift, Command, and X on a Mac or Shift, Control, and X on Windows. Type Pylance in the search bar, and press the install button.

To check to ensure that type checking has been enabled, type the following into a Python file:

a: int = 'not an int'

You should see a red underline under 'not an int'. If you hover over 'not an int', VS Code displays an error message, explaining that 'not an int' does not match the expected type int. Keep an eye out for these errors while you're writing code! They're usually a hint about where your code has a bug.

We do this so when you have to program in a group setting, your fellow programmers can understand what types of intputs and outputs functions give. When you work on a large codebase, type hints are really important!

Keep in mind there are things in scope of this class which we do not type hint such as functions.

Lists

Q1: WWPD: Lists & Ranges

Predict what Python will display when the following lines are entered into an interactive session, then unlock the test to check your answers:

python3 -m pytest -k lists_wwpd --unlock
Unlocking Examples

>>> s = [7//3, 5, [4, 0, 1], 2]
>>> s[0]
______
>>> s[2]
______
>>> s[-1]
______
>>> len(s)
______
>>> 4 in s
______
>>> 4 in s[2]
______
>>> s[2] + [3 + 2]
______
>>> 5 in s[2]
______
>>> s[2] * 2
______
>>> list(range(3, 6))
______
>>> range(3, 6)
______
>>> r = range(3, 6)
>>> [r[0], r[2]]
______
>>> range(4)[-1]
______

Q2: Print If

Implement print_if, which takes a list of integers s and a one-argument function f. It prints each element x of s for which f(x) returns a true value.

def print_if(s: list[int], f) -> None:
    """Print each element of s for which f returns a true value.

    >>> print_if([3, 4, 5, 6], lambda n: n > 4)
    5
    6
    >>> result = print_if([3, 4, 5, 6], lambda n: n % 2 == 0)
    4
    6
    >>> print(result)  # print_if should return None
    None
    """
    for x in s:
        "*** YOUR CODE HERE ***"
python3 -m pytest -k print_if

Q3: Close

Implement close, which takes a list of integers s and a non-negative integer k. It returns how many of the elements of s are within k of their index. That is, the absolute value of the difference between the element and its index is less than or equal to k.

Remember that a list is "zero-indexed"; the index of the first element is 0.

def close(s: list[int], k: int) -> int:
    """Return how many elements of s are within k of their index.

    >>> t = [6, 2, 4, 3, 5]
    >>> close(t, 0)  # Only 3 is equal to its index
    1
    >>> close(t, 1)  # 2, 3, and 5 are within 1 of their index
    3
    >>> close(t, 2)  # 2, 3, 4, and 5 are all within 2 of their index
    4
    >>> close(list(range(10)), 0)
    10
    """
    assert k >= 0
    count = 0
    for i in range(len(s)):  # Use a range to loop over indices
        "*** YOUR CODE HERE ***"
    return count
python3 -m pytest -k close

List Comprehensions

Q4: WWPD: List Comprehensions

Predict what Python will display when the following lines are entered into an interactive session, then unlock the test to check your answers:

python3 -m pytest -k list_comprehensions_wwpd --unlock
Unlocking Examples

>>> [2 * x for x in range(4)]
______
>>> [y for y in [6, 1, 6, 1] if y > 2]
______
>>> [[1] + s for s in [[4], [5, 6]]]
______
>>> [z + 1 for z in range(10) if z % 3 == 0]
______

Q5: Squares Only

Implement the function squares, which takes in a list of positive integers. It returns a list that contains the square roots of the elements of the original list that are perfect squares. Use a list comprehension.

To find if x is a perfect square, you can check if sqrt(x) equals round(sqrt(x)).

def squares(s: list[int]) -> list[int]:
    """Returns a new list containing square roots of the elements of the
    original list that are perfect squares.

    >>> seq = [8, 49, 8, 9, 2, 1, 100, 102]
    >>> squares(seq)
    [7, 3, 1, 10]
    >>> seq = [500, 30]
    >>> squares(seq)
    []
    """
    return [___ for n in s if ___]
python3 -m pytest -k squares

Recursion

Q6: Double Eights

Write a recursive function that takes in a positive integer n and determines if its digits contain two adjacent 8s (that is, two 8s right next to each other). You implemented this function with iteration in Lab 1; now implement it with recursion.

Hint: Start by coming up with a recursive plan: the digits of a number have double eights if either ____ (think of something that is straightforward to check) or double eights appear in the rest of the digits.

Important: Use recursion; the tests will fail if you use any loops (for, while), the in operator, or the str function.

def double_eights(n: int) -> bool:
    """Returns whether or not n has two digits in row that
    are the number 8.

    >>> double_eights(1288)
    True
    >>> double_eights(880)
    True
    >>> double_eights(538835)
    True
    >>> double_eights(284682)
    False
    >>> double_eights(588138)
    True
    >>> double_eights(78)
    False
    >>> double_eights(1077)
    False
    >>> # This test checks that you used recursion, with no loops or `in`.
    >>> import inspect, ast
    >>> tree = ast.parse(inspect.getsource(double_eights))
    >>> banned = ['For', 'While', 'In', 'JoinedStr']
    >>> [type(x).__name__ for x in ast.walk(tree) if type(x).__name__ in banned]
    []
    >>> # This test checks that you did not call the str function.
    >>> [x.id for x in ast.walk(tree) if type(x).__name__ == 'Name' and x.id == 'str']
    []
    """
    "*** YOUR CODE HERE ***"
python3 -m pytest -k double_eights

Q7: Flatten

Definition. A nested list of integers is a list of items that are either integers or nested lists of integers.

Write a function flatten that takes in a nested list of integers s and returns a list of the integers that appear in s.

(If you know about list mutation operations already, don't use them to change the input. We'll cover those later.)

Hint: you can check if something is a list two ways:

(1) by using the built-in type function.

>>> type(3) == list
False
>>> type([1, 2, 3]) == list
True

(2) by using the built-in isinstance function.

>>> isinstance(3, list)
False
>>> isinstance([1, 2, 3], list)
True
def flatten(s: list) -> list:
    """Returns a flattened version of list s, which contains lists and integers.

    >>> flatten([1, 2, 3])
    [1, 2, 3]
    >>> deep = [1, [[2], 3], 4, [[[[5], 6]], 7]]
    >>> flatten(deep)
    [1, 2, 3, 4, 5, 6, 7]
    >>> flatten(deep[1])
    [2, 3]
    >>> deep                                # input list should be unchanged
    [1, [[2], 3], 4, [[[[5], 6]], 7]]
    """
    "*** YOUR CODE HERE ***"
python3 -m pytest -k flatten

Submit Assignment

Submit this assignment by running Provenance: Prepare Submission Bundle from the VS Code command palette and uploading the resulting zip to Gradescope. The zip already contains the files you've edited. Lab 00 has detailed instructions.

Your responses to WWPD questions are not submitted, and they do not need to be. Lab credit is based on the code writing questions.

Optional Questions

These questions are optional. If you don't complete them, you will still receive credit for this assignment. They are great practice, so do them anyway!

Q8: Ten-Pairs

Write a function that takes a positive integer n and returns the number of ten-pairs it contains. A ten-pair is a pair of digits within n that sums to 10.

The number 7,823,952 has 3 ten-pairs. The first and fourth digits sum to 7+3=10, the second and third digits sum to 8+2=10, and the second and last digit sum to 8+2=10.

Important notes:

  • A digit can be part of more than one ten-pair.
  • One 5 does not make a ten-pair with itself.

Recommended: Complete and use the helper function count_digit to calculate how many times a digit appears in n.

Important: Use recursion; the tests will fail if you use any loops (for, while).

def ten_pairs(n):
    """Return the number of ten-pairs within positive integer n.

    >>> ten_pairs(7823952) # 7+3, 8+2, and 8+2
    3
    >>> ten_pairs(55055)
    6
    >>> ten_pairs(9641469) # 9+1, 6+4, 6+4, 4+6, 1+9, 4+6
    6
    >>> # This test checks that you used recursion, with no loops.
    >>> import inspect, ast
    >>> tree = ast.parse(inspect.getsource(ten_pairs))
    >>> [type(x).__name__ for x in ast.walk(tree) if type(x).__name__ in ['For', 'While']]
    []
    """
    "*** YOUR CODE HERE ***"


def count_digit(n, digit):
    """Return how many times digit appears in n.

    >>> count_digit(55055, 5) # digit 5 appears 4 times in 55055
    4
    >>> # This test checks that you used recursion, with no loops.
    >>> import inspect, ast
    >>> tree = ast.parse(inspect.getsource(count_digit))
    >>> [type(x).__name__ for x in ast.walk(tree) if type(x).__name__ in ['For', 'While']]
    []
    """
    "*** YOUR CODE HERE ***"
python3 -m pytest -k count_digit
python3 -m pytest -k ten_pairs

Q9: Making Onions

Write a function make_onion that takes in two one-argument functions, f and g. It returns a function that takes in three arguments: x, y, and limit. The returned function returns True if it is possible to reach y from x using up to limit calls to f and g, and False otherwise.

For example, if f adds 1 and g doubles, then it is possible to reach 25 from 5 in four calls: f(g(g(f(5)))).

def make_onion(f, g):
    """Return a function can_reach(x, y, limit) that returns
    whether some call expression containing only f, g, and x with
    up to limit calls will give the result y.

    >>> up = lambda x: x + 1
    >>> double = lambda y: y * 2
    >>> can_reach = make_onion(up, double)
    >>> can_reach(5, 25, 4)      # 25 = up(double(double(up(5))))
    True
    >>> can_reach(5, 25, 3)      # Not possible
    False
    >>> can_reach(1, 1, 0)      # 1 = 1
    True
    >>> add_ing = lambda x: x + "ing"
    >>> add_end = lambda y: y + "end"
    >>> can_reach_string = make_onion(add_ing, add_end)
    >>> can_reach_string("cry", "crying", 1)      # "crying" = add_ing("cry")
    True
    >>> can_reach_string("un", "unending", 3)     # "unending" = add_ing(add_end("un"))
    True
    >>> can_reach_string("peach", "folding", 4)   # Not possible
    False
    """
    def can_reach(x, y, limit):
        if limit < 0:
            return ____
        elif x == y:
            return ____
        else:
            return can_reach(____, ____, limit - 1) or can_reach(____, ____, limit - 1)
    return can_reach
python3 -m pytest -k make_onion

Back to Top

Accessibility Nondiscrimination

Copyright ©2026, Regents of the University of California and respective authors.

This site is built following the Berkeley Class Site template, which is generously based on the Just the Class, and Just the Docs templates.

View all course offerings