Python Data Model: __getitem__, __iter__, __enter__, and Descriptor Protocol
- Last updated
- Prerequisites:
- Python Classes and OOP
- Iterator protocol
- python
- data-model
- dunder-methods
- descriptors
- protocols
- metaprogramming
Read at your depth
The practical view
The Python Data Model defines how user-defined classes interact with language operators. Key dunder (double-underscore) methods include: Sequences (`__len__`, `__getitem__`, `__setitem__`), Iterators (`__iter__`, `__next__`), Context Managers (`__enter__`, `__exit__`), String representations (`__repr__`, `__str__`), and Callable objects (`__call__`). Implementing these protocols makes custom objects feel like native built-in types.
The same idea in other frameworks
Dunder protocols (`__getitem__`, `__iter__`, `__enter__`).Standard library traits (`Index`, `Iterator`, `Drop`, `Display`).Operator overloading (`this[int index]`, `IDisposable`).Operator overloading (`operator[]`, `operator*`).Legacy vs modern
Java-Style Getters/Setters vs Python Data Model Protocols
Writing explicit `getItem()` and `getLength()` methods is un-Pythonic; implementing dunder methods integrates with native syntax.
deck.getItem(0); deck.getLength(); // ❌ Un-Pythonic, cannot use len(), for-in, or slicingdef __len__(self): ... def __getitem__(self, i): ... // ✅ NEW: Supports slicing, iteration, and len()Interview gotchas
Context
Interviewers test your deep understanding of CPython's slot lookup optimization.
The mechanical answer
Special dunder methods are resolved directly via the class's type object slots (`Py_TYPE(obj)->tp_as_mapping`). CPython bypasses the instance `__dict__` entirely for performance and internal consistency. If special methods were looked up on instances, every single `len()` or arithmetic `+` operation would require an expensive instance dictionary lookup and descriptor binding pass.
Trap
Setting `my_instance.__len__ = lambda: 42` will **not** work; `len(my_instance)` will continue calling the class's `__len__` or throw a `TypeError`.
Context
Checks mastery of Python's attribute lookup resolution order.
The mechanical answer
A **Data Descriptor** defines both `__get__` and `__set__` (or `__delete__`) (e.g. `@property` with a setter, ORM field descriptors). A **Non-Data Descriptor** defines only `__get__` (e.g. standard methods, `@classmethod`, `@staticmethod`). The key difference is resolution priority: **Data Descriptors override instance attributes** in the instance `__dict__`, whereas **instance attributes override Non-Data Descriptors**.
Trap
If an instance dictionary has a key named `foo`, but the class has a Data Descriptor named `foo`, the Data Descriptor's `__get__` wins.