Every program keeps things in groups: in order, by key, or without duplicates. Python gives each grouping a literal and a type of its own.
In C++ and Java a container is a library class you name and
construct. Python builds its containers into the grammar.
[1, 2], {"a": 1}, and
{1, 2} are literals, in and
len() work on all of them, and slicing works on the
sequences, without importing anything. Lists, tuples,
dictionaries, and sets are fundamental data types.
A list holds objects, of any kind, in an
ordered, mutable sequence. The for statement
iterates through the elements of a list rather than counting
through a sequence of numbers:
# list.py
odds = [1, 3, 5, 7, 9, 11]
print(odds)
#: [1, 3, 5, 7, 9, 11]
odds.append(13)
for x in odds:
print(x)
#: 1
#: 3
#: 5
#: 7
#: 9
#: 11
#: 13The first line creates a list.
append() adds new elements to odds.
The list grows automatically. The for
statement iterates through odds, so x
takes on each value in the list.
Indexing starts at zero, and negative indices count from the
end. A slice [start:stop:step] copies a
subrange, with stop excluded:
# slicing.py
xs = [10, 20, 30, 40, 50]
print(xs[0], xs[-1]) # First and last
#: 10 50
print(xs[1:3]) # The stop index is excluded
#: [20, 30]
print(xs[:2]) # From the start
#: [10, 20]
print(xs[2:]) # To the end
#: [30, 40, 50]
print(xs[::2]) # Every second item
#: [10, 30, 50]
print(xs[::-1]) # Reversed
#: [50, 40, 30, 20, 10]Slicing works on the built-in sequences, including strings and tuples.
Lists grow, shrink, and answer questions about themselves:
# list_ops.py
xs = [10, 20, 30]
xs.append(40) # Add one item at the end
xs.extend([50, 60]) # Add every item of an iterable
xs.insert(1, 15) # Insert before index 1
print(xs, len(xs))
#: [10, 15, 20, 30, 40, 50, 60] 7
xs.remove(15) # Remove the first item equal to 15
del xs[0] # Remove by index
print(xs, 30 in xs)
#: [20, 30, 40, 50, 60] Trueappend() adds its argument as a single element,
so xs.append([1, 2]) puts a list
inside the list. extend() adds each
item of its argument instead.
sorted() builds a new sorted list from any
iterable. list.sort() reorders a list in place and
returns None:
# sorting.py
words = ["pear", "Fig", "apple"]
print(sorted(words)) # A new list; words is untouched
#: ['Fig', 'apple', 'pear']
print(words)
#: ['pear', 'Fig', 'apple']
print(words.sort()) # Sorts in place and returns None
#: None
print(words)
#: ['Fig', 'apple', 'pear']
print(sorted(words, reverse=True))
#: ['pear', 'apple', 'Fig']sorted(x) returns the new list while
x.sort() returns None, so
x = x.sort() binds None and loses the
list.
Uppercase sorts before lowercase because Python compares
strings by code point. Functions
shows how a key= function changes the ordering.
key=str.lower folds the case here.
Each slot of a list holds a reference to
whatever object you put there, so the same list can
mix strings, numbers, None, and other
containers:
# mixed_types.py
mixed = [1, "two", 3.0, True, None, [5, 6]]
for item in mixed:
print(item, type(item).__name__)
#: 1 int
#: two str
#: 3.0 float
#: True bool
#: None NoneType
#: [5, 6] listMixing types is convenient but easy to overuse. A
list of mixed types usually means each element
needs different handling. A tuple, a data
class, or distinct lists, each holding a single type,
express those differing roles better.
Two list operations produce surprises. * repeats
a reference rather than copying the object it references, so a
grid built that way has one row under three names:
# list_traps.py
grid = [[0]] * 3 # Three names for one inner list # [1]
grid[0][0] = 1
print(grid)
#: [[1], [1], [1]]
# [2]
grid = [[0] for _ in range(3)] # Three separate lists
grid[0][0] = 1
print(grid)
#: [[1], [0], [0]]The grid at [1] is Variables
and References again. * binds the same object
into every slot, and assignment never copies.
The second grid ([2]) comes from a
comprehension: a single expression that produces a new
list, in place of a loop with append(). The
comprehension evaluates [0] three times, once for
each value from range(3), so the rows are three
separate lists. Control
Flow introduces comprehensions, and Comprehensions
explores every form in depth.
Removing items from a list while iterating over
it is the second surprise:
# remove_while_iterating.py
xs = [1, 2, 3, 4]
for x in xs:
xs.remove(x)
print(xs)
#: [2, 4]Each remove() slides the following elements one
place to the left, and the loop then advances its index, so it
steps over the element that moved into the vacated slot.
Skipping every other element leaves half of xs, and
no exception reports the skip. Build a new list instead, with a
comprehension that keeps the items you need, or iterate over a
copy with for x in xs[:].
A tuple is an immutable sequence. The comma makes the tuple, not the parentheses. Tuples are the natural way to return several values from a function and to group values for unpacking:
# tuples.py
point = (3, 4)
point = 3, 4 # Also a tuple; the comma matters
empty = ()
x, y = point # Unpacking
print(x, y)
#: 3 4
# A one-element tuple needs the trailing comma
single = (42,)
print(len(single))
#: 1
print(tuple([1, 2, 3]))
#: (1, 2, 3)
print(tuple("abc"))
#: ('a', 'b', 'c')
def min_max(values):
return min(values), max(values) # Returns a tuple
low, high = min_max([5, 2, 9, 1])
print(low, high)
#: 1 9Without its comma, (42) is the integer
42 inside parentheses. The empty tuple
() is the exception to the comma rule, because it
has nothing to separate.
One name per element is the simplest unpacking, and two extensions relax it. A starred name absorbs whatever remains, and a target can nest to match the shape of the value:
# unpacking_assignment.py
first, *rest = [10, 20, 30, 40]
print(first, rest) # A starred name always collects a list
#: 10 [20, 30, 40]
head, *middle, tail = "abcde" # Any iterable unpacks
print(head, middle, tail)
#: a ['b', 'c', 'd'] e
(name, age), city = ("Alice", 30), "Rome" # Nested targets
print(name, age, city)
#: Alice 30 Rome
values = [1, 2, 3]
try:
x, y = values # Without a star the counts must match
except ValueError as e:
print(e)
#: too many values to unpack (expected 2, got 3)At most one target at each level can carry the star, and the
starred target always receives a list, whether the
source is a list, a tuple, or a string. Without a star the
number of names must equal the number of elements, or the
assignment raises a ValueError. By convention, an
unused value gets the name _, so *_
discards a run of elements. Pattern Matching
matches case patterns against the same shapes.
The star also works in the other direction. Inside a list or
tuple display, * spreads an iterable into the new
container, element by element:
# unpacking_displays.py
evens = [0, 2, 4]
odds = (1, 3)
print([*evens, *odds]) # A list from a list and a tuple
#: [0, 2, 4, 1, 3]
print((*odds, 5)) # A tuple display takes stars too
#: (1, 3, 5)
print([*"ab", *range(2)]) # Any iterable spreads
#: ['a', 'b', 0, 1]
print([*sorted("cab"), "d"]) # So does a call's result
#: ['a', 'b', 'c', 'd']
first, *rest = evens
print([*rest, first]) # Collect, then spread
#: [2, 4, 0]A starred operand can be any expression that produces an
iterable: a list, a tuple, a string, a range, or
the value a call returns, as with *sorted("cab").
Unlike a starred target, any number of operands can carry a
star, and they mix freely with ordinary elements.
The last two lines use both directions. *rest
collects on the left of the assignment and spreads inside the
display, so the pair rotates evens by one place.
+ joins two lists as well, but both operands must
be lists, and [0, 2] + (1, 3) raises a
TypeError. Functions
uses the same star to spread an iterable into a call’s
arguments.
Tuples are often heterogeneous, with each position a different type:
# heterogeneous.py
person = ("Alice", 30, 1.65) # Name, age, height
name, age, height = person
print(name, age, height)
#: Alice 30 1.65
print(person[0], type(person[0]).__name__)
#: Alice str
print(person[1], type(person[1]).__name__)
#: 30 int
print(person[2], type(person[2]).__name__)
#: 1.65 floatA tuple used this way is a fixed-length immutable record,
where each position has a distinct meaning. A tuple holding many
values of one type is instead an immutable
list.
A dictionary (dict) maps keys to values, with
fast lookup. Lookup computes a hash from each key: an
integer derived from the key’s contents. Python reduces that
integer to the slot where the entry lives. A key whose hash
changed after insertion would send the next lookup to a
different slot, and the lookup would miss the entry. So keys
must be hashable: able to produce a hash that stays the
same, and comparable with ==, since lookup checks
that the key stored in the slot equals the one it was given.
Strings, numbers, and tuples of hashable values are hashable.
The mutable built-in containers (list,
dict, set) are not, because their
contents can change, so they cannot be keys.
# dictionaries.py
ages = {"Alice": 30, "Bob": 25}
print(ages)
#: {'Alice': 30, 'Bob': 25}
print(ages["Alice"])
#: 30
ages["Carol"] = 41 # Add or update
print("Bob" in ages) # Membership tests the keys
#: True
# A default when the key is missing
print(ages.get("Dan", 0))
#: 0
print(list(ages)) # Iterating over a dict yields its keys
#: ['Alice', 'Bob', 'Carol']
print(list(ages.values()))
#: [30, 25, 41]
for name, age in ages.items():
print(name, age)
#: Alice 30
#: Bob 25
#: Carol 41Use dict.get() instead of [] to
avoid a KeyError when a key might be absent.
Iterating over a dict follows insertion order,
and the language guarantees that order.
A dict has three views: keys(),
values(), and items(). Iterating over
the dict iterates over keys(), so
for name in ages walks the names.
items() alone yields (key, value)
pairs, and leaving it off is a common slip.
for name, age in ages iterates over the keys and
tries to unpack each one. Unpacking "Alice" into
two names raises a ValueError, since the string has
more than two characters. A two-character key such as
"Bo" unpacks into its letters and the loop finishes
with no error.
keys() is also set-like, and so is
items() when every value is hashable. Each supports
&, |, -, and
^ against another dict’s view or against any set.
Sets covers those operators.
# dict_views.py
ages = {"Alice": 30, "Bob": 25, "Carol": 41}
other = {"Bob": 0, "Dan": 0}
print(ages.keys() & other.keys()) # Set algebra on a view
#: {'Bob'}Entries come out as easily as they go in, and two dictionaries combine:
# dict_ops.py
a = {"x": 1, "y": 2}
b = {"y": 20, "z": 3}
print(a | b) # Merge; the right side wins a collision
#: {'x': 1, 'y': 20, 'z': 3}
print({**a, **b}) # The same merge by unpacking
#: {'x': 1, 'y': 20, 'z': 3}
print(a.pop("x"), a) # Remove and return
#: 1 {'y': 2}
del b["z"]
print(b)
#: {'y': 20}
print(dict(zip("abc", [1, 2, 3]))) # Build from pairs
#: {'a': 1, 'b': 2, 'c': 3}| builds a merged dict and
|= updates in place, the same job
update() does. For sets, | means
union, and the order of the operands makes no difference. For
dictionaries the order matters. When both dictionaries hold the
same key, the right operand’s value wins, so "y"
comes out as 20. {**a, **b} builds the
same merged dict, with ** spreading
each dictionary’s entries the way * spreads a
list’s elements, and the later entry wins a collision. Like a
list display, a dict display accepts any number of
starred operands, with ordinary key: value entries
among them.
The last line feeds dict() an iterable of
(key, value) pairs, and dict() accepts
any iterable that yields such pairs. zip() pairs up
two sequences element by element. Control
Flow covers it with the other loop tools.
The list in remove_while_iterating.py
skips elements and reports nothing. Changing a
dict’s size while iterating over it raises a
RuntimeError:
# dict_iteration_trap.py
d = {"a": 1, "b": 2, "c": 3}
try:
for k in d:
del d[k]
except RuntimeError as e:
print(e)
#: dictionary changed size during iterationA set raises the same exception, with the
message Set changed size during iteration. Only the
list hides the mistake. The dict and
the set both report it.
A set is an unordered collection of unique items. Like the
dict, it finds an item by its hash, so membership
tests are fast and every item must be hashable. Sets also
provide the expected set algebra:
# sets.py
a = {1, 2, 3, 3} # Duplicates collapse
print(a)
#: {1, 2, 3}
print(type({}).__name__, type(set()).__name__)
#: dict set
b = {3, 4, 5}
print(a & b) # Intersection
#: {3}
print(a | b) # Union
#: {1, 2, 3, 4, 5}
print(a - b) # Difference
#: {1, 2}
print(a ^ b) # Symmetric difference
#: {1, 2, 4, 5}
c = {1, 2}
print(c <= a) # Subset
#: True
print(a >= c) # Superset
#: True
print(2 in a)
#: Truedict claimed the {} literal first,
so an empty set is set(). The order these sets
print comes from CPython’s hashing, not from any guarantee, so
do not write code, or a test, that depends on it.
Every set-algebra operator in sets.py has a named
method. The methods are more flexible. They accept any iterable
where the operators need a set on both sides, and
union(), intersection(), and
difference() take several arguments at once.
isdisjoint() adds one more test, with no operator
form:
# set_methods.py
a = {1, 2, 3}
b = {3, 4, 5}
print(a.intersection(b)) # Same as a & b
#: {3}
print(a.union(b)) # Same as a | b
#: {1, 2, 3, 4, 5}
print(a.difference(b)) # Same as a - b
#: {1, 2}
print(a.symmetric_difference(b)) # Same as a ^ b
#: {1, 2, 4, 5}
print(a.intersection([2, 3, 9])) # Arg can be any iterable
#: {2, 3}
print(a.union(b, [6, 7])) # Several args
#: {1, 2, 3, 4, 5, 6, 7}
c = {1, 2}
print(c.issubset(a)) # Same as c <= a
#: True
print(a.issuperset(c)) # Same as a >= c
#: True
print(a.isdisjoint({8, 9})) # No operator form
#: Truesets.py leaves out a few
operators. < and > test
proper subset and superset. They behave like
<= and >= but also require the
two sets to differ. The augmented assignments |=,
&=, -=, and ^= modify
a set in place. They match the update(),
intersection_update(),
difference_update(), and
symmetric_difference_update() methods. Single
elements move in and out with add(),
remove(), and discard().
remove() raises a KeyError on a
missing element. discard() stays silent.
Repeated lookups run faster against a set than
against a list. A list compares the
item you look for against every element in turn. A
set computes one hash and looks in one place.
timeit() runs a callable number
times and returns the total elapsed seconds. The lambda:
prefix wraps an expression into the callable
timeit() needs:
# membership_cost.py
from timeit import timeit
from benchmark import report
def scan_gap(n):
items = list(range(n))
lookup = set(items)
missing = -1
list_time = timeit(lambda: missing in items, number=20)
set_time = timeit(lambda: missing in lookup, number=20)
return list_time / set_time
small_gap = scan_gap(20_000)
large_gap = scan_gap(200_000)
report(small_n=small_gap, large_n=large_gap)
print(large_gap > small_gap) # The gap widens
#: TrueSearching the list is O(n) and searching the
set is O(1), so the gap widens without limit as
n grows. At ten times the size the ratio is bigger
rather than the same, so large_gap comes out bigger
than small_gap. The probe value is missing on
purpose. Searching for an absent item is the list’s
worst case, since the scan compares all n elements
before reporting False.
A timing depends on the machine that took it, so nearly every
measured listing in this book prints a comparison rather than a
number. report() comes from a small helper the book
supplies. By default a measured listing prints the comparison
alone. Running one with --numbers
also prints the measurements behind the comparison.
The collections module in the standard library
includes container types built for specific jobs. Four of these
show up consistently: Counter,
defaultdict, deque, and
namedtuple.
CounterA Counter tallies the frequency of each
item:
# counter.py
from collections import Counter
words = "a cat sat on a mat a cat".split()
counts = Counter(words)
print(counts)
#: Counter({'a': 3, 'cat': 2, 'sat': 1, 'on': 1, 'mat': 1})
print(counts["a"])
#: 3
print(counts["dog"])
#: 0
print("dog" in counts) # Reading it added nothing
#: False
print(counts.most_common(2))
#: [('a', 3), ('cat', 2)]
print(Counter("aab") - Counter("ab")) # Multiset diff
#: Counter({'a': 1})A missing key counts as zero rather than raising a
KeyError, and most_common() returns
the highest counts first. -, &,
and | work between two counters as well, the same
operators the Sets section just used, now reading as multiset
difference, minimum, and maximum. A fourth operator,
+, sums the counts of both counters.
defaultdictA defaultdict supplies a value the first time
you touch a missing key, and so removes the setup-on-first-use
boilerplate:
# defaultdict.py
from collections import defaultdict
pets = [("dog", "Rex"), ("cat", "Felix"), ("dog", "Fido")]
# A plain dict makes you create each list first:
plain = {}
for kind, name in pets:
if kind not in plain:
plain[kind] = []
plain[kind].append(name)
print(plain["dog"])
#: ['Rex', 'Fido']
# A defaultdict creates the missing list:
by_kind = defaultdict(list)
for kind, name in pets:
by_kind[kind].append(name)
print(by_kind["dog"])
#: ['Rex', 'Fido']
# A missing key gets a fresh empty list
print(by_kind["fish"])
#: []
print("fish" in by_kind) # Reading it added the key
#: TrueThe defaultdict constructor argument is a
factory, a callable that builds the default. Here,
list produces a fresh empty list for each new key.
Reading a missing key runs the factory and stores its result, so
the lookup of "fish" adds that key to the
dictionary. For a lookup that leaves the dictionary as it was,
use in or dict.get().
A plain dict has a second option,
setdefault(). In
plain.setdefault(kind, []).append(name),
setdefault() returns the list already stored under
kind. When kind is missing, it stores
the new [] and returns that instead. The
[] argument builds an empty list on every call,
used or not. Every place that touches the dictionary must also
repeat the whole expression. A defaultdict states
the default once, where you create the dictionary.
dequeA deque (double-ended queue) adds and removes
items at either end in constant time. A list is
fast only at its append end:
# deque.py
from collections import deque
dq = deque([1, 2, 3])
dq.append(4) # Add on the right
dq.appendleft(0) # Add on the left
print(dq)
#: deque([0, 1, 2, 3, 4])
print(dq.popleft()) # Remove from the left
#: 0
print(dq.pop()) # Remove from the right
#: 4
print(dq)
#: deque([1, 2, 3])
window = deque(maxlen=3) # A bounded sliding window
for i in range(5):
window.append(i)
print(window)
#: deque([2, 3, 4], maxlen=3)A list can also add and remove at either
end:
# list_as_deque.py
lst = [1, 2, 3]
lst.append(4) # Add at the end
lst.insert(0, 0) # Add at the start
print(lst)
#: [0, 1, 2, 3, 4]
print(lst.pop(0)) # Remove from the start
#: 0
print(lst.pop()) # Remove from the end
#: 4
print(lst)
#: [1, 2, 3]A list can stand in for a deque,
but insert(0, x) and pop(0) must shift
every remaining element, so both are O(n) instead of O(1).
# deque_timing.py
from collections import deque
from timeit import timeit
from benchmark import report
n = 20_000
def list_left_ops():
items = []
for i in range(n):
items.insert(0, i)
while items:
items.pop(0)
def deque_left_ops():
items = deque()
for i in range(n):
items.appendleft(i)
while items:
items.popleft()
list_time = timeit(list_left_ops, number=1)
deque_time = timeit(deque_left_ops, number=1)
report(list_ops=list_time, deque_ops=deque_time)
print(deque_time * 20 < list_time) # Not close
#: TrueUse a deque for a single-threaded queue.
Indexing its middle is O(n), though, so keep a list
wherever you index by position. A deque(maxlen=n)
also caps its length, discarding from the other end when a new
item overflows it. That is a sliding window, and a
list has no equivalent. For a queue shared between
threads, use queue.Queue,
and for a priority queue, heapq.
namedtupleA namedtuple builds a tuple subclass whose
positions also have names:
# named_tuple.py
from collections import namedtuple
Person = namedtuple("Person", ["name", "age", "height"])
alice = Person("Alice", 30, 1.65)
print(alice)
#: Person(name='Alice', age=30, height=1.65)
print(alice.name, alice.age) # Access by name
#: Alice 30
print(alice[0]) # Still indexable like a tuple
#: Alice
name, age, height = alice # And unpackable
print(height)
#: 1.65A namedtuple is a fixed-length record like the
tuple in heterogeneous.py, but its
fields are self-documenting. typing.NamedTuple is
the class form of the same idea: you write
class Person(NamedTuple): and annotate each field
in the body, name: str, instead of listing bare
names, so a type checker knows what each one holds. For a record
that must be mutable, use a data
class. Data
Transfer Objects compares typing.NamedTuple
with a data class.
The standard library has more specialized containers. Performance covers
compact homogeneous storage (array), a zero-copy
view onto another object’s memory (memoryview),
binary search in a sorted list
(bisect), and a heap-backed priority queue
(heapq).
Each of the three built-in mutable containers has an
immutable counterpart. A tuple is an immutable
list, and a frozenset is an immutable
set. Since Python 3.15, frozendict (PEP 814) completes
the trio: a built-in, hashable mapping that rejects changes
after creation. MappingProxyType, from the
types module, is a read-only view onto a
dict you still hold, not a container of its own. A
tuple, a frozenset, and a MappingProxyType each
reject changes:
# immutable_containers.py
from types import MappingProxyType
# The immutable list is tuple; the immutable set, frozenset:
nums = (1, 2, 3)
primes = frozenset({2, 3, 5, 7})
print(5 in primes)
#: True
# A frozenset is hashable, so it can be a set member
# or a dictionary key. A plain list or set cannot:
groups = {frozenset({1, 2}), frozenset({3, 4})}
print(frozenset({1, 2}) in groups)
#: True
# MappingProxyType wraps a dict in a read-only view:
settings = {"debug": False, "level": 3}
config = MappingProxyType(settings)
print(config["level"])
#: 3
settings["level"] = 4 # The view is live, not a copy
print(config["level"])
#: 4
# Mutating any of them is an error:
try:
primes.add(11) # type: ignore
except AttributeError as e:
print(e)
#: 'frozenset' object has no attribute 'add'
try:
config["level"] = 9 # type: ignore
except TypeError as e:
print(e)
#: 'mappingproxy' object does not support item assignmentModifying an immutable container is a type error as well as a
runtime error, so each line that attempts it carries a
# type: ignore. The comment silences the type
checker.
Use the immutable form whenever a container should stay the same after you build it. Neither you nor the code that receives it can add, remove, or replace an element by accident, so a container of immutable elements needs no defensive copy before you share it. An immutable container is safe as a default argument, unlike the mutable default.
A MappingProxyType is the one exception to
watch. It blocks writes through the view, but it is a window
onto the original dict, so changes to that
dict still show through. In immutable_containers.py,
writing to settings changes what
config reports.
frozendictA MappingProxyType is a view onto a
dict that still exists and can change. A
frozendict owns its contents. This listing requires
Python 3.15:
# frozendict_demo.py
prefs = frozendict(theme="dark", zoom=125)
print(prefs["zoom"])
#: 125
# Equal contents compare equal; entry order is ignored:
print(prefs == frozendict(zoom=125, theme="dark"))
#: True
cache = {prefs: "rendered"} # Usable as a dict key
print(cache[frozendict(zoom=125, theme="dark")])
#: rendered
try:
prefs["zoom"] = 150 # type: ignore
except TypeError as e:
print(e)
#: 'frozendict' object does not support item assignmentBecause a frozendict cannot change, it is
hashable when its values are. Like a tuple or a
frozenset, it can then be a dictionary key or a set
member. A dictionary key must be hashable, though it need not be
immutable. Immutability keeps a container’s hash stable.
Immutability is shallow. An immutable container freezes which objects it holds, not what those objects contain:
# shallow_immutability.py
nested = (1, [2, 3])
nested[1].append(4) # The tuple's element is still mutable
print(nested)
#: (1, [2, 3, 4])
try:
hash(nested) # So the tuple cannot be hashed
except TypeError as e:
print(e)
#: unhashable type: 'list'
try:
nested[0] = 9 # type: ignore
except TypeError as e:
print(e)
#: 'tuple' object does not support item assignmentThe tuple holds the same list for
its whole life, and that list stays free to change.
A container holding an unhashable object is unhashable too.
Immutability pays off when it goes all the way down, because
then you can share or hash a value and trust that everything
inside it stays the same. Rethinking
Objects shows the same shallow immutability inside a frozen
data class.
Choosing a container comes down to one question: what do you
do with it most? Ordered items you walk through are a
list. A record of set length whose positions mean
different things is a tuple or a
namedtuple. Lookup by key is a dict.
Uniqueness and membership are a set. Go past those
four only when a measurement or a specific job calls for it, and
freeze whichever you pick as soon as it stops changing.
This chapter’s solutions give a hint, usually the shape of the code, and a full answer for each exercise.
deque_timing.py, change
n from 20_000 to 2_000,
change the printed comparison to
deque_time < list_time, and run the timing
again. Does deque_time < list_time still hold?
Change n to 200_000 and try again. The
list version takes several seconds at that size, and much longer
on a slow machine. Explain what changes about the comparison as
n grows.defaultdict.py, replace
defaultdict(list) with
defaultdict(int), change the loop to count
occurrences of each kind instead of collecting
names, and print the result.set_methods.py, add a set
d = {1, 5, 9} and print a.union(b, d)
and a.intersection(b, d).immutable_containers.py,
add a line that tries groups.add([1, 2]) (a plain
list, not a frozenset) and catch the exception it
raises. Explain, in terms of hashability, why a
frozenset works as a set member but a
list does not.xs = [10, 20, 30, 40, 50], write one
slice expression for each of: the last two items, everything but
the first and last, and a reversed copy of the middle
three.counter.py’s tally using a
defaultdict(int) and no Counter. Which
parts of Counter did you write yourself?heterogeneous.py with a
namedtuple. Show that the unpacking line still
works unchanged.pairs = [("a", 1), ("b", 2), ("c", 3)],
build a dict from it, then print its keys, its
values, and the result of merging it with
{"c": 30, "d": 4}. Which value ends up under
"c", and why?row = [1, 2, 3, 4, 5]. Then explain why
a, b = row raises a ValueError while
a, *b = row does not.frozendict from the pairs
[("host", "localhost"), ("port", 8080)], then use
it as a key in a dict that maps a configuration to
a connection name. Look the connection name up again with a
separately built, equal frozendict. Catch the
TypeError that assigning to one of its entries
raises. Finally, build a frozendict whose value is
a list, try to hash it, and explain the result in
terms of shallow immutability.