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

CategoryRepresentative 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


Asyncio Dunders

Asyncio integrates with Python's dunder protocol to enable await, async with, and async for syntax. The key hooks are:

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

FeatureHook
Make object awaitable__await__
Async context manager__aenter__; __aexit__
Async iterator__aiter__; __anext__
Attribute control__get__; __set__; __delete__