Prerequisites
- Basic understanding of programming concepts ๐
- Python installation (3.8+) ๐
- VS Code or preferred IDE ๐ป
What you'll learn
- Understand the concept fundamentals ๐ฏ
- Apply the concept in real projects ๐๏ธ
- Debug common issues ๐
- Write clean, Pythonic code โจ
๐ฏ Introduction
Welcome to this exciting tutorial on Pythonโs Container Protocol! ๐ In this guide, weโll explore how __len__
and __getitem__
transform your custom objects into powerful, container-like structures that work seamlessly with Pythonโs built-in functions.
Youโll discover how these special methods (also called โdunder methodsโ or โmagic methodsโ) can make your objects behave like lists, dictionaries, or any other container you can imagine. Whether youโre building data structures ๐, game inventories ๐ฎ, or custom collections ๐, understanding the container protocol is essential for writing elegant, Pythonic code.
By the end of this tutorial, youโll feel confident creating your own container classes that integrate beautifully with Pythonโs ecosystem! Letโs dive in! ๐โโ๏ธ
๐ Understanding Container Protocol
๐ค What is the Container Protocol?
The Container Protocol is like giving your objects superpowers ๐ฆธโโ๏ธ. Think of it as teaching your custom objects to speak Pythonโs native language, allowing them to work with built-in functions like len()
, indexing with []
, and iteration.
In Python terms, the container protocol consists of special methods that define how your object behaves as a container. This means you can:
- โจ Use
len()
to get the size of your object - ๐ Access items using square bracket notation
obj[index]
- ๐ก๏ธ Make your objects iterable with
for
loops
๐ก Why Use Container Protocol?
Hereโs why developers love implementing container protocols:
- Pythonic Interface ๐: Your objects work like built-in types
- Intuitive Usage ๐ป: Users already know how to use containers
- Powerful Integration ๐: Works with Pythonโs built-in functions
- Clean Code ๐ง: No need for custom getter methods
Real-world example: Imagine building a playlist manager ๐ต. With the container protocol, users can check playlist length with len(playlist)
and access songs with playlist[0]
- just like they would with a regular list!
๐ง Basic Syntax and Usage
๐ Simple Example
Letโs start with a friendly example:
# ๐ Hello, Container Protocol!
class SimplePlaylist:
def __init__(self):
self.songs = [] # ๐ต Our song collection
def __len__(self):
# ๐ Return the number of songs
return len(self.songs)
def __getitem__(self, index):
# ๐ฏ Return song at given index
return self.songs[index]
def add_song(self, song):
# โ Add a new song
self.songs.append(song)
print(f"๐ต Added: {song}")
# ๐ฎ Let's use it!
playlist = SimplePlaylist()
playlist.add_song("Python Blues ๐ธ")
playlist.add_song("Container Rock ๐น")
print(f"Playlist length: {len(playlist)}") # Uses __len__
print(f"First song: {playlist[0]}") # Uses __getitem__
๐ก Explanation: Notice how we can use len()
and []
indexing on our custom object! Python automatically calls our __len__
and __getitem__
methods.
๐ฏ Common Patterns
Here are patterns youโll use daily:
# ๐๏ธ Pattern 1: Dictionary-like container
class ConfigManager:
def __init__(self):
self._config = {} # ๐ฆ Private storage
def __len__(self):
return len(self._config)
def __getitem__(self, key):
if key not in self._config:
raise KeyError(f"๐ซ Config '{key}' not found!")
return self._config[key]
def __setitem__(self, key, value):
# ๐จ Bonus: Allow setting items too!
self._config[key] = value
print(f"โ
Set {key} = {value}")
# ๐ Pattern 2: Slicing support
class NumberSequence:
def __init__(self, start, end):
self.numbers = list(range(start, end))
def __len__(self):
return len(self.numbers)
def __getitem__(self, index):
# ๐ Support both indexing and slicing!
if isinstance(index, slice):
return NumberSequence(0, 0).__class__(self.numbers[index])
return self.numbers[index]
๐ก Practical Examples
๐ Example 1: Shopping Cart Container
Letโs build something real:
# ๐๏ธ A smart shopping cart
class ShoppingCart:
def __init__(self):
self.items = [] # ๐ฆ Cart items
self.quantities = {} # ๐ข Item quantities
def __len__(self):
# ๐ Total number of unique items
return len(self.items)
def __getitem__(self, index):
# ๐ฏ Get item by index or name
if isinstance(index, int):
return self.items[index]
elif isinstance(index, str):
# ๐จ Allow string indexing too!
for item in self.items:
if item['name'] == index:
return item
raise KeyError(f"๐ซ Item '{index}' not in cart!")
def add_item(self, name, price, emoji="๐๏ธ"):
# โ Add or update item
for item in self.items:
if item['name'] == name:
self.quantities[name] += 1
print(f"โ Added another {emoji} {name}")
return
# ๐ New item
self.items.append({
'name': name,
'price': price,
'emoji': emoji
})
self.quantities[name] = 1
print(f"๐ Added {emoji} {name} - ${price}")
def __iter__(self):
# ๐ Make cart iterable
return iter(self.items)
def total(self):
# ๐ฐ Calculate total price
total = 0
for item in self.items:
total += item['price'] * self.quantities[item['name']]
return total
# ๐ฎ Let's go shopping!
cart = ShoppingCart()
cart.add_item("Python Book", 29.99, "๐")
cart.add_item("Coffee", 4.99, "โ")
cart.add_item("Python Book", 29.99, "๐") # Adding another!
print(f"\n๐ Cart has {len(cart)} unique items")
print(f"๐ First item: {cart[0]['name']}")
print(f"โ Coffee details: {cart['Coffee']}")
print(f"๐ฐ Total: ${cart.total():.2f}")
# ๐ Iterate through cart
print("\n๐ Cart contents:")
for item in cart:
qty = cart.quantities[item['name']]
print(f" {item['emoji']} {item['name']} x{qty} - ${item['price'] * qty:.2f}")
๐ฏ Try it yourself: Add a __delitem__
method to remove items from the cart!
๐ฎ Example 2: Game Inventory System
Letโs make it fun:
# ๐ RPG-style inventory system
class GameInventory:
def __init__(self, capacity=20):
self.items = [] # ๐ Inventory items
self.capacity = capacity # ๐ Max capacity
self.categories = {
'weapon': 'โ๏ธ',
'armor': '๐ก๏ธ',
'potion': '๐งช',
'treasure': '๐',
'food': '๐'
}
def __len__(self):
# ๐ Current inventory size
return len(self.items)
def __getitem__(self, key):
# ๐ฏ Get by index, name, or category
if isinstance(key, int):
return self.items[key]
elif isinstance(key, str):
# ๐ Search by name
matching_items = [item for item in self.items if item['name'] == key]
if matching_items:
return matching_items[0] if len(matching_items) == 1 else matching_items
# ๐ท๏ธ Search by category
category_items = [item for item in self.items if item['category'] == key]
if category_items:
return category_items
raise KeyError(f"๐ซ No items found for '{key}'")
def add_item(self, name, category, value, quantity=1):
# โ Add item to inventory
if len(self.items) >= self.capacity:
print(f"๐ Inventory full! Can't add {name}")
return False
# ๐ Check if item exists
for item in self.items:
if item['name'] == name:
item['quantity'] += quantity
print(f"โ Added {quantity} more {name}(s)")
return True
# ๐ New item
emoji = self.categories.get(category, 'โ')
self.items.append({
'name': name,
'category': category,
'value': value,
'quantity': quantity,
'emoji': emoji
})
print(f"{emoji} Found {name}! (Value: {value} gold)")
return True
def __contains__(self, item_name):
# ๐ Check if item exists (enables 'in' operator)
return any(item['name'] == item_name for item in self.items)
def get_stats(self):
# ๐ Inventory statistics
total_value = sum(item['value'] * item['quantity'] for item in self.items)
print(f"\n๐ Inventory Stats:")
print(f" ๐ Items: {len(self)}/{self.capacity}")
print(f" ๐ฐ Total Value: {total_value} gold")
# ๐ท๏ธ Items by category
for category, emoji in self.categories.items():
category_items = self[category] if category in ['weapon', 'armor', 'potion', 'treasure', 'food'] else []
if category_items:
count = sum(item['quantity'] for item in category_items)
print(f" {emoji} {category.title()}: {count}")
# ๐ฎ Adventure time!
inventory = GameInventory(capacity=10)
# ๐ก๏ธ Finding loot!
inventory.add_item("Iron Sword", "weapon", 100)
inventory.add_item("Health Potion", "potion", 50, 3)
inventory.add_item("Dragon Scale", "armor", 500)
inventory.add_item("Bread", "food", 5, 2)
inventory.add_item("Ruby", "treasure", 250)
# ๐ฏ Using container protocol
print(f"\n๐ Inventory has {len(inventory)} item types")
print(f"โ๏ธ First item: {inventory[0]['name']}")
print(f"๐งช Potions: {inventory['potion']}")
# ๐ Check for items
if "Iron Sword" in inventory:
print("โ๏ธ You have a weapon equipped!")
inventory.get_stats()
๐ Advanced Concepts
๐งโโ๏ธ Advanced Topic 1: Supporting Negative Indexing
When youโre ready to level up, add support for negative indexing:
# ๐ฏ Advanced container with full indexing support
class SmartList:
def __init__(self, items=None):
self._items = items or []
def __len__(self):
return len(self._items)
def __getitem__(self, index):
# ๐ Support negative indexing and slicing!
if isinstance(index, slice):
# ๐จ Return new SmartList for slices
return SmartList(self._items[index])
# ๐ก๏ธ Handle negative indices
if isinstance(index, int):
if index < 0:
index += len(self)
if 0 <= index < len(self):
return self._items[index]
raise IndexError(f"๐ซ Index {index} out of range!")
raise TypeError(f"๐ซ Invalid index type: {type(index)}")
def __setitem__(self, index, value):
# โจ Allow item modification
if isinstance(index, int):
if index < 0:
index += len(self)
if 0 <= index < len(self):
self._items[index] = value
return
raise IndexError(f"๐ซ Cannot set index {index}")
def __repr__(self):
# ๐จ Pretty representation
return f"SmartList({self._items})"
# ๐ช Using the smart list
smart = SmartList(['Python', 'is', 'awesome', '!'])
print(f"Last item: {smart[-1]}") # Negative indexing!
print(f"Slice: {smart[1:3]}") # Slicing support!
smart[-1] = '๐' # Modify with negative index
print(f"Modified: {smart._items}")
๐๏ธ Advanced Topic 2: Lazy Evaluation Container
For the brave developers:
# ๐ Container with lazy evaluation
class FibonacciSequence:
def __init__(self, max_items=None):
self.max_items = max_items
self._cache = [0, 1] # ๐พ Cache computed values
def __len__(self):
# ๐ Return max items or infinity indicator
if self.max_items is None:
return float('inf')
return self.max_items
def __getitem__(self, index):
# ๐ฏ Compute Fibonacci numbers on demand
if isinstance(index, slice):
# ๐จ Handle slicing
start = index.start or 0
stop = index.stop
if stop is None:
raise ValueError("๐ซ Need a stop value for slicing!")
return [self[i] for i in range(start, stop)]
if index < 0:
raise IndexError("๐ซ Negative indices not supported")
# ๐ก Extend cache if needed
while len(self._cache) <= index:
self._cache.append(
self._cache[-1] + self._cache[-2]
)
return self._cache[index]
def __iter__(self):
# ๐ Iterate up to max_items
for i in range(len(self)):
yield self[i]
# ๐ฎ Infinite Fibonacci sequence!
fib = FibonacciSequence()
print(f"10th Fibonacci: {fib[10]}")
print(f"Slice [5:10]: {fib[5:10]}")
# ๐ฏ Limited sequence
fib_limited = FibonacciSequence(max_items=10)
print(f"\nFirst 10 Fibonacci numbers:")
for i, num in enumerate(fib_limited):
print(f" F({i}) = {num}")
โ ๏ธ Common Pitfalls and Solutions
๐ฑ Pitfall 1: Forgetting Index Validation
# โ Wrong way - no bounds checking!
class UnsafeContainer:
def __init__(self):
self.items = [1, 2, 3]
def __getitem__(self, index):
return self.items[index] # ๐ฅ Will crash on bad index!
# โ
Correct way - validate your indices!
class SafeContainer:
def __init__(self):
self.items = [1, 2, 3]
def __len__(self):
return len(self.items)
def __getitem__(self, index):
# ๐ก๏ธ Proper validation
if not isinstance(index, int):
raise TypeError(f"๐ซ Index must be integer, not {type(index).__name__}")
# ๐ฏ Handle negative indices
if index < 0:
index += len(self)
# โ
Bounds checking
if 0 <= index < len(self):
return self.items[index]
raise IndexError(f"๐ซ Index {index} out of range!")
๐คฏ Pitfall 2: Inconsistent Length
# โ Dangerous - len doesn't match actual items!
class ConfusingContainer:
def __init__(self):
self.items = [1, 2, 3]
def __len__(self):
return 10 # ๐ฅ Lies about length!
def __getitem__(self, index):
return self.items[index]
# โ
Safe - length matches reality!
class HonestContainer:
def __init__(self):
self.items = [1, 2, 3]
self._virtual_size = 10 # ๐ Track separately if needed
def __len__(self):
return len(self.items) # โ
Always return true length
def __getitem__(self, index):
if 0 <= index < len(self):
return self.items[index]
elif index < self._virtual_size:
return None # ๐ฏ Return placeholder for virtual items
raise IndexError(f"๐ซ Index out of range!")
๐ ๏ธ Best Practices
- ๐ฏ Always Implement
__len__
: If you have__getitem__
, add__len__
too! - ๐ Validate Inputs: Check index types and ranges
- ๐ก๏ธ Handle Edge Cases: Negative indices, slices, and special values
- ๐จ Be Consistent: Your container should behave predictably
- โจ Consider Iterator Protocol: Add
__iter__
for enhanced functionality
๐งช Hands-On Exercise
๐ฏ Challenge: Build a Matrix Container
Create a 2D matrix class that supports the container protocol:
๐ Requirements:
- โ
Support 2D indexing with tuples:
matrix[row, col]
- ๐ท๏ธ Implement
len()
to return total elements - ๐ค Add row and column access:
matrix[0]
gets first row - ๐ Support slicing for submatrices
- ๐จ Pretty print the matrix
๐ Bonus Points:
- Add matrix multiplication support
- Implement transpose operation
- Create addition and subtraction operators
๐ก Solution
๐ Click to see solution
# ๐ฏ Our 2D Matrix container!
class Matrix:
def __init__(self, data):
# ๐ Initialize with 2D list
if not data or not all(len(row) == len(data[0]) for row in data):
raise ValueError("๐ซ Invalid matrix data!")
self.data = data
self.rows = len(data)
self.cols = len(data[0]) if data else 0
def __len__(self):
# ๐ Total number of elements
return self.rows * self.cols
def __getitem__(self, key):
# ๐ฏ Support various indexing styles
if isinstance(key, tuple):
# ๐ 2D indexing: matrix[row, col]
row, col = key
if 0 <= row < self.rows and 0 <= col < self.cols:
return self.data[row][col]
raise IndexError(f"๐ซ Index {key} out of bounds!")
elif isinstance(key, int):
# ๐ Get entire row
if 0 <= key < self.rows:
return self.data[key]
raise IndexError(f"๐ซ Row {key} out of bounds!")
elif isinstance(key, slice):
# ๐จ Return submatrix
return Matrix(self.data[key])
raise TypeError(f"๐ซ Invalid index type: {type(key)}")
def __setitem__(self, key, value):
# โ๏ธ Allow modification
if isinstance(key, tuple):
row, col = key
if 0 <= row < self.rows and 0 <= col < self.cols:
self.data[row][col] = value
return
raise IndexError(f"๐ซ Cannot set at {key}")
def __str__(self):
# ๐จ Pretty print
result = "Matrix(\n"
for row in self.data:
result += f" {row}\n"
result += ")"
return result
def transpose(self):
# ๐ Return transposed matrix
transposed = [[self.data[i][j] for i in range(self.rows)]
for j in range(self.cols)]
return Matrix(transposed)
def __add__(self, other):
# โ Matrix addition
if not isinstance(other, Matrix):
raise TypeError("๐ซ Can only add matrices!")
if self.rows != other.rows or self.cols != other.cols:
raise ValueError("๐ซ Matrix dimensions must match!")
result = [[self.data[i][j] + other.data[i][j]
for j in range(self.cols)]
for i in range(self.rows)]
return Matrix(result)
def shape(self):
# ๐ Return matrix dimensions
return (self.rows, self.cols)
# ๐ฎ Test it out!
# Create a 3x3 matrix
matrix = Matrix([
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
])
print(f"๐ Matrix has {len(matrix)} elements")
print(f"๐ Shape: {matrix.shape()}")
print(f"\n๐ฏ Element at [1, 2]: {matrix[1, 2]}")
print(f"๐ First row: {matrix[0]}")
# Modify element
matrix[0, 0] = 99
print(f"\nโ๏ธ After modification:")
print(matrix)
# Create another matrix and add
matrix2 = Matrix([
[9, 8, 7],
[6, 5, 4],
[3, 2, 1]
])
result = matrix + matrix2
print(f"\nโ Addition result:")
print(result)
# Transpose
transposed = matrix.transpose()
print(f"\n๐ Transposed:")
print(transposed)
๐ Key Takeaways
Youโve learned so much! Hereโs what you can now do:
- โ
Implement
__len__
and__getitem__
to create container objects ๐ช - โ Support advanced indexing including negative indices and slicing ๐ก๏ธ
- โ Build custom collections that integrate with Pythonโs built-ins ๐ฏ
- โ Handle edge cases and validate inputs properly ๐
- โ Create powerful data structures that feel natural to use! ๐
Remember: The container protocol makes your objects feel like native Python containers. Itโs a powerful way to create intuitive, Pythonic interfaces! ๐ค
๐ค Next Steps
Congratulations! ๐ Youโve mastered the container protocol with __len__
and __getitem__
!
Hereโs what to do next:
- ๐ป Practice with the matrix exercise above
- ๐๏ธ Add
__setitem__
and__delitem__
to your containers - ๐ Explore the iterator protocol with
__iter__
and__next__
- ๐ Build a custom collection for your next project!
Remember: Every Python expert started by implementing their first __getitem__
. Keep coding, keep learning, and most importantly, have fun! ๐
Happy coding! ๐๐โจ