135

I would like to use asyncio to get webpage html.

I run the following code in jupyter notebook:

import aiofiles
import aiohttp
from aiohttp import ClientSession

async def get_info(url, session):
    resp = await session.request(method="GET", url=url)
    resp.raise_for_status()
    html = await resp.text(encoding='GB18030')
    with open('test_asyncio.html', 'w', encoding='utf-8-sig') as f:
        f.write(html)
    return html
    
async def main(urls):
    async with ClientSession() as session:
        tasks = [get_info(url, session) for url in urls]
        return await asyncio.gather(*tasks)

if __name__ == "__main__":
    url = ['http://huanyuntianxiazh.fang.com/house/1010123799/housedetail.htm', 'http://zhaoshangyonghefu010.fang.com/house/1010126863/housedetail.htm']
    result = asyncio.run(main(url))

However, it returns RuntimeError: asyncio.run() cannot be called from a running event loop

What is the problem?

How to solve it?

mkrieger1
  • 19,194
  • 5
  • 54
  • 65
Chan
  • 3,605
  • 9
  • 29
  • 60

8 Answers8

199

The asyncio.run() documentation says:

This function cannot be called when another asyncio event loop is running in the same thread.

In your case, jupyter (IPython ≥ 7.0) is already running an event loop:

You can now use async/await at the top level in the IPython terminal and in the notebook, it should — in most of the cases — “just work”. Update IPython to version 7+, IPykernel to version 5+, and you’re off to the races.

Therefore you don't need to start the event loop yourself and can instead call await main(url) directly, even if your code lies outside any asynchronous function.

Jupyter (IPython ≥ 7.0)

async def main():
    print(1)
    
await main()

Python ≥ 3.7 and IPython < 7.0

import asyncio

async def main():
    print(1)
    
asyncio.run(main())

In your code that would give:

url = ['url1', 'url2']
result = await main(url)

for text in result:
    pass # text contains your html (text) response

Caution

There is a slight difference on how Jupyter uses the loop compared to IPython.

cglacet
  • 8,873
  • 4
  • 45
  • 60
  • Thank you, cglacet. However, there is a warning : `c:\program files\python37\lib\site-packages\ipykernel_launcher.py:29: RuntimeWarning: coroutine 'main' was never awaited` – Chan Mar 29 '19 at 02:32
  • 1
    That's probably because you called `main(url)` instead of `await main(url)`. – cglacet Mar 29 '19 at 02:33
  • 1
    Is there a way to have a code snippet that works both inside and outside of Jupyter? – Foad S. Farimani Apr 02 '20 at 10:58
  • I don't know if it's possible, but I'm not sure you would really want that anyway. Maybe ask this as a separate question and see if people have ideas on whether it's possible or desirable. – cglacet Apr 11 '20 at 13:06
  • 7
    Interestingly, when I run the above in jupyter, I get: SyntaxError: 'await' outside function – Luca Jul 27 '20 at 22:55
  • Maybe try to upgrade jupyter, older versions do not work the same way. – cglacet Nov 27 '20 at 14:25
  • @Luca i used the wait it and it worked for me ( in a very simple program to just sign in and receive a token from telethon ) – Mauro Abrantes Feb 04 '22 at 00:09
  • solved my issue in vs code Jupyter notebook – Capi Hidalgo Oct 03 '22 at 11:39
  • Was the error the exact same? It may be worth to add the vscode tag to the question if that's the case (I think more and more people will use the extension as it improves). – cglacet Oct 03 '22 at 12:27
60

To add to cglacet's answer - if one wants to detect whether a loop is running and adjust automatically (ie run main() on the existing loop, otherwise asyncio.run()), here is a snippet that may prove useful:

# async def main():
#     ...

try:
    loop = asyncio.get_running_loop()
except RuntimeError:  # 'RuntimeError: There is no current event loop...'
    loop = None

if loop and loop.is_running():
    print('Async event loop already running. Adding coroutine to the event loop.')
    tsk = loop.create_task(main())
    # ^-- https://docs.python.org/3/library/asyncio-task.html#task-object
    # Optionally, a callback function can be executed when the coroutine completes
    tsk.add_done_callback(
        lambda t: print(f'Task done with result={t.result()}  << return val of main()'))
else:
    print('Starting new event loop')
    result = asyncio.run(main())
Jean Monet
  • 2,075
  • 15
  • 25
  • 1
    Nice, indeed adding a callback here is probably the only way of getting the result back. That would probably be interesting to have this code in a function so you would only have to call something like `run(main(*args, **kwargs))`. The function could also make sure you get your output back, not sure that's really simple tho (because of the callback). – cglacet Apr 29 '20 at 22:35
  • This is an intriguing solution. How would you get `t.result()` in a variable instead of printing it out? – alec_djinn Aug 10 '22 at 12:13
  • @alec_djinn no immediate easy answer (at least not from *within the sync code*), since introducing a blocking method in the current thread ie. in sync code (such as `Queue.get()`, with a `Queue.put(t.result())` in callback) would also block the existing async loop from running (in current thread), thus no result would arrive -> deadlock. If you're in a Jupyter NB you could still use a messaging system and manually check for the result (in non-blocking manner). Note using `time.sleep()` in sync code (current thread) doesn't help since it would also pause the existing async loop. – Jean Monet Aug 10 '22 at 13:08
  • 1
    @JeanMonet brilliant solution works like a charm. – Peter Trcka Jun 21 '23 at 21:07
  • 1
    @JeanMonet this is awesome! Deserve more upvotes. – tyson.wu Jul 14 '23 at 07:59
34

Just use this:

https://github.com/erdewit/nest_asyncio

import nest_asyncio
nest_asyncio.apply()
vgoklani
  • 10,685
  • 16
  • 63
  • 101
  • 6
    The docs for `nest_asyncio` mention a [issue report with asyncio](https://bugs.python.org/issue22239) where its explicitly stated that this is no intended behaviour for asyncio. So I would consider `nest_asyncio` a big fat hack which I wouldn't trust with in my codebase to not break everything with a new Python version. – Gregor Müllegger Oct 14 '20 at 11:29
  • Another issue is that nest_asyncio requires Python 3.5 or above, which is not helpful to me as I am stuck on Python 2.7. – mknote Nov 17 '20 at 21:23
  • 2
    this allowed me to solve the issue for the imported code (where you can't change it yourself) – Alex S. Nov 30 '20 at 17:16
  • 1
    This worked so well fixing a imported asyncio issue, thanks alot – Mika C. May 09 '22 at 19:15
  • This works like a charm! – Hansimov Sep 02 '23 at 15:12
24

Combining the methods from Pankaj Sharma and Jean Monet, I wrote the following snippet that acts as asyncio.run (with slightly different syntax), but also works within a Jupyter notebook.

class RunThread(threading.Thread):
    def __init__(self, func, args, kwargs):
        self.func = func
        self.args = args
        self.kwargs = kwargs
        self.result = None
        super().__init__()

    def run(self):
        self.result = asyncio.run(self.func(*self.args, **self.kwargs))

def run_async(func, *args, **kwargs):
    try:
        loop = asyncio.get_running_loop()
    except RuntimeError:
        loop = None
    if loop and loop.is_running():
        thread = RunThread(func, args, kwargs)
        thread.start()
        thread.join()
        return thread.result
    else:
        return asyncio.run(func(*args, **kwargs))

Usage:

async def test(name):
    await asyncio.sleep(5)
    return f"hello {name}"

run_async(test, "user")  # blocks for 5 seconds and returns "hello user"
rfedorov
  • 689
  • 7
  • 11
Mark
  • 1,306
  • 13
  • 19
  • 1
    thank you for this clever solution. Do you have any suggestion for "asyncio attached to a different loop" error? Inside child loops i need to use 2 coroutines from main loop. – Hakan Aug 11 '21 at 06:35
7

To add to existing answers, a somewhat shorter version without external libraries, that allows running inside and outside of jupyter and allows fetching the return value:

try:
    asyncio.get_running_loop()
    # we need to create a separate thread so we can block before returning
    with ThreadPoolExecutor(1) as pool:
        result = pool.submit(lambda: asyncio.run(myfunc())).result()
except RuntimeError:
    # no event loop running
    result = asyncio.run(myfunc())
Kyle Kelley
  • 13,804
  • 8
  • 49
  • 78
Andreas Mueller
  • 27,470
  • 8
  • 62
  • 74
  • Attention: calling `result()` on the `Future` returned after submitting the lambda to the pool may raise `RuntimeError` on its own, depending on what is inside `myfunc`. This is then caught and causes running `myfunc()` again in the default event loop. – lnstadrum Apr 09 '23 at 15:01
5

I found the unsync package useful for writing code that behaves the same way in a Python script and the Jupyter REPL.

import asyncio
from unsync import unsync


@unsync
async def demo_async_fn():
    await asyncio.sleep(0.1)
    return "done!"

print(demo_async_fn().result())
ostrokach
  • 17,993
  • 11
  • 78
  • 90
2

Slight simplification of the solution by Mark:

import threading

class RunThread(threading.Thread):
    def __init__(self, coro):
        self.coro = coro
        self.result = None
        super().__init__()

    def run(self):
        self.result = asyncio.run(self.coro)


def run_async(coro):
    try:
        loop = asyncio.get_running_loop()
    except RuntimeError:
        loop = None
    if loop and loop.is_running():
        thread = RunThread(coro)
        thread.start()
        thread.join()
        return thread.result
    else:
        return asyncio.run(coro)

Use run_async() like async.run(), i.e., run_async(test("user")).

sigvaldm
  • 564
  • 4
  • 15
1

As cglacet mentioned that documentation says

This function cannot be called when another asyncio event loop is running in the same thread.

You can use another thread i.e -

class ResolveThread(threading.Thread):
            def __init__(self,result1,fun,url):
                self.result1= result1
                self.fun = fun
                self.url = url
                threading.Thread.__init__(self)
            def run(self):
                result1[0] = asyncio.run(self.fun(self.url))


result1 = [None]
sp = ResolveThread(result1)
sp.start()
sp.join() # connect main thread
result = result1[0]
Pankaj Sharma
  • 2,185
  • 2
  • 24
  • 50