Skip to main content

Class 14 - Operating on Lists

Date: October 14, 2026

⚠️Homework
Homework 5 due October 18, 2026
‼️Ending Skills
Last week for skills: 1, 2

Reference

For all work with tables, refer to the Tables page in the menu at the top of the page!

Skills: None​

Notes (review before class)​

We continue with cleaned_data from the Class 13 reading.

Filtering Lists​

The Discount Code column holds strings, including "none", which we added while cleaning; it isn't a real code:

codes = cleaned_data.column("Discount Code")
# -> ['BIRTHDAY', 'none', 'STUDENT', 'none', 'STUDENT', 'none', 'STUDENT']

As filter keeps some rows of a table, the built-in filter keeps some elements of a list:

def real_code(c: str) -> bool:
"""determines whether c is an actual discount code (not none)"""
return not (c == "none")

def test_real_code():
assert real_code("STUDENT")
assert not real_code("none")

list(filter(real_code, codes)) # -> ['BIRTHDAY', 'STUDENT', 'STUDENT', 'STUDENT']
  • For lists, filter takes the function first and the list second.
  • Wrap the result in list(...) to get an ordinary list (see the aside).
  • The function can check anything about an element, not just equality (e.g., whether a string is all lowercase).

Getting Elements by Position​

How many sales went to web.com email addresses? A task plan (Class 12): get the email addresses (column), keep the web.com ones (filter), and count them (len, which works on lists as well as strings).

To check an address (rather than look at its last 8 characters), split (Class 12) gives a list of the pieces of a string around a separator. To get one element, write its position in square brackets; positions start at 0, so [1] is the second:

"this-has-hyphens".split("-")  # -> ['this', 'has', 'hyphens']
"bonnie@pyret.org".split("@")[1] # -> 'pyret.org'
def web_com_address(email: str) -> bool:
"""determines whether email is from web.com"""
return email.split("@")[1] == "web.com"

def test_web_com_address():
assert not web_com_address("bonnie@pyret.org")
assert web_com_address("parrot@web.com")

emails = ["parrot@web.com", "bonnie@pyret.org", "polly@web.com"]
len(list(filter(web_com_address, emails))) # -> 2

In the events data, Ernie's address, "ernie.mail.com", has no @: it splits into a one-element list, so [1] is an error. What could you do about that?

Transforming Lists​

map transforms each element, like transform_column on tables (filter only keeps elements, unchanged); wrap it in list(...) too. To get the usernames from a list of addresses:

def extract_username(email: str) -> str:
"""extracts the portion of an email address before the @ sign"""
return email.split("@")[0]

def test_extract_username():
assert extract_username("bonnie@pyret.org") == "bonnie"
assert extract_username("parrot@web.com") == "parrot"

list(map(extract_username, ["parrot@web.com", "bonnie@pyret.org"])) # -> ['parrot', 'bonnie']

Summary of List Operations​

If lst is a list[T] (a list whose elements all have some type T):

  • list(filter(keep, lst)), where keep is a function from T to bool: a list[T] of the elements on which keep returns True.
  • list(map(f, lst)), where f is a function from T to some type R: a list[R] of the results of calling f on each element.
  • len(lst): the number of elements, an int.
  • lst[i]: the element at position i (counting from 0), as t.row(i) is for a table.
  • x in lst: whether x is an element of lst (as in checks whether one string contains another): "rice" in ["rice", "lentils"] is True.

A type variable like T stands for some unspecified type, and shows relationships between types: filter's output has the same element type as its input; map's may differ. Python has more built-in functions.

Try: with recipes as lists of ingredients,

stir_fry = ["peppers", "pork", "onions", "rice"]
dosa = ["rice", "lentils", "potato"]
misir_wot = ["lentils", "berbere", "tomato"]

write recipe_uses (does a recipe use an ingredient?), make_vegetarian (replace "pork", "chicken", and "beef" with "tofu"), and protein_veg_count (count ingredients other than "rice" and "noodles"). Harder: the lists, in a list of ingredient lists, that contain an ingredient; the ingredients common to two lists. (Why is a named helper for filter a problem? A lambda, from Class 10, helps.)

Combining Lists and Tables​

  • Table methods mostly process rows, list functions columns; an analysis of some rows and some columns uses both.
  • There's often more than one plan. "How many people with .org addresses bought more than 8 tickets?":
    1. filter rows by tickets, then by .org; count rows (length).
    2. One filter, checking both; length.
    3. filter rows by tickets; extract emails (column); list filter for .org; len.
  • None is the one right answer. Are intermediate results (the large orders, their emails) useful elsewhere? Do you prefer working on rows, extracting lists only for what tables can't do? Which uses fewer resources? (Filtering a table costs no more than filtering a list; more later.) Teams may set design standards; otherwise, consider several approaches, then pick one.
  • Soon we'll write list functions ourselves, and they won't seem so magical.

Recap (20 mins)​

A magazine records the discount code used on each subscription order (perhaps from a column of a table), and wants to know what discount each order got. Read through the program:

discount_codes = ["NEWYEAR", "student", "NONE", "student", "VIP", "none"]

def percent_off(code: str) -> int:
"""the percent discount that code gives (0 if it isn't a real code)"""
if code == "VIP":
return 30
elif code.lower() == "student":
return 20
elif code == "NEWYEAR":
return 10
else:
return 0

def test_percent_off():
assert percent_off("VIP") == 30
assert percent_off("student") == 20
assert percent_off("STUDENT") == 20
assert percent_off("none") == 0

discounts = list(map(percent_off, discount_codes))
big_discounts = list(filter(lambda p: p >= 20, discounts))
  • What is discounts? What is discounts[4], and which code produced it?
  • Does big_discounts hold codes or numbers? What is len(big_discounts)?
  • Why does the test check both "student" and "STUDENT"? What are percent_off("vip") and "vip" in discount_codes?

Class Exercise (35 mins)​

  1. Using the discount codes from the intro, use map to convert all codes to uppercase, and then use filter to remove the codes that represent no discount. How many codes do you have after cleaning? (len works on lists, too!)
  2. Given this list of survey responses: ["yes", "NO", "maybe", "Yes", "no", "Maybe"], use map to create a list of the responses in lowercase. Then use filter to keep only definitive answers (filter out "maybe").
  3. Create a table of product prices:
    products = table(
    ["name", "price"],
    [
    ["laptop", 999.99],
    ["mouse", 25.50],
    ["keyboard", 75.00],
    ["monitor", 299.99],
    ],
    )
    Extract the price column and use filter to find prices under $100. Then use map to apply a 10% discount to those prices.
  4. Take the list ["apple", "banana", "cherry", "date", "elderberry"] and use square brackets to access the third item (remember: 0-indexed). Separately, use filter to keep only fruits with names longer than 5 characters.
  5. Load the Boston employees dataset (as in previous days), extract the "NAME" column, and use filter to find names that contain "Smith" (the in operator, as in "Smith" in name, checks whether one string contains another).
  6. Create a list of student names: ["alice", "Bob", "CHARLIE", "diana"]. Use map to normalize all names to proper case (first letter uppercase, rest lowercase). You'll need to write a helper function that uses upper on the first character and lower on the rest, and use slicing (see Class 11) to extract the first character and rest.

Wrap-up (5 mins)​