Subset and Superset

Concepts covered: Set Operations

Every operation so far built a new set. The last group asks yes-or-no questions about containment, and then changes a set where it stands. a <= b, or a.issubset(b), is True when every element of a is also in b. Read from the other side, b >= a, or b.issuperset(a), says b contains all of a. A data contract check fits this exactly: are all required fields present in a record?

required = {"order_id", "amount", "currency"}
record = {"order_id": 9, "amount": 12.5, "currency": "EUR", "coupon": None}

fields = set(record)
print(required <= fields)
print(required.issubset(fields))
print(fields >= required)
print(sorted(required - fields))

Output:

True
True
True
[]

set(record) collects the dict's keys. The record carries an extra coupon field and the check still passes: a subset test ignores whatever the larger set holds beyond the smaller one. When the check fails, required - fields names the missing fields for the error message, which tells the on-call engineer far more than a bare False.

Proper subsets and the empty set

The < operator adds a condition to <=: the sets must also differ, so the larger set holds at least 1 extra element. Every set is a subset of itself and never a proper subset of itself. The empty set is a subset of every set, since it has no element that could be missing.

a = {1, 2}
print(a <= a, a < a)
print(set() <= a)
print({1, 2} < {1, 2, 3})
print({1, 4} <= {1, 2, 3})

Output:

True False
True
True
False

When neither set contains the other

With numbers, if x <= y is False then x >= y is True. Sets drop that rule. {1, 2} and {3} each hold something the other lacks, so <=, >=, <, > and == all come back False between them. The isdisjoint() method asks a separate question: do the 2 sets share nothing at all?

x = {1, 2}
y = {3}
print(x <= y, x >= y, x == y)
print(x.isdisjoint(y))
print(x.isdisjoint({2, 9}))

Output:

False False False
True
False

Changing a set in place

Each operation in this lesson has an in-place form that rewrites the left set and builds nothing new. The augmented assignment operators work on sets, and each has a method twin. The methods take lists and other iterables, the operators still want a set on the right, and every method returns None.

Builds a new setChanges the left setMethod form
a | ba |= ba.update(b)
a & ba &= ba.intersection_update(b)
a - ba -= ba.difference_update(b)
a ^ ba ^= ba.symmetric_difference_update(b)
seen = {1, 2}
report = seen
seen = seen | {3}
print(sorted(report), report is seen)

seen = {1, 2}
report = seen
seen |= {3}
print(sorted(report), report is seen)

Output:

[1, 2] False
[1, 2, 3] True

seen = seen | {3} builds a new set and points seen at it, so report still names the old object holding 1 and 2. seen |= {3} adds 3 to the object seen already names, and report names that same object, so it sees the change and is returns True. When 2 names share a set, choose the form deliberately: an in-place change reaches every name.

seen = {1, 2}
result = seen.update([3, 4])
print(result)
print(sorted(seen))

Output:

None
[1, 2, 3, 4]

update() changed seen and handed back None, as every in-place method does. Writing seen = seen.update(batch) therefore replaces the set with None, and the next method call on seen fails with AttributeError.

A dashboard keeps a second name, snapshot, for a set of active session IDs. What does this print?

active = {1, 2, 3}
snapshot = active
active -= {2}
active = active | {9}
print(sorted(snapshot))

When a loop collects batches, call update() as a statement on its own line, or use |= with the batch wrapped in set(). Either way loaded stays the same object and grows with each batch.

batches = [[3, 1], [1, 4], [5]]

loaded = set()
for batch in batches:
    loaded.update(batch)
print(sorted(loaded))

loaded = set()
for batch in batches:
    loaded |= set(batch)
print(sorted(loaded), len(loaded))

Output:

[1, 3, 4, 5]
[1, 3, 4, 5] 4

Learn to spot the broken version: assigning the result of update() back to the name leaves None after the first batch, and the second pass raises AttributeError: 'NoneType' object has no attribute 'update'.

Containment tests compose with the other operators, since a | b is a set like any other, and issuperset() accepts a list. Comparisons bind looser than | and &, so the second line compares flagged with the whole union. The third line asks for IDs present on both days, and fails.

mon = {1, 2}
tue = {2, 3}
flagged = [3, 1]

print((mon | tue).issuperset(flagged))
print(set(flagged) <= mon | tue)
print(set(flagged) <= mon & tue)

Output:

True
True
False

The last exercise uses each operator from this lesson. Returning users are an intersection, new users a difference that also subtracts the banned list, users seen in only 1 week a symmetric difference, and the banned check a subset test against the union.

About this lesson

This section is part of the Sets: Intermediate lesson on DataDriven, the home of data engineering. Lessons cover SQL, Python, Spark, data modeling and pipeline architecture, and each section pairs its explanation with worked examples and practice you run as you go.