collections.abc.Sequence __iter__/in Loop Forever Without IndexError (tested)

collections.abc.Sequence only requires you to implement __getitem__ and __len__ yourself -- in exchange it gives you working __iter__, __contains__, __reversed__, index(), and count() for free. Those mixin methods are not built from __len__: the default __iter__ calls __getitem__(0), __getitem__(1), __getitem__(2), ... and treats catching an IndexError as the one and only signal that the sequence has ended. A __getitem__ that instead returns a sentinel value like None for out-of-range indices -- a common instinct if you're used to dict.get()-style fallbacks -- never raises that signal, so the mixin methods never stop.

1. A __getitem__ that returns None instead of raising IndexError

from collections.abc import Sequence

class BadSeq(Sequence):
    def __init__(self, data):
        self._data = data
    def __len__(self):
        return len(self._data)
    def __getitem__(self, i):
        if i < 0 or i >= len(self._data):
            return None   # looks safe, like dict.get()
        return self._data[i]

s = BadSeq([1, 2, 3])
print("len:", len(s))
print("s[0], s[1], s[2]:", s[0], s[1], s[2])
print("s[5] (out of range):", s[5])
len: 3
s[0], s[1], s[2]: 1 2 3
s[5] (out of range): None

Measured: indexing directly looks completely fine -- out-of-range access just returns None instead of raising. len() is also correct. Nothing here hints that the mixin methods are broken.

2. The inherited __iter__ never stops

it = iter(s)
first_10 = [next(it) for _ in range(10)]
print(first_10)
[1, 2, 3, None, None, None, None, None, None, None]

Measured: Sequence's default __iter__ is implemented as "call self[0], self[1], ... incrementing, until __getitem__ raises IndexError, then stop." It does not call len() and does not treat None as a stop signal. Because BadSeq.__getitem__ never raises, list(s) or for x in s will not terminate on its own -- it ran past index 3 straight through our 10-item cap and kept going, yielding None forever.

3. __contains__ hangs the same way when the value is absent

print(2 in s)      # present -- found during the walk, returns fast
print(99 in s)      # absent -- hangs; this call never returns
True
# (process timed out -- 99 in s never returns)

Measured: Sequence.__contains__ is also built on the same "iterate via __getitem__ until IndexError" mixin logic. Checking membership of a value that is present still works, because the walk finds it and returns early. Checking a value that is absent has no other way to know the sequence ended, so it loops forever -- confirmed with a 3-second subprocess timeout that killed the call mid-hang.

4. The fix: raise IndexError, not a sentinel

class GoodSeq(Sequence):
    def __init__(self, data):
        self._data = data
    def __len__(self):
        return len(self._data)
    def __getitem__(self, i):
        if i < 0 or i >= len(self._data):
            raise IndexError(i)
        return self._data[i]

g = GoodSeq([1, 2, 3])
print(list(g))
print(2 in g, 99 in g)
[1, 2, 3]
True False

Measured: once __getitem__ raises IndexError for out-of-range access -- the same contract built-in sequences like list and tuple follow -- __iter__, __contains__, and every other inherited mixin method terminate correctly, including the absent-value membership check that previously hung.

Frequently asked questions

Why does collections.abc.Sequence's __iter__ hang instead of using __len__?

We measured that the default mixin __iter__ is implemented purely in terms of __getitem__: it calls index 0, 1, 2, ... and stops only when it catches an IndexError. It never calls len() to know where to stop, even though you're required to implement __len__ too.

Is this specific to __iter__, or do other Sequence methods break too?

We measured __contains__ breaking the same way, since it's built on the same __getitem__-until-IndexError walk. __reversed__, index(), and count() rely on the same contract and would be expected to have the same failure mode for a __getitem__ that doesn't raise IndexError.

What should __getitem__ return for an out-of-range index in a Sequence subclass?

It must raise IndexError, not return a sentinel like None or False. We measured that built-in types (list, tuple) follow this contract, and Sequence's mixin methods depend on it being followed exactly.

Does this affect a Sequence that only implements positive-index access correctly?

Yes if __getitem__ silently returns a placeholder instead of raising for any out-of-range index, positive or negative, regardless of whether direct indexing inside the valid range works correctly.

Written with AI assistance. Measured on 2026-10-09 with Python 3.13.5's built-in collections.abc.Sequence: subclassed it with a __getitem__ that returns None for out-of-range indices instead of raising IndexError, then called the inherited (mixin) __iter__ and __contains__ and observed them loop without end. PlainHeaders is our extension; Python's collections.abc module is built in.