Introduction
Welcome back. In the previous lesson, you successfully implemented __getitem__ to support integer-based indexing, allowing you to access items in your custom objects with the familiar my_object[i] syntax. You learned that this syntax is just a convenient wrapper for a call to the __getitem__ method.
This lesson extends that knowledge to one of Python's most powerful features: slicing. We'll see how the very same __getitem__ method is responsible for handling slice notation like my_object[1:5:2]. You'll learn that when Python encounters a slice, it constructs a special slice object and passes it to __getitem__. Your goal is to learn how to interpret this object to enable full, Pythonic slicing on your custom classes.
Understanding the slice Object
The first thing to understand is that the colon-based slicing syntax is, like indexing, syntactic sugar. When you write my_list[1:5:2], Python internally translates this into my_list.__getitem__(slice(1, 5, 2)).
The key here is the built-in slice class. It's an object that simply holds the start, stop, and step parameters.

To get a formal introduction to the slice object, let's look at the following reading.
Inner workings of sequence slicing
This article, "Inner workings of sequence slicing" from Pydon't, explains that slicing syntax is a shortcut for creating slice objects and that these objects have .start, .stop, and .step attributes.
Read the first two sections, "The slice class" and "Slicing parameters". This will solidify your understanding of how slice notation maps to a slice object.
Detecting a Slice in __getitem__
Since __getitem__ handles both indexing and slicing, your method needs to be able to distinguish between an integer key and a slice key. This is straightforwardly done with an isinstance() check.
Let's prove that Python passes a slice object. We can create a simple class that just prints the key it receives.
class SliceDetector:
def __getitem__(self, key):
print(f"__getitem__ received a key: {key}")
print(f"The key's type is: {type(key)}")
detector = SliceDetector()
print("--- Calling with an integer index ---")
detector[5]
print("\n--- Calling with a slice ---")
detector[2:10:2]
print("\n--- Calling with an explicit slice object ---")
my_slice = slice(1, 3)
detector[my_slice]
Running this code produces the following output:
--- Calling with an integer index ---
__getitem__ received a key: 5
The key's type is: <class 'int'>
--- Calling with a slice ---
__getitem__ received a key: slice(2, 10, 2)
The key's type is: <class 'slice'>
--- Calling with an explicit slice object ---
__getitem__ received a key: slice(1, 3, None)
The key's type is: <class 'slice'>
This confirms that our __getitem__ method must be prepared to handle both int and slice objects.
The following reading provides another example of a class that intercepts the call to __getitem__ to demonstrate this very point.
Inner workings of sequence slicing
This section from the same Pydon't article shows a class S that inherits from str and overrides __getitem__ to show what argument it receives.
Read the section "Getting items from sequences". Focus on the second code block, where the __getitem__ method is modified to print the idx argument. Observe how s[1::2] results in idx being a slice object.
Implementing Slicing: Two Approaches
Now that we can detect a slice, how do we implement the logic? There are two main scenarios.
1. The Easy Way: Delegation
If your class wraps a built-in sequence like a list or tuple, the work is already done for you. Built-in sequences know how to handle slice objects. You can simply delegate the slicing operation to your internal data structure.
The idiomatic pattern is to have the slice operation return a new instance of your class, containing the sliced data.
class MySequence:
def __init__(self, data):
self._data = list(data) # Store data internally as a list
def __repr__(self):
return f"{self.__class__.__name__}({self._data!r})"
def __len__(self):
return len(self._data)
def __getitem__(self, key):
if isinstance(key, slice):
# If it's a slice, delegate to the internal list
# and wrap the result in a new instance of this class.
return self.__class__(self._data[key])
else:
# If it's an integer, just return the item.
return self._data[key]
# --- Usage ---
seq = MySequence("abcdefg")
print(f"Original: {seq}")
sliced_seq = seq[1:5] # __getitem__ receives slice(1, 5, None)
print(f"Sliced: {sliced_seq}")
print(f"Type of sliced_seq: {type(sliced_seq)}")
This elegant approach leverages Python's existing machinery. The following resource demonstrates this exact pattern with a ProxySequence class.
This article from Python Morsels explains how to implement slicing. Pay close attention to the simplified __getitem__ implementation at the end.
Read the section "Implementing slicing". Focus on the final, simplified version of the __getitem__ method, which demonstrates delegating the slice operation directly to the wrapped sequence and returning a new instance of its own class.
2. The Manual Way: When You Can't Delegate
Sometimes your data isn't stored in a sliceable container. It might be generated on the fly, or stored in a complex structure. In these cases, you need to implement the slicing logic yourself.
This presents a challenge: the start, stop, and step attributes of a slice object can be None or negative. Writing the logic to correctly interpret my_obj[:-3] or my_obj[2:] for any sequence length is tedious and error-prone.
Fortunately, Python provides a powerful helper method: slice.indices().
The slice.indices(length) method takes the length of your sequence and returns a three-element tuple (start, stop, step) with all None and negative values resolved into concrete, non-negative indices ready to be used in a range().
Let's explore how this works.
The Python Morsels article you just saw also provides an excellent explanation of the slice.indices() method.
Now, read the section "Using slice objects". This will explain the .indices() method and how it translates abstract slices into concrete indices for a given sequence length.
With slice.indices(), a manual implementation becomes much cleaner. For a great example of a class that cannot delegate—because it represents an infinite sequence—let's examine the GeometricProgression class.
Inner workings of sequence slicing
The GeometricProgression example from the Pydon't article demonstrates a perfect use case for manual slice implementation. Since the progression is infinite, it can't be stored in a list, so we must calculate the requested terms on the fly.
Read the section "Custom arithmetic and geometric sequences". Notice how the __getitem__ method checks if idx is an int or a slice. For slices, it unpacks the attributes and uses them in a list comprehension with range(). (Note: This example manually handles None values with or; using slice.indices() as shown in the Python Morsels article is generally more robust, but this shows the underlying logic.)
Exercise: Implement Slicing for ReadOnlyPortfolio
It's time to apply what you've learned. In the last lesson, you implemented integer indexing for the ReadOnlyPortfolio class. Now, extend its __getitem__ method to support slicing. A slice of a portfolio should return a new ReadOnlyPortfolio instance containing the sliced tickers.
Since the tickers are stored in a list, delegation is the most suitable approach.
# initial_code.py
class ReadOnlyPortfolio:
"""A read-only collection of stock tickers."""
def __init__(self, tickers: list[str]):
self._tickers = list(tickers)
def __len__(self) -> int:
return len(self._tickers)
def __repr__(self) -> str:
"""Provides a developer-friendly representation."""
return f"{self.__class__.__name__}({self._tickers!r})"
def __getitem__(self, key):
"""
TODO: Update this method.
It should handle both integer and slice keys.
- If key is an integer, return the ticker (str) at that index.
- If key is a slice, return a new ReadOnlyPortfolio instance
containing the sliced tickers.
"""
# Your implementation from the last lesson should be the starting point.
# Now extend it.
return self._tickers[key]
# --- Tests ---
portfolio = ReadOnlyPortfolio(["AAPL", "GOOG", "MSFT", "TSLA", "AMZN", "NVDA"])
# Test 1: Integer indexing should still work
assert portfolio[0] == "AAPL"
assert portfolio[-1] == "NVDA"
# Test 2: Basic slicing
sliced1 = portfolio[1:4]
assert isinstance(sliced1, ReadOnlyPortfolio), "Slicing should return a ReadOnlyPortfolio instance."
assert list(sliced1) == ["GOOG", "MSFT", "TSLA"]
# Test 3: Slicing with steps
sliced2 = portfolio[0:6:2]
assert isinstance(sliced2, ReadOnlyPortfolio)
assert list(sliced2) == ["AAPL", "MSFT", "AMZN"]
# Test 4: Slicing with open ends
sliced3 = portfolio[3:]
assert isinstance(sliced3, ReadOnlyPortfolio)
assert list(sliced3) == ["TSLA", "AMZN", "NVDA"]
# Test 5: The representation should be correct
assert repr(sliced3) == "ReadOnlyPortfolio(['TSLA', 'AMZN', 'NVDA'])"
print("All tests passed!")
Click to see the solution
# solution_code.py
class ReadOnlyPortfolio:
"""A read-only collection of stock tickers."""
def __init__(self, tickers: list[str]):
self._tickers = list(tickers)
def __len__(self) -> int:
return len(self._tickers)
def __repr__(self) -> str:
"""Provides a developer-friendly representation."""
return f"{self.__class__.__name__}({self._tickers!r})"
def __getitem__(self, key):
"""
Handles both integer and slice keys.
- If key is an integer, return the ticker (str) at that index.
- If key is a slice, return a new ReadOnlyPortfolio instance
containing the sliced tickers.
"""
if isinstance(key, slice):
# Delegate slicing to the internal list and wrap the result
# in a new instance of the current class (`self.__class__`).
return self.__class__(self._tickers[key])
# If not a slice, handle as an integer index (also by delegation).
return self._tickers[key]
# --- Tests ---
portfolio = ReadOnlyPortfolio(["AAPL", "GOOG", "MSFT", "TSLA", "AMZN", "NVDA"])
# Test 1: Integer indexing should still work
assert portfolio[0] == "AAPL"
assert portfolio[-1] == "NVDA"
# Test 2: Basic slicing
sliced1 = portfolio[1:4]
assert isinstance(sliced1, ReadOnlyPortfolio), "Slicing should return a ReadOnlyPortfolio instance."
assert list(sliced1) == ["GOOG", "MSFT", "TSLA"]
# Test 3: Slicing with steps
sliced2 = portfolio[0:6:2]
assert isinstance(sliced2, ReadOnlyPortfolio)
assert list(sliced2) == ["AAPL", "MSFT", "AMZN"]
# Test 4: Slicing with open ends
sliced3 = portfolio[3:]
assert isinstance(sliced3, ReadOnlyPortfolio)
assert list(sliced3) == ["TSLA", "AMZN", "NVDA"]
# Test 5: The representation should be correct
assert repr(sliced3) == "ReadOnlyPortfolio(['TSLA', 'AMZN', 'NVDA'])"
print("All tests passed!")
Conclusion
You have now mastered __getitem__, enabling your custom objects to fully support both indexing and slicing. This brings your classes another large step closer to feeling like native Python types.
Key Takeaways:
- The
__getitem__method handles both indexing (intkey) and slicing (slicekey). - Slicing syntax
[start:stop:step]is a shortcut for creating asliceobject. - Use
isinstance(key, slice)to determine how to handle the request. - Delegation is best: If your class wraps a sliceable container (like a list), simply pass the key to it and return a new instance of your class with the result:
return self.__class__(self._data[key]). - For manual implementation: Use the
slice.indices(length)method to convert the abstractsliceobject into a concrete(start, stop, step)tuple suitable for iteration.
We've now covered how to make your objects behave like native sequences for reading data. But what about controlled access for writing or computing attributes? In the next lesson, we will explore the Pythonic way to manage attributes with the @property decorator—the direct replacement for the Java-style getter and setter methods you're aiming to move beyond.