Index Ai Academy
Phase 0 · Tools · Python core

Sets: unique items and fast lookups

Duplicates, fast membership, set operations and comparing two lists.

About 30 minutes · Free lesson

Start this lesson

Why it matters

Two questions come up in almost every program that handles data: "have I seen this before?" and "what's different between these two lists?" Which emails signed up twice, which products vanished from today's stock list, which hikers are on both trips. You can answer them with loops over lists, but Python has a type built for exactly this, the set, and it turns pages of loops into one line that also runs much faster. Sets are a favorite in coding interviews for the same reason.

What you'll be able to do

  • Make a set, add and remove items, and explain why it never holds duplicates
  • Check membership quickly, and say why a set is faster than a list for it
  • Compare collections with union, intersection, difference and subset tests
  • Reconcile two lists: what's new, what's gone and what stayed

What it covers

  1. A collection with no repeats
  2. Adding, removing and asking
  3. Comparing collections
  4. What two lists share
  5. Reconciling two lists
  6. The first repeat
  7. Worked example: two numbers that add up to a target
  8. Where sets turn up next

A collection with no repeats

A set holds items with no duplicates and no order. You write one with curly braces, or build one from any collection with set(). Adding something that's already there changes nothing:

gear = {"tent", "stove", "lantern"}
print(gear, len(gear))

gear.add("stove")   # already there: nothing happens
gear.add("rope")
print(gear, len(gear))

signups = ["ana@ex.org", "bo@ex.org", "ana@ex.org", "cy@ex.org", "bo@ex.org"]
print(len(signups), "sign-ups from", len(set(signups)), "people")
It runs right here in your browser, with nothing to install.

Run it a few times, and look at the order the gear prints in. A set doesn't keep the order you added things in, so you can't ask for "item 0": gear[0] is an error. If you need order, sort it on the way out, with sorted(gear), which gives you a list.

One trap catches everyone once: {} is an empty dictionary, not an empty set, because dictionaries had the braces first. An empty set is set().

Quick check
What does this print?
letters = set("banana")
print(len(letters), sorted(letters))
  1. 1 ['banana']
  2. 3 ['b', 'a', 'n']
  3. 6 ['a', 'a', 'a', 'b', 'n', 'n']
  4. 3 ['a', 'b', 'n']
Show the answer

3 ['a', 'b', 'n']

set("banana") takes each letter once: b, a and n. sorted returns them as a list in alphabetical order.

Adding, removing and asking

  • s.add(x) adds one item, and s.update(other) adds everything from another collection.
  • s.discard(x) removes an item if it's there and does nothing if it isn't. s.remove(x) raises a KeyError when it's missing, which is what you want when a missing item means a bug.
  • x in s asks whether an item is there.

That last one is where sets earn their keep. To answer x in some_list, Python checks the items one by one until it finds a match, so a list of a million items can take a million checks. A set works out where the item would be stored from the item itself (a number called its hash) and looks only there. That takes about the same time for ten items as for ten million:

import time

ids = list(range(1_000_000))
id_set = set(ids)

start = time.perf_counter()
for _ in range(20):
    999_999 in ids
list_time = time.perf_counter() - start

start = time.perf_counter()
for _ in range(20):
    999_999 in id_set
set_time = time.perf_counter() - start

print(f"list: {list_time:.3f}s   set: {set_time:.6f}s")

The price of that speed is a rule about what a set can hold: only hashable items, ones that can't change. Numbers, strings and tuples are fine. A list isn't, because changing it would change its hash and lose it inside the set. {[1, 2]} raises TypeError: unhashable type: 'list'; use a tuple, {(1, 2)}, instead.

Try it now

The right collection usually follows from one question: do you need order, names, or uniqueness?

Comparing collections

The trail club runs two trips this weekend. Sets answer every "who's on what" question with one operator:

saturday = {"Ana", "Bo", "Cy", "Dee"}
sunday = {"Cy", "Dee", "Eli"}

print("both days:", sorted(saturday & sunday))       # intersection
print("either day:", sorted(saturday | sunday))      # union
print("Saturday only:", sorted(saturday - sunday))   # difference
print("just one day:", sorted(saturday ^ sunday))    # symmetric difference

print({"Cy", "Dee"} <= saturday)        # subset: is every one of them on Saturday?
print(saturday.isdisjoint({"Fay"}))    # no one in common

Read the operators as questions: & is "in both", | is "in either", - is "in the first but not the second" (so a - b and b - a differ), and ^ is "in exactly one". Each also has a method name that reads like English and takes any collection, not only a set: saturday.intersection(names_list).

A set comprehension builds a set the way a list comprehension builds a list, with braces instead of brackets. It's the tidy way to clean and dedupe in one go: {e.strip().lower() for e in emails}.

Drill

What two lists share

Write in_common(a, b): the items that appear in both lists, each once, as a sorted list. Use sets, not nested loops.

def in_common(a, b):
    result = []
    return result


print(in_common(["tent", "stove", "rope", "stove"], ["rope", "map", "stove"]))
In the lesson, your code is checked as you write it.

Reconciling two lists

Here's a job you'll meet at work: yesterday's export and today's, and someone wants to know what changed. Real exports are messy, so clean first, with a set comprehension, then let the set operators do the rest:

yesterday = ["TE-101", "ST-114", "la-102 ", "BO-120"]
today = ["TE-101", "LA-102", "PA-111", "ST-114", "TE-101"]

def clean(skus):
    return {s.strip().upper() for s in skus}

before, after = clean(yesterday), clean(today)
print("added:", sorted(after - before))
print("removed:", sorted(before - after))
print("kept:", sorted(before & after))

Without the cleaning step, "la-102 " and "LA-102" would look like two different products, one removed and one added. That's the usual bug in this kind of report, and it's worth a test.

Two more things you'll reach for. When you need to dedupe and keep the original order, a set can't do it alone, but a dictionary keeps insertion order: list(dict.fromkeys(items)) keeps the first of each. And when you need a set inside a set, or a set as a dictionary key, use a frozenset, a set that can't change after it's made, which makes it hashable.

Drill

The first repeat

Write first_repeat(items): the first item that appears for a second time, reading from the left, or None if nothing repeats. Keep a set of what you've seen so far.

def first_repeat(items):
    return None


print(first_repeat(["b", "a", "c", "a", "b"]))
In the lesson, your code is checked as you write it.
Worked example

Worked example: two numbers that add up to a target

A classic interview question: given a list of prices and a budget, is there a pair that adds up to exactly the budget? Checking every pair is slow for long lists. A set of the prices seen so far answers it in one pass.

def pair_for(prices, budget):
    seen = set()
    for p in prices:
        if budget - p in seen:
            return (budget - p, p)
        seen.add(p)
    return None

prices = [45, 170, 12, 39, 88, 61]
print(pair_for(prices, 100))   # 39 + 61
print(pair_for(prices, 1000))  # no pair
In the lesson you can change this and run it.

For each price p, the partner it needs is budget - p. If that partner has already gone by, it's in seen, and the set answers instantly. If not, p goes into seen so a later price can find it. Checking every pair takes about n × n steps, so 1,000 prices means half a million comparisons; this takes 1,000 set lookups. The pattern "keep a set of what you've seen" turns up again and again: duplicates, cycles in a route, pages a crawler has already visited.

Quick check
What does this print?
a = {1, 2, 3, 4}
b = {3, 4, 5}
print(sorted(a - b), sorted(b - a), len(a ^ b))
  1. [5] [1, 2] 2
  2. [1, 2] [1, 2] 3
  3. [1, 2, 5] [] 3
  4. [1, 2] [5] 3
Show the answer

[1, 2] [5] 3

a - b is what's in a but not b: 1 and 2. b - a is just 5. a ^ b is everything in exactly one of them, 1, 2 and 5, so its length is 3.

Where sets turn up next

Once you know sets, you'll spot them all over the rest of the course and in real code:

  • Not doing work twice. A web crawler keeps a set of the pages it has visited, and a route planner keeps a set of the places it has explored, so neither goes round in circles. You'll build both later.
  • Permissions. A user's roles are a set, and so are the roles a page needs. "Can this person see it?" is required <= roles: is every required role among theirs?
  • Cleaning data. "Which of these customer IDs aren't in our system?" is a difference, and "which records appear in both exports?" is an intersection, long before you reach for pandas.
  • Language models. A tokenizer's vocabulary is, in effect, a set of the pieces of text it knows, and checking whether a word is in it is exactly the fast lookup from earlier.

A good rule of thumb: if you catch yourself writing if x not in some_list inside a loop, ask whether some_list should be a set.

Practice what you just read

  • 5 code examples you can edit and run
  • a practice exercise that checks your code
  • a 5-question quiz
  • 5 review cards for spaced repetition

Start this lesson

Try a quiz question

Why is x in a_set fast even for a huge set?

  1. The set works out where x would be stored from its hash and looks only there
  2. Sets keep their items sorted, so Python can use binary search
  3. Python checks the items in parallel
  4. Sets can only hold small numbers

Answer it at the end of the lesson to see if you're right, and why.

Before this lesson