__setitem__

__setitem__(self, key, value)
Returns: None · Added in vPython 1.0 · Updated March 18, 2026 · Keywords
python dunder subscript assignment container

__setitem__

Python’s __setitem__ method lets you define what happens when you assign to a subscript—that is, when you use bracket notation to set a value: obj[key] = value.

Overview

__setitem__ is a special method that gets called automatically whenever you assign to a subscripted expression. It’s the setter counterpart to __getitem__, which handles reading values.

Python calls __setitem__ automatically when you use:

  • obj[key] = value
  • obj[key] += value (calls __setitem__ after __getitem__)
  • Slice assignment like obj[start:end] = values

Syntax

def __setitem__(self, key, value):
    # Handle the assignment
    # Typically doesn't return anything (returns None implicitly)

Key points:

  • Takes three parameters: self, key, and value
  • The key can be an integer, string, or even a slice object
  • Does not need to return a value (returning a value is ignored)

How It Works

When you define __setitem__, you control what happens when someone assigns to your object using bracket notation. This is essential for building custom containers that behave like lists or dictionaries.

class ScoreBoard:
    def __init__(self):
        self.scores = {}
    
    def __setitem__(self, player, score):
        self.scores[player] = score

board = ScoreBoard()
board["Alice"] = 100     # Calls __setitem__
board["Bob"] = 85        # Calls __setitem__

print(board.scores)

Basic Example

Here’s a simple custom list that tracks assignments:

class TrackedList:
    def __init__(self):
        self.items = []
    
    def __setitem__(self, index, value):
        print(f"Setting index {index} to {value}")
        self.items[index] = value
    
    def __getitem__(self, index):
        return self.items[index]
    
    def __len__(self):
        return len(self.items)

lst = TrackedList()
lst[0] = "first"        # Setting index 0 to first
lst[1] = "second"       # Setting index 1 to second

print(lst[0], lst[1])   # first second
# output: first second

Sequences vs Mappings

The type of exception you raise matters. Python distinguishes between sequences and mappings:

  • Sequences (list-like): raise IndexError for invalid integer indices
  • Mappings (dict-like): raise KeyError for invalid keys
class IntList:
    """A list that only accepts integers."""
    def __init__(self, size):
        self.data = [None] * size
    
    def __setitem__(self, key, value):
        if not isinstance(key, int):
            raise TypeError(f"indices must be integers, not {type(key).__name__}")
        if key < 0 or key >= len(self.data):
            raise IndexError(f"index {key} out of range for size {len(self.data)}")
        if not isinstance(value, int):
            raise TypeError(f"only integers allowed, got {type(value).__name__}")
        self.data[key] = value

nums = IntList(3)
nums[0] = 10
nums[1] = 20
nums[2] = 30

nums[0] = "bad"        # TypeError: only integers allowed
nums[5] = 100          # IndexError: index 5 out of range for size 3

Handling Slice Objects

When you assign to a slice like obj[1:4] = [a, b, c], the key parameter is a slice object. Handle it explicitly if you want slice support:

class SliceableList:
    def __init__(self):
        self.items = []
    
    def __setitem__(self, key, value):
        if isinstance(key, slice):
            # Handle slice assignment
            start, stop, step = key.indices(len(self.items))
            self.items[start:stop:step] = value
        else:
            self.items[key] = value
    
    def __getitem__(self, key):
        return self.items[key]
    
    def __len__(self):
        return len(self.items)

lst = SliceableList()
lst.items = [1, 2, 3, 4, 5]

lst[1:4] = [20, 30, 40]
print(lst.items)      # [1, 20, 30, 40, 5]
# output: [1, 20, 30, 40, 5]

Common Gotchas

Confusing __setitem__ with __setattr__

These are different! __setattr__ handles any attribute assignment (obj.x = value), while __setitem__ only handles subscript assignment (obj[key] = value).

class Example:
    def __setattr__(self, name, value):
        print(f"Setting attribute {name} = {value}")
        object.__setattr__(self, name, value)
    
    def __setitem__(self, key, value):
        print(f"Setting item {key} = {value}")
        object.__setattr__(self, key, value)

e = Example()
e.name = "Alice"      # Calls __setitem__? No! Calls __setattr__
e["name"] = "Alice"   # Calls __setitem__

# output: Setting attribute name = Alice
# output: Setting item name = Alice

Infinite recursion

Be careful not to call self.key = value inside __setattr__ or __setitem__—that triggers the same method again!

class Broken:
    def __setitem__(self, key, value):
        self[key] = value  # INFINITE RECURSION!

# Broken()[0] = 1  # RecursionError: maximum recursion depth exceeded

Fix: Use a different storage mechanism:

class Fixed:
    def __init__(self):
        self._data = {}
    
    def __setitem__(self, key, value):
        self._data[key] = value  # Safe - uses dict directly

Forgetting to handle the value parameter

Don’t ignore the value! You must actually store it somewhere:

class Forgotten:
    def __setitem__(self, key, value):
        pass  # Silently ignores the value - usually a bug!

f = Forgotten()
f["x"] = 100
print(f._data)  # AttributeError: 'Forgotten' object has no attribute '_data'

Practical Examples

Observable dictionary

class ObservableDict:
    def __init__(self):
        self._data = {}
        self._changes = []
    
    def __setitem__(self, key, value):
        old = self._data.get(key, "<unset>")
        self._data[key] = value
        self._changes.append(f"{key}: {old} -> {value}")
    
    def __getitem__(self, key):
        return self._data[key]
    
    def history(self):
        return self._changes.copy()

d = ObservableDict()
d["name"] = "Alice"
d["age"] = 30
d["name"] = "Bob"

print(d.history())
# output: ['name: <unset> -> Alice', 'age: <unset> -> 30', 'name: Alice -> Bob']

Range-validated list

class BoundedList:
    def __init__(self, max_value=100):
        self._data = []
        self._max = max_value
    
    def __setitem__(self, key, value):
        if not isinstance(value, (int, float)):
            raise TypeError("only numeric values allowed")
        if value < 0 or value > self._max:
            raise ValueError(f"value must be between 0 and {self._max}")
        
        # Expand list if necessary
        while len(self._data) <= key:
            self._data.append(None)
        self._data[key] = value
    
    def __getitem__(self, key):
        return self._data[key]
    
    def __len__(self):
        return len(self._data)

scores = BoundedList(max_value=100)
scores[0] = 95
scores[1] = 87
print(scores[0], scores[1])
# output: 95 87

scores[2] = 150    # ValueError: value must be between 0 and 100

Summary

  • __setitem__ defines behavior for subscript assignment: obj[key] = value
  • Takes three parameters: self, key, and value
  • The key can be an integer, string, or slice object
  • Raise IndexError for sequences with invalid indices
  • Raise KeyError for mappings with invalid keys
  • Avoid infinite recursion by using a different storage mechanism
  • Different from __setattr__ which handles all attribute assignments

Use __setitem__ when building custom containers that need to respond to bracket notation assignment.

See Also

  • str — The string representation dunder; complement to __repr__
  • repr — The official representation dunder; used by debuggers and REPL
  • init — Initializes object state; called after object creation