Why can `a <= b` and `b <= a` both be False for two Python sets?
answer
- The operators do not mean bigger
- Containment, and containment can fail both ways
- Some pairs are simply incomparable
- Negating one direction proves nothing
- Partial order, so sorting is meaningless
basics
~20 sComparison operators between sets mean subset and superset, not ordering by size or content. Two sets that merely overlap are incomparable, so every one of <, <=, > and >= is False in both directions. Sets form a partial order, not a total one.
solid answer
~40 sFor sets, `a <= b` is `a.issubset(b)`, `a < b` is proper subset, and `>=`/`>` are the superset mirrors - none of them compare sizes or sort contents. Given `{'A1','B2'}` and `{'B2','C3'}`, neither contains the other, so all four comparisons return False and so does `==`. That is what a partial order means: `not (a <= b)` does **not** imply `b < a`, which is the bug this trips. It also means sets cannot be meaningfully sorted; `sorted()` over a list of sets or `max()` over them produces something arbitrary rather than an error. Use `a.isdisjoint(b)` to ask whether they share nothing - it takes any iterable and stops at the first common element instead of building an intersection.
code
python · 7 linesa = {'A1', 'B2'}
b = {'B2', 'C3'}
print(a <= b, b <= a, a == b) # False False False
print(a.isdisjoint(b)) # False - B2 is shared
print({'A1'} < a, a < a) # True False - proper subset
print(a.issubset(['A1', 'B2', 'C3'])) # method takes any iterablego deeper
Know that <= between sets asks subset, not size, and that == compares contents regardless of order. Recall that issubset and issuperset are the method spellings of the same tests.
Explain why two overlapping sets make every comparison False - containment is a partial order - and why the negation of one direction never establishes the other. Know that isdisjoint has no operator form.
Catch the real defect in review: branches that negate a subset test and then assume the reverse containment, and sorted or max applied to sets. Prefer set difference so the error message names the missing elements.
Watch for this in permission, capability and feature-flag models where sets encode policy. Ordering assumptions baked into such comparisons produce authorization logic that is wrong only for partially-overlapping cases, which is precisely where the interesting failures live.
## The comparison operators are set algebra, not ordering On `set` and `frozenset`, the rich-comparison operators are overloaded to mean containment: - `a <= b` - every element of `a` is in `b` (`a.issubset(b)`) - `a < b` - subset **and** `a != b` (proper subset) - `a >= b` - `a.issuperset(b)` - `a > b` - proper superset - `a == b` - exactly the same elements, size and order irrelevant Nothing here compares sizes, and nothing sorts. `{1, 2, 3} > {9}` is False: the left side is larger but does not contain the right side's element. ## Why both directions can be False Containment is a *partial* order. In a total order - integers, strings - any two values are comparable, so exactly one of `a < b`, `a == b`, `a > b` holds. Sets do not have that property. Take `a = {'A1', 'B2'}` and `b = {'B2', 'C3'}`. `a` has `A1`, which `b` lacks, so `a <= b` is False. `b` has `C3`, which `a` lacks, so `b <= a` is False. They are equal? No. All five comparisons are False simultaneously, and that is correct, not a quirk. ## The bug this causes The practical consequence is a De Morgan-style mistake in real code: ```python if not required <= granted: # tempting to read this as 'granted is a strict subset of required' ... ``` `not (a <= b)` means only 'a is not contained in b'. It does **not** mean `b < a`. Code that branches on the negation and then assumes the other direction holds will take the wrong branch on every partially-overlapping pair - exactly the common case for permission sets, feature flags and tag sets. Write what you mean: `required - granted` gives the missing elements and is both truthful and more useful in the error message. The same trap appears in sorting. `sorted(list_of_sets)` and `max(list_of_sets)` do not raise; they call the comparison operators and produce a result that depends on the input order and on which pairs happened to be comparable. If you want size ordering, say so with a key: `sorted(sets, key=len)`. ## isdisjoint, and why it is not `not (a & b)` The third containment predicate is `a.isdisjoint(b)`: True when the two share no elements. It has no operator spelling. It matters because the obvious alternative, `not (a & b)`, builds the entire intersection set before throwing it away, whereas `isdisjoint` iterates and returns False the moment it finds one common element. On large collections with an early hit that is the difference between a few comparisons and a full pass plus an allocation. ## The iterable rule extends to the predicates As with the combining operations, the **operator** spellings require sets on both sides - `{1} <= [1]` raises `TypeError: '<=' not supported between instances of 'set' and 'list'` - while the **method** spellings take any iterable: `{1}.issubset([1, 2])`, `s.issuperset(range(3))`, `s.isdisjoint(some_generator)`. When one side arrives from a caller as an arbitrary sequence, the method form saves you a conversion, and `isdisjoint` in particular can then short-circuit without materialising the argument at all. ## Empty sets and equality of types Two edge cases worth having ready. The empty set is a subset of everything, so `set() <= anything` is True and `set().isdisjoint(anything)` is True. And `set` and `frozenset` compare across types: `{1, 2} == frozenset({1, 2})` is True, and subset comparisons work between them too, because equality is defined on contents rather than on the concrete type. That is also why a set and a frozenset with the same elements hash-collide sensibly when the frozenset is used as a dict key. ## What a good answer sounds like Name the meaning (subset, not size), name the property (partial order, so incomparable pairs exist), give the concrete pair where everything is False, and finish with the consequence: never infer one direction from the negation of the other, and never sort sets without a key. Mentioning `isdisjoint` and its short-circuit, and the operator-versus-method type rule, is what separates a complete answer from a correct one.
- Why prefer `a.isdisjoint(b)` over `not (a & b)`?`a & b` builds the full intersection as a new set before the truth test discards it, so it always pays for a complete pass plus an allocation. `isdisjoint` iterates the smaller side and returns False at the first shared element, which on real data is usually almost immediately. It also accepts any iterable, so a generator argument can be abandoned part-way rather than materialised. The intent reads more clearly too.
- What does `sorted()` do to a list of sets, and is that a bug?It does not raise: `sorted` calls `<` between sets, which is proper-subset, so the result depends on the input order and on which pairs were comparable at all. Nothing is well-defined, and two runs over the same data in a different order can give different output. It is a bug in the calling code, not in `sorted`. If you want size or content ordering, pass a key such as `key=len` or `key=sorted`.
- Is `{1, 2} == frozenset({1, 2})` True?Yes. Set equality is defined on contents, not on the concrete type, so a `set` and a `frozenset` with the same elements compare equal, and the subset and superset comparisons work across the two types as well. The practical consequence is that swapping one for the other at an API boundary does not break equality checks, only mutability.
Set comparison is like asking whether one shopping basket's contents fit inside another's: two baskets that each hold something the other lacks are simply not rankable, however many items each contains.
saying these in an interview costs you the question
- Reads a < b as a has fewer elements than b
- Assumes not (a <= b) implies b < a
- Believes comparing two sets always yields a definite ordering
- Sorts a list of sets and expects a stable, meaningful order
- Writes not (a & b) where isdisjoint would short-circuit
- Thinks a set and a frozenset with equal contents compare unequal