What does `__getitem__` receive when a custom object is sliced as obj[2:9:2]?
answer
- The brackets build one object first
- Colons are syntax for a small object
- Omitted bounds arrive as None
- A helper method resolves bounds against a length
- Negative integers arrive unchanged
basics
~20 sA single slice object, slice(2, 9, 2), with start, stop and step attributes that may each be None. getitem always takes exactly one key argument, so a slice arrives as one object rather than as separate bounds.
solid answer
~50 s`obj[2:9:2]` calls `__getitem__(slice(2, 9, 2))`. The colon syntax builds a `slice` object before your method runs, and its `start`, `stop` and `step` attributes are whatever was written, with `None` for anything omitted — `obj[::2]` gives `slice(None, None, 2)`. Nothing is validated or clamped in advance: `slice(2, 900)` is a perfectly legal object even on a three-element container. Your method must therefore branch on the key type, typically with `isinstance(key, slice)`, and normalise the bounds itself; `key.indices(len(self))` does that work, returning a concrete `(start, stop, step)` triple already clamped to the length and with negatives resolved. The same applies to plain integers: for a custom class, `obj[-1]` passes `-1` straight through, and converting it to a positive offset is your job. By convention a slice of a sequence returns the same type as the original, while an integer index returns one element.
code
python · 19 linesclass Postings:
def __init__(self, doc_ids):
self._ids = list(doc_ids)
def __len__(self):
return len(self._ids)
def __getitem__(self, key):
if isinstance(key, slice):
return Postings(self._ids[key])
if key < 0:
key += len(self._ids)
if not 0 <= key < len(self._ids):
raise IndexError("postings index out of range")
return self._ids[key]
p = Postings([4, 9, 15, 23, 42])
print(p[1:4:2]._ids, p[-1], len(p[10:99]))go deeper
Know that the colon form builds one slice object and that your method receives it as a single key. Recognise slice(None, None, 2) as what obj[::2] produces, and that omitted bounds show up as None.
Explain the branch on isinstance(key, slice), use slice.indices(len(self)) to normalise, and state clearly that a negative integer index arrives unchanged. Know that IndexError, not KeyError, is the correct out-of-range error for a sequence.
Show the conventions that keep a custom sequence usable by everything else: slices return the same type, out-of-range slices clamp instead of raising, IndexError terminates the legacy iteration and unpacking, and slice assignment is supported honestly or refused with TypeError.
Decide how much of the sequence contract a shared type should promise at all. A partially implemented subscription surface, where indexing works but slice writes quietly misbehave, costs more across a codebase than a small interface that raises clearly on everything it does not support.
## One key argument, always `__getitem__` has a fixed signature: it takes the object and exactly one key. Everything you write between the square brackets is turned into a single object before your method is entered. For `obj[3]` that object is the integer `3`; for `obj['name']` it is the string; and for `obj[2:9:2]` the colon syntax constructs a `slice` instance and passes that. The interpreter does not helpfully expand a slice into three parameters, which is why a method written as `def __getitem__(self, start, stop)` never works. ## The slice object A `slice` has three read-only attributes — `start`, `stop` and `step` — and each is exactly what the source text said, with `None` standing in for anything omitted: - `obj[2:9:2]` gives `slice(2, 9, 2)` - `obj[:5]` gives `slice(None, 5, None)` - `obj[::-1]` gives `slice(None, None, -1)` - `obj[:]` gives `slice(None, None, None)` Crucially, none of these are checked against your container. Slices are just data: `slice(2, 900, 3)` is a valid object regardless of how many items you hold, and out-of-range slice bounds are meant to clamp silently rather than raise, which is the behaviour built-in sequences show when `[1:99]` on a short list quietly returns what exists. Reproducing that convention is your responsibility. ## Normalising with `slice.indices` Writing the clamping logic by hand is fiddly — negative starts, negative steps, missing values and out-of-range bounds interact. The standard tool is `slice.indices(length)`, which returns a three-tuple of concrete integers describing exactly the elements a slice of that length selects, with negatives already resolved and bounds already clamped. Feeding it to `range` gives you the index sequence directly: ```python for i in range(*key.indices(len(self))): ... ``` That one line handles `[::-1]`, `[-3:]`, `[5:1]` (empty) and everything else correctly, which is far better than an ad-hoc chain of `if key.start is None`. ## Negative indices are not normalised for you This is the detail interviewers probe. For built-in sequences, `xs[-1]` returns the last element — but that wrapping is implemented *inside* the built-in type's own subscription code, not by the interpreter before the call. For your class, `obj[-1]` calls `__getitem__(-1)` with the raw negative integer. If you forward it to an underlying list, you inherit list's behaviour and it happens to work; if you index a dict of positions or compute an offset into a buffer, `-1` will do something wrong or raise. Handle it explicitly: ```python if index < 0: index += len(self) if not 0 <= index < len(self): raise IndexError("index out of range") ``` Raising `IndexError` specifically — not `KeyError`, not `ValueError` — matters, because the legacy iteration fallback, `reversed()` over a sequence-shaped class, and unpacking all use `IndexError` as their stop signal. ## What else rides on the sequence contract Getting `__len__` and `__getitem__` right buys more than square brackets. `reversed(obj)` works on any object that offers both, walking indices from `len(obj) - 1` down to zero, so a class with no dedicated `__reversed__` still reverses correctly. Tuple unpacking, `list(obj)` and `for` all lean on the legacy index protocol terminating with `IndexError`. That is why the error type is a contract rather than a taste: raise `KeyError` for an out-of-range integer and iteration does not stop cleanly, it propagates an exception out of a `for` loop that had every right to end quietly. ## Return type conventions An integer key should return a single element; a slice key should return a container of the same kind, so that `postings[1:4]` is itself sliceable and behaves like the original. Returning a bare list from a slice of a richer type is legal but surprising, and it silently drops whatever behaviour the class added. ## Commas and Ellipsis Two more forms turn up. `obj[1:3, 'x']` does not error at the syntax level: the comma builds a tuple, so `__getitem__` receives `(slice(1, 3), 'x')`. That is how multi-dimensional array libraries offer `a[1:3, ::2]` — it is plain Python tuple-building, not special syntax. Likewise `obj[...]` passes the built-in `Ellipsis` object. A one-dimensional class should reject a tuple key with a clear `TypeError` rather than half-handling it. ## The mutating siblings `__setitem__` and `__delitem__` receive exactly the same keys, slices included. `obj[1:3] = values` calls `__setitem__(slice(1, 3), values)`, and `del obj[::2]` calls `__delitem__(slice(None, None, 2))`. If you support slice reads, decide deliberately whether you support slice writes, because a partially implemented protocol — indexing works, slice assignment silently misbehaves — is worse than one that raises `TypeError` honestly.
- How do you turn an arbitrary slice into concrete positions for your own storage?Call `key.indices(len(self))`. It returns a `(start, stop, step)` triple of plain integers, with `None` values filled in, negative values resolved against the length, and out-of-range bounds clamped, including for a negative step. Passing that triple to `range` gives the exact index sequence to read, which handles reversal and empty slices without any hand-written conditionals.
- Who converts obj[-1] into a positive index for a custom class?Nobody — you do. The interpreter passes `-1` through to `__getitem__` unchanged. Built-in sequences appear to handle it automatically only because their own subscription code adds the length before indexing. In a custom class, add `len(self)` to a negative index yourself and raise `IndexError` if the result is still out of range.
- What does a custom class receive for obj[1:3, 'x']?A single tuple, `(slice(1, 3, None), 'x')`. The comma builds a tuple before the call, which is ordinary Python syntax rather than anything special to subscription — it is exactly how multi-dimensional array libraries express `a[1:3, ::2]`. A one-dimensional container should detect a tuple key and raise `TypeError` with a clear message instead of partially handling it.
saying these in an interview costs you the question
- Expecting __getitem__ to be called with separate start and stop arguments
- Assuming Python normalises negative indices for your class
- Thinking slice bounds are validated before __getitem__ runs
- Ignoring the step attribute and only reading start and stop
- Raising KeyError instead of IndexError for an out-of-range index
- Believing a comma inside brackets is a syntax error