Skip to content
VLSI Mentor

Python Part I · Module 7 · Sets

Changing a Set: add, update, remove, discard

There is a misconception about sets that is worth clearing up before anything else, because it comes directly from something true that you learned last chapter. Set elements must be hashable, which rules out lists — and a great many people take that to mean the set itself cannot be changed. It can. A set is a mutable collection: you can add to it and remove from it, and it is the same set afterwards. Those are two separate claims about two different things, and keeping them apart is most of what this chapter is for. The rest is four methods, two of which differ only in what they do when the item you named is not there.

Foundation14 min readPythonPart IFundamentalsSetsMutation

Part I · Module 7 · Chapter 7.3 · Changing a Set: add, update, remove, discard

1. The Set Is Mutable. Its Elements Must Be Hashable.

Two claims, about two different things, constantly collapsed into one:

ClaimAboutTrue?
A set can be changed after it is createdthe collectionyes
A set's elements must be hashablethe items inside ityes
A set cannot be changedthe collectionno

Chapter 7.1 established the second claim, with {["a"]} raising TypeError: unhashable type: 'list'. The mistake is to carry that restriction up to the container and conclude the set is frozen too. It is not.

Here is the proof:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
failed = {"burst"}
print(len(failed))
failed.add("reset")
print(len(failed))
print(sorted(failed))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
1
2
['burst', 'reset']

One element before, two after, and failed was never reassigned — there is no failed = on that middle line. The set itself changed.

Note also how the "after" state was shown: len() and sorted(). Chapter 7.2 measured why printing a set directly is not something to rely on, and that rule applies to every before-and-after check in this chapter.

2. add(): One Element

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
s = {"a"}
print(s.add("b"))
print(sorted(s))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
None
['a', 'b']

Two things in that output. The set gained its element — and add evaluated to None.

That is the rule Chapter 5.3 established for every mutating method: the change already happened to the collection you have, so there is nothing useful to hand back. Which means the same trap applies:

Adding something already present is not an error and changes nothing:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
t = {"burst", "reset"}
print(len(t))
t.add("burst")
print(len(t))
print(t == {"burst", "reset"})
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
2
2
True

Two before, two after, and the set is equal to what it was. This is Chapter 7.1's uniqueness showing up as behaviour: there is no way for a set to hold "burst" twice, so adding it again has nothing to do. add is safe to call without knowing whether the item is already there — a genuinely useful property when you are collecting names as you find them.

3. update(): Elements From an Iterable

add takes one value. update takes a collection and adds each of its elements:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
sigs = {"clk"}
sigs.update(["valid", "ready"])
print(len(sigs))
print(sorted(sigs))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
3
['clk', 'ready', 'valid']

Three elements, all strings, nothing nested. It works from any iterable — another set, or a string supplying its characters:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
v = {"a"}
v.update({"b", "c"})
print(len(v), sorted(v))

w = {"x"}
w.update("yz")
print(len(w), sorted(w))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
3 ['a', 'b', 'c']
3 ['x', 'y', 'z']

That second one is the same iterable rule Chapters 5.6 and 6.6 met: a string hands over its characters. Rarely what you want, and worth recognising when it happens.

4. add and update: The Shape Difference

Chapter 5.6 drew this line for append and extend. On a set it has a sharper edge.

add of a tuple adds one element, which happens to be a tuple:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
u = {"clk"}
u.add(("valid", "ready"))
print(len(u))
print(("valid", "ready") in u)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
2
True

Two elements: the string "clk" and one tuple. Exactly like append nesting a list inside a list.

But add of a list does not merely produce an odd shape — it raises:

Azvya Education Pvt. Ltd.VLSI Mentor
BROKEN ON PURPOSE
s = {"a"}
s.add(["b", "c"])
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
TypeError: unhashable type: 'list'

Chapter 7.1's rule, enforced at the moment you try. A list cannot be a set element, so there is no wrong-shaped result to end up with. update is what you wanted:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
s = {"a"}
s.update(["b", "c"])
print(len(s), sorted(s))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
3 ['a', 'b', 'c']

And update needs something with elements to hand over:

Azvya Education Pvt. Ltd.VLSI Mentor
BROKEN ON PURPOSE
s = {"a"}
s.update(5)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
TypeError: 'int' object is not iterable

Iterable again — Chapter 5.9's word. A single number has no contents to distribute.

5. remove() and discard(): Identical, Until the Item Is Absent

When the element is present, these two do exactly the same thing:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
r = {"burst", "reset"}
r.remove("burst")
print(sorted(r), len(r))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
['reset'] 1
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
d = {"burst", "reset"}
d.discard("burst")
print(sorted(d), len(d))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
['reset'] 1

Same result. The entire difference appears when the element is not there.

remove raises:

Azvya Education Pvt. Ltd.VLSI Mentor
BROKEN ON PURPOSE
s = {"reset"}
s.remove("missing")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
KeyError: 'missing'

discard does nothing at all:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
d2 = {"reset"}
d2.discard("missing")
print(sorted(d2), len(d2))
print(d2 == {"reset"})
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
['reset'] 1
True

No error, no change, and the set is equal to what it was.

Both return None, like every mutating method here:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
x = {"a", "b"}
print(x.remove("a"))
print(x.discard("zzz"))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
None
None

Which one to use

This is a design decision, not a preference, and it is worth stating as one:

Until Module 9 gives you if, checking first with in from Chapter 7.2 is the way to make remove safe when you are unsure — or simply use discard.

One more verified case, because it is the one that catches people in real scripts:

Azvya Education Pvt. Ltd.VLSI Mentor
BROKEN ON PURPOSE
set().remove("x")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
KeyError: 'x'

An empty set is a completely normal state — nothing failed yet — and remove on it raises.

6. frozenset: The Read-Only Variant

The canonical curriculum names this here, and it takes one section.

A frozenset is a set that cannot be changed. Everything about reading it is identical:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
fz = frozenset({"clk", "reset_n"})
print(type(fz))
print(len(fz))
print("clk" in fz)
print(sorted(fz))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
<class 'frozenset'>
2
True
['clk', 'reset_n']

Length, membership and sorted() all work. What is missing is everything in this chapter:

Azvya Education Pvt. Ltd.VLSI Mentor
BROKEN ON PURPOSE
f = frozenset({"a"})
f.add("b")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
AttributeError: 'frozenset' object has no attribute 'add'

AttributeError, not TypeError — the method does not exist, exactly as Chapter 6.4 found for tuples and their missing list methods. The same relationship holds here:

ChangeableRead-only
listtuple
setfrozenset

And it buys the same thing it bought a tuple. Because a frozenset cannot change, it is hashable — so it can be a set element where an ordinary set cannot:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
holder = {frozenset({"a", "b"})}
print(len(holder))
print(frozenset({"a", "b"}) in holder)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
1
True

Compare that with {{"a"}} from Chapter 7.1, which raised unhashable type: 'set'. The restriction was never arbitrary: a set cannot be an element because a set can change, and a frozenset can be one because it cannot.

Equality works across the two, since both compare by membership:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
print(frozenset({"a"}) == {"a"})
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
True

7. What Each Operation Does

Now that each has arrived from a problem — a summary, not the lesson:

OperationChanges the set?When the item is absent
s.add(x)adds one elementn/a — adding an existing item is a no-op
s.update(xs)adds each element of xsn/a
s.remove(x)removes xraises KeyError
s.discard(x)removes xdoes nothing

Every one of these evaluates to None, so none of them is ever assigned — the rule from Section 2.

8. Predict, Run, Observe

Predict every line before reading on. The duplicate handling is the part to be careful with.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
failed = {"burst", "reset"}
failed.add("burst")
print(len(failed))
failed.update(["crc", "reset"])
print(len(failed))
print(sorted(failed))
failed.discard("nope")
print(len(failed))
print(failed == {"burst", "reset", "crc"})
print(failed.add("timeout"))
print(len(failed))

The verified output:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
2
3
['burst', 'crc', 'reset']
3
True
None
4

Trace it one step at a time.

add("burst") leaves the length at 2 — already present, nothing to do.

update(["crc", "reset"]) is handed two values of which only "crc" is new, so the length goes to 3, and sorted() confirms exactly which three.

discard("nope") leaves it at 3: the element was absent, and discard does nothing about that.

The equality check is True, which is the cleanest possible confirmation of the state — it compares membership and ignores everything else.

Then print(failed.add("timeout")) prints None, because that is what add evaluates to — and the very next line shows 4, because the element really was added. Both are true at once, which is exactly why assigning the result is such a dangerous mistake.

9. Exercises

Exercise 1 — before and after. Starting from sigs = {"clk"}, write the statements that: add "reset_n", add every name from ["valid", "ready"], remove "clk" in a way that raises if it is missing, and remove "nope" in a way that does not. Print len() and sorted() after each.

Exercise 2 — predict the length. Starting from s = {"a", "b"} each time, give len(s) afterwards: (a) s.add("c") (b) s.add("a") (c) s.update(["a", "c"]) (d) s.update("cd") (e) s.discard("z") (f) s.remove("a")

Exercise 3 — add or update. For each, say which method you want and what the other one would do: (a) record one newly failing test (b) merge in every failure from a second run (c) store a (test, seed) pair as one item (d) add every character of a string as a separate item

Exercise 4 — read the error. Run {"a"}.add(["b"]) and write down the exact message. Explain in two sentences why this is not a shape problem like append had in Chapter 5.6, and give the corrected call.

Exercise 5 — remove or discard. For each situation, choose one and justify in one sentence: (a) removing a test from a set you built one line earlier from the same source (b) clearing a name that may or may not have been recorded (c) removing an entry you believe must exist, where its absence would mean your bookkeeping is wrong

Exercise 6 — the None trap. This prints None and then fails later. Explain the three steps that produced it and fix it with the smallest change:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
failed = {"burst"}
failed = failed.add("reset")
print(failed)

Exercise 7 — the two claims. In two sentences, state what is mutable about a set and what must be immutable, and name the error you would see if you got the second one wrong.

Exercise 8 — frozenset. Predict which of these succeed, verify all four, and explain the pattern in two sentences: (a) {frozenset({"a"})} (b) {{"a"}} (c) frozenset({"a"}) == {"a"} (d) frozenset({"a"}).add("b")

10. Summary

  • A Python set is mutable. It can be changed after it is created; it is the elements that must be hashable. Two separate claims about two different things.
  • add(x) adds one element and evaluates to None. Adding something already present is a harmless no-op, so add is safe to call without checking.
  • update(xs) adds each element of an iterable. Given a string it adds the characters.
  • add of a list raises TypeError: unhashable type: 'list' — not a wrong shape, a rejected element. You wanted update.
  • update of a non-iterable raises TypeError: 'int' object is not iterable.
  • remove(x) and discard(x) are identical when x is present. When it is absent, remove raises KeyError and discard does nothing.
  • Choose by what absence means — remove when it would indicate a bug, discard when it is a normal outcome. Neither is generally safer.
  • Every one of these returns None. s = s.add(x) sets s to None, silently.
  • frozenset is the read-only variant, standing to set as tuple stands to list. .add on one is an AttributeError.
  • A frozenset is hashable, so it can be a set element where a plain set raises unhashable type: 'set'.
  • When a collection changes in place, look at it. Print len() or sorted() after each step rather than reasoning about accumulated state.

Three chapters have built up what a set is. The next one is what sets are actually for.

Where this fits

Part of the Python curriculum.