Class 14 - Operating on Lists
Date: October 14, 2026
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,
filtertakes 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)), wherekeepis a function fromTtobool: alist[T]of the elements on whichkeepreturnsTrue.list(map(f, lst)), wherefis a function fromTto some typeR: alist[R]of the results of callingfon each element.len(lst): the number of elements, anint.lst[i]: the element at positioni(counting from0), ast.row(i)is for a table.x in lst: whetherxis an element oflst(asinchecks whether one string contains another):"rice" in ["rice", "lentils"]isTrue.
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
.orgaddresses bought more than 8 tickets?":filterrows by tickets, then by.org; count rows (length).- One
filter, checking both;length. filterrows by tickets; extract emails (column); listfilterfor.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 isdiscounts[4], and which code produced it? - Does
big_discountshold codes or numbers? What islen(big_discounts)? - Why does the test check both
"student"and"STUDENT"? What arepercent_off("vip")and"vip" in discount_codes?
Class Exercise (35 mins)
- Using the discount codes from the intro, use
mapto convert all codes to uppercase, and then usefilterto remove the codes that represent no discount. How many codes do you have after cleaning? (lenworks on lists, too!) - Given this list of survey responses:
["yes", "NO", "maybe", "Yes", "no", "Maybe"], usemapto create a list of the responses in lowercase. Then usefilterto keep only definitive answers (filter out "maybe"). - Create a table of product prices:
Extract the price column and use
products = table(
["name", "price"],
[
["laptop", 999.99],
["mouse", 25.50],
["keyboard", 75.00],
["monitor", 299.99],
],
)filterto find prices under $100. Then usemapto apply a 10% discount to those prices. - Take the list
["apple", "banana", "cherry", "date", "elderberry"]and use square brackets to access the third item (remember: 0-indexed). Separately, usefilterto keep only fruits with names longer than 5 characters. - Load the Boston employees dataset (as in previous days), extract the "NAME"
column, and use
filterto find names that contain "Smith" (theinoperator, as in"Smith" in name, checks whether one string contains another). - Create a list of student names:
["alice", "Bob", "CHARLIE", "diana"]. Usemapto normalize all names to proper case (first letter uppercase, rest lowercase). You'll need to write a helper function that usesupperon the first character andloweron the rest, and use slicing (see Class 11) to extract the first character and rest.
Wrap-up (5 mins)
- Lists have many built-in operations -- we showed a few, but there are more at https://docs.python.org/3/library/functions.html.