Descriptors, Dunders, Asyncio in Python
A concise, practical guide to Python descriptors, special methods, and how asyncio uses dunder hooks to enable async syntax.
Overview
Python's expressive object model exposes many extension points via special methods (commonly called dunder methods because they use double underscores). These hooks let objects participate in language-level operations such as arithmetic, iteration, context management, and — for asyncio — asynchronous control flow. Understanding these hooks plus descriptors gives you the tools to design ergonomic, Pythonic APIs and to interoperate cleanly with the language's syntax.
Dunder Methods
At a high level, dunder methods are special method names like __init__, __str__, and __add__ that Python looks for to implement built-in behaviors. Implementing the right dunders makes your objects behave like built-in types (e.g., support iteration, comparison, or arithmetic).
Common Categories
| Category | Representative Methods |
|---|---|
| Construction and Representation | __new__; __init__; __repr__; __str__ |
| Attribute Access | __getattr__; __getattribute__; __setattr__ |
| Container Protocols | __len__; __iter__; __contains__ |
| Operator Overloading | __add__; __eq__; __lt__ |
| Async and Await | __await__; __aenter__; __aexit__; __aiter__; __anext__ |
Tip: prefer implementing the smallest set of dunders needed for your API; overloading too many behaviors can make objects surprising.
Descriptors
Descriptors are objects that define attribute access behavior by implementing one or more of __get__, __set__, and __delete__. They are the mechanism behind properties, functions on classes, and many framework-level attribute systems. Use descriptors when you need reusable, composable attribute logic that works across many classes.
Simple Descriptor Example
# A simple typed descriptor
class Typed:
def __init__(self, name, expected_type):
self.name = name
self.expected_type = expected_type
def __get__(self, instance, owner):
if instance is None:
return self
return instance.__dict__.get(self.name)
def __set__(self, instance, value):
if not isinstance(value, self.expected_type):
raise TypeError(f"{self.name} must be {self.expected_type}")
instance.__dict__[self.name] = value
class Point:
x = Typed("x", int)
y = Typed("y", int)
p = Point()
p.x = 10
p.y = 20
When to Use Descriptors
- To centralize validation or transformation for attributes.
- To implement computed attributes with caching.
- When you need the same attribute behavior across many classes.
Asyncio Dunders
Asyncio integrates with Python's dunder protocol to enable await, async with, and async for syntax. The key hooks are:
__await__— makes an object awaitable; Python calls this when you useawaiton the object.__aenter__and__aexit__— implement asynchronous context managers used withasync with.__aiter__and__anext__— implement asynchronous iteration used withasync for.
Awaitable Example
import types
class AwaitableValue:
def __init__(self, value):
self.value = value
def __await__(self):
# __await__ must return an iterator
yield from ()
return self.value
async def main():
v = await AwaitableValue(42)
print(v) # 42
# run with asyncio.run(main())
Async Context Manager Example
class AsyncLock:
async def __aenter__(self):
await acquire_lock()
return self
async def __aexit__(self, exc_type, exc, tb):
await release_lock()
# usage:
# async with AsyncLock():
# ...
The official asyncio HOWTO and reference explain the runtime semantics and recommended patterns for these hooks in detail.
Practical Patterns and Pitfalls
Composition Over Inheritance
Prefer composing small descriptor objects or awaitable wrappers rather than creating large monolithic base classes. Composition keeps behavior explicit and testable.
Keep Dunders Minimal
Only implement the dunders you need. Overloading many behaviors can make debugging harder and can surprise users of your API. Use clear names and document the semantics of any non-obvious dunder behavior.
Testing Async Dunders
Test awaitables and async context managers with an event loop (for example, asyncio.run or pytest-asyncio). Verify both success and error paths for __aexit__ to ensure resources are released correctly.
Quick Reference
| Feature | Hook |
|---|---|
| Make object awaitable | __await__ |
| Async context manager | __aenter__; __aexit__ |
| Async iterator | __aiter__; __anext__ |
| Attribute control | __get__; __set__; __delete__ |