Published on

Async in Python

Authors
  • avatar
    Name
    Wenzhuo Zhao
    Twitter

Python async

  • Python async Usage
  • The async syntax Rule
  • iterator vs async iterator - break down async for
  • async with
  • The 3-Part Pattern - return a list from iterator
  • "Coroutine function" and "Coroutine object"

Python async Usage

SyntaxPurposeUsage
async defDefine async/coroutine functionsasync def my_func():
- async forIterate over async iteratorsasync for item in async_generator():
- awaitWait for coroutine objectsresult = await some_coroutine()
async withAsync context managersasync with async_resource():

The async syntax Rule

Function PatternHow to ConsumeWhat You Get
async def + yieldasync for item in func():Each yielded item
async def + returnresult = await func()The returned value

async def with both yield and return

  • Any yield → AsyncGenerator → use async for (HIGH priority)
  • No yield → Coroutine → use await

async def + yield

# Pattern 1: yield -> use async for
async def get_users():
    yield user1
    yield user2
    yield user3
    # same as `return None`


# ✅ Correct consumption
async def my_function():
    # `for user in await get_users():` -  ❌ Wrong - You may expect to see the keyword `await`
    # but in fact - ✅ `async for` does the "await" in an iteration format
    async for user in get_users():
        process(user)


# ❌ Wrong - can't await an AsyncGenerator
result = await get_users()  # TypeError

async def + return

# Pattern 2: return -> use await
async def get_user():
    return single_user


# ✅ Correct consumption
async def my_function():
    user = await get_user()


# ❌ Wrong - can't iterate a Coroutine
async for u in get_user():  # TypeError

iterator vs async iterator - break down async for

  • regular iterator - 1 is the same as regular iterator - 2
  • async iterator - 1 is the same as async iterator - 2
  • Note: async for is not applying the async def pattern to for
# regular iterator - 1
def print_items():
    for item in [1, 2, 3]:
        print(item)


# regular iterator - 2
def print_items():
    iterator = iter([1, 2, 3])
    while True:
        try:
            item = next(iterator)  # ← This throws StopIteration when done
            print(item)
        except StopIteration:
            break


# async iterator - 1
async def process_stream():
    async for chunk in openai_stream:  # means: `async def` + `yield`
        print(chunk)
        yield chunk


# async iterator - 2
async def process_stream():
    iterator = openai_stream.__aiter__()
    while True:
        try:
            chunk = await iterator.__anext__()
            print(chunk)
            yield chunk
        except StopAsyncIteration:
            break

What if we want to get 1

async def get_users():
    yield user1
    yield user2
    yield user3
    return 1  # This is the "return value"


# Any yield → AsyncGenerator → use async for (HIGH priority)
# No yield → Coroutine → use await
result = await get_users()  # ❌ Wrong - TypeError - get_users() has `yield`


# Method 1: Not possible
async def get_return_value():
    async for user in get_users():  # The return value is lost in async for!
        print(user)


# Method 2: Using the generator protocol directly
async def get_return_value():
    gen = get_users()
    # iter = get_users().__aiter__() - also correct, use iterator protocol
    try:
        while True:
            user = await gen.__anext__()
            # user = await iter.__anext__()
            print(user)
    except StopAsyncIteration as e:
        return_value = e.value  # The return value is here!
        print(f"Return value: {return_value}")  # 1

async with

async def get_session() -> AsyncGenerator[AsyncSession, None]:  # 3 ✅
    # db_local_session = async_sessionmaker(...)
    async with Database.db_local_session() as session:  # 1 ✅
        try:
            yield session  # 4a ✅ `yield` relates to `async def`, the caller needs to use `async for`
            await session.commit()  # 2 ✅
        except Exception:
            await session.rollback()
            raise


# ✅ Correct usage - async context manager
@asynccontextmanager
async def get_db_session() -> AsyncIterator[AsyncSession]:
    async for session in Database.get_session():  # 4b ✅
        yield session

The sequence:

  1. ✅ async with needed because Database.db_local_session() returns an async context manager
  2. ✅ await needed because session.commit() is an async function
  3. ✅ async def needed because we use async with keyword inside
  4. ✅ async def needed because we use await keyword inside
  5. ✅ async def + yield (4a) needs async for (4b)

Async Context Manager - means it has __aenter__ and __aexit__

# Database.session() returns an object that has:
class AsyncSession:
    async def __aenter__(self): ...  # ← async context manager

    async def __aexit__(self): ...  # ← async context manager

await has nothing to do with async with

KeywordWhy Needed
awaitsession.commit() is async
async withDatabase.db_local_session() returns async context manager

The 3-Part Pattern - return a list from iterator

PartSync VersionAsync Version
1. Generator executiongenerator()async_generator()
2. Collection conversionfor item in generator():async for item in async_generator():
3. Variable assignmentresult = collect_func()result = await collect_func()

SYNC VERSION

def sync_generator() -> Generator[int, None, None]:
    yield 1
    yield 2


def collect_sync() -> list[int]:
    items = []
    for item in sync_generator():  # Part 2: regular for
        items.append(item)
    return items


result = collect_sync()  # Part 3: direct assignment

ASYNC VERSION

async def async_generator() -> AsyncGenerator[int, None]:
    yield 1
    yield 2


async def collect_async() -> list[int]:
    items = []
    async for item in async_generator():  # Part 2: `async for`, not `await`
        items.append(item)
    return items


result = await collect_async()  # Part 3: await assignment

The Pattern Summary

Part 1: Generator Creation
    ↓
Part 2: Collection (iterate + convert to list/etc)
    ↓
Part 3: Assignment (get final result)

"Coroutine function" and "Coroutine object"

Function ContainsFunction TypeReturns When CalledDefineUse
return (with async def)Coroutine functionCoroutine objectasync def get_users():await get_user()
yield (with async def)Async generator functionAsyncGenerator objectstream_users()async for user in stream_users()
# Pattern 1: Async Function - use await
async def get_user() -> User:  # User is NOT a `coroutine object`, this specifies the type you get when calling `await get_user()`
    response = await api_call()
    return User(response.data)  # ← return = use await -- function returns a Coroutine OBJECT


user = await get_user()


# Pattern 2: Async Generator - use async for
async def stream_users() -> AsyncGenerator[User, None]:
    async for data in api_stream():
        yield User(data)  # ← yield = use async for


async for user in stream_users():
    process(user)