+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Part 143 of 365

๐Ÿ“˜ __len__, __getitem__: Container Protocol

Master __len__, __getitem__: container protocol in Python with practical examples, best practices, and real-world applications ๐Ÿš€

๐Ÿš€Intermediate
25 min read

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:

  1. Pythonic Interface ๐Ÿ: Your objects work like built-in types
  2. Intuitive Usage ๐Ÿ’ป: Users already know how to use containers
  3. Powerful Integration ๐Ÿ“–: Works with Pythonโ€™s built-in functions
  4. 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

  1. ๐ŸŽฏ Always Implement __len__: If you have __getitem__, add __len__ too!
  2. ๐Ÿ“ Validate Inputs: Check index types and ranges
  3. ๐Ÿ›ก๏ธ Handle Edge Cases: Negative indices, slices, and special values
  4. ๐ŸŽจ Be Consistent: Your container should behave predictably
  5. โœจ 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:

  1. ๐Ÿ’ป Practice with the matrix exercise above
  2. ๐Ÿ—๏ธ Add __setitem__ and __delitem__ to your containers
  3. ๐Ÿ“š Explore the iterator protocol with __iter__ and __next__
  4. ๐ŸŒŸ 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! ๐ŸŽ‰๐Ÿš€โœจ