đď¸ Containers & Pythonic#
Goal: Strengthen understanding of Pythonâs builtâin containers and idioms.
Time: 50 minutes
Prereqs: Variables, loops, basic functions. No external packages required.
Agenda#
Builtâin containers: lists, tuples, sets, dicts
Advanced usage & idioms
Miniâproject (Grades and Sensor tracks included)
Wrapâup & resources
1. Builtâin Containers#
Lists#
Lists are mutable ordered sequences of objects, created with square brackets. Common ops: append, extend, insert, pop, sort.
nums = [10, 30, 20]
nums.append(40)
nums[1] = 25
print(nums)
Lists are indexed by integers, starting with zero. Use the indexing operator to access and modify individual items of the list:
names = ["Dave", "Mark", "Ann", "Phil"]
names[0] = "Jeff"
names.append("Paula")
names.insert(2, "Thomas")
print(names)
Lists can be sliced and concatenated using:
x[start:stop:stride]
mixed = [0, 1, 2, 'e', 3, 'pi']
print(mixed)
print(mixed[0:2])
print(mixed[:2])
print(mixed[2:5])
print(mixed[2:])
print(mixed[-2:])
mixed[5] = 'pi'
print(mixed)
mixed.extend([4,5])
print(mixed)
mixed.append([6,7,8])
print(mixed)
Use the plus + operator to concatenate lists:
a = [1,2,3]+[4,5,6]
print(a)
An empty list can be created by one of two ways
names = []
print(names)
names = list()
print(names)
Lists can contain any kind of Python object, including other lists, as in the following example:
a = [1, "Dave", 3.14, ["Mark", 7, 9, [0, 101]], 10]
print(a)
Items contained in nested lists are accessed by applying more than one indexing operation, as follows
print(a[1])
print(a[3])
print(a[3][1])
print(a[3][3][1])
Tuples#
Tuples are immutable ordered sequences of objects. These simple data structures are great for fixedâsize records. You can create a tuple by enclosing group of values in parentheses like this:
weekdays = ("Mon", "Tue", "Wed", "Thu", "Fri")
Python often recognizes that a tuple is intended even if the parentheses are missing
weekdays = "Mon", "Tue", "Wed", "Thu", "Fri"
print(weekdays)
point = (3.0, 4.0)
print(point, "length:", len(point))
For completeness, 0- and 1-element tuples can be defined, but have special syntax:
a = () # 0-tuple, empty tuple
b = (1,) # 1-tuple
c = 1, # 1-tuple
print(type(a))
print(type(b))
print(type(c))
b = (1) # not a tuple - IMPORTANT
c = 1 # not a tuple
print(type(b))
print(type(c))
The values in a tuple can be extracted by numerical index just like a list. However, it is more common to unpack tuples into a set of variables like this
stock = "GOOG", 100, 490.10
name, shares = stock[0:2]
print(name, shares)
name, shares, _ = stock
print(name, shares)
Although tuples support most of the same operations as lists, such as indexing, slicing, and concatenation, the contents of a tuple cannot be modified after creation. That is, you cannot replace, delete, or append new elements to an exisiting tuple.
print(stock[1])
stock[1] = 200 # error
Note: Python tuples and strings are immutable. This means that once it is created, its contents cannot be changed â elements cannot be added, removed, or altered. On the other hand, Python lists and sets are mutable.
Sets#
A set in Python is an unordered, mutable collection that stores unique elements. It allows for fast membership testing, making it efficient to check whether an item exists in the set. Sets also support mathematical operations such as union (|), intersection (&), and difference (-).
You can create a set using curly braces {} or the set() constructor:
fruits = {"apple", "banana", "cherry"}
print(fruits)
colors = set(["red", "green", "blue"])
print(colors)
Sets automatically eliminate duplicate values:
nums = {1, 2, 2, 3, 4, 4, 5}
print(nums) # Output: {1, 2, 3, 4, 5}
Sets are unordered, so they do not support indexing or slicing:
print(fruits[0]) # Error: 'set' object is not subscriptable
You can add elements to a set using the .add() method:
fruits.add("orange")
print(fruits)
Sets support fast membership testing using the in keyword:
print("apple" in fruits)
print("grape" in fruits)
Sets support powerful mathematical operations:
# Union (`|`) combines elements from both sets:
a = {1, 2, 3}
b = {3, 4, 5}
print(a | b) # Output: {1, 2, 3, 4, 5}
# Intersection (`&`) returns common elements:
print(a & b) # Output: {3}
# Difference** (`-`) returns elements in one set but not the other:
print(a - b) # Output: {1, 2}
You can also use .union(), .intersection(), and .difference() methods:
print(a.union(b))
print(a.intersection(b))
print(a.difference(b))
Note: Sets are ideal for storing unique items and performing membership tests or set algebra. Unlike lists and tuples, sets do not maintain order and do not support indexing. They are mutable, but their elements must be immutable types (e.g., numbers, strings, tuples).
Dictionaries#
A dictionary is a mutable, unordered collection of key-value pairs. You can create one using curly braces {} or the dict() constructor:
person = {"name": "Alice", "age": 30}
print(person)
empty_dict = dict()
Use square brackets to access values by key, and assign new values or add new pairs:
print(person["name"])
person["age"] = 31
person["email"] = "alice@example.com"
print(person)
The .get() method lets you access values without raising an error if the key is missing:
print(person.get("phone")) # None
print(person.get("phone", "N/A")) # N/A
Use .pop() or del to remove key-value pairs:
person.pop("email")
del person["age"]
print(person)
You can iterate over keys, values, or both:
person["age"] = 31
person["email"] = "alice@example.com"
for key, value in person.items():
print(f"{key}: {value}")
Note: Dictionaries are perfect for storing structured data. Keys must be immutable and unique, while values can be of any type. Unlike lists, dictionaries use keysânot indicesâto access data.
Strings#
A string is a immutable sequence of characters enclosed in single, double, or triple quotes:
greeting = "Hello"
print(greeting)
name = 'Alice'
print(name)
message = """This is a multi-line string."""
print(message)
The same type of quote used to start a string must be used to terminate it. Triple-quoted strings capture all the text that appears prior to the terminating triple quote, as opposed to single- and double-quoted strings, which must be specified on one logical line. Triple-quoted strings are useful when the contents of a string literal span multiple lines of text such as the following:
print('''Content-type: text/html
<h1> Hello World </h1>
Click <a href="http://www.python.org">here</a>.
''')
Strings are indexed like listsâuse square brackets to access individual characters:
print(greeting[0]) # Output: H
print(name[-1]) # Output: e
Use + to join strings and * to repeat them:
full = greeting + ", " + name + "!"
print(full) # 'Hello, Alice!'
echo = "Hi! " * 3
print(echo) # 'Hi! Hi! Hi! '
Strings can be sliced using the same syntax as lists:
word = "Python"
print(word[0:3]) # 'Pyt'
print(word[::2]) # 'Pto'
Strings are immutable, meaning their contents cannot be changed after creation:
text = "hello"
# text[0] = "H" # Error: strings can't be modified
new_text = "H" + text[1:]
print(new_text) # 'Hello'
Python offers multiple ways to format strings. Here are some common methods:
title = "The legendary pirate captain"
age = 726.25
# method 1: (formatted-string)%(values)
print("%s is %f years old."%(title, age)) # print a string and a floating point
print("%s is %.4f years old."%(title, age)) # 4 decimal places
print("%s is %10.4f years old."%(title, age)) # allocate 10 spaces
print("%s is %d years old."%(title, age)) # print it as integer
print("%s is %10d years old."%(title, age)) # allocate 10 spaces
# method 2: (formatted-string).format(values)
print("{} is {} years old.".format(title, age))
print("{} is {:0.4f} years old.".format(title, age))
print("{} is {:10.4f} years old.".format(title, age))
# method 3: f(formatted-string with values)
print(f"{title} is {age} years old.")
print(f"{title} is {age:.4f} years old.")
print(f"{title} is {age:10.4f} years old.")
note: Strings are one of the most commonly used data types in Python. They are immutable, support indexing and slicing, and come with a rich set of methods for formatting, searching, and transforming text.
Exercise 1#
Pick the right container for each scenario and write a oneâliner creating it:
unique student IDs from
[101,102,101,103]3D position
(x,y,z)from floatsgradebook mapping names to grades for
Ana=92, Bo=90, Cat=88recent sensor window for last 5 readings
1..5
Type your answers in the next cell.
# Your answers here
unique_ids = ...
position = ...
gradebook = ...
window = ...
# Quick checks (uncomment when you've written your answers)
# assert unique_ids == {101,102,103}
# assert isinstance(position, tuple) and len(position) == 3
# assert gradebook == {"Ana":92, "Bo":90, "Cat":88}
# assert window == [1,2,3,4,5]
2. Pythonic Coding#
"Pythonic" refers to writing code in a way that is idiomatic and aligns with the principles and style of the Python programming language. It emphasizes readability, simplicity, and elegance, often leveraging Pythonâs unique features and conventions.
Comprehensions#
Using list, set, and dictionary comprehensions to write concise and expressive code.
# list comprehension: square every number 0-9
squares = [n*n for n in range(10)]
# set comprehension: keep only even numbers
evens = {n for n in range(20) if n % 2 == 0}
# dict comprehension: map each character to its ASCII code
ascii = {c: ord(c) for c in "ACE"}
print(squares)
print(evens)
print(ascii)
Comparing Pythonic vs Non-Pythonic List Construction#
The following Pythonic version is faster than the explicit loop, even though both versions build the list one element at a time â a list comprehension does not secretly pre-allocate the whole list up front. The speed gain comes from how each element gets added: an explicit loop looks up the .append method and calls it as a full Python function on every iteration, while a comprehension compiles to a dedicated bytecode instruction that appends directly, skipping that repeated lookup and function call.
# Non-Pythonic: builds the list one element at a time with .append()
squares = []
for i in range(10):
# each call looks up .append and grows the list
squares.append(i * i)
print(squares)
# Pythonic: concise, and faster (see explanation below)
squares = [i * i for i in range(10)]
print(squares)
The expression:
squares = [i * i for i in range(10)]
does not pre-allocate memory for the whole list in advance â Python has no general way to know how many items a comprehension will produce (the source could be a generator, filtered by an if clause, etc.), so the list still grows one item at a time using the same amortized strategy as .append().
What actually makes it faster:
No repeated attribute lookup or function call.
squares.append(i * i)has to look up.appendonsquaresand call it as a Python function on every pass through the loop. A comprehension compiles its body into a dedicatedLIST_APPENDbytecode instruction that adds the value directly, without that overhead.Less loop bookkeeping. The comprehension runs as its own small, specialized code object, with less general-purpose overhead than a full Python
forstatement.
You can see this directly with dis:
import dis
dis.dis(compile('[i * i for i in range(10)]', '<string>', 'eval'))
Look for BUILD_LIST 0 (an empty list) followed by LIST_APPEND inside the loop â confirmation that the list is still built incrementally, not pre-sized.
Simplifying Conditional Statements#
Using Pythonâs expressive syntax to make conditional logic cleaner and more readable.
The followng code checks each condition separately, which works but is verbose and harder to maintain.
# hand = input("What would you like to play? ")
hand = 'Shears' # intentionally invalid, so the code falls through to the else branch
# Non-Pythonic: each valid option needs its own branch
if hand == "Rock":
print("It is a valid play.")
elif hand == "Paper":
print("It is a valid play.")
elif hand == "Scissors":
print("It is a valid play.")
else:
print("It is an invalid play.")
Combining conditions with or reduces repetition and improves clarity.
# hand = input("What would you like to play? ")
hand = 'Shears'
# Better: one combined condition, but `hand ==` is still repeated three times
if hand == "Rock" or hand == "Paper" or hand == "Scissors":
print("It is a valid play")
else:
print("It is an invalid play")
The most Pythonic approach uses in to check if a value exists in a tuple, making the code concise and elegant.
# hand = input("What would you like to play? ")
hand = 'Shears'
# Pythonic: `in` checks membership against a tuple in one concise expression
if hand in ("Rock", "Paper", "Scissors"):
print("It is a valid play")
else:
print("It is an invalid play")
Ternary Operator#
Pythonâs ternary operator provides a compact way to write simple conditional assignments.
Sandard if-else Assignment: This version uses a full if-else block to assign a value based on a condition.
y = 1
# Standard if-else assignment
if y == 1:
x = 1
else:
x = 0
print(x)
Pythonic Ternary Expression: This version condenses the logic into a single line, improving readability and conciseness.
x = 1 if y == 1 else 0 # Pythonic ternary: <value_if_true> if <condition> else <value_if_false>
print(x)
Loops#
Exploring Pythonic ways to iterate over sequences with index tracking.
Why Iterating with an Index Is Not Pythonic#
A pattern often carried over from other languages (C, Java, MATLAB) is to loop over a range of indices and use each index to look up an element:
for i in range(len(names)):
print(names[i])
This works, but it isnât idiomatic Python:
Indirect: every iteration computes an index
i, then uses it to look upnames[i]â an extra step Pythonâsforloop doesnât need.Less general:
range(len(x))requiresxto support bothlen()and integer indexing, so it fails on iterables that donât â generators, file objects, sets, and more.Error-prone: off-by-one mistakes (
range(len(names) - 1), starting at 1 instead of 0, âŚ) are a classic source of bugs.Harder to read:
for name in names:reads like English;for i in range(len(names)):forces the reader to mentally substitutenames[i]everywhereiappears.
Pythonâs for loop is built to iterate directly over any iterable, so in most cases you donât need an index at all.
names = ['Peter Parker', 'Clark Kent', 'Wade Wilson', 'Bruce Wayne', 'Dr. Baek']
# Non-Pythonic: loop over indices, then index into the list on every iteration
for i in range(len(names)):
print(names[i])
The Pythonic version iterates over the elements directly â no indices, no lookups:
# Pythonic: iterate directly over the elements
for name in names:
print(name)
Indexing also costs a little more at runtime: each names[i] triggers a separate __getitem__ call, on top of the overhead of range() and len(). Pythonâs direct iteration protocol avoids that extra call. You can see the difference with timeit:
import timeit
names_big = [f'name{i}' for i in range(100_000)]
# Time the indexed version
t_indexed = timeit.timeit(
'for i in range(len(names_big)): names_big[i]',
globals=globals(), number=100
)
# Time the direct iteration
t_direct = timeit.timeit(
'for name in names_big: name',
globals=globals(), number=100
)
print(f'Indexed loop: {t_indexed:.4f} s')
print(f'Direct loop: {t_direct:.4f} s')
print(f'Indexed loop is {t_indexed / t_direct:.2f}x slower')
Indexed loop: 0.3451 s
Direct loop: 0.1445 s
Indexed loop is 2.39x slower
Of course, sometimes you genuinely need the index too â for example, to print a line number, or to look up a corresponding item in a parallel list. Python still gives you a Pythonic way to do that: enumerate() and zip(), covered next.
Manual Indexing with a Loop: This approach manually tracks the index using a separate variable, which works but is more error-prone and verbose.
names = ['Peter Parker', 'Clark Kent', 'Wade Wilson', 'Bruce Wayne', 'Dr. Baek']
# Manual index tracking: a separate counter variable has to be created,
# incremented by hand, and kept in sync with the loop -- easy to get wrong
# (e.g. incrementing in the wrong place, or forgetting it entirely).
index = 0
for name in names:
print(index, name)
index += 1
Using enumerate() for Cleaner Looping: enumerate() simplifies index tracking by automatically pairing each item with its index.
names = ['Peter Parker', 'Clark Kent', 'Wade Wilson', 'Bruce Wayne', 'Dr. Baek']
# enumerate() yields (index, value) pairs directly -- no manual counter needed
for index, name in enumerate(names):
print(index, name)
Starting Index from a Custom Value: You can customize the starting index with enumerate(..., start=1) for more control over output formatting.
names = ['Peter Parker', 'Clark Kent', 'Wade Wilson', 'Bruce Wayne', 'Dr. Baek']
# start=1 shifts the reported index without changing the underlying list
for index, name in enumerate(names, start=1):
print(index, name)
Iterating Through Multiple Lists#
Python provides elegant ways to iterate over multiple sequences in parallel.
Manual Indexing with enumerate(): This approach uses enumerate() to access both the index and the value, allowing you to manually retrieve corresponding elements from another list.
heroes = ['Spiderman', 'Superman', 'Deadpool', 'Batman', 'Pirate Captain']
# Using the index from enumerate() to look up the matching item in a second list --
# works, but still relies on indexing and assumes both lists are the same length
for index, name in enumerate(names):
hero = heroes[index]
print(f'{name} is actually {hero}')
Using zip() for Parallel Iteration: zip() pairs elements from multiple lists, making the code cleaner and more readable when iterating through them together.
# zip() pairs up corresponding elements from both lists directly -- no indexing at all
for name, hero in zip(names, heroes):
print(f'{name} is actually {hero}')
Extending zip() to More Than Two Lists: You can use zip() with three or more lists to combine related data from multiple sources in a single loop.
universes = ['Marvel', 'DC', 'Marvel', 'DC', 'USAFA']
# zip() scales to any number of lists; it stops at the shortest one if lengths differ
for name, hero, universe in zip(names, heroes, universes):
print(f'{name} is actually {hero} from {universe}')
3. Wrapâup#
Takeaways
Choose the right container: list/tuple/set/dict.
Prefer comprehensions,
enumerate,zip, andkey=sorts for readability.
Further reading
Python Tutorial â Data Structures (lists, dicts, sets, comprehensions): https://docs.python.org/3/tutorial/datastructures.html
collections(Counter, defaultdict, namedtuple): https://docs.python.org/3/library/collections.htmlBuiltâins (
enumerate,zip,map,filter) &functools.reduce: https://docs.python.org/3/library/functions.html , https://docs.python.org/3/library/functools.html