When are two Python range objects equal, and are range objects hashable?
answer
- What the object denotes, not stores
- Equal when they denote the same values
- Two empty ranges are always equal
- Immutability buys hashability
- range(0, 10, 2) equals range(0, 9, 2)
basics
~10 sTwo ranges are equal when they represent the same sequence of values, not when their attributes match: range(0) equals range(2, 2, 3). Ranges are immutable and hashable, and equal ranges hash equal.
solid answer
~40 sSince Python 3.3, `range.__eq__` compares the *values* a range denotes rather than its `start`, `stop` and `step` fields. So `range(0) == range(2, 2, 3)` is `True` (both empty) and `range(0, 10, 2) == range(0, 9, 2)` is `True` (both yield 0, 2, 4, 6, 8, since 9 is never reached). The canonical rule is: empty ranges are all equal; otherwise same length, same first element, same step. Because the three integers are read-only, a range is hashable and equal ranges hash equal, so it works as a dict key. Two limits: a range is never equal to a list or tuple with the same values, and ordering comparison raises — `range(3) < range(4)` is a `TypeError`.
code
pycon · 8 lines>>> range(0) == range(2, 2, 3)
True
>>> range(0, 10, 2) == range(0, 9, 2)
True
>>> hash(range(0, 10, 2)) == hash(range(0, 9, 2))
True
>>> range(0, 10, 2) == [0, 2, 4, 6, 8]
Falsego deeper
You are not expected to know this. If it comes up, the safe answer is that ranges compare by the values they produce and that two empty ranges are equal regardless of how they were constructed.
Be able to state the rule and apply it: same length, same first element, same step, with all empty ranges equal. Also know that a range never compares equal to a list holding the same numbers.
Connect it up: __eq__ is a semantic contract a type's author chooses, immutability is what licenses hashability, and a range's lack of ordering forces an explicit sort key. Recognize this as a curiosity, not a gate.
Use it as a design precedent. When you define a value type, decide deliberately whether equality means same fields or same denotation, and keep __hash__ consistent with whichever you pick — the range shows both halves done well.
### Equality compares the values, not the fields Two `range` objects are equal when they represent the *same sequence of values*, which is not the same thing as having equal `start`, `stop` and `step` attributes: ```pycon >>> range(0) == range(2, 2, 3) True >>> range(0, 10, 2) == range(0, 9, 2) True >>> range(5) == range(0, 5, 1) True ``` The first pair are both empty, so they are equal even though every attribute differs. The second pair both produce 0, 2, 4, 6, 8 — the stop of 9 is never reached, so the extra unit of room is invisible in the values and therefore invisible to `==`. The third pair is the trivial case: the defaults `start=0` and `step=1` are filled in at construction, and the objects are indistinguishable. Practically, CPython normalizes the comparison to a canonical form: an empty range is equal to any other empty range; a one-element range is equal to any range with the same single element regardless of step; otherwise two ranges are equal when they have the same length, the same first element and the same step. ### Hashing follows equality Ranges are hashable, and the hash respects the same rule — equal ranges hash equal, so a range works as a dict key or a set member without surprises: ```pycon >>> hash(range(0, 10, 2)) == hash(range(0, 9, 2)) True >>> {range(3): "plan"}[range(0, 3, 1)] 'plan' ``` That hashability is a direct consequence of immutability. A range's three integers can never be reassigned (`r.start = 1` raises `AttributeError: readonly attribute`), so its value — and therefore its hash — is fixed for life, which is exactly the contract a hash-based container requires. ### What equality does *not* extend to Two limits catch people. *A range is never equal to a list or a tuple.* `range(0, 10, 2) == [0, 2, 4, 6, 8]` is `False`, even though iterating both yields identical values. Python's sequence types only compare equal within compatible families — `list` and `tuple` do not compare equal to each other either. If you mean "same values", say `list(r) == other`. *Ranges are unordered with respect to each other.* `range(3) < range(4)` raises `TypeError: '<' not supported between instances of 'range' and 'range'`. There is no lexicographic comparison the way there is for lists and tuples, because a range deliberately does not materialize its elements to compare them. Sorting a collection of ranges therefore needs an explicit key, such as their length or their first element. ### Why the design is the way it is `range` is a *value* type describing a set of numbers, not a record of the arguments you happened to pass. `range(0, 9, 2)` and `range(0, 10, 2)` denote the same five numbers, and a user has no way to observe a difference between them through any public operation except `repr()` and the raw `stop` attribute. Making them compare equal keeps `==` consistent with everything else the object exposes — `len()`, indexing, iteration, membership — and the alternative (field-wise comparison) would have made two objects that behave identically in every observable way compare unequal. ### Version note This is one of the few `range` behaviours that changed within Python 3. Before Python 3.3, `range` objects did not define `==` at all, so comparison fell back to identity and `range(0) == range(2, 2, 3)` was `False`. Python 3.3 defined `==` and `!=` in terms of the sequence of values, and that is the behaviour on 3.14. If you read an answer online claiming ranges compare by identity, it predates 3.3. ### Interview register This is a curiosity question, not a gate. Nobody is turned down for not knowing that `range(0, 10, 2) == range(0, 9, 2)`. What it does reveal is whether a candidate reasons about `__eq__` as a *semantic* contract chosen by a type's author rather than an automatic field-by-field check, and whether they connect immutability to hashability without prompting. Both of those generalize far beyond `range`.
- Why is `range(0, 10, 2) == range(0, 9, 2)` True when their stop values differ?Because stop is exclusive and the step skips over 9: both produce 0, 2, 4, 6, 8. The extra unit of room in the first range is unobservable through length, indexing, iteration or membership, so `==` treats the two as the same sequence.
- What makes a range hashable when a list is not?Immutability. A range's `start`, `stop` and `step` are read-only, so its value can never change and its hash is stable for the object's lifetime — exactly the contract a dict or set requires. A list can be mutated after insertion, which would corrupt the container, so `list` defines no `__hash__`.
- How would you sort a collection of range objects?With an explicit key, because ranges are unordered relative to each other and `<` raises `TypeError`. Sort by whatever property matters — `len(r)`, `r.start`, or `(r.start, r.step)` — using `sorted(ranges, key=...)`.
saying these in an interview costs you the question
- Says ranges compare field by field on start, stop and step
- Claims range(0, 10, 2) equals the list [0, 2, 4, 6, 8]
- Thinks ranges are unhashable like lists
- Assumes range supports < the way tuples do
- Believes equality falls back to identity comparison