If you’re switching between Python 2 and Python 3, you might think that Python 2’s xrange
objects are pretty much the identical to Python 3’s range
object. It seems like they probably just renamed xrange
to range
, right?
Not quite.
Python 2’s xrange
is somewhat more limited than Python 3’s range
. In this article we’re going to take a look at how xrange
in Python 2 differs from range
in Python 3.
The inspiration for this article came from a question I addressed during a Weekly Python Chat session I did last year on range objects.
Python 2 vs Python 3: range
The first thing I need to address is how range
works in Python 2 and Python 3.
In Python 2, the range
function returned a list of numbers:
1 2 |
|
And the xrange
class represented an iterable that provided the same thing when looped over, but it was lazy:
1 2 |
|
This laziness was really embraced in Python 3. In Python 3, they removed the original range
function and renamed xrange
to range
:
1 2 |
|
So if you wanted the Python 2 behavior for range
in Python 3, you could always convert the range
object to a list:
1 2 |
|
Okay now let’s compare Python 2’s xrange
class to Python 3’s range
class.
Similarities
Before we take a look at differences between xrange
and range
objects, let’s take a look at some of the similarities.
Python 2’s xrange
has a fairly descriptive string representation:
1 2 |
|
And so does Python 3’s range
object:
1 2 |
|
The xrange
object in Python 2 is an iterable (anything you can loop over is an iterable):
1 2 3 4 5 6 |
|
And the range
object in Python 3 is also an iterable:
1 2 3 4 5 6 |
|
The xrange
object has a start, stop, and step. Step is optional and so is start:
1 2 3 4 5 6 7 8 |
|
So does the range
object:
1 2 3 4 5 6 7 8 |
|
Both have a length and both can be indexed in forward or reverse order:
1 2 3 4 5 6 |
|
Python considers both range
and xrange
to be sequences:
1 2 3 |
|
So much of the basic functionality is the same between xrange
and range
. Let’s talk about the differences.
Dunder Methods
The first difference we’ll look at is the built-in documentation that exists for Python 2’s xrange
and Python 3’s range
.
If we use the help
function to ask xrange
for documentation, we’ll see a number of dunder methods. Dunder methods are what Python uses when you use many operators on objects (like +
or *
) as well as other features shared between different objects (like the len
and str
functions).
Here are the core dunder methods which Python 2’s xrange
objects fully implement:
| __getitem__(...)
| x.__getitem__(y) <==> x[y]
|
| __iter__(...)
| x.__iter__() <==> iter(x)
|
| __len__(...)
| x.__len__() <==> len(x)
|
| __reduce__(...)
|
| __repr__(...)
| x.__repr__() <==> repr(x)
|
| __reversed__(...)
| Returns a reverse iterator.
And here are the core dunder methods which Python 3’s range
objects fully implement:
| __contains__(self, key, /)
| Return key in self.
|
| __eq__(self, value, /)
| Return self==value.
|
| __getitem__(self, key, /)
| Return self[key].
|
| __iter__(self, /)
| Implement iter(self).
|
| __len__(self, /)
| Return len(self).
|
| __ne__(self, value, /)
| Return self!=value.
|
| __repr__(self, /)
| Return repr(self).
|
| __reversed__(...)
| Return a reverse iterator.
|
| count(...)
| rangeobject.count(value) -> integer -- return number of occurrences of value
|
| index(...)
| rangeobject.index(value, [start, [stop]]) -> integer -- return index of value.
| Raise ValueError if the value is not present.
Notice that range
objects support many more operations than xrange
does. Let’s take a look at some of them.
Comparability
Python 3’s range
support equality checks:
1 2 3 4 |
|
Python 2’s xrange
objects may seem like they support equality:
1 2 |
|
But they’re actually falling back to Python’s default identity check:
1 2 |
|
Two xrange
objects will not be seen as equal unless they are actually the same exact object:
1 2 3 4 5 6 |
|
Whereas a comparison between two range
objects in Python 3 actually checks whether the start, stop, and step of each object is equal:
1 2 3 4 5 6 |
|
Sliceabiltiy
We already saw that both Python 2’s xrange
and Python 3’s range
support indexing:
1 2 3 4 |
|
Python 3’s range
object also supports slicing:
1 2 3 4 |
|
But xrange
doesn’t:
1 2 3 4 |
|
Containment
Both range
and xrange
support containment checks:
1 2 |
|
But this support is a little deceptive with xrange
. Python 2’s xrange
objects don’t actually implement the __contains__
method that is used to implement Python’s in
operator.
So while we can ask whether an xrange
object contains a number, in order to answer our question Python will have to manually loop over the xrange
object until it finds a match.
This takes about 20 seconds to run on my computer in Python 2.7.12:
1 2 |
|
But in Python 3 this returns an answer immediately:
1 2 |
|
Python 3 is able to return an answer immediately for range
objects because it can compute an answer based off the start, stop, and step we provided.
Start, stop, and step
In Python 3, range
objects have a start, stop, and step:
1 2 3 4 5 6 7 |
|
These can be useful when playing with or extending the capability of range
.
We might for example wish that range
objects could be negated to get a mirrored range
on the opposite side of the number line:
1 2 3 4 5 |
|
While range
objects don’t support this feature, we could implement something similar by negating the start, stop, and step ourselves and making a new range
:
1 2 3 |
|
While you can provide start, stop, and step as arguments to Python 2’s xrange
objects, they don’t have these start, stop, and step attributes at all:
1 2 3 4 5 |
|
If you wanted to get start, stop, and step from an xrange
object, you would need to calculate them manually. Something like this might work:
1 2 3 4 5 6 7 8 |
|
Support for big numbers
The last difference I’d like to mention is sort of a silly one, but it could be important for some interesting use cases of range
and xrange
.
In Python 3, the range
object will accept integers of any size:
1 2 |
|
But Python 2’s xrange
objects are limited in the size of integers they can accept:
1 2 3 4 |
|
I run into this difference most often during my on-site team training sessions because I sometimes use silly examples with big numbers when I teach.
Is any of this important to know?
Most of the time you use either Python 2’s xrange
objects or Python 3’s range
objects, you’ll probably just be creating them and looping over them immediately:
1 2 3 4 5 6 7 |
|
So the missing xrange
features I noted above don’t matter most of the time.
However, there are times when it’s useful to have a sequence of consecutive numbers that supports features like slicing, fast containment checks, or equality. In those cases, Python 2 users will be tempted to fall back to the Python 2 range
function which returns a list. In Python 3 though, you’ll pretty much always find what you’re looking for in the range
class. For pretty much every operation you’ll want to perform, Python 3’s range
is fast, memory-efficient, and powerful.
Python 3 put a lot of work into making sure its built-ins are memory efficient and fast. Many built-in functions (e.g. zip
, map
, filter
) now return iterators and lazy objects instead of lists.
At the same time, Python 3 made common functions and classes, like range
, more featureful.
There are many big improvements that Python 3 made over Python 2, but there are many many more tiny benefits to upgrading to Python 3. If you haven’t already, I’d strongly consider whether it makes sense for you to upgrade your code to Python 3.