89

How can I find out if a function or method is a normal function or an async function? I would like my code to automatically support normal or async callbacks and need a way to test what type of function is passed.

async def exampleAsyncCb():
    pass

def exampleNomralCb():
    pass

def isAsync(someFunc):
    #do cool dynamic python stuff on the function
    return True/False

async def callCallback(cb, arg):
    if isAsync(cb):
        await cb(arg)
    else:
        cb(arg)

And depending on what type of function gets passed it should either run it normally or with await. I tried various things but have no idea how to implement isAsync().

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
Ecko
  • 1,185
  • 1
  • 9
  • 11
  • 7
    But... but `True/False` is dividing by `0`! :O – Shadow Aug 27 '17 at 23:21
  • Note that any given asynchronous function could be either **coroutine function** or **asynchronous generator function** , please see my answer for detail – Ham Jul 02 '21 at 16:17

8 Answers8

89

Use the inspect module of Python.

inspect.iscoroutinefunction(object)

Return true if the object is a coroutine function (a function defined with an async def syntax).

This function is available since Python 3.5. The module is available for Python 2 with lesser functionalities and certainly without the one you are looking for: inspect

Inspect module as the name suggests is useful to inspect a whole lot of thing. The documentation says

The inspect module provides several useful functions to help get information about live objects such as modules, classes, methods, functions, tracebacks, frame objects, and code objects. For example, it can help you examine the contents of a class, retrieve the source code of a method, extract and format the argument list for a function, or get all the information you need to display a detailed traceback.

There are four main kinds of services provided by this module: type checking, getting source code, inspecting classes and functions, and examining the interpreter stack.

Some basic capabilities of this module are:

inspect.ismodule(object)
inspect.isclass(object)
inspect.ismethod(object)
inspect.isfunction(object)

It also packs capability to retrieve the source code

inspect.getdoc(object)
inspect.getcomments(object)
inspect.getfile(object) 
inspect.getmodule(object)

Methods are named intuitively. Description if needed can be found in documentation.

Community
  • 1
  • 1
Sharad
  • 1,867
  • 14
  • 33
  • 3
    Is there a practical difference between `inspect.iscoroutinefunction()` and `asyncio.iscoroutinefunction()`? –  Dec 29 '17 at 05:39
  • 3
    @M.I.Wright - no, it is not. Based on the source code of Python 3.6, `asyncio` version is just a reimport from `inspect` module. – Ales Teska Apr 02 '18 at 11:50
  • 8
    @AlesTeska I haven't looked at the source, but the py3.7 documentation has a clarification that suggests they are subtly different, specifically: `This method is different from inspect.iscoroutinefunction() because it returns True for generator-based coroutine functions decorated with @coroutine.` – roganartu Jun 10 '19 at 18:28
  • 3
    Good answer - two suggestions: 1) mention `inspect.isawaitable`, like one of the other answers does - it checks a slightly different thing but covers certain other cases, and noting the tradeoffs would make for a more comprehensive answer, and 2) the aside about the Python 2 backport should probably either be expanded to note that `async`/`await` don't even exist in Python 2, or else left out entirely. – mtraceur Nov 22 '19 at 20:11
  • 1
    @hallo They're not identical, see https://docs.python.org/3/library/asyncio-task.html#asyncio.iscoroutinefunction : `different from inspect.iscoroutinefunction() because it returns True for generator-based coroutine functions decorated with @coroutine.` – Max Barraclough Jan 07 '21 at 17:43
  • FYI, if class C is callable, and that might be async, e.g. `async def __call__`, and `c = C()`, you want to check `inspect.iscoroutinefunction(c.__call__)`, not `inspect.iscoroutinefunction(c)` – Sam H. Jan 06 '22 at 22:42
62

If you don't want to introduce another import with inspect, iscoroutine is also available inside asyncio.

import asyncio

def isAsync(someFunc):
    return asyncio.iscoroutinefunction(someFunc)
dirn
  • 19,454
  • 5
  • 69
  • 74
  • 24
    The `asyncio.iscoroutinefunction()` function does *two* tests; it uses `inspect.iscoroutinefunction()` first, and if that test fails tests if the function is a function with the [`@acyncio.coroutine` decorator](https://docs.python.org/3/library/asyncio-task.html#asyncio.coroutine) applied. Take this into account! – Martijn Pieters Feb 08 '18 at 09:02
  • i wonder how the supported versionset of this solution compares to the accepted one. note there are further solutions in other answers. – fuzzyTew Nov 30 '21 at 16:47
44

TLDR

If you want to check something should be used with await, use inspect.isawaitable (as when you test something being callable() other than just being a function).

Unlike iscoroutine or iscoroutinefunction it will also work for Futures and objects that implement __await__ method.


Detailed

Solutions above will work for simple cases, when you pass coroutine function. In some cases you may like to pass awaitable object function that acts like coroutine function, but is not coroutine function. Two examples is Future class or Future-like object class (class that implements __await__ magic method). In this cases iscoroutinefunction will return False, what is not you need.

It's easier to understand on non-async example with passing non-function callable as callback:

class SmartCallback:
    def __init__(self):
        print('SmartCallback is not function, but can be used as function')

callCallback(SmartCallback)  # Should work, right?

Back to async world, a similar situation:

class AsyncSmartCallback:
    def __await__(self):
        return self._coro().__await__()

    async def _coro(self):
        print('AsyncSmartCallback is not coroutine function, but can be used as coroutine function')
        await asyncio.sleep(1)

await callCallback(AsyncSmartCallback)  # Should work, but oops! iscoroutinefunction(AsyncSmartCallback) == False

Way to solve it not to use iscoroutine or iscoroutinefunction, but use inspect.isawaitable instead. It works with ready object so you must create it first. In other words, solution I would advise to use:

async def callCallback(cb, arg):
    if callable(cb):
        res = cb()  # here's result of regular func or awaitable
        if inspect.isawaitable(res):
            res = await res  # await if awaitable
        return res  # return final result
    else:
        raise ValueError('cb is not callable')

It's more universal (and I'm sure logically correct) solution.

Mikhail Gerasimov
  • 36,989
  • 16
  • 116
  • 159
  • 2
    But would't that change behavior if a normal function is passed as callback that happens to return an awaitable object, in that case the returned object would also get awaited/executed. Like `def testcb(): return AsyncSmartCallback` – Ecko Mar 19 '16 at 08:36
  • 2
    @Ecko in case you'll pass this `testcb`, result you'll got is `AsyncSmartCallback` as it should be, nothing would be awaited. It happens because `AsyncSmartCallback` is *not* awaitable object, it's class that returns awaitable object: `AsyncSmartCallback()` - this is awaitable object. In case your function `def testcb(): return AsyncSmartCallback()`, this object would be awaited. But I see nothing wrong with it: imagine you pass `def testcb(): return Callback()` - in that case `Callback()` would also be executed. – Mikhail Gerasimov Mar 19 '16 at 09:10
  • I like that callable option at the end. The second could also be written in one function as `def __await__(self): return (yield from asyncio.sleep(1).__await__())` – Break Feb 08 '21 at 09:13
  • 1
    if the callback `cb()` is a blocking sync function, this will block the entire event loop, no? – MikeTwo May 21 '21 at 03:12
  • @MikeTwo sure. It's assumed that OP understands it and won't pass long-running sync callbacks. – Mikhail Gerasimov May 22 '21 at 00:28
  • 5
    As for python 3.9, async def functions are not recognized by inspect.isawaitable: ``` >>> async def f(): pass ... >>> inspect.isawaitable(f) False ``` – makeroo Aug 17 '21 at 07:28
  • 2
    @makeroo I was confused at first, everything is correct: `f` is not awaitable, but `f()` is. `inspect.isawaitable(f()) True` – Mikhail Gerasimov Aug 17 '21 at 09:43
  • You're right @MikhailGerasimov, problem is, I found this API confusing. – makeroo Aug 17 '21 at 16:00
26

Co-routines have the COROUTINE flag set, bit 7 in the code flags:

>>> async def foo(): pass
>>> foo.__code__.co_flags & (1 << 7)
128   # not 0, so the flag is set.

The value 128 is stored as a constant in the inspect module:

>>> import inspect
>>> inspect.CO_COROUTINE
128
>>> foo.__code__.co_flags & inspect.CO_COROUTINE
128

The inspect.iscoroutinefunction() function does just that; test if the object is a function or method (to ensure there is a __code__ attribute) and test for that flag. See the source code.

Of course, using inspect.iscoroutinefunction() is the most readable and guaranteed to continue to work if ever the code flags were to change:

>>> inspect.iscoroutinefunction(foo)
True
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
2

Extend the answers above. There has been 4 types of functions since python 3.6 :

  • function
  • generator function
  • coroutine function
  • asynchronous generator function

If your application does not have prior knowledge about the type of the given function, it could be one of them above, the asynchronous function could be either coroutine function or asynchronous generator function . asyncio.iscoroutinefunction(someFunc) only checks whether a function is coroutine function, for asynchronous generator, you can use inspect.isasyncgenfunction(). The sample code is shown below :

import inspect, asyncio

def isAsync(someFunc):
    is_async_gen = inspect.isasyncgenfunction(someFunc)
    is_coro_fn = asyncio.iscoroutinefunction(someFunc)
    return is_async_gen or is_coro_fn
Ham
  • 703
  • 8
  • 17
  • 1
    There's also e.g. `partial(some_async_func, ...)` which is neither of those. Seems a heuristic is the closest we can get as we try to guess whether it returns an awaitable without actually calling it. I wonder whether `async def f() -> Awaitable` would make `partial(...)`, etc easier to detect. – Tim Diels Dec 14 '21 at 15:37
2

I couldn't find the other answers here to satisfy this. At least in py 3.10:

class Foo:
    async def __call__():
        pass

foo = Foo()
asyncio.iscoroutinefunction(foo)  #  produces False.

instead, to test you can do:

asyncio.iscoroutinefunction(foo) or asyncio.iscoroutinefunction(foo.__call__)

Note that:

async def bar():
    pass

asyncio.iscoroutinefunction(bar.__call__)  # produces False

In my own code I used to have a lot of:


if asyncio.iscoroutinefunction(foo):
    await foo()
else:
    foo()

a clearer pattern where you want to handle either could be:


async def if_coro(result):
    if asyncio.iscoroutine(result): # or inspect.iscoroutine,... and so on
        return await result
    else:
        return result

result = await if_coro(async_func())  # result is as expected
result = await if_coro(sync_func())  # result is as expected

There is likely many different variations of the above.

LetsDoThis
  • 95
  • 6
  • Thank you. Having to call a function to determine if it is async is not always practical (calling it may have unwanted side effects), and knowing how to handle async functors is a big help. – Russell Owen May 02 '23 at 23:59
1

What about applying EAFP here:

try:
    result = await cb()
except TypeError as err:
    if "can't be used in 'await' expression" in str(err):
        result = cb()
    else:
        raise

this also solves the problem, when cb is an instance of partial as well

Known limitations:

  • sync function returning an awaitable. In this case the logic will fail
zhukovgreen
  • 1,551
  • 16
  • 26
  • I think the problem with this is that if `cb` is a non-async regular function which returns an awaitable for eg. then the `try` block would succeed and you would be actually awaiting the result of `cb()` (which is an awaitable) although `cb` itself is a normal function. That may not be what you desired - maybe you wanted to collect the awaitable result form calling `cb()` but not await that yet. – ustulation Jun 16 '23 at 12:46
  • Makes sense, @ustulation – zhukovgreen Jun 16 '23 at 15:39
0

use asyncio.iscoroutine() for judging coroutine, and asyncio.isfuture()for judging task or future

import asyncio


async def task():
    await asyncio.sleep(0.01)
    print(1)

async def main():
    t = task()
    print(type(t))# <class 'coroutine'>
    print(asyncio.iscoroutine(t)) # True
    print(asyncio.isfuture(t)) # False
    await t

async def main2():
    t = asyncio.create_task(task())
    print(type(t)) # <class '_asyncio.Task'>
    print(asyncio.iscoroutine(t)) # False
    print(asyncio.isfuture(t)) # True
    await t

if __name__ == '__main__':
    asyncio.run(main())
    asyncio.run(main2())
eastonsuo
  • 935
  • 9
  • 11