# PEP 852: Asynchronous functions in the C API

**URL:** <https://discuss.python.org/t/pep-852-asynchronous-functions-in-the-c-api/110003>\
**Category:** PEPs\
**Created:** [November 10, 2026, 3:50pm UTC](https://discuss.python.org/t/pep-852-asynchronous-functions-in-the-c-api/110003 "2026-11-10T15:50:55Z")\
**Posts on this page:** 13\
**Page:** 1

<div class="post-metadata">

**Author:** ![ZeroIntensity](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/zerointensity/32/39016_2.png) [@ZeroIntensity](https://discuss.python.org/u/ZeroIntensity)\
**Post date:** [November 10, 2026, 3:50pm UTC](https://discuss.python.org/t/pep-852-asynchronous-functions-in-the-c-api/110003/1 "2026-11-10T15:50:56Z")

</div>

Nearly four years after the original discussion, I am pleased to present PEP 852, which adds a proper interface for using `async`/`await` in the C API. Here’s an example:

```c
static int
return_result(PyObject *asyncobj, PyObject *result)
{
    PyAsync_SetResult(asyncobj, result);
    return 1;
}

/* Equivalent to: async def await_from_c(func): return await func() */
static PyObject *
await_from_c(PyObject *self, PyObject *func) /* METH_O */
{
    PyObject *asyncobj = PyAsync_New();
    if (asyncobj == NULL) {
        return NULL;
    }
    if (PyAsync_AddExpr(asyncobj, PyObject_CallNoArgs(func), return_result, NULL) < 0) {
        PyAsync_Cancel(asyncobj);
        Py_DECREF(asyncobj);
        return NULL;
    }
    return asyncobj;
}

```

This proposal is largely based on my [PyAwaitable](https://github.com/ZeroIntensity/pyawaitable) library, though with a number of tweaks.

Here’s the full text:

> **[PEP 852 – Asynchronous functions in the C API | peps.python.org](https://peps.python.org/pep-0852/)**
>
> This PEP describes a new suite of functions in the C API to interact with coroutines. For example:

---

<div class="post-metadata">

**Author:** ![vstinner](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/vstinner/32/15130_2.png) [@vstinner](https://discuss.python.org/u/vstinner)\
**Post date:** [November 10, 2026, 5:09pm UTC](https://discuss.python.org/t/pep-852-asynchronous-functions-in-the-c-api/110003/2 "2026-11-10T17:09:52Z")

</div>

I had a quick look at the C API and I like it! It’s well designed and seems convenient to use!

> A PyAsync object is recognized by asyncio.iscoroutine() and inspect.isawaitable(), and can be passed to asyncio.create\_task().

Sorry, I didn’t follow async frameworks recently. With these properties, can `PyAsync` be used with event loops other than asyncio such as Twisted or Tornado? Or is it designed only for asyncio? The PEP mostly refers to asyncio (which is expected since it’s part of the stdlib).

> int PyAsync\_SetResult(PyObject \*asyncobj, PyObject \*result)  
> It cannot fail and returns 0.

Either the function can fail and return -1 on error, or it cannot fail and its return type should be `void`. What do you prefer? 🙂 IMO it should be possible to fail if the first argument is not an async object (set `TypeError` exception and return `-1` in this case). It may also set a `SystemError` if _result_ is NULL (maybe only in debug mode?).

Is it really needed to add `PyAsync_AddExpr()`? It seems to be a convenient helper function. It’s less obvious to me what “Expr” refers to. With `PyAsync_AddAwait` name, I can get that it referes to an awaitable object. Or maybe it’s just a naming issue and `PyAsync_AddAwaitSteal()` would be more explicit?

PyAsync\_Cancel() cannot fail. So if something bad happens, the exception is logged with `sys.unraisablehook`? It might be more future proof to leave the door open to report an exception to the caller. Even if it cannot fail in the _current implementation_.

> PyAsync\_SetArbValue / PyAsync\_GetArbValue

Bikeshedding: you might call them PyAsync\_SetData / PyAsync\_GetData

---

<div class="post-metadata">

**Author:** ![ZeroIntensity](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/zerointensity/32/39016_2.png) [@ZeroIntensity](https://discuss.python.org/u/ZeroIntensity)\
**Post date:** [November 10, 2026, 5:52pm UTC](https://discuss.python.org/t/pep-852-asynchronous-functions-in-the-c-api/110003/3 "2026-11-10T17:52:51Z")

</div>

> [@vstinner](#):
>
> Sorry, I didn’t follow async frameworks recently. With these properties, can `PyAsync` be used with event loops other than asyncio such as Twisted or Tornado? Or is it designed only for asyncio? The PEP mostly refers to asyncio (which is expected since it’s part of the stdlib).

A `PyAsync` is just an object that implements `collections.abc.Coroutine`, so yes, it can be used with other event loops; see [this section](https://peps.python.org/pep-0852/#an-api-for-coroutine-functions-not-futures) in the PEP.

> [@vstinner](#):
>
> Either the function can fail and return -1 on error, or it cannot fail and its return type should be `void`. What do you prefer? 🙂 IMO it should be possible to fail if the first argument is not an async object (set `TypeError` exception and return `-1` in this case). It may also set a `SystemError` if _result_ is NULL (maybe only in debug mode?).

That seems like a reasonable change. I put the `int` result there for future-proofing, but _currently_ it can’t fail. But, we might as well add the debug check to make it useful.

> [@vstinner](#):
>
> Is it really needed to add `PyAsync_AddExpr()`? It seems to be a convenient helper function. It’s less obvious to me what “Expr” refers to. With `PyAsync_AddAwait` name, I can get that it referes to an awaitable object. Or maybe it’s just a naming issue and `PyAsync_AddAwaitSteal()` would be more explicit?

`AddExpr` is really helpful in practice, because often you have to call a method in order to get a coroutine object, which with `AddAwait` requires an extra error handling branch. `AddExpr` lets you avoid that.

I would be fine with `PyAsync_AddAwaitSteal` if you prefer that.

> [@vstinner](#):
>
> PyAsync\_Cancel() cannot fail. So if something bad happens, the exception is logged with `sys.unraisablehook`? It might be more future proof to leave the door open to report an exception to the caller. Even if it cannot fail in the _current implementation_.

I see `PyAsync_Cancel` as similar to a deallocation routine, because it only calls destructors (by decrefing the coroutines queued with `AddAwait`), so realistically, it shouldn’t ever set exceptions. I think leaving failures as unraisable exceptions here is fine.

> [@vstinner](#):
>
> Bikeshedding: you might call them PyAsync\_SetData / PyAsync\_GetData

Good idea, I like this more than the current spelling.

---

<div class="post-metadata">

**Author:** ![pitrou](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/pitrou/32/28_2.png) [@pitrou](https://discuss.python.org/u/pitrou)\
**Post date:** [November 10, 2026, 9:41pm UTC](https://discuss.python.org/t/pep-852-asynchronous-functions-in-the-c-api/110003/4 "2026-11-10T21:41:15Z")

</div>

I’m curious: why don’t `PyAsync_Callback` and `PyAsync_Error` take a arbitrary user data parameter (presumably a `void*` or `uintptr_t`)?

In other words, I would expect something like:

```c
typedef int (*PyAsync_Callback)(PyObject *asyncobj,
                                PyObject *result, uintptr_t data);
typedef int (*PyAsync_Error)(PyObject *asyncobj,
                             PyObject *exception, uintptr_t data);
int PyAsync_AddAwait(
    PyObject *asyncobj, PyObject *awaitable,
    PyAsync_Callback result_cb, uintptr_t result_cb_data,
    PyAsync_Error error_cb, uintptr_t error_cb_data);

```

(disclaimer: I have no use case for these APIs in particular; I just know that C APIs that take callbacks but do not allow passing arbitrary params with them are a major PITA; and, yes, I saw the `PyAsync_SetArbValue`, but it seems much less convenient as it sets a single pointer for the entire async object, instead of per-callback)

---

<div class="post-metadata">

**Author:** ![ZeroIntensity](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/zerointensity/32/39016_2.png) [@ZeroIntensity](https://discuss.python.org/u/ZeroIntensity)\
**Post date:** [November 10, 2026, 10:31pm UTC](https://discuss.python.org/t/pep-852-asynchronous-functions-in-the-c-api/110003/5 "2026-11-10T22:31:55Z")

</div>

> [@pitrou](#):
>
> it seems much less convenient as it sets a single pointer for the entire async object, instead of per-callback

The idea is that you’ll actually _want_ a single pointer across the entire async object. Remember, the callbacks are basically a formality because C doesn’t have native `async`/`await`. The “data” pointer is typically going to be a struct initialized in the original function (the one that calls `PyAsync_New`) to make up for the lack of closures, and then all the callbacks simply reach for it when they need the data. If any of the callbacks want to populate that struct with more data, they can just do that – this is perfectly safe because the developer knows the order in which every callback will be executed.

We could make it per-callback, but it leads to longer signatures and can (maybe?) lead to reference leaks if the callback data is a Python object pointer.

---

<div class="post-metadata">

**Author:** ![pitrou](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/pitrou/32/28_2.png) [@pitrou](https://discuss.python.org/u/pitrou)\
**Post date:** [November 11, 2026, 9:10am UTC](https://discuss.python.org/t/pep-852-asynchronous-functions-in-the-c-api/110003/6 "2026-11-11T09:10:22Z")

</div>

> [@ZeroIntensity](#):
>
> The idea is that you’ll actually _want_ a single pointer across the entire async object.

That depends on the use case. My first question when seeing this API was “how would I wrap this in a convenience C++ API”, and I would probably want each individual callback to be a [`std::function`](https://en.cppreference.com/cpp/utility/functional/function)… in which case each callback has its own closure.

Individual closures may also make things more composable. If there is a single closure inside the async object, it introduces coupling between the various components (the async object and the various callbacks have to share specific knowledge, such as a custom structure definition).

(of course the single closure can be used to manually track each callback’s closure data; it’s just more work for the user of this API)

---

<div class="post-metadata">

**Author:** ![ZeroIntensity](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/zerointensity/32/39016_2.png) [@ZeroIntensity](https://discuss.python.org/u/ZeroIntensity)\
**Post date:** [November 11, 2026, 11:01pm UTC](https://discuss.python.org/t/pep-852-asynchronous-functions-in-the-c-api/110003/7 "2026-11-11T23:01:30Z")

</div>

> [@pitrou](#):
>
> the async object and the various callbacks have to share specific knowledge, such as a custom structure definition

But why is that such a bad thing? The callbacks will need to share a lot of information anyway, because a callback isn’t meant to be reusable across any arbitrary `PyAsync` object.

I’m really hesitant to do per-callback closures because it would make the proposed API much more verbose. For what it’s worth, a C++ wrapper should still be fairly easy to make when using a single data pointer. Something like this should work:

```cpp
// No error handling

using Callback = std::function<void(PyObject *asyncobj, PyObject *result)>;
using Queue = std::deque<Callback>;

static int trampoline(PyObject *asyncobj, PyObject *result) {
    auto *queue = static_cast<Queue *>(PyAsync_GetArbValue(asyncobj));
    Callback callback = std::move(queue->front());
    queue->pop_front();
    callback(asyncobj, result);
    return 1;
}

static void await(PyObject *asyncobj, PyObject *awaitable, Callback callback) {
    auto *queue = static_cast<Queue *>(PyAsync_GetArbValue(asyncobj));
    if (queue == nullptr) {
        queue = new Queue; // never freed, for brevity
        PyAsync_SetArbValue(asyncobj, queue);
    }
    queue->push_back(std::move(callback));
    PyAsync_AddAwait(asyncobj, awaitable, trampoline, nullptr);
}

/* async def both(a, b): print(await a, "first"); print(await b, "second") */
static PyObject *
both(PyObject *self, PyObject *args)
{
    PyObject *a, *b;
    if (!PyArg_ParseTuple(args, "OO", &a, &b)) {
        return nullptr;
    }
    PyObject *asyncobj = PyAsync_New();
    await(asyncobj, a, [](PyObject *, PyObject *result) {
        PySys_FormatStdout("%R first\n", result);
    });
    await(asyncobj, b, [](PyObject *, PyObject *result) {
        PySys_FormatStdout("%R second\n", result);
    });
    return asyncobj;
}

```

The only thing a per-callback data pointer would do here is avoid the need for an extra queue, so it wouldn’t save all that much. (You could argue that the data pointer approach requires more memory, but keep in mind that adding per-callback data pointers will increase the size of every callback entry by `2 * sizeof(void *)`, so I’m not sure it would actually be better in practice.)

---

<div class="post-metadata">

**Author:** ![pitrou](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/pitrou/32/28_2.png) [@pitrou](https://discuss.python.org/u/pitrou)\
**Post date:** [November 12, 2026, 10:15am UTC](https://discuss.python.org/t/pep-852-asynchronous-functions-in-the-c-api/110003/8 "2026-11-12T10:15:47Z")

</div>

> [@ZeroIntensity](#):
>
> For what it’s worth, a C++ wrapper should still be fairly easy to make when using a single data pointer. Something like this should work:

I suppose this needs to be slightly more complicated for the handling of the error callback. You need two similar trampolines and your queue elements are pairs of `std::function`, presumably?

Also, how is the queue lifetime supposed to be handled in that example? Does the async object offer a finalization callback?

---

<div class="post-metadata">

**Author:** ![ZeroIntensity](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/zerointensity/32/39016_2.png) [@ZeroIntensity](https://discuss.python.org/u/ZeroIntensity)\
**Post date:** [November 12, 2026, 2:25pm UTC](https://discuss.python.org/t/pep-852-asynchronous-functions-in-the-c-api/110003/9 "2026-11-12T14:25:03Z")

</div>

> [@pitrou](#):
>
> Also, how is the queue lifetime supposed to be handled in that example? Does the async object offer a finalization callback?

It does not, so that might be worth adding. When using PyAwaitable in pure C, I didn’t bother adding one because I would just deal with deallocation in the last callback, but I think it would make sense for C++ wrappers.

Would adding a simple `freefunc` parameter for the deallocator be enough?

---

<div class="post-metadata">

**Author:** ![pitrou](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/pitrou/32/28_2.png) [@pitrou](https://discuss.python.org/u/pitrou)\
**Post date:** [November 12, 2026, 2:39pm UTC](https://discuss.python.org/t/pep-852-asynchronous-functions-in-the-c-api/110003/10 "2026-11-12T14:39:52Z")

</div>

> [@ZeroIntensity](#):
>
> When using PyAwaitable in pure C, I didn’t bother adding one because I would just deal with deallocation in the last callback

How does a callback know it’s the last one being invoked? Especially if an exception occurs between two callbacks?

> [@ZeroIntensity](#):
>
> Would adding a simple `freefunc` parameter for the deallocator be enough?

I think so, yes, though it might make sense for it to be called from `tp_clear` as well.

---

<div class="post-metadata">

**Author:** ![pitrou](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/pitrou/32/28_2.png) [@pitrou](https://discuss.python.org/u/pitrou)\
**Post date:** [November 12, 2026, 2:40pm UTC](https://discuss.python.org/t/pep-852-asynchronous-functions-in-the-c-api/110003/11 "2026-11-12T14:40:43Z")

</div>

Also cc @da-woods @scoder , as I suspect Cython would be interested in this API.

---

<div class="post-metadata">

**Author:** ![ZeroIntensity](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/zerointensity/32/39016_2.png) [@ZeroIntensity](https://discuss.python.org/u/ZeroIntensity)\
**Post date:** [November 12, 2026, 2:54pm UTC](https://discuss.python.org/t/pep-852-asynchronous-functions-in-the-c-api/110003/12 "2026-11-12T14:54:11Z")

</div>

> [@pitrou](#):
>
> How does a callback know it’s the last one being invoked? Especially if an exception occurs between two callbacks?

It was just up to the developer to know. A `PyAsync` object isn’t going to schedule new coroutines on its own, so the developer would just write a callback with the `free()` calls and schedule that one last.

---

<div class="post-metadata">

**Author:** ![da-woods](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/da-woods/32/7507_2.png) [@da-woods](https://discuss.python.org/u/da-woods)\
**Post date:** [November 12, 2026, 6:25pm UTC](https://discuss.python.org/t/pep-852-asynchronous-functions-in-the-c-api/110003/13 "2026-11-12T18:25:34Z")

</div>

I think Stefan knows more about the async code in Cython than me, but

> For example, `mypyc` still does not support all asynchronous features, because they are forced to build coroutine types from scratch, which is difficult to do correctly.

Cython’s does have the coroutine types built from scratch (with a pretty large amount of copy-paste from the CPython implementations). I think we’d probably continue to use those on the basis that they’re already there, and they have some extra stuff so they’re introspectable more like the Python types.

I don’t know if we’d have done things differently if this had been available when async functions were first added to Cython.
