◀ Course contents Part 2 · Module 2-06

Tuples & Sets

Two containers that exist because lists cannot do everything

A list is changeable and ordered, which is usually right and occasionally exactly wrong. A tuple is a sequence that cannot be changed, which is what makes it safe to use as a dictionary key and honest about a record whose shape is fixed. A set throws away order and duplicates, which sounds like a loss until the question is "have I seen this before?".

Ready?

1

A Tuple Is a Sequence That Cannot Change

Round brackets, or often no brackets at all — it is the commas that make a tuple.

point = (3, 4)
row = "T-1003", "high", 90     # brackets optional
one = ("only",)                # the trailing comma is what makes it a tuple

row[0]      # "T-1003"
len(row)    # 3
row[1] = "low"    # TypeError — tuples do not support item assignment

Everything you can do to read a list works: indexing, slicing, len(), in, looping. Everything that would change it does not exist.

That sounds like a list with features removed, and the point is what the restriction buys:

It can be a dictionary key

Keys have to be unchangeable, so ("eu", "2026-09") works and ["eu", "2026-09"] raises.

Nobody can edit it behind you

The aliasing bug from the last module cannot happen, because there is no operation that would cause it.

It says the shape is fixed

A list means "any number of these". A tuple means "exactly these, in this order" — a row, a coordinate, a pair.

Quick check

What is ("only") — with brackets but no comma?

2

Unpacking Is Everywhere Once You See It

Putting several names on the left of an assignment takes a sequence apart in one line:

ticket_id, priority, minutes = "T-1003,high,90".split(",")

a, b = b, a                       # the swap from module 1-02 was a tuple
for key, value in counts.items(): # so was this

Both of those were unpacking all along. The swap builds a tuple on the right and takes it apart on the left; .items() hands over a two-item tuple per pass and the for line unpacks it.

The count has to match exactly. Three names and four values raises ValueError: too many values to unpack — which is a genuinely useful failure, because it means a line of your input file had a comma in it that you did not expect.

When you want "the first one and everything else", a star collects the rest into a list:

command, *arguments = "deploy staging --dry-run".split()
# command   -> "deploy"
# arguments -> ["staging", "--dry-run"]

Use it to name things at the boundary

parts[0], parts[1] and parts[2] scattered through a function are three chances to use the wrong index. Unpacking once, into names, means the rest of the code reads priority rather than parts[1] — and the ValueError tells you immediately if the shape was not what you thought.

Quick check

a, b = "x,y,z".split(","). What happens?

3

A Set Gives Up Order to Gain Speed and Uniqueness

Curly braces like a dictionary, but with values rather than pairs. A set holds each thing at most once and does not keep the order you added them in.

seen = {"loops", "lists", "loops"}   # {"loops", "lists"} — the duplicate is gone
tags = set(["a", "b", "a"])         # the usual way: from a list
empty = set()                       # {} would be an empty dictionary

Two things follow, and they are the only reasons to reach for one:

Deduplicating. set(items) removes duplicates in one word. len(set(items)) is "how many distinct".

Membership. x in some_set is a direct lookup, the same as a dictionary key. x in some_list walks the list. On ten items nobody notices; on a hundred thousand, checking each of a hundred thousand against a list is the difference between a second and an afternoon.

And the cost, which is real: a set has no order and no positions. tags[0] raises. If you need a stable order for output, ask for one with sorted(tags), which hands back a list.

Loses the order

tags = set(raw.split(","))

Fine for "have I seen this", wrong if the file order was information.

Keeps it

for t in raw.split(","):
    if t not in seen: ...

A set alongside the list, used only for the check.

Quick check

You need the distinct tags, in the order they first appeared. What does sorted(set(tags)) give you?

4

Four Operations That Replace Four Loops

Comparing two collections is a loop with a condition in it, done four slightly different ways. Sets make each of them one line.

a = {"loops", "lists", "errors"}
b = {"loops", "dicts"}

a | b   # union         — everything in either
a & b   # intersection  — in both
a - b   # difference    — in a but not b
a ^ b   # symmetric     — in one but not both

The named forms — a.union(b), a.intersection(b), a.difference(b) — do the same and read better in a line already busy with other punctuation.

The reason to know these is that "what changed between yesterday and today" is a question you will be asked constantly, and it is exactly two differences:

added   = current - previous
removed = previous - current
kept    = current & previous

Note the direction. a - b is not b - a, and getting them the wrong way round produces a report that is confidently backwards.

Which container, then?

List when order matters or duplicates are meaningful. Tuple when the shape is fixed, or you need a key. Set when you care about membership and uniqueness and nothing else. Dictionary when each thing has something attached to it.

Picking the container is most of the design. The right one turns a loop with a condition into a single operator.

Quick check

Which tags were removed between last week and this week?

0 of 9 completed

Real Python runs right here in your browser — nothing to install, nothing sent to a server. The interpreter downloads once the first time you press Run, then stays cached.

01

To do

Round brackets, or often none at all — it is the commas that make a tuple. Everything that reads a list works on one.

row = ("T-1003", "high", 90)
row[0] # "T-1003"
row[-1] # 90
len(row) # 3

A list says "any number of these". A tuple says "exactly these, in this order" — which is what a row of a file actually is.

Your task: build row with those three values, then print the id, the minutes, the length, and whether "high" is in it:

T-1003
90
3
True
your_code.py
Python
Hint

The minutes are a number, so no quotes. row[-1] reaches the last item, and `"high" in row` answers the last line by itself.

Output

      
    02

    To do

    row[1] = "low" raises TypeError: 'tuple' object does not support item assignment. There is no method that would do it either — the whole point of the type is that there is not.

    What you do instead is build a new one. Slicing and + both work on tuples exactly as they do on strings, and for the same reason:

    row = row[:1] + ("low",) + row[2:]

    Note the comma in ("low",). Without it those brackets are just grouping, and you would be trying to add a plain string to a tuple.

    Your task: run it, read the error, then rebuild row with the priority changed to low:

    ('T-1003', 'low', 90)
    your_code.py
    Python
    Hint

    Take everything before position 1, add a one-item tuple holding "low", then everything from position 2 on. The trailing comma in ("low",) is what makes it a tuple.

    Output
    
          
      03

      To do

      Several names on the left of an assignment take a sequence apart in one line:

      ticket_id, priority, minutes = line.split(",")

      This is worth doing at the moment data arrives. parts[1] scattered through the code below is a chance to use the wrong index every time it appears; priority is not.

      The count must match exactly. Too many or too few values raises a ValueError, which is a useful failure — it means the line was not the shape you assumed.

      Your task: unpack the line into three names, convert the minutes, and print:

      T-1003
      high
      91

      The last line is the minutes plus one, to prove it is a number.

      your_code.py
      Python
      Hint

      line.split(",") gives three pieces, which matches the three names. The minutes arrive as text, so convert with int() before the arithmetic.

      Output
      
            
        04

        To do

        When you want the front of a sequence by name and everything else in one go, a star collects the remainder into a list:

        command, *arguments = "deploy staging --dry-run".split()
        # command -> "deploy"
        # arguments -> ["staging", "--dry-run"]

        The starred name always ends up a list, even when it catches one item or none. It can go at either end — *body, last = lines is equally valid — but only one star is allowed, because two would be ambiguous.

        Your task: split the command line, take the command by name and the rest into arguments, then print the command, the arguments, and how many there are:

        deploy
        ['staging', '--dry-run']
        2
        your_code.py
        Python
        Hint

        Put the star on the second name: command, *arguments = command_line.split(). Splitting with no argument cuts on whitespace.

        Output
        
              
          05

          To do

          A set holds each value at most once. Building one from a list is how you remove duplicates:

          distinct = set(items)
          len(set(items)) # how many distinct

          A set has no order and no positions, so distinct[0] raises. When output has to be stable, sorted() hands you back a list in a predictable order.

          Your task: from the raw tags, print how many there are in total, how many are distinct, how many duplicates that means, and the distinct ones sorted:

          7
          5
          2
          ['dicts', 'errors', 'lists', 'loops', 'sets']
          your_code.py
          Python
          Hint

          set(tags) removes the duplicates. len() on each gives the two counts, and their difference is the number dropped. sorted(distinct) gives a list you can print predictably.

          Output
          
                
            06

            To do

            x in some_list walks the list until it finds a match. x in some_set goes straight there, the same way a dictionary finds a key.

            On ten items the difference is invisible. Checking each of a hundred thousand things against a list of a hundred thousand things is the difference between a second and an afternoon — and it is the same code either way, so the container is the whole decision.

            Your task: build a set of the known tags, then report on three candidates. Print the set's size, then True or False for each candidate, then the ones that were not known, sorted:

            4
            True
            False
            False
            ['files', 'threads']
            your_code.py
            Python
            Hint

            Print len(known) first. Then loop over candidates, print `candidate in known`, and append to unknown when it is not there. Finish with print(sorted(unknown)).

            Output
            
                  
              07

              To do

              Comparing two collections is a loop with a condition, four slightly different ways. Sets make each of them one line:

              a | b   # union — in either
              a & b # intersection — in both
              a - b # difference — in a but not b
              a ^ b # in one but not both

              Watch the direction. a - b is not b - a, and a report that has them the wrong way round is confidently backwards.

              Your task: work out what was added, what was removed, and what stayed, then print each sorted, and finally the size of the union:

              ['dicts', 'sets']
              ['functions', 'strings']
              ['errors', 'lists', 'loops']
              7
              your_code.py
              Python
              Hint

              Added is in current and not in previous, so current - previous. Removed is the other way round. Kept is the intersection, current & previous. The union is current | previous.

              Output
              
                    
                08

                To do

                Dictionary keys have to be unchangeable, which rules out lists and lets in tuples. That is what makes a compound key possible:

                sales[("eu", "09")] = 1750

                Region and month together identify one figure, and neither alone does. The alternative — a dictionary of dictionaries, or a string key like "eu-09" that has to be split apart again — is more code and more chances to disagree with itself.

                Your task: total the sales by region and month. Then print the totals for two specific cells and the number of cells:

                1750
                900
                3
                your_code.py
                Python
                Hint

                The key is the tuple (region, month). Use the counting pattern with it: sales[key] = sales.get(key, 0) + int(amount).

                Output
                
                      
                  09

                  To do

                  This code deduplicates the queue and reports the next three jobs. It deduplicates correctly and reports the wrong three, because set() threw the order away and nothing downstream can get it back.

                  A set is the right tool for "have I seen this". It is the wrong tool for "what came first". When you need both — distinct, in original order — keep the list and use a set only for the check:

                  seen = set()
                  for job in queue:
                      if job not in seen:
                          seen.add(job)
                          unique.append(job)

                  Your task: rewrite it so the duplicates are gone and the queue order survives:

                  ['build', 'test', 'deploy', 'notify']
                  4
                  your_code.py
                  Python
                  Hint

                  Start unique as an empty list and seen as an empty set. Loop the queue in order; when a job is not in seen, add it to both.

                  Output
                  
                        

                    The tag audit

                    To do

                    Two exports of the content tags, a week apart. Somebody has to say what changed — and the second export has a duplicate in it, because the tool that produced it does not deduplicate.

                    Build these:

                    • previous and current — a set of distinct tags from each export
                    • duplicates_dropped — how many entries the current export lost to deduplication
                    • added — in the current set and not the previous one
                    • removed — in the previous set and not the current one
                    • kept — in both
                    • all_tags — every distinct tag across both

                    Then print exactly seven lines, with each group of tags sorted:

                    Previous: 5
                    Current: 5
                    Duplicates dropped: 1
                    Added: ['dicts', 'sets']
                    Removed: ['functions', 'strings']
                    Kept: ['errors', 'lists', 'loops']
                    Total distinct: 7
                    your_code.py
                    Python
                    Hint

                    set(last_week.split(",")) for each side. The duplicates dropped is the length of this_week.split(",") less the length of current. Added is current - previous, removed is previous - current, kept is current & previous, and all_tags is current | previous. Sort each group before printing it.

                    Output
                    
                          

                      Notification