Why is range(0) == range(2, 2, 2) True in Python 3?

Why do ranges which are initialized with different values compare equal to one another in Python 3?

When I execute the following commands in my interpreter:

>>> r1 = range(0)
>>> r2 = range(2, 2, 2)
>>> r1 == r2
True

The result is True. Why is this so? Why are two different range objects with different parameter values treated as equal?


The range objects are special:

Python will compare range objects as Sequences. What that essentially means is that the comparison doesn't evaluate how they represent a given sequence but rather what they represent.

The fact that the start, stop and step parameters are completely different plays no difference here because they all represent an empty list when expanded:

For example, the first range object:

list(range(0))  # []

and the second range object:

list(range(2, 2, 2)) # []

Both represent an empty list and since two empty lists compare equal (True) so will the range objects that represent them.

As a result, you can have completely different looking range objects; if they represent the same sequence they will compare equal:

range(1, 5, 100) == range(1, 30, 100) 

Both represent a list with a single element [1] so these two will also compare equal.


No, range objects are really special:

Do note, though, that even though the comparison doesn't evaluate how they represent a sequence the result of comparing can be achieved using solely the values of start, step along with the len of the range objects; this has very interesting implications with the speed of comparisons:

r0 = range(1, 1000000)    
r1 = range(1, 1000000)

l0 = list(r0)    
l1 = list(r1)

Ranges compares super fast:

%timeit r0 == r1
The slowest run took 28.82 times longer than the fastest. This could mean that an intermediate result is being cached 
10000000 loops, best of 3: 160 ns per loop

on the other hand, the lists..

%timeit l0 == l1
10 loops, best of 3: 27.8 ms per loop

Yeah..


As @SuperBiasedMan noted, this only applies to the range objects in Python 3. Python 2 range() is a plain ol' function that returns a list while the 2.x xrange object doesn't have the comparing capabilies (and not only these..) that range objects have in Python 3.

Look at @ajcr's answer for quotes directly from the source code on Python 3 range objects. It's documented in there what the comparison between two different ranges actually entails: Simple quick operations. The range_equals function is utilized in the range_richcompare function for EQ and NE cases and assigned to the tp_richcompare slot for PyRange_Type types.

I believe the implementation of range_equals is pretty readable (because it is nice as simple) to add here:

/* r0 and r1 are pointers to rangeobjects */

/* Check if pointers point to same object, example:    
       >>> r1 = r2 = range(0, 10)
       >>> r1 == r2
   obviously returns True. */
if (r0 == r1)
    return 1;

/* Compare the length of the ranges, if they are equal 
   the checks continue. If they are not, False is returned. */
cmp_result = PyObject_RichCompareBool(r0->length, r1->length, Py_EQ);
/* Return False or error to the caller
       >>> range(0, 10) == range(0, 10, 2)  
   fails here */
if (cmp_result != 1)
    return cmp_result;

/* See if the range has a lenght (non-empty). If the length is 0
   then due to to previous check, the length of the other range is 
   equal to 0. They are equal. */
cmp_result = PyObject_Not(r0->length);
/* Return True or error to the caller. 
       >>> range(0) == range(2, 2, 2)  # True
   (True) gets caught here. Lengths are both zero. */
if (cmp_result != 0)
    return cmp_result;

/* Compare the start values for the ranges, if they don't match
   then we're not dealing with equal ranges. */
cmp_result = PyObject_RichCompareBool(r0->start, r1->start, Py_EQ);
/* Return False or error to the caller. 
   lens are equal, this checks their starting values
       >>> range(0, 10) == range(10, 20)  # False
   Lengths are equal and non-zero, steps don't match.*/
if (cmp_result != 1)
    return cmp_result;

/* Check if the length is equal to 1. 
   If start is the same and length is 1, they represent the same sequence:
       >>> range(0, 10, 10) == range(0, 20, 20)  # True */
one = PyLong_FromLong(1);
if (!one)
    return -1;
cmp_result = PyObject_RichCompareBool(r0->length, one, Py_EQ);
Py_DECREF(one);
/* Return True or error to the caller. */
if (cmp_result != 0)
    return cmp_result;

/* Finally, just compare their steps */
return PyObject_RichCompareBool(r0->step, r1->step, Py_EQ);

I've also scattered some of my own comments here; look at @ajcr's answer for the Python equivalent.


Direct quote from the docs (emphasis mine):

Testing range objects for equality with == and != compares them as sequences. That is, two range objects are considered equal if they represent the same sequence of values. (Note that two range objects that compare equal might have different start, stop and step attributes, for example range(0) == range(2, 1, 3) or range(0, 3, 2) == range(0, 4, 2).)

If you compare ranges with the "same" list, you'll get inequality, as stated in the docs as well:

Objects of different types, except different numeric types, never compare equal.

Example:

>>> type(range(1))
<class 'range'>
>>> type([0])
<class 'list'>
>>> [0] == range(1)
False
>>> [0] == list(range(1))
True

Note that this explicitly only applies to Python 3. In Python 2, where range just returns a list, range(1) == [0] evaluates as True.


To add a few additional details to the excellent answers on this page, two range objects r0 and r1 are compared roughly as follows:

if r0 is r1:                 # True if r0 and r1 are same object in memory
    return True
if len(r0) != len(r1):       # False if different number of elements in sequences
    return False
if not len(r0):              # True if r0 has no elements
    return True
if r0.start != r1.start:     # False if r0 and r1 have different start values
    return False
if len(r0) == 1:             # True if r0 has just one element
    return True
return r0.step == r1.step    # if we made it this far, compare step of r0 and r1

The length of a range object is easily to calculate using the start, stop and step parameters. In the case where start == stop, for example, Python can immediately know that the length is 0. In non-trivial cases, Python can just do a simple arithmetic calculation using the start, stop and step values.

So in the case of range(0) == range(2, 2, 2), Python does the following:

  1. sees that range(0) and range(2, 2, 2) are different objects in memory.
  2. computes the length of both objects; both lengths are 0 (because start == stop in both objects) so another test is needed.
  3. sees that len(range(0)) is 0. This means that len(range(2, 2, 2)) is also 0 (the previous test for inequality failed) and so the comparison should return True.