19b Sets, Counters, and defaultdict

This lesson introduces three containers from the standard library: sets for collections of unique keys, Counters for counting occurrences, and defaultdict for generating a value when a key is missing.

Subject: Python · 65 slides · code lesson

Open the interactive version of this deck

What this lesson covers

The lesson, slide by slide

1. Lesson 19b Sets, Counters, and defaultdict

Title

Python · Chapter 19 — The Goodies

§19.5-19.7, pp. 186-188

2. By the end of this lesson you can

Objectives

Five things, each one you can check yourself at an interpreter prompt.

Think Python, 2nd edition — Allen B. Downey §19.5-19.7, pp. 186-188 — the pages these objectives are drawn from

3. Before we start: a dictionary whose values are all None

Warm-up

You wrote one in chapter 13.

Discussion prompt

The subtract function built a dictionary and set every value to None, because we never use them. What does that suggest about the structure being used?

Hint: What is actually wanted from it?

Answer:

That only the keys matter — the dictionary is being used for fast membership testing and automatic removal of duplicates, and the values are there because a dictionary requires them.

Which means the program is emulating a container it does not have: a collection of keys with no values.

Python provides exactly that, and this lesson introduces it — along with two others that each replace something the course has already done the long way.

4. The one idea behind this lesson: the right container makes the code disappear

Concept

Each of these three types replaces several lines of dictionary manipulation with one, because each is a dictionary specialised for a job the course has already done by hand.

All three are specialisations of the dictionary, and each one exists because the plain dictionary version of the same job needs boilerplate that the specialised version does not.

Figure (svg): Two columns pairing each specialised container with the dictionary pattern it replaces

Every one of these arrives as an answer to something the course already did the long way.

Think Python, 2nd edition — Allen B. Downey §19.5-19.7, pp. 186-188

5. Sets

Section

Section 1

6. A collection of keys with no values

Concept

Python provides another built-in type, called a set, that behaves like a collection of dictionary keys with no values. Adding elements to a set is fast; so is checking membership. And sets provide methods and operators to compute common set operations.

# the dictionary version, from chapter 13
def subtract(d1, d2):
    res = dict()
    for key in d1:
        if key not in d2:
            res[key] = None
    return res

# with sets
def subtract(d1, d2):
    return set(d1) - set(d2)
VersionHow longNote
the dictionary versionsix lineswith None values
the set versionone lineno values at all
the minus operatorset differencebuilt in

In all of those dictionaries, the values are None because we never use them, and as a result we waste some storage space. Set subtraction is available as a method called difference or as an operator, and the result is a set rather than a dictionary — but for operations like iteration, the behaviour is the same.

Think Python, 2nd edition — Allen B. Downey §19.5-19.7, pp. 186-186

7. Picture it: what the dictionary was pretending to be

Picture it

Keys, and values nobody reads.

Figure (svg): Two columns contrasting a dictionary used as a set with an actual set

A set behaves like a collection of dictionary keys with no values.

Which is why lesson 11b's subtract felt like an approximation: it was, and the type it was approximating is this one.

8. Worked example: has_duplicates in one line

Worked example

The set's uniqueness does the work.

# with a dictionary
def has_duplicates(t):
    d = {}
    for x in t:
        if x in d:
            return True
        d[x] = True
    return False

# with a set
def has_duplicates(t):
    return len(set(t)) < len(t)
VersionHow it worksNote
the dictionary versiontracks what it has seenseven lines
the set versioncompares two lengthsone line
why it worksan element can only appear in a set once

Note what a set enforces.

Why: An element can only appear in a set once, so building one from a sequence discards the duplicates.

Compare the lengths.

Why: If an element in t appears more than once, the set will be smaller than t; if there are no duplicates, it will be the same size.

Note what disappeared.

Why: The loop, the accumulator and the early return — the uniqueness the dictionary was tracking by hand is a property of the container.

Figure (svg): The state of the program after each line of Worked example has duplicates in one line, drawn as a ladder with one rung per traced line

The whole run at once: each drop is one line of the program.

One comparison in place of seven lines. The set's defining property is exactly the question being asked.

Verify: Ask what the two versions do differently.

Why: The dictionary version stops at the first duplicate and the set version builds the whole set first — so on a long sequence with an early duplicate, the loop is faster. Both are correct, and the shorter one is not universally better, which is worth noticing before adopting it everywhere.

9. Predict: how many elements?

Prediction

A set discards duplicates.

t = ['a', 'b', 'a', 'c', 'b']
print(len(set(t)), len(t))
ContainerHow manyNote
the listfive elementswith repeats
the setthree distincta, b, c
the comparison3 < 5duplicates present

Predict first

What does this print?

  • 3 5
  • 5 5
  • 3 3
  • 5 3

Correct: 3 5 — an element can only appear in a set once, so the three distinct values survive.

Why: That difference is exactly what has_duplicates tests: if an element appears more than once, the set will be smaller than the sequence. Equal lengths would mean no duplicates, which is why the one-line version works.

10. Worked example: the subset operator

Worked example

uses_only, rewritten.

# with a loop
def uses_only(word, available):
    for letter in word:
        if letter not in available:
            return False
    return True

# with sets
def uses_only(word, available):
    return set(word) <= set(available)
PartWhat it meansNote
<=subset, including equala set operator
the questionare all word's letters available?
the loop versionthe same question, spelled out

Note what the loop asks.

Why: uses_only checks whether all letters in word are in available — which is the subset relation.

Note the operator.

Why: The <= operator checks whether one set is a subset of another, including the possibility that they are equal.

Note the reading.

Why: The letters of word are among the letters available, which is what the mathematical relation means.

Figure (svg): Set operators paired with the questions they answer

Sets provide methods and operators to compute common set operations.

One operator in place of a search loop, because the question was a set question all along. Sets provide methods and operators to compute common set operations.

Verify: Check the equal case.

Why: The <= operator includes equality, so a word using exactly the available letters reports True — which is right for uses only. The strict version < would exclude it, which would be a subtly different and less useful question.

11. Trap: expecting a set to keep its order

Trap

The trap

A program builds a set from a list and expects to iterate over it in the original order.

Treat it as a list without duplicates

Why: Which is what it looks like.

A set behaves like a collection of dictionary keys, and dictionary keys have no order — so the traversal order is unpredictable, exactly as chapter 11 warned.

The fix

Use a set for membership and uniqueness, not for order.

sorted(set(t)) if you need a defined order

Why: Which gives a list, ordered.

Or keep the original list alongside

Why: The list carries the order and the set carries the membership.

This is chapter 11's rule inherited: a set is a dictionary's keys without the values, so everything true of key ordering is true here. Fast membership and no order arrive together.

12. Discriminate: set or dictionary?

Discrimination

Ask whether the values matter.

Sort into buckets

For each job, which container fits?

a set
the words in a document, for membership testing; which words are unique; the letters used in a word
a dictionary
how often each word appears; a mapping from English to Spanish; a lookup from a code to a name
set
In each, only membership matters — is this thing present, and how many distinct ones are there. There is nothing to store against each element.
dict
In each, every key has a value that the program reads: a count, a translation, or a name. That is what a dictionary is for.

13. Complete it: set subtraction

Faded example

Words in the book that are not in the word list.

Fill in the blanks

def subtract(d1, d2):
return set(d1) - set(d2)

Why: Set subtraction is available as a method called difference or as the minus operator, and it gives the elements of the first that are not in the second. That is the whole of chapter 13's six-line subtract, which was building a dictionary whose values were all None because we never use them.

14. Explain it yourself: why was the dictionary version wasteful?

Explain it to yourself

The book says so directly.

Discussion prompt

chapter 13's subtract set every value to None. Explain what that cost and what it revealed.

Hint: What is a dictionary storing?

Answer:

It cost storage: in all of those dictionaries, the values are None because we never use them, and as a result we waste some space.

What it revealed is that the program wanted a different container. It was using a dictionary for fast membership and automatic uniqueness, and paying for a values column it never read.

So the None was a signal rather than a detail — a dictionary whose values are all the same placeholder is almost always a set that has not been written as one. Recognising that signal is worth more than the specific rewrite.

15. Counters

Section

Section 2

16. A set that counts

Concept

A Counter is like a set, except that if an element appears more than once, the Counter keeps track of how many times it appears. If you are familiar with the mathematical idea of a multiset, a Counter is a natural way to represent one.

>>> from collections import Counter
>>> count = Counter('parrot')
>>> count
Counter({'r': 2, 't': 1, 'o': 1, 'p': 1, 'a': 1})
>>> count['d']
0
ExpressionWhat happensNote
Counter('parrot')counts the lettersin one call
the resultlike a dictionarykeys to counts
a missing keyreturns 0rather than raising

Counter is defined in a standard module called collections, so you have to import it. You can initialise one with a string, list, or anything else that supports iteration — and as in dictionaries, the keys have to be hashable.

Think Python, 2nd edition — Allen B. Downey §19.5-19.7, pp. 187-187

17. Picture it: chapter 11's histogram, in one call

Picture it

The function you wrote is now a constructor.

Figure (svg): Two columns comparing the hand-written histogram with a Counter

Counters behave like dictionaries in many ways; they map from each key to the number of times it appears.

The zero-for-missing behaviour is the substantive difference, and it removes the get-with-a-default that lesson 11a introduced for exactly this problem.

18. Worked example: is_anagram in one line

Worked example

Two words are anagrams if their Counters match.

def is_anagram(word1, word2):
    return Counter(word1) == Counter(word2)
PartWhat it computesNote
Counter(word1)letters to countsfor the first word
Counter(word2)likewisefor the second
==same letters, same countswhich is what anagram means

State the definition.

Why: If two words are anagrams, they contain the same letters with the same counts.

Note what that is.

Why: Exactly what a Counter records — so two anagrams have equivalent Counters.

Note that == works.

Why: Counters behave like dictionaries in many ways, including comparing by contents rather than by identity.

Figure (svg): The state of the program after each line of Worked example is anagram in one line, drawn as a ladder with one rung per traced line

The whole run at once: each drop is one line of the program.

One line, and it is the definition of an anagram written directly. The container matches the question, which is what makes it so short.

Verify: Check that == compares contents.

Why: Two Counters built from anagrams are equal even though they are separate objects, because they compare like dictionaries rather than like the programmer-defined classes of chapter 15. That is worth confirming, since the default for a class you write is identity — Counter is a library type that defines equality by value.

19. Predict: what does a missing key give?

Prediction

The letter is not in the word.

count = Counter('parrot')
print(count['d'])
AspectWhat happensNote
'd'not in 'parrot'absent
a plain dictionarywould raise KeyError
a Counterreturns 0

Predict first

What does this print?

  • 0
  • None
  • A KeyError
  • An empty line

Correct: 0 — unlike dictionaries, Counters don't raise an exception if you access an element that doesn't appear.

Why: The zero is the natural answer for a count, and it removes the need for the get-with-a-default idiom that a plain dictionary histogram required. It is also the one substantive way a Counter differs from a dictionary, which is why code that expects a KeyError from one silently never triggers.

20. Worked example: most_common

Worked example

Lesson 13b's ranking, built in.

>>> count = Counter('parrot')
>>> for val, freq in count.most_common(3):
...     print(val, freq)
r 2
p 1
a 1
PartWhat it givesNote
most_common(3)the three commonestas pairs
each pairvalue and frequencyunpacked
the ordermost common to leastsorted for you

Note what it returns.

Why: A list of value-frequency pairs, sorted from most common to least.

Note the argument.

Why: How many pairs to return — three here, and omitting it gives all of them.

Compare with chapter 13.

Why: That chapter built the same thing by hand: a list of (frequency, word) tuples with a reverse sort, chosen so the sort field came first.

Figure (svg): Two columns comparing the hand-built ranking with the Counter method

The same result, with the tuple order reversed — which is worth noticing when converting code.

The commonest elements, already ordered. Note the pairs are (value, frequency) rather than the (frequency, value) that lesson 13b needed for sorting.

Verify: Compare the tuple order with lesson 13b's.

Why: most_common gives (value, frequency), where most_common's own sorting removed the need to put the frequency first. That reversal is a small trap when converting code: the hand-written version's unpacking order does not transfer, and the swap is silent.

21. Trap: expecting a KeyError for a missing element

Trap

The trap

A program tests whether a letter appeared by catching a KeyError from a Counter.

Expect dictionary behaviour

Why: Counters behave like dictionaries in many ways.

Unlike dictionaries, Counters don't raise an exception if you access an element that doesn't appear — they return 0. The except clause never runs, and every absent letter reports as present with a count of zero.

The fix

Test the count, or use in.

if count[letter] > 0

Why: Which reads naturally, since absent means zero.

Or if letter in count

Why: Which distinguishes absent from present-with-zero.

The zero-for-missing behaviour is a feature: it removes the get-with-a-default that a plain dictionary histogram needed. It is also the one place a Counter does not behave like a dictionary, which is exactly why it catches people.

22. Complete it: test for anagrams

Faded example

Same letters, same counts.

Fill in the blanks

from collections import Counter

def is_anagram(word1, word2):
return Counter(word1) == Counter(word2)

Why: If two words are anagrams, they contain the same letters with the same counts, so their Counters are equivalent. Counters compare by contents rather than by identity — unlike the programmer-defined classes of chapter 15, where == falls back on is.

23. Compare: a dictionary and a Counter

Comparison

Fill the blanks. They differ in one substantive way.

Comparison matrix

QuestionA dictionaryA Counter
How do you build one from a string?a loop with getCounter(the string)
A missing key?raises KeyErrorreturns 0
The commonest elements?build tuples and sortmost_common(n)
Do the keys have to be hashable?yesyes — as in dictionaries

The second row is the one that catches people, and it is also what makes the counting idiom unnecessary.

24. Explain it: what happened to my histogram function?

Explain it

Chapter 11's four lines are now one call.

Discussion prompt

A classmate has just discovered Counter and asks whether chapter 11's histogram was a waste of time. Answer them.

Hint: What did writing it teach?

Answer:

Not a waste: writing it is how you learn the accumulator pattern, the get-with-a-default idiom, and why the first sighting is a special case — all of which recur constantly and none of which is specific to counting.

And Counter is a specialised dictionary, so understanding it means understanding the dictionary underneath. The zero-for-missing behaviour only makes sense once you know what a plain dictionary does instead.

Which is the book's whole approach: teach as little Python as possible first, and come back for the good bits. The shortcut is more useful once you could have written it yourself.

25. defaultdict

Section

Section 3

26. A value generated on the fly

Concept

The collections module also provides defaultdict, which is like a dictionary except that if you access a key that doesn't exist, it can generate a new value on the fly.

factory — A function used to create objects, often passed as a parameter to a function like defaultdict.

>>> from collections import defaultdict
>>> d = defaultdict(list)
>>> t = d['new key']
>>> t
[]
>>> t.append('new value')
>>> d
defaultdict(<class 'list'>, {'new key': ['new value']})
StepWhat happensNote
defaultdict(list)list is the factorya class object
d['new key']the key is absentso a new list is made
the new listalso added to the dictionarynot just returned

When you create a defaultdict, you provide a function that's used to create new values. Notice that the argument is list, which is a class object, not list(), which is a new list.

Think Python, 2nd edition — Allen B. Downey §19.5-19.7, pp. 188-188

27. Picture it: the factory runs only when needed

Picture it

A missing key triggers it; a present one does not.

Figure (svg): A flowchart showing a defaultdict calling its factory only for a missing key

The function you provide doesn't get called unless you access a key that doesn't exist.

And the new value is added to the dictionary as well as returned — which is what makes the append in the book's example stick.

28. Worked example: the argument is a class object

Worked example

list, not list().

d = defaultdict(list)      # correct: the class object

d = defaultdict(list())    # wrong: one list, not a factory
ArgumentWhat it isNote
lista class objectcallable — a factory
list()a new empty listnot callable
the differenceone makes lists on demandthe other is a list

Note what is wanted.

Why: A function that's used to create new values — the built-in functions that create lists, sets and other types can be used as factories.

Note the distinction.

Why: The argument is list, which is a class object, not list(), which is a new list.

Note why it matters.

Why: The defaultdict needs to call it once per missing key, so it needs something callable rather than one already-made value.

Figure (svg): Two columns contrasting passing a class object with passing an instance

Chapter 15's factory distinction, in a place where it decides correctness.

The class object, uncalled. This is the same distinction as chapter 15's Point against Point(): one is the factory and the other is a product.

Verify: Ask what a single shared list would do.

Why: Every missing key would get the same list, so appending under one key would appear under all of them — the mutable-default trap of lesson 13b, in a new place. Passing a factory rather than a value is precisely what avoids it.

29. Predict: what does reading a missing key give?

Prediction

The factory is list.

d = defaultdict(list)
t = d['new key']
print(t)
StepWhat happensResult
the keyabsentso the factory runs
list()a new empty list
the resultreturned and stored[]

Predict first

What does this print?

  • an empty list
  • None, since nothing was stored
  • a KeyError, as for a plain dictionary
  • the class object list itself

Correct: an empty list — the factory generates a new value on the fly, which is both returned and added to the dictionary.

Why: The function you provide doesn't get called unless you access a key that doesn't exist, and here it produces an empty list. The storing is what matters: the new list is also added to the dictionary, so appending to it modifies what is kept.

30. Worked example: a dictionary of lists

Worked example

The pattern defaultdict is for.

# with a plain dictionary
if key not in d:
    d[key] = [value]
else:
    d[key].append(value)

# with a defaultdict
d[key].append(value)
VersionHow the first sighting is handledNote
the conditionalhandles the first sightingfour lines
the defaultdictmakes the list on demandone line
the appendworks either waythere is always a list

Recall the plain version.

Why: This is lesson 11b's invert_dict: a singleton on the first sighting and an append thereafter.

Note what the factory removes.

Why: The first sighting stops being a special case, because a missing key produces an empty list to append to.

Note the book's own use.

Why: If you are making a dictionary of lists, you can often write simpler code using defaultdict — as in its solution mapping sorted letters to the words that can be spelled with them.

Figure (svg): The state of the program after each line of Worked example a dictionary of lists, drawn as a ladder with one rung per traced line

The whole run at once: each drop is one line of the program.

One line in place of four, with the first-sighting conditional gone. This is the same simplification get gave the histogram, applied to lists instead of counters.

Verify: Check that the new list persists.

Why: The new list is also added to the dictionary, so appending to what d[key] returned modifies what is stored — which is why the one-line version works. If the factory's result were merely returned, the append would go into a list nobody kept.

31. Trap: creating keys by reading them

Trap

The trap

A program checks whether a key is present by reading d[key] on a defaultdict.

Read the value and see what is there

Why: Which is how you check a plain dictionary, via KeyError.

Reading a missing key calls the factory and stores the result, so the check creates the key it was asking about — and the dictionary grows every time something is looked up.

The fix

Use in to ask without creating.

if key in d

Why: Which does not invoke the factory.

Or use get, which also does not

Why: d.get(key) returns None for an absent key and adds nothing.

The difference from a plain dictionary is that reading has a side effect here. That is exactly the behaviour that makes the append idiom work, and it means a defaultdict cannot be inspected the way a dictionary can.

32. Complete it: a dictionary of lists

Faded example

The factory is a class object.

Fill in the blanks

from collections import defaultdict
d = defaultdict(list)
d['opst'].append('opts')

Why: The argument is list, which is a class object, not list(), which is a new list — the defaultdict calls it once per missing key, so it needs something callable. Passing a single list would share one list between every key, which is the mutable-default trap in another setting.

33. Sort: does this call the factory?

Sorting

Only a missing key does.

Sort into buckets

For each operation on a defaultdict, is the factory invoked?

the factory runs
d['absent']; d['absent'].append(x)
it does not
'absent' in d; d.get('absent'); d['present']; len(d)
yes
Both read a missing key with the bracket operator, which is what triggers the factory — and both leave a new entry behind.
no
None reads a missing key with brackets: in and get ask without creating, a present key needs no factory, and len only counts.

34. Think it through: why must the factory be uncalled?

Socratic

list rather than list().

Discussion prompt

Explain what would go wrong if defaultdict took a value rather than a function that produces one.

Hint: How many keys might be missing?

Answer:

Every missing key would get the same object. For an immutable default that would be harmless, and for a list it means appending under one key appears under all of them.

Passing a factory means the defaultdict can call it once per missing key, producing a fresh value each time — which is what makes a dictionary of independent lists possible.

It is the same reasoning as lesson 13b's mutable-default warning: a default value created once and shared is almost never what was meant. The fix in both cases is to create the value at the moment it is needed.

35. Choosing among the three

Section

Section 4

36. Three specialisations, three questions

Concept

All three are dictionaries adapted to a particular job, and the question each time is what you need to store against a key.

And a plain dictionary when the values are arbitrary and every key is assigned deliberately — which is still the common case.

Think Python, 2nd edition — Allen B. Downey §19.5-19.7, pp. 186-188

37. Picture it: what you store against a key

Picture it

The answer picks the container.

Figure (svg): A decision flowchart choosing among a set, a Counter, a defaultdict and a dictionary

Three questions, and each answer names a container.

Every one of them is a dictionary underneath, which is why everything you know about keys — hashability, unpredictable order — applies to all four.

38. Worked example: the same problem, four ways

Worked example

Counting words, with each container.

# a set: which distinct words appear?
set(words)

# a Counter: how often does each appear?
Counter(words)

# a defaultdict: which lines did each appear on?
d = defaultdict(list)
d[word].append(line_number)
ContainerWhat it answersNote
a setdistinct wordsno values
a Counterwords to countsvalues are numbers
a defaultdict of listswords to line numbersvalues are collections

Ask what is stored.

Why: Nothing, a number, or a growing collection — which is the whole of the decision.

Note that all three are keyed the same way.

Why: By word, with the same hashability requirement and the same unpredictable order.

Note the plain dictionary's place.

Why: For values that are neither counts nor accumulated — a translation, a definition, a configuration value.

Figure (svg): Two columns pairing each container with the chapter that built it by hand

Nothing here is new work — it is the same work, already understood.

Three containers for three questions about one collection of words. The choice is decided by what a key needs to carry.

Verify: Ask which of these chapter 13 built by hand.

Why: All three: a dictionary with None values for the set, the histogram for the Counter, and invert_dict's singleton-then-append for the defaultdict. Recognising them retrospectively is the point of this chapter — each container is an answer to something already done the long way.

39. Discriminate: which container?

Discrimination

What does each key need to carry?

Sort into buckets

For each job, which container fits best?

a set
the distinct words in a document; checking whether a word is in a large list, repeatedly
a Counter
how often each word appears; the ten commonest words
a defaultdict, or a plain dictionary
which page numbers each word appears on; a mapping from a word to its definition
set
Both need membership and uniqueness with nothing stored against each element — which is what a set is.
cnt
Both are about counts, and one of them is most_common exactly. A Counter maps each key to the number of times it appears.
dd
One accumulates a list per key, which a defaultdict simplifies; the other stores an arbitrary value per key, which is a plain dictionary's job.

40. Worked example: what they inherit from dictionaries

Worked example

Three specialisations, and the same constraints.

# all three:
#   keys must be hashable
#   order is unpredictable
#   membership is fast

set([1, 2]) - set([2])       # works
set([[1], [2]])              # TypeError: unhashable
PropertyInherited?Note
hashabilityrequiredas for dictionary keys
orderunpredictablelikewise
speedfast membershipthe hashtable

Note the hashability requirement.

Why: A set behaves like a collection of dictionary keys, and as in dictionaries, a Counter's keys have to be hashable.

Note the ordering.

Why: All three are unordered, for the reason chapter 11 gave: the storage location is computed from the key.

Note the speed.

Why: Adding elements to a set is fast; so is checking membership — which is the reason to use one.

Figure (svg): The state of the program after each line of Worked example what they inherit from dictionaries, drawn as a ladder with one rung per traced line

The whole run at once: each drop is one line of the program.

Three types with one set of constraints, all inherited from the dictionary. Nothing new has to be learned about keys.

Verify: Test the hashability rule on a set of lists.

Why: set([[1], [2]]) raises TypeError: unhashable type: 'list' — the same restriction and the same message as a list dictionary key. So chapter 11's argument about mutable keys applies unchanged, and a set of tuples works where a set of lists does not.

41. Trap: reaching for a specialised container by default

Trap

The trap

Every dictionary in a program is replaced by a defaultdict, in case a key is missing.

Avoid KeyErrors everywhere

Why: Which the default value does.

A KeyError is often the right outcome — it says a key that should exist does not. Replacing every dictionary means a typo in a key name silently creates an entry rather than raising, which loses a genuinely useful error.

The fix

Choose by what the values are.

A defaultdict when the values are accumulated

Why: Lists, counters, sets being built up.

A plain dictionary when a missing key is a mistake

Why: So the KeyError still tells you.

This is lesson 11a's get-against-brackets decision at the level of the container: absence is sometimes a legitimate state and sometimes a bug, and the choice should say which.

42. Predict: does this work?

Prediction

A set of lists.

s = set([[1], [2]])
AspectWhat is trueNote
a setbehaves like dictionary keyshashable required
a listunhashableas chapter 11
the resultTypeError

Predict first

What happens?

  • A TypeError — lists are unhashable, so they cannot be set elements
  • It works, giving a set of two lists
  • It works, but the lists are converted to tuples
  • It gives an empty set

Correct: A TypeError — a set behaves like a collection of dictionary keys, so its elements must be hashable.

Why: The same restriction as a dictionary key, for the same reason: the storage location is computed from the element, so a mutable one could move after it was filed. A set of tuples works, which is chapter 12's escape route applying here too.

43. Two truths and a lie: the three containers

Two truths and a lie

Two are true. Keep the lie.

Eliminate the wrong options

Rule out the two true statements.

  • A. A set behaves like a collection of dictionary keys with no values
  • B. Unlike dictionaries, Counters return 0 rather than raising for a missing element
  • C. All three keep their elements in the order they were added

Survives elimination: C

Why: C contradicts what all three inherit from the dictionary. The storage location is computed from the key, so there is no order to preserve — which is exactly chapter 11's point, and it applies to sets, Counters and defaultdicts alike. If an order is needed, sorted gives one.

44. Where the right container removes the code

Real world

A structure that already does what you were about to write.

Discussion prompt

Think of a tool or a form that already handled something you were about to do by hand. What did using it remove?

Hint: Not effort — a class of mistake.

Answer:

Usually not just effort but a class of mistake: the hand-rolled version had edge cases, and the ready-made one had them handled by someone who thought about them for longer.

Which is what these three containers do. has_duplicates with a set cannot get the tracking wrong, because uniqueness is the container's property rather than the program's job.

And the reason to have written the long version first is that you can now recognise which container you need — the None values, the get-with-a-default, and the first-sighting conditional are each a signal that a specialised type is waiting.

45. Recognising the signals

Section

Section 5

46. Three patterns that say *use a different container*

Concept

Each of these types replaces a specific piece of dictionary boilerplate, and each piece of boilerplate is recognisable on sight.

Recognising the pattern is more useful than remembering the type, because the pattern is what appears in code you are reading or writing.

Think Python, 2nd edition — Allen B. Downey §19.5-19.7, pp. 186-188

47. Picture it: what each pattern is asking for

Picture it

The boilerplate names the container.

Figure (svg): Three dictionary patterns paired with the container each one indicates

Each of these appeared in the course before the container that replaces it.

Which is deliberate on the book's part: the long version teaches the pattern, and the short version is only obvious once you have written the long one.

48. Worked example: reading code for the signal

Worked example

Two functions, and what each is really asking for.

# signal: values are all True
def has_duplicates(t):
    d = {}
    for x in t:
        if x in d: return True
        d[x] = True
    return False

# so: len(set(t)) < len(t)
ObservationWhat it indicatesNote
the valuesall Truenever read
what is wanteduniquenessa set's property
the rewriteone comparison

Look at the values.

Why: Every one is True, and nothing reads them — the same signal as chapter 13's Nones.

Ask what is actually wanted.

Why: Whether an element has been seen before, which is membership, and whether any repeats, which is uniqueness.

Name the container.

Why: Both are what a set provides, so the tracking code is unnecessary.

Figure (svg): The state of the program after each line of Worked example reading code for the signal, drawn as a ladder with one rung per traced line

The whole run at once: each drop is one line of the program.

A seven-line function whose values are decoration. Reading the values first is the fastest way to spot a set written as a dictionary.

Verify: Check whether the rewrite preserves the behaviour.

Why: It gives the same answer and does different work: the loop stops at the first duplicate and the set version builds everything first. For a long sequence with an early repeat the loop wins, so the rewrite is a simplification rather than an optimisation — which is worth being honest about.

49. Sort: which container does this code want?

Sorting

Read the values.

Sort into buckets

For each pattern, which container is being emulated?

a set
res[key] = None, values never read; d[x] = True, to record what has been seen
a Counter
d[k] = d.get(k, 0) + 1; building a list of (count, key) tuples and sorting
a defaultdict, or leave it alone
if k not in d: d[k] = [x] else: d[k].append(x); a mapping from a code to a description
set
Both use a placeholder value nobody reads, which means only membership and uniqueness are wanted.
cnt
One is the counting idiom and the other is most_common built by hand — both are what a Counter provides directly.
oth
One accumulates a list per key, which defaultdict(list) simplifies; the other is an ordinary mapping with nothing to specialise.

50. Worked example: what these do not replace

Worked example

The plain dictionary is still the common case.

# a real mapping: keys to meaningful values
eng2sp = {'one': 'uno', 'two': 'dos'}

# no boilerplate to remove:
#   the values are not counts
#   they are not accumulated
#   they are read
AspectWhat is trueNote
the valuesmeaningfulread by the program
the assignmentdeliberateone per key
no patternnothing to specialise

Note what is absent.

Why: No repeated boilerplate, no first-sighting conditional, and values the program actually uses.

Note what a specialised type would add.

Why: A defaultdict would silently create entries on a typo; a Counter would answer a question nobody asked.

Conclude.

Why: The plain dictionary is right here, and it remains the common case.

Figure (svg): Two columns separating dictionaries worth specialising from those that should stay plain

Most mappings are in the right-hand column.

A dictionary that should stay one. The specialised types exist for specific patterns, and most mappings do not exhibit any of them.

Verify: Ask what a defaultdict would cost here.

Why: A misspelled key would create an entry rather than raising KeyError — losing the error that says a key which should exist does not. That is lesson 11a's brackets-against-get decision: absence is a bug here, so the loud version is right.

51. Trap: converting a working dictionary out of enthusiasm

Trap

The trap

A program's dictionaries are all replaced with defaultdicts and Counters after reading this section.

Use the better container

Why: The specialised types handle their cases well.

A container is only better for the pattern it was designed for. Applied elsewhere it changes behaviour — silently creating keys, or answering a question the program was not asking.

The fix

Convert where the boilerplate is.

Look for the three signals

Why: Values nobody reads, a counting idiom, an accumulate-per-key conditional.

And leave the rest

Why: Most dictionaries have no boilerplate to remove.

This is the same discipline as the previous lesson's: the point is to be able to choose. A specialised container adopted without its pattern is a change with no benefit and a behavioural cost.

52. Predict: which is the signal?

Prediction

One detail says the container is wrong.

def subtract(d1, d2):
    res = dict()
    for key in d1:
        if key not in d2:
            res[key] = None
    return res
PartIts roleNote
the keysusedthe answer
the valuesall Nonenever read
the signala set in disguise

Predict first

What indicates that a set is wanted here?

  • Every value is None and nothing reads them
  • The function uses a for loop
  • It returns a dictionary
  • The parameters are named d1 and d2

Correct: Every value is None and nothing reads them — the dictionary is being used as a collection of keys.

Why: The book says so directly: in all of those dictionaries, the values are None because we never use them, and as a result we waste some storage space. A dictionary whose values are all one placeholder is almost always a set that has not been written as one.

53. Complete it: has_duplicates with a set

Faded example

Uniqueness is the container's property.

Fill in the blanks

def has_duplicates(t):
return len(set(t)) < len(t)

Why: An element can only appear in a set once, so if an element in t appears more than once, the set will be smaller than t. The tracking dictionary is unnecessary because uniqueness is what a set is, rather than something the program has to maintain.

54. Explain it: how do I know which to use?

Explain it

Three containers and a plain dictionary.

Discussion prompt

A classmate asks how to choose between a set, a Counter, a defaultdict and an ordinary dictionary. Give them the question that decides it.

Hint: What goes against each key?

Answer:

Ask what each key needs to carry. Nothing means a set; a count means a Counter; something built up per key means a defaultdict; anything else means a plain dictionary.

And when reading existing code, look for the signals: values that are all None or all True, a get-with-a-default counting line, or a first-sighting conditional around an append. Each names a container.

But most dictionaries have none of those and should stay as they are — a defaultdict in place of a plain one turns a useful KeyError into a silently created entry, which is a real loss.

55. Compare: a dictionary and its three specialisations

Comparison

Fill the blanks. All four are keyed the same way.

Comparison matrix

QuestionA setA CounterA defaultdict
What is stored per key?nothinga countwhatever the factory makes
A missing key?not presentreturns 0the factory runs and stores a value
Which pattern it replacesa dictionary with None valuesthe get-and-increment histogramthe first-sighting conditional
What is inherited from dictionaries?hashable keys, no orderhashable keys, no orderhashable keys, no order

The bottom row is why nothing new has to be learned about keys: all three are dictionaries underneath.

56. The procedure: choosing a container

Pattern

Five steps, and the last is the one that keeps a KeyError useful.

  1. Ask what each key needs to carry — nothing, a count, an accumulated collection, or an arbitrary value.
  2. Nothing: use a set, and take the operators with it.
  3. A count: use a Counter, and remember it returns zero rather than raising.
  4. An accumulated collection: use a defaultdict with a factory — the class object, not a call to it.
  5. Anything else, or where a missing key means a mistake: keep the plain dictionary and its KeyError.

Step 4's class object, not a call is the detail that decides correctness: passing list() would share one list between every key, which is lesson 13b's mutable-default trap.

Python documentation — Data Structures Data Structures

57. Check yourself 1 of 3: sets

Check

The set discards duplicates.

print(len(set('parrot')), len('parrot'))
ValueHow manyNote
'parrot'six characterswith a repeated r
the setp, a, r, o, tfive distinct
the comparison5 < 6a duplicate present

Check your understanding

What does this print?

  • A. 5 6 (correct)
  • B. 6 6
  • C. 5 5
  • D. 6 5

Answer: A

Why: An element can only appear in a set once, so the two r's become one and five distinct characters remain. That difference is exactly what has_duplicates tests: if the set is smaller than the sequence, something repeats.

Why B tempts people
This would mean no duplicates, and parrot has two r's.
Why C tempts people
The string's own length is six; only the set loses the repeat.
Why D tempts people
The set can never be larger than the sequence it was built from.

58. Check yourself 2 of 3: Counters

Check

The element is absent.

Check your understanding

What does count['d'] give for a Counter built from 'parrot'?

  • A. 0 (correct)
  • B. A KeyError
  • C. None
  • D. An empty string

Answer: A

Why: Unlike dictionaries, Counters don't raise an exception if you access an element that doesn't appear — instead, they return 0. That is the natural answer for a count, and it removes the get-with-a-default idiom that a plain dictionary histogram needed.

Why B tempts people
This is what a plain dictionary would do, and the difference is the one substantive way a Counter departs from one.
Why C tempts people
get on a plain dictionary returns None by default; a Counter returns a count.
Why D tempts people
The values are counts, so a number is the only sensible answer.

59. Check yourself 3 of 3: defaultdict

Check

The argument matters.

Check your understanding

Why is the argument list rather than list()?

  • A. list is a class object that can be called once per missing key; list() is one already-made list (correct)
  • B. list() is not valid syntax in an argument
  • C. list() would be slower
  • D. There is no difference

Answer: A

Why: When you create a defaultdict, you provide a function that's used to create new values — a factory. Passing list() would supply a single list shared by every missing key, so appending under one key would appear under all of them: lesson 13b's mutable-default trap in a new setting.

Why B tempts people
It is perfectly valid syntax and produces a list, which is exactly the problem.
Why C tempts people
Speed is not the issue; correctness is. One shared list is wrong regardless of speed.
Why D tempts people
The difference is between a factory and a product, and it decides whether each key gets its own list.

60. Where this shows up outside this course

Real world

Using a container that already enforces what you need.

Discussion prompt

Think of a form or a container that prevents a mistake by its own structure rather than by a rule someone has to follow. What does that buy?

Hint: A slot that only fits one way.

Answer:

A connector that only fits one way, a form field that accepts only a date, a box with one compartment per item — none of them needs a rule, because the shape does the enforcing.

What it buys is that the mistake becomes impossible rather than discouraged, so nobody has to remember and nobody has to check.

Which is what a set does for uniqueness. has_duplicates with a tracking dictionary can get the tracking wrong; with a set it cannot, because uniqueness is what the container is rather than what the program maintains.

61. Confidence wager: commit before you check

Commit first

Answer, then rate your confidence.

Predict first

You write defaultdict(list()) instead of defaultdict(list). What goes wrong?

  • Every missing key shares one list, so appending under one key appears under all of them
  • Nothing — the two are equivalent
  • It raises a TypeError immediately
  • Missing keys return None instead of a list

Correct: Every missing key shares one list, so appending under one key appears under all of them.

Why: The book flags this precisely: notice that the argument is list, which is a class object, not list(), which is a new list. A defaultdict needs a function it can call once per missing key — a factory — so that each key gets a fresh value. Passing an already-made list supplies one object to be shared by every key, which is lesson 13b's mutable-default trap in a new setting, and it produces the same bewildering symptom: data appearing under keys nothing has touched. The distinction is the same one chapter 15 drew between Point and Point(): one is the factory and the other is a product. And it matters here because the values are mutable — a shared immutable default would be harmless, which is why the trap is specific to lists, sets and dictionaries.

62. Explain it to someone else

Explain it

A dictionary whose values are all None.

Discussion prompt

A classmate is reading chapter 13's subtract and asks why every value is None. Explain what that signals.

Hint: What is the dictionary actually being used for?

Answer:

That only the keys matter. The dictionary is providing fast membership testing and automatic uniqueness, and the values are there because a dictionary requires them — the book says so: the values are None because we never use them, and as a result we waste some storage space.

Which means the program is emulating a set: a type that behaves like a collection of dictionary keys with no values. Rewritten, the whole function is set(d1) - set(d2).

And the signal generalises. A dictionary whose values are all one placeholder is almost always a set that has not been written as one, which is worth recognising in code you read as well as code you write.

63. Exit ticket

Exit ticket

One honest answer. It decides what the next lesson opens with.

Predict first

Which of these is still least solid for you?

  • Sets, and the operators they provide
  • Counters, and the zero-for-missing behaviour
  • defaultdict, and why the factory must be uncalled
  • Recognising which container a piece of code wants

Correct: Whichever you picked is the right answer — this one is for you, not for a mark.

Why: Sets are the simplest of the three and the operators are worth knowing, since a subset test replaces a search loop. The Counter's zero for a missing element is its one real departure from a dictionary, and it is what makes the counting idiom unnecessary. The defaultdict's factory is a correctness detail rather than a stylistic one — list against list() decides whether the keys share a value. And the recognition skill is what makes any of it useful, because the patterns appear in code long before anyone names the container.

64. Synthesis: draw the map of this lesson

Connect it up

One page, from memory.

Draw it

Draw three boxes labelled set, Counter and defaultdict, and beside each write what is stored per key and what happens when a key is missing. Underneath, write the dictionary pattern each one replaces, naming the chapter where you wrote it by hand. Finally note what all three inherit from dictionaries, and write the one-line versions of subtract, has_duplicates and is_anagram.

65. What you can do now

Recap

Three pages, and three containers that were waiting behind the code you wrote.

If you remember one thingIt is this
From setsA dictionary whose values are all None is a set in disguise.
From CountersA missing element gives 0, not a KeyError.
From most_commonIt returns (value, frequency) — the reverse of the hand-built version.
From defaultdictThe argument is a factory, so each key gets its own value.
From all threeThey are dictionaries underneath: hashable keys, and no order.

The next lesson finishes the chapter with named tuples, which give a tuple's elements names and can replace a whole class, and the double star that gathers keyword arguments into a dictionary — plus the operator that scatters one back out.

Think Python, 2nd edition — Allen B. Downey §19.5-19.7, pp. 186-188 — everything on these slides traces back here

Sources

  1. Think Python, 2nd edition — Allen B. Downey — Allen B. Downey, Think Python: How to Think Like a Computer Scientist, 2nd edition (Green Tea Press, 2015), §19.5-19.7, pp. 186-188
  2. Python documentation — Data Structures
  3. Python documentation — collections — Container datatypes

Want this taught 1-on-1? Alexander tutors Python — $55/session, free consultation.

Book on Wyzant · Text (657) 465-8108