Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Python  Module 5 – Built-in data structures: lists, dicts, sets and more

Sets and frozensets for membership and maths

Remove duplicates and test membership fast with Python sets, use union, intersection and difference, see why set order changes, and use frozenset.

  • Beginner
  • 20 minutes
  • Examples run with Python 3.14.8 and Pyodide 314.0.7
  • By MySmartCoPilot
Free lesson, Pro answers: 20 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Remove duplicates with set() and keep the first-seen order with dict.fromkeys()
  • Apply union, intersection, difference and symmetric difference
  • Explain why x in a set stays fast as the set grows, and when a list scan is just as good
  • Explain why a set of strings can print in a different order on every run
  • Use frozenset when a hashable set is needed

Before you start

What you’ll learn · Before you start

What you will learn

  • Remove duplicates with set() and keep the first-seen order with dict.fromkeys()
  • Apply union, intersection, difference and symmetric difference
  • Explain why x in a set stays fast as the set grows, and when a list scan is just as good
  • Explain why a set of strings can print in a different order on every run
  • Use frozenset when a hashable set is needed

Before you start

On this page

Proving that a number is not in a list of 100,000 takes 100,000 comparisons, because a list can only be checked item by item. A set, such as {"tea", "rice"}, is an unordered collection of distinct values that answers the same question by looking in one place, so its membership test stays fast however big it grows. Adding a value that is already there changes nothing, so turning a list into a set removes its duplicates, and sets do the arithmetic of groups: union, intersection, difference and symmetric difference, written |, &, - and ^.

Below you can watch a list and a set search the same five seat numbers, sort the members of two clubs into regions, see why a set of strings prints in another order on each run, and remove repeats from a list while keeping its order. frozenset, the set that cannot change, closes the lesson.

A set keeps one copy of each value

A visitor log of six entries, [3, 1, 3, 2, 1, 3], holds only three different visitors, and set(visits) finds them in one step, because a set keeps each value once. Braces with values make a set too, and repeats vanish as it is built:

Creating, adding, removing Python · set_basics.py
empty = set()  # the only way to write an empty set ...
not_a_set = {}  # ... because {} is an empty dictionary
print(type(empty).__name__, type(not_a_set).__name__)

tags = {"python", "lists", "python"}  # the repeat disappears
tags.add("sets")
tags.add("lists")  # already there: nothing changes
tags.discard("java")  # discard() is quiet about a missing item
print(len(tags), "sets" in tags, sorted(tags))

visits = [3, 1, 3, 2, 1, 3]
print(sorted(set(visits)), len(set(visits)), "different visitors")

tags.remove("java")  # remove() raises KeyError for a missing item

Output (exit status 1)

set dict
3 True ['lists', 'python', 'sets']
[1, 2, 3] 3 different visitors

Printed as an error (standard error)

Traceback (most recent call last):
  File "set_basics.py", line 14, in <module>
    tags.remove("java")  # remove() raises KeyError for a missing item
    ~~~~~~~~~~~^^^^^^^^
KeyError: 'java'

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 set_basics.py

The empty set has to be written set(), because {} was taken long ago by the empty dictionary. add(x) adds one value and does nothing when it is already there. discard(x) removes a value if it is present, while remove(x) insists on it and raises KeyError, as the last line shows. len() counts the distinct values, so len(set(visits)) answers “how many different visitors?” in one expression. Press Edit, change the last line to tags.discard("java") and run it again: the program now ends without an error.

Sets have no positions, so there is no s[0] and no slicing. To print one in a predictable order, sort it, as the examples in this lesson do, apart from the one that shows why: sorted(s) returns a sorted list. Like the keys of a dictionary, the items of a set must be hashable: A value that can be a dict key or a set item, because its hash never changes while it exists: numbers, strings and tuples of hashable values are hashable; lists, dicts and sets are not. More in “functools: cache, partial, reduce and more”; the next sections show why.

{} for an empty set

seen = {} makes an empty dictionary, so a later seen.add(x) fails with AttributeError: a dictionary has no add() method. Write seen = set() for a set that starts empty.

Checkpoint

Check: Making and changing a set

  1. Question 1 of 2 What does this print?

    Read the code, then choose one answer.

    tags = {"python", "lists"}
    tags.add("lists")
    tags.discard("java")
    print(len(tags))
    Show the answer: Question 1 of 2

    Answer: 2

    add() of a value already in the set and discard() of a value not in it both leave the set as it was, so it still holds its two tags.

  2. Question 2 of 2 What is len(set("banana"))?

    Type a number.

    A hint

    A string gives its characters one by one.

    Show the answer: Question 2 of 2

    Answer: 3 values

    set("banana") takes the six characters and keeps each one once: 'b', 'a' and 'n'. Three distinct values, so the length is 3.

Union, intersection and difference pick regions of two sets

Two clubs that share some members show all four operations:

Students in both clubs, either club, or only one Python · clubs.py
chess = {"Asha", "Ravi", "Mei", "Dev"}
drama = {"Mei", "Kiran", "Asha", "Zoya"}

print(sorted(chess & drama))  # in both clubs: intersection
print(sorted(chess | drama))  # in either club: union
print(sorted(chess - drama))  # in chess but not drama: difference
print(sorted(chess ^ drama))  # in exactly one club: symmetric difference

print({"Asha", "Mei"} <= chess, chess.isdisjoint({"Zoya", "Bilal"}))
print(sorted(chess.union(["Bilal", "Asha"])))  # methods take any iterable

Output

['Asha', 'Mei']
['Asha', 'Dev', 'Kiran', 'Mei', 'Ravi', 'Zoya']
['Dev', 'Ravi']
['Dev', 'Kiran', 'Ravi', 'Zoya']
True True
['Asha', 'Bilal', 'Dev', 'Mei', 'Ravi']

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 clubs.py

Every student of either club falls in one of three regions: chess only, both clubs, or drama only. The diagram sorts the six students into those regions, and each operator keeps some of them:

Members of two clubs in three regions, chess only, both and drama only, and which regions -, &, ^ and | each keep.chess | drama: everyone in either clubWhich regions each operator keepschess onlyDev, Raviboth clubsAsha, Meidrama onlyKiran, Zoyachess - dramachess onlychess ^ dramachess only + drama onlychess & dramaboth clubschess | dramaall three regions

The four set operators on two clubs

Text description of the diagram

The diagram shows the example clubs.py. The top row splits everyone in either club into three regions.

  1. Chess only: Dev and Ravi.
  2. Both clubs: Asha and Mei.
  3. Drama only: Kiran and Zoya.

The bottom row says which regions each operator keeps.

  1. chess - drama, the difference, keeps the chess-only region: Dev and Ravi.
  2. chess & drama, the intersection, keeps the middle region: Asha and Mei.
  3. chess ^ drama, the symmetric difference, keeps both outer regions: Dev, Ravi, Kiran and Zoya.
  4. chess | drama, the union, keeps all three regions: all six people.
  • a | b, the union, holds everything in either set.
  • a & b, the intersection, holds what is in both.
  • a - b, the difference, holds what is in a but not in b; b - a is a different set.
  • a ^ b, the symmetric difference: The items that are in exactly one of two sets, a ^ b: everything in either set except what the two share. {1, 2} ^ {2, 3} is {1, 3}., holds what is in exactly one of them.

a <= b asks whether every item of a is also in b, which makes a a subset: A set whose every item is also in another set. a <= b tests it; a < b also needs b to hold something more, which makes a a proper subset. of b (< also requires b to have something more), and a.isdisjoint(b) whether the two share nothing. The operators need sets on both sides: chess | ["Bilal"] raises TypeError. The named methods, union(), intersection(), difference(), symmetric_difference(), issubset() and the others, accept any iterable, which is why chess.union(["Bilal", "Asha"]) works. Each operator also has an in-place form, such as a |= b, that changes a instead of building a new set. Press Edit, add print(sorted(drama - chess)) and run it: Kiran and Zoya, the drama-only region.

Checkpoint

Check: Union, intersection and difference

  1. Question 1 of 2 With the two clubs of the lesson, which expression gives Kiran and Zoya, the members who are only in drama?

    Choose one answer.

    A hint

    Find the region in the diagram first.

    Show the answer: Question 1 of 2

    Answer: drama - chess

    a - b keeps the items of a that are not in b. The drama members who are not in chess are Kiran and Zoya, so the expression is drama - chess; chess - drama is a different set.

  2. Question 2 of 2 chess | ["Bilal"] raises TypeError. Which expression gives a new set of all chess members plus Bilal?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: chess.union(["Bilal"])

    The operators need sets on both sides, while union(), intersection(), difference() and the other methods take any iterable. chess.union(["Bilal"]) and chess | {"Bilal"} both make a new set and leave chess as it was.

A set finds a value without looking through every item

Take five booked seat numbers, [12, 30, 7, 25, 41], and ask whether seat 19 is among them. A list has no shortcut: 19 in seats compares 19 with each item in turn, and only after the last one can it say no. Step through it:

Interactive · step through it 19 in seats: a list checks every item Check each value from the left until the target turns up or the array ends.
Array of 5: 12 30 7 25 41.12030172253414
  • being compared or looked at
  • visited or ruled out

Pseudo-code

  1. for i from 0 to n − 1:
  2. if a[i] = target: return i
  3. return "not found"

Look for 19 by checking each value from the left until it turns up or the array ends.

When it finishes: 19 not found.

Values checked 0

Five comparisons for five seats, and for a list of 100,000 it would be 100,000. A set stores its items in a hash table: A table of slots in which each key is kept at a place worked out from its hash, so a lookup looks in one slot or a few instead of comparing every key. Python's dict and set are hash tables. More in “Dictionaries: keys, values and views”, as a dictionary stores its keys. To answer 19 in booked, for the set booked = set(seats), Python computes the hash: The whole number hash() works out from a value. Values that are equal have equal hashes, and a dict or a set uses the hash to decide where to look, then compares the keys it finds there with ==. More in “Dictionaries: keys, values and views” of 19 and looks only where 19 would be stored. The next figure is a small model of such a table, the textbook kind with 8 slots: a whole number k goes to slot k mod 8, and when that slot is taken, to the next one. Step through the five seats going in, then the two lookups:

Interactive · step through it The same five seats in a table of 8 slots Each key goes to slot k mod m; watch what happens when two keys want the same slot.
Hash table of 8 slots, 0 keys. All empty.01234567
  • being compared or looked at
  • being moved or written
  • in its final place
  • visited or ruled out

Pseudo-code

  1. insert(k): for i from 0: s ← (k mod m + i) mod m
  2. if slot s is empty or a tombstone: put k there; stop
  3. find(k): probe the same sequence until k or an empty slot
  4. delete(k): find k, then leave a tombstone in its slot

A table of 8 slots with linear probing: on a collision, try the next slot, then the one after. The home slot of a key k is k mod 8.

When it finishes: 5 keys stored.

Slots probed 0
Collisions 0
Load factor 0.00

41 wanted slot 1, which 25 had taken, so it went into slot 2, and finding it took two looks. Finding that 19 is missing took one: slot 3 was empty, so 19 cannot be anywhere else. Python’s dictionaries and sets are resizable hash tables, and the time a test takes depends on how many values share the places it looks in, not on how many values the set holds. This program measures it on 100,000 numbers:

Looking for a missing value among 100,000 numbers Python · membership_timing.py
from timeit import timeit

n = 100_000
as_list = list(range(n))
as_set = set(as_list)
missing = -1  # not there: the list must look at every item to be sure

for name, items, runs in (("list", as_list, 200), ("set", as_set, 200_000)):
    seconds = timeit("missing in items", globals={"missing": missing, "items": items}, number=runs)
    print(f"{name}: {seconds / runs * 1e6:.3f} microseconds per lookup of a missing value")

Output

list: 613.981 microseconds per lookup of a missing value
set: 0.013 microseconds per lookup of a missing value

This output changes from run to run: the times depend on the computer and on what else it is doing

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 membership_timing.py

The value is missing on purpose, the worst case for the list, which had to look at all 100,000 numbers on every lookup. The set answered in a small fraction of a microsecond, tens of thousands of times faster. The comparison favours the set more the longer the list gets, but two things limit it. Building the set is itself a pass over the list, so for a single lookup the list scan is just as good; the set pays off when you look up many values. And for a handful of items both are so fast that the difference does not matter.

Key idea

A list answers in by comparing item after item, while a set goes by the hash to the one place the value would be, so build a set when you will test many values against the same collection.

Checkpoint

Check: Fast membership tests

  1. Question 1 of 2 A list holds 100,000 numbers. How many of its items does x in the_list compare with x when x is not in the list?

    Type a number.

    Show the answer: Question 1 of 2

    Answer: 100000 items

    A list has no shortcut: in compares x with each item from the first, and only after the last one can it say that x is missing, so a miss costs one comparison per item, 100,000 here.

  2. Question 2 of 2 When is a plain scan of a list just as good as building a set for the membership test?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: When you test one value, once

    Building a set costs one pass over the list. The set wins back that cost as soon as you test many values; for one test, or for a handful of items, the scan is just as good.

A set has no order you can rely on

The order in which a set prints comes from the hashes of its items, and the hashes of strings change from one run of Python to the next. Python picks a random salt for string hashes when it starts, a protection against input crafted to make every hash collide; this is called hash randomisation: The random value Python mixes into the hashes of str and bytes objects, chosen afresh when each process starts, so a set of strings can come out in another order on each run. PYTHONHASHSEED fixes it.. This program starts a new Python three times, each time with a fixed salt chosen through the PYTHONHASHSEED environment variable, and prints the same set:

One set of names, printed by three runs of Python Python · hash_order.py
import os
import subprocess
import sys

program = 'print({"mango", "apple", "kiwi", "fig", "plum"}); print({16, 1, 8})'

for seed in ("1", "2", "3"):
    env = dict(os.environ, PYTHONHASHSEED=seed)  # fix the hash seed of a new Python
    run = subprocess.run([sys.executable, "-c", program], env=env, capture_output=True, text=True)
    first, second = run.stdout.splitlines()
    print(f"seed {seed}: {first}  {second}")

print("sorted:", sorted({"mango", "apple", "kiwi", "fig", "plum"}), sorted({16, 1, 8}))

Output

seed 1: {'mango', 'kiwi', 'plum', 'fig', 'apple'}  {16, 1, 8}
seed 2: {'fig', 'plum', 'mango', 'apple', 'kiwi'}  {16, 1, 8}
seed 3: {'mango', 'fig', 'kiwi', 'apple', 'plum'}  {16, 1, 8}
sorted: ['apple', 'fig', 'kiwi', 'mango', 'plum'] [1, 8, 16]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 hash_order.py

The five fruits come out in a different order each time, while the set of numbers prints the same way in every run, because the hash of a small integer is the integer itself. That order is still not sorted: {16, 1, 8} prints as it was written, not as {1, 8, 16}. Two rules follow. Sort a set before you show it, and compare sets with ==, which ignores order, never by comparing their printed text. Setting PYTHONHASHSEED to a fixed value is a debugging aid, not a fix.

Checkpoint

Check: Why a set's order changes

  1. Question 1 of 2 Two runs of a program print {'kiwi', 'fig'} and {'fig', 'kiwi'}. What does that tell you?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Both runs made the same set; only the order it was printed in differs

    String hashes are salted per run, and a set's printed order follows the hashes, so the same set can print in another order on each run. Sort a set to show it; compare sets with ==.

  2. Question 2 of 2 Why does the set {16, 1, 8} print in the same order on every run, while the set of fruits does not?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The hash of a small integer is the integer itself, and only str and bytes hashes get the random salt

    A set's order follows the hashes of its items. Integer hashes are not salted, so they are the same on every run; string hashes are, so a set of strings can come out differently each time. Neither order is sorted.

A dictionary removes duplicates and keeps the first-seen order

set(items) removes duplicates but forgets the order of the items. When the first-seen order matters, use a dictionary, which keeps insertion order, and take its keys. Step through dict.fromkeys() on a list of five emails with two repeats:

Interactive · step by step dict.fromkeys(emails), one email at a time Step through the five emails. Each one either becomes a new key, after the keys already made, or finds that it is a key already.
dict.fromkeys(emails), one email at a time, step 7 of 7. Showing b@x.org, a@x.org, b@x.org, c@x.org, a@x.org, key b@x.org, key a@x.org, key c@x.org.new keynew keyalready a keynew keyalready a keyb@x.orgposition 0a@x.orgposition 1b@x.orgposition 2c@x.orgposition 3a@x.orgposition 4key b@x.orgfirst keykey a@x.orgsecond keykey c@x.orgthird key

The drawing shows the whole scene; its 7 steps are listed below.

Every step as text
  1. Five emails, two repeats. dict.fromkeys(emails) reads the list from position 0 and makes each email a key of a new, empty dictionary.
  2. A new key. b@x.org is not a key yet, so it becomes the first key of the dictionary.
  3. Another new key. a@x.org is new as well, and goes in after it, as the second key.
  4. A repeat changes nothing. b@x.org is already a key. A dictionary holds each key once, so nothing is added, and the key keeps the place it got at position 0.
  5. The third key. c@x.org has not been seen before, so it becomes the third key.
  6. Another repeat. a@x.org is a key already, so again nothing changes.
  7. Back to a list. list() of the dictionary gives its keys in the order they were added: ['b@x.org', 'a@x.org', 'c@x.org'], each email once, in the order it first appeared. Five emails and three keys make the 2 repeats the program counts.

A repeat finds its email already a key and changes nothing, so every key keeps the place where its email first appeared. The program does the same in one line, next to the set version for comparison:

dict.fromkeys() keeps the order, a set does not Python · dedupe_order.py
emails = ["b@x.org", "a@x.org", "b@x.org", "c@x.org", "a@x.org"]

print(list(dict.fromkeys(emails)))  # first-seen order kept
print(sorted(set(emails)))  # a set keeps no order, so sort it to show it
print(len(emails) - len(set(emails)), "repeats")

rows = [["a", 1], ["a", 1]]
print(set(rows))  # lists cannot be set elements

Output (exit status 1)

['b@x.org', 'a@x.org', 'c@x.org']
['a@x.org', 'b@x.org', 'c@x.org']
2 repeats

Printed as an error (standard error)

Traceback (most recent call last):
  File "dedupe_order.py", line 8, in <module>
    print(set(rows))  # lists cannot be set elements
          ~~~^^^^^^
TypeError: cannot use 'list' as a set element (unhashable type: 'list')

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 dedupe_order.py

dict.fromkeys(emails) makes a dictionary whose keys are the emails, each once, in the order they first appeared, and list() turns its keys back into a list. Both ways need hashable items. A list of lists cannot become a set, and Python 3.14 says so plainly: TypeError: cannot use 'list' as a set element (unhashable type: 'list'). Turn each inner list into a tuple first.

Checkpoint

Check: Removing duplicates in order

  1. Question 1 of 2 What does this print?

    Read the code, then choose one answer.

    print(list(dict.fromkeys("banana")))
    Show the answer: Question 1 of 2

    Answer: ['b', 'a', 'n']

    dict.fromkeys("banana") makes the keys 'b', 'a' and 'n', in the order they first appear, and list() gives them back in that order.

  2. Question 2 of 2 Why does set(rows) raise TypeError for rows = [["a", 1], ["a", 1]]?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The items of rows are lists, which are not hashable, so they cannot be set elements

    Turn each inner list into a tuple first: tuples of hashable values are hashable, so they can go into a set or become keys of dict.fromkeys().

Duplicate Line Remover Remove repeated lines and keep the first-seen order, as dict.fromkeys() does.

frozenset is a set that cannot change, so it can be a key

A set can change, so it is not hashable: it cannot be a dictionary key or an item of another set. frozenset is the immutable version, with every operation that reads a set and none that changes one:

frozenset as a key and inside a set Python · frozen.py
relations = {
    frozenset({"Asha", "Ravi"}): "cousins",
    frozenset({"Mei", "Dev"}): "neighbours",
}
print(relations[frozenset({"Ravi", "Asha"})])  # the order inside does not matter

seen = set()
seen.add(frozenset({"tea", "milk"}))
print(frozenset({"milk", "tea"}) in seen)

letters = frozenset("abc")
print(sorted(letters | {"d"}), type(letters | {"d"}).__name__)

seen.add({"sugar"})  # a plain set cannot go inside a set

Output (exit status 1)

cousins
True
['a', 'b', 'c', 'd'] frozenset

Printed as an error (standard error)

Traceback (most recent call last):
  File "frozen.py", line 14, in <module>
    seen.add({"sugar"})  # a plain set cannot go inside a set
    ~~~~~~~~^^^^^^^^^^^
TypeError: cannot use 'set' as a set element (unhashable type: 'set')

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 frozen.py

A frozenset makes a good key for something that has no order of its own, such as a pair of people: {"Asha", "Ravi"} and {"Ravi", "Asha"} are equal, so both find the same entry. A set of sets must be a set of frozensets, and a plain set inside one fails. Operators between a frozenset and a set return the type of the left-hand operand, so letters | {"d"} is a new frozenset.

Checkpoint

Check: frozenset as a key

  1. Question 1 of 2 relations = {frozenset({"Asha", "Ravi"}): "cousins"}. Which lookup finds "cousins"?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: relations[frozenset({"Ravi", "Asha"})]

    A frozenset is hashable and compares by its items, so frozenset({"Ravi", "Asha"}) is the same key as frozenset({"Asha", "Ravi"}).

  2. Question 2 of 2 What does this print?

    Read the code, then choose one answer.

    letters = frozenset("ab")
    print(type(letters | {"c"}).__name__, type({"c"} | letters).__name__)
    Show the answer: Question 2 of 2

    Answer: frozenset set

    Mixed operations follow the left-hand operand: letters | {"c"} is a frozenset, and {"c"} | letters is a set. Neither changes letters, which cannot change at all.

Compare Two Lists See what is only in list A, only in list B, in both and in either: the four set operations on your own lists.

Practise: hours that suit everyone, and spotting repeats

The exercise has two small set problems from a timetable: the hours that every person in a group has open, which is an intersection of their calendars, and a test for repeated items, which compares a list with the set made from it. Write each function and run the sample tests; the first hint names the operator to start with:

Exercise · Easy · Python

Find the hours everyone is free, and spot repeated items

Write two functions with sets.

  • common_free_slots(calendars) takes one list per person of the hours that person is free, such as [[9, 10, 11, 14], [10, 11, 15], [11, 10, 9]], and returns the hours free for everyone, as a sorted list: [10, 11]. A person may list an hour twice. With no calendars at all, or when one person has no free hours, the answer is [].
  • has_duplicates(xs) returns True when some item of the list xs appears more than once and False otherwise; an empty list has none. It must work with a set, so a list holding unhashable items, such as [[1], [1]], raises TypeError (the set does that for you).

Starter code · slots.py

def common_free_slots(calendars):
    """Hours free in every calendar, as a sorted list."""
    return []


def has_duplicates(xs):
    """True when some item of xs appears more than once."""
    return False
The sample tests · test_slots.py
from slots import common_free_slots, has_duplicates


def test_common_slots():
    """finds the hours free in every calendar, sorted"""
    assert common_free_slots([[9, 10, 11, 14], [10, 11, 15], [11, 10, 9]]) == [10, 11]
    assert common_free_slots([[16, 9, 12], [12, 16]]) == [12, 16]


def test_one_person_and_repeats():
    """works for one calendar and for hours listed twice"""
    assert common_free_slots([[14, 9, 14]]) == [9, 14]
    assert common_free_slots([[10, 10], [10]]) == [10]


def test_no_common_slots():
    """returns an empty list when nobody, or not everybody, is free"""
    assert common_free_slots([]) == []
    assert common_free_slots([[9, 10], []]) == []
    assert common_free_slots([[9], [10]]) == []


def test_duplicates():
    """spots repeated items"""
    assert has_duplicates([1, 2, 1]) is True
    assert has_duplicates(["tea", "rice", "tea"]) is True
    assert has_duplicates([1, 2, 3]) is False
    assert has_duplicates(["a", "A"]) is False
    assert has_duplicates([]) is False


def test_unhashable_items():
    """raises TypeError for items a set cannot hold"""
    try:
        has_duplicates([[1], [1]])
    except TypeError:
        return
    raise AssertionError("expected TypeError for a list of lists")
A hint

Start with a set of the first person's hours and keep only the hours that are also in each next person's set: &, or &= to update in place. Sort the set at the end, because a set has no order. For has_duplicates, compare the length of the list with the length of a set made from it: repeats disappear in the set.

The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Python Online Compiler Build sets of your own and try the four operators on them, in your browser.

Six ideas, from making a set to frozensets, are in the self-check. It opens with whichever of them your answers so far have shown to be the shakiest, and each choice comes with the reason it is right or wrong:

Practice that adapts to you

Practise this lesson

17 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 17 Which expression creates an empty set?

    Choose one answer.

    Show the answer: Question 1 of 17

    Answer: set()

    {} is an empty dictionary, {None} is a set with one item, None, and [] is an empty list. Only set() makes an empty set.

  2. Question 2 of 17 What does this print?

    Read the code, then choose one answer.

    s = {3, 1, 3}
    s.add(2)
    s.discard(5)
    print(sorted(s), len(s))
    Show the answer: Question 2 of 17

    Answer: [1, 2, 3] 3

    {3, 1, 3} holds 1 and 3, add(2) makes it three values, and discard(5) leaves it alone. sorted() returns them as a sorted list.

  3. Question 3 of 17 A clean-up function deletes the tag "java" from a set of tags when it is there, and a missing tag is not an error. Which call should it use?

    Choose one answer.

    Show the answer: Question 3 of 17

    Answer: tags.discard("java")

    Choose remove() when a missing value means a bug that should stop the program, and discard() when a missing value is fine.

  4. Question 4 of 17 Which operator gives the students who are in both clubs?

    Choose one answer.

    Show the answer: Question 4 of 17

    Answer: &

    & is the intersection, | the union, - the difference and ^ the symmetric difference.

  5. Question 5 of 17 What does clubs.py print?

    What does this program print? Choose one answer.

    chess = {"Asha", "Ravi", "Mei", "Dev"}
    drama = {"Mei", "Kiran", "Asha", "Zoya"}
    
    print(sorted(chess & drama))  # in both clubs: intersection
    print(sorted(chess | drama))  # in either club: union
    print(sorted(chess - drama))  # in chess but not drama: difference
    print(sorted(chess ^ drama))  # in exactly one club: symmetric difference
    
    print({"Asha", "Mei"} <= chess, chess.isdisjoint({"Zoya", "Bilal"}))
    print(sorted(chess.union(["Bilal", "Asha"])))  # methods take any iterable
    Show the answer: Question 5 of 17

    Answer: it prints

    ['Asha', 'Mei']
    ['Asha', 'Dev', 'Kiran', 'Mei', 'Ravi', 'Zoya']
    ['Dev', 'Ravi']
    ['Dev', 'Kiran', 'Ravi', 'Zoya']
    True True
    ['Asha', 'Bilal', 'Dev', 'Mei', 'Ravi']

    - keeps the chess members who are not in drama (Dev and Ravi), and ^ keeps everyone in exactly one club. A set never holds a value twice, so the union lists Asha and Mei once. Neither Zoya nor Bilal is in the chess club, so isdisjoint is True, and union() accepts a plain list.

  6. Question 6 of 17 Which expressions are True exactly when every member of chess is also in drama?

    Choose every answer that is right.

    Show the answer: Question 6 of 17

    Answer:

    • (chess - drama) == set()
    • chess.issubset(drama)
    • chess <= drama

    chess is a subset of drama when the difference chess - drama is empty, which is what chess <= drama and chess.issubset(drama) test. chess < drama, the proper subset, is stricter.

  7. Question 7 of 17 Why does x in a_set take about the same time for a set of 100 values and for one of 100,000?

    Choose one answer.

    Show the answer: Question 7 of 17

    Answer: Python computes the hash of x and looks only where x would be stored

    A set is a hash table: the hash of x says where to look, and only the few values stored there are compared. A list compares x with its items one by one.

  8. Question 8 of 17 In the figure's table of 8 slots, holding 12, 30, 7, 25 and 41, how many slots does find(41) look at?

    Type a number.

    A hint

    41 mod 8 is 1. Which number got to slot 1 first?

    Show the answer: Question 8 of 17

    Answer: 2 slots

    41 mod 8 is 1, but 25 took slot 1 first, so 41 was stored in the next empty slot, 2. Finding it looks at slot 1, sees 25, and then at slot 2, where 41 is.

  9. Question 9 of 17 A program checks 50,000 user IDs, one by one, with uid in banned, where banned is a list of 100,000 IDs. Which change helps most?

    Choose one answer.

    Show the answer: Question 9 of 17

    Answer: Build banned = set(banned) once, before the loop

    Many tests against one fixed collection is exactly when a set pays off: build it once and test against the set. For a missing ID the list costs 100,000 comparisons; the set looks in one place.

  10. Question 10 of 17 A program prints {"red", "green", "blue"}, and the order of the colours changes between runs. Why?

    Choose one answer.

    Show the answer: Question 10 of 17

    Answer: Python salts string hashes with a random value in each run, and a set's order follows the hashes

    String hashes are salted per process as a protection against crafted collisions, so the slots, and with them the printed order, change from run to run. Sort a set before showing it.

  11. Question 11 of 17 What does this print on every run?

    Read the code, then choose one answer.

    fruits = {"pear", "fig", "apple"}
    print(sorted(fruits), len(fruits))
    Show the answer: Question 11 of 17

    Answer: ['apple', 'fig', 'pear'] 3

    Sorting turns a set with no fixed order into a list with a fixed one, which is why the examples of this lesson sort their sets before printing them.

  12. Question 12 of 17 Which of these print their items in the same order on every run of the program?

    Choose every answer that is right.

    Show the answer: Question 12 of 17

    Answer:

    • print(sorted({"kiwi", "fig"}))
    • print({"kiwi": 1, "fig": 2})
    • print(["kiwi", "fig"])

    Dictionaries find keys by hash too, but they keep the order the keys were added in, as a language guarantee. Only the set's own printed order can change from run to run.

  13. Question 13 of 17 Which expression removes the repeats from the list items and keeps the order in which the values first appear?

    Choose one answer.

    Show the answer: Question 13 of 17

    Answer: list(dict.fromkeys(items))

    set(items) forgets the order; dict.fromkeys(items) keeps each value once, at the place where it first appeared.

  14. Question 14 of 17 What does this print?

    Read the code, then choose one answer.

    cities = ["Pune", "Goa", "Pune", "Agra", "Goa"]
    print(list(dict.fromkeys(cities)), len(set(cities)))
    Show the answer: Question 14 of 17

    Answer: ['Pune', 'Goa', 'Agra'] 3

    dict.fromkeys() keeps each city once, in the order it first appeared, and the set of the five names has 3 distinct items.

  15. Question 15 of 17 Which of these can be an item of a set?

    Choose every answer that is right.

    Show the answer: Question 15 of 17

    Answer:

    • "12"
    • (1, 2)
    • frozenset({1, 2})

    Set items must be hashable. A frozenset, a tuple of numbers and a string are. A plain set and a list can change, so they cannot be items of a set.

  16. Question 16 of 17 What does this print?

    Read the code, then choose one answer.

    seen = set()
    seen.add(frozenset({"tea", "milk"}))
    print(frozenset({"milk", "tea"}) in seen, len(seen))
    Show the answer: Question 16 of 17

    Answer: True 1

    A frozenset is hashable and compares by its items, so it works inside a set, and an equal frozenset finds it.

  17. Question 17 of 17 A dictionary needs one key per pair of people, so that ("Asha", "Ravi") and ("Ravi", "Asha") find the same entry. Which key type does that?

    Choose one answer.

    Show the answer: Question 17 of 17

    Answer: frozenset, such as frozenset({"Asha", "Ravi"})

    A frozenset is both hashable and unordered, which is what a key for an unordered pair needs: a tuple keeps its order, and a plain set cannot be a key.

Interview questions

Fresher · Why is x in a_set usually faster than x in a_list, and when is it not?

A set is a hash table. For x in a_set, Python computes hash(x), goes straight to the slot where x would be and compares only the few items stored there, so the average cost hardly grows with the size of the set. x in a_list compares x with each item in turn and, when x is missing, with every item, so its cost grows with the length of the list. The set loses its edge in three cases: when the collection is tiny and both are fast anyway; when you search only once, because building the set takes a full pass over the list first; and when many items have equal hashes, which makes lookups slow down towards a scan. It also needs hashable items, so a list of lists cannot become a set at all.

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid to senior

    A file of 5 million lines holds one user ID per line, many of them repeated. Write each ID out once, in the order first seen, and report how many lines were repeats. Which structure do you use, how much memory does it need, and what changes if each line is a JSON record instead?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Fresher to mid

    A unit test checks that str(active_users()) equals "{'asha', 'ravi', 'mei'}", where active_users() returns a set. It passes on a developer's laptop and fails in CI now and then. Explain the cause, why a similar test on a set of small integers never failed, and fix the test.

    Model answer, Pro. First step and follow-ups

Key takeaways

  • A set holds distinct, hashable values with no order, set() is the empty set because {} is a dictionary, and remove() raises KeyError for a missing value where add() and discard() never complain.
  • |, &, - and ^ give the union, intersection, difference and symmetric difference of two sets, <= tests for a subset, and the named methods also accept lists.
  • x in s looks only where the hash of x leads, so it takes about the same time for any size of set, while x in a_list checks the items one by one.
  • String hashes are salted per run, so a set of strings can print in another order every time: sort a set before showing it, and compare sets with ==.
  • list(dict.fromkeys(items)) removes duplicates and keeps the first-seen order, which set(items) forgets.
  • frozenset is hashable and immutable, so it can be a dictionary key or an item of another set.

Terms in this lesson

hash randomisation
The random value Python mixes into the hashes of str and bytes objects, chosen afresh when each process starts, so a set of strings can come out in another order on each run. PYTHONHASHSEED fixes it.
subset
A set whose every item is also in another set. a <= b tests it; a < b also needs b to hold something more, which makes a a proper subset.
symmetric difference
The items that are in exactly one of two sets, a ^ b: everything in either set except what the two share. {1, 2} ^ {2, 3} is {1, 3}.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.