elegent
elegent

Reputation: 4007

How to raise an IndexError when slice indices are out of range?

The Python Documentation states that

slice indices are silently truncated to fall in the allowed range

and therefor no IndexErrors are risen when slicing a list, regardless what start or stop parameters are used:

>>> egg = [1, "foo", list()]
>>> egg[5:10]
[]

Since the list egg does not contain any indices greater then 2, a egg[5] or egg[10] call would raise an IndexError:

>> egg[5]
Traceback (most recent call last):
IndexError: list index out of range

The question is now, how can we raise an IndexError, when both given slice indices are out of range?

Upvotes: 8

Views: 21043

Answers (2)

Martijn Pieters
Martijn Pieters

Reputation: 1123440

There is no silver bullet here; you'll have to test both boundaries:

def slice_out_of_bounds(sequence, start=None, stop=None, step=1):
    length = len(sequence)
    if start is None:  # default depends on step direction
        start = length - 1 if step < 0 else 0
    elif start < 0:  # relative to length
        start += length
    if stop is None:  # default depends on step direction
        stop = -1 if step < 0 else length
    elif stop < 0:  # relative to length
        stop += length
    # stop can range [0 .. length] for positive steps or
    # [-1 .. (length - 1)] for negative steps (both bounds inclusive).
    # adjust stop for negative steps to make the bounds check easier.
    if step < 0:
        stop += 1
    if not (0 <= start < length and 0 <= stop <= length):
        raise IndexError("Container slice indices out of bounds")

Note the way we need to handle stop specially. The end value in slicing is exclusive, so is allowed to range up to length. For negative strides, valid stop values lie in the range [-1 .. length) (or [-1 .. (length - 1)] with inclusive bounds), but adding 1 to the stop value makes it possible to re-use the same 0 <= stop <= length test.

If you create your own container class with an __getitem__() method, then slicing the container gives you a slice() instance. Don't use the slice.indices() method in that case, it adjusts the bounds for you rather than raise an IndexError. Instead, us the .start, .stop and .step attributes. Each of those can be None, including .step, so make sure to handle that too.

Here's a __getitem__ version of the above, with a few added checks for other edge cases to replicate how list slicing works:

class SomeContainer:
    ...

    def __getitem__(self, key):
        length = len(self)

        if isinstance(key, int) and not (0 <= key < length):
            raise IndexError("Container index out of range")

        elif isinstance(key, slice):
            if key.step == 0:
                raise ValueError("Slice step cannot be zero")
            start, stop, step = key.start, key.stop, key.step or 1
            if start is None:  # default depends on step direction
                start = length - 1 if step < 0 else 0
            elif start < 0:  # relative to length
                start += length
            if end is None:  # default depends on step direction
                end = -1 if step < 0 else length
            elif end < 0:  # relative to length
                end += length
            # end can range [0 .. length] for positive steps or
            # [-1 .. (length - 1)] for negative steps (both bounds inclusive).
            # adjust end for negitive steps to make the bounds check easier.
            # Don't do this if you also wanted to calculate the slice length!
            if step < 0:
                end += 1
            if not (0 <= start < length and 0 <= end <= length):
                raise IndexError("Container slice indices out of bounds")

         else:
            raise TypeError(f"list indices must be integers or slices, not {type(key)}")


Upvotes: 3

Vladimir Chub
Vladimir Chub

Reputation: 471

In Python 2 you can override __getslice__ method by this way:

class MyList(list):
    def __getslice__(self, i, j):
        len_ = len(self)
        if i > len_ or j > len_:
            raise IndexError('list index out of range')
        return super(MyList, self).__getslice__(i, j)

Then use your class instead of list:

>>> egg = [1, "foo", list()]
>>> egg = MyList(egg)
>>> egg[5:10]
Traceback (most recent call last):
IndexError: list index out of range

Upvotes: 4

Related Questions