Back to Blog
Python

Python Reverse Iterator: Using reversed() and More

Learn how to iterate over Python sequences in reverse with reversed(), slicing, and custom iterators, including memory and performance tradeoffs.

pythoniterationreversedcustom iteratorperformancememory
Illustration of a Python reverse iterator showing a sequence traversed from end to start.

To iterate through a Python sequence from its last element to its first, you have several options. The built-in reversed() function is the most direct, but the best choice depends on whether you are working with a sequence, an iterator, or a custom class. This article covers the common approaches, their behavior, and when each one makes sense in production code.

The Built-in reversed() Function

For any object that supports the sequence protocol—lists, tuples, strings, and ranges—reversed() returns a reverse iterator that yields elements from the end to the beginning. It does not copy the sequence; it walks backward using the object's __len__ and __getitem__ methods.

numbers = [1, 2, 3, 4, 5] for n in reversed(numbers): print(n)

The loop prints each value on its own line: 5, 4, 3, 2, 1. Because reversed() returns an iterator, you can use it in a for loop, convert it to a list with list(), or pass it to any function that accepts an iterable.

This approach is ideal when you only need to traverse the sequence once. It is memory-efficient because no copy is created, and the overhead is minimal. The main limitation is that reversed() only works with objects that have a defined sequence length and support indexing. It will not work on arbitrary iterators like generators.

Reversing with Slicing

A common alternative is slicing with a step of -1:

numbers = [1, 2, 3, 4, 5] for n in numbers[::-1]: print(n)

For lists, slicing creates a new list containing all elements in reverse order. It is a shallow copy: the list container is new, but the elements themselves are not duplicated. The result is a materialized list, not an iterator.

The key difference from reversed() is memory usage. For a list of n items, slicing allocates an additional list of size n. If you only need to iterate once, that allocation is wasteful. However, slicing is useful when you need the reversed sequence multiple times or when you want to pass it to code that expects a list rather than an iterable.

Slicing also works on strings and tuples, and returns the same type as the original. For example, "abc"[::-1] returns "cba".

Building a Custom Reverse Iterator

If you have a custom class that should support reversed(), you can implement __reversed__ to return a dedicated iterator. Alternatively, if you implement __len__ and __getitem__ so the object behaves like a sequence, reversed() will use those methods automatically.

class Playlist: def __init__(self, tracks): self.tracks = tracks def __len__(self): return len(self.tracks) def __getitem__(self, index): return self.tracks[index] def __reversed__(self): return reversed(self.tracks)

Now reversed(playlist) will call your __reversed__ method. If you do not define __reversed__, Python falls back to using __len__ and __getitem__ automatically, as long as they are present. That fallback is exactly how reversed() works for built-in sequences.

For cases where you need more control than a simple index walk, you can build a dedicated iterator that encapsulates the reverse logic:

class ReverseIterator: def __init__(self, data): self.data = data self.index = len(data) - 1 def __iter__(self): return self def __next__(self): if self.index < 0: raise StopIteration value = self.data[self.index] self.index -= 1 return value

This iterator stores its data and yields items in reverse. It is useful when the reverse order is not simply the opposite of the forward order—for example, when you need to skip certain elements or apply a transformation. If the underlying data is not indexable, you will need to store it in a list or another indexable container first.

Memory and Performance Tradeoffs

The choice between reversed() and slicing has direct memory and performance implications. reversed() creates an iterator that holds a reference to the original sequence and an index. It consumes O(1) extra memory. Slicing creates a new sequence of size n, so it uses O(n) additional memory.

In terms of speed, reversed() is generally faster for a single pass because it avoids the copy. Slicing requires allocating a new container and copying references, which takes time proportional to the sequence length. For small sequences the difference is negligible, but for large lists the copy can cause noticeable latency and memory pressure.

There is also a practical difference when you need to iterate more than once. With slicing, the reversed list is created once and can be reused. With reversed(), each call creates a fresh iterator; if you need the elements again, materialize them with list(reversed(data)) and reuse that list. Recreating a reversed iterator is cheap, so the tradeoff is mainly about memory.

Reversing Without Creating a Copy

When you want to reverse a list in place, use the list.reverse() method. It modifies the original list and returns None.

numbers = [1, 2, 3, 4, 5] numbers.reverse() print(numbers) # [5, 4, 3, 2, 1]

If you need to keep the original list unchanged, copy it first, either with numbers[::-1] or by calling list(reversed(numbers)). The latter creates a new list from the reverse iterator, while slicing allocates a new list directly. Both preserve the original list and are equivalent for most purposes; choose the one that is clearer in context.

Common Pitfalls and Edge Cases

A common mistake is assuming reversed() works on any iterable. It does not work on generators or other one-pass iterators because they have no length and no indexing. For example, reversed(generator_object) raises TypeError. If you need to reverse a generator, you must first convert it to a list or tuple, which uses memory proportional to the input size.

Another pitfall is using slicing on a string when you need to compare it to its reverse. Slicing a string creates a new string, which is fine, but if the string is very large, the copy can be expensive. For palindrome checks, s == s[::-1] is idiomatic, but for extremely long strings you might consider a manual loop that compares characters from both ends to avoid the copy.

When working with custom classes, forgetting to implement __len__ and __getitem__ (unless you define __reversed__) will cause reversed() to fail. If your class is not a sequence and lacks __reversed__, Python raises a TypeError because it cannot determine the length or access elements by index.

Finally, be aware that reversed() returns an iterator, not a list. If you need to index into the reversed result, you must materialize it first. This is a common source of confusion for developers coming from languages where reversing always produces a collection.

Choosing the Right Approach for Your Use Case

The decision comes down to whether you need a one-time reverse traversal or a reusable reversed collection. Use reversed() when you only need to iterate once and want to avoid the memory overhead. Use slicing when you need a reversed copy that you can pass around or index into. Use a custom iterator when the reverse order is not a simple index decrement, or when you need to encapsulate complex reverse logic inside a class.

For in-place reversal, list.reverse() is the most efficient because it operates on the existing list without allocating new memory. However, it mutates the original, so it is only appropriate when you no longer need the original order.

In performance-sensitive code, prefer reversed() over slicing for large sequences. The O(1) memory footprint and lack of copy make it a safe default. If you later need the reversed sequence multiple times, materialize it once with list(reversed(data)) and reuse that list.

Understanding how reversed() interacts with the sequence protocol also helps you design custom classes to support reverse iteration cleanly. By implementing __len__ and __getitem__, you get reverse support for free. By adding __reversed__, you can customize the behavior when the default is not optimal.

A final edge case: when the sequence is empty, reversed() returns an iterator that immediately raises StopIteration, and slicing returns an empty sequence. Both behave gracefully, so you do not need special handling for empty inputs.

Whether you are processing logs, implementing undo stacks, or analyzing time series, knowing the options for reverse iteration will help you write code that is both clear and performant. The next time you need to traverse a collection backward, start with reversed() and reach for slicing or custom iterators only when the situation demands it.

python reverse iterator: Practical Usage and Code Examples | RYUSLOG DEV