Hardware FixRecommendedDevice not working? Your driver may be the problemCheck updates for common hardware issues.Fix DriversOctober DealsAmazon USOctober deal check: compare before you payAmazon US: current deals, useful picks and tech finds.Check DealsClean PCRecommendedOne scan can reveal what keeps slowing WindowsLook for cleanup and repair opportunities.Run Scan×
Skip to content
World desk8 min

Python asyncio: A Practical Guide to Asynchronous Programming

A practical, version-aware guide to Python asyncio: understand coroutines and cooperative scheduling, manage concurrent tasks, and avoid common event-loop errors.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

asyncio helps Python run multiple I/O-bound operations concurrently on one thread: a coroutine runs until it reaches an await that suspends it, giving the event loop a chance to run other ready work. It does not automatically make CPU-heavy Python code parallel. The Python documentation describes it as “a library to write concurrent code using the async/await syntax.”

This guide uses APIs available in Python 3.11 and later, including TaskGroup. Check the documentation for your specific Python release and platform when relying on newer or platform-specific features.

When should you use asyncio?

Use asyncio when a program spends substantial time waiting for network operations, subprocesses, or other asynchronous I/O, and you want to make progress on other work during those waits. It is commonly suited to network clients and servers, concurrent HTTP requests, and applications coordinating many I/O operations.

A synchronous program is often simpler when it performs a small amount of I/O or when its libraries are synchronous. For CPU-bound work such as heavy numerical computation, asyncio alone is not a parallel-computing solution: synchronous computation holds the event-loop thread until it finishes. Consider a process pool, native code that releases the GIL, or another suitable approach for CPU parallelism.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
  • Good fit: many operations that spend time waiting and have asynchronous library support.
  • Not an automatic speedup: CPU-bound code or blocking libraries called directly from an async task.
  • Important prerequisite: the libraries used along the async path must themselves avoid blocking the event-loop thread, or blocking work must be moved elsewhere.

How asyncio scheduling works

An async def function defines a coroutine function. Calling it creates a coroutine object; it does not run the function to completion. The coroutine must be awaited by another coroutine or scheduled as a task.

import asyncio

async def greet(name: str) -> str:
    await asyncio.sleep(0.1)
    return f"Hello, {name}!"

async def main() -> None:
    message = await greet("Ada")
    print(message)

if __name__ == "__main__":
    asyncio.run(main())

Save this as a Python file and run it with Python 3.11 or later. asyncio.run(main()) creates and manages the event loop for this top-level program, runs the coroutine, and closes the loop when it is done. For ordinary scripts, this is the recommended entry point rather than manually constructing and managing an event loop.

Concurrency is cooperative. While a task is running ordinary Python code, another task on that event loop does not preempt it. When it awaits an operation that suspends, the loop can run another ready task. An await does not guarantee a switch on every occasion: if the awaited operation is already complete, execution may continue without yielding.

Waiting asynchronously versus blocking

In the example, asyncio.sleep() suspends the coroutine without blocking the event-loop thread. By contrast, calling time.sleep() inside a coroutine blocks that thread; other tasks on the same loop cannot run during the block. The same risk applies to synchronous network calls and long-running CPU work.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
import asyncio
import time

async def bad_wait() -> None:
    time.sleep(2)  # Blocks the event-loop thread.

async def good_wait() -> None:
    await asyncio.sleep(2)  # Lets other tasks run.

Do not use asyncio.sleep(0) as a replacement for making blocking code asynchronous. It yields control, but it does not make the blocking operation itself nonblocking.

Run related coroutines concurrently

Use a task group when several operations belong to one unit of work and should finish before that unit returns. asyncio.TaskGroup, available in Python 3.11 and later, provides structured concurrency: the group owns its child tasks, waits for them on exit, and handles child failures as a group.

import asyncio

async def fetch_label(label: str, delay: float) -> str:
    await asyncio.sleep(delay)
    return label

async def main() -> None:
    async with asyncio.TaskGroup() as group:
        first = group.create_task(fetch_label("first", 1.0))
        second = group.create_task(fetch_label("second", 0.5))

    print(first.result(), second.result())

asyncio.run(main())

The tasks overlap while waiting, so total waiting time is approximately the longer delay rather than the sum of both delays. This is an illustration of scheduling, not a general performance benchmark. After the async with block exits successfully, both tasks are complete and their results are available.

TaskGroup failure and cancellation

If a child task raises an exception other than asyncio.CancelledError, the group cancels its remaining child tasks, waits for their cleanup, and reports failures using an exception group. Handle failures outside the group with except* ExceptionType when you need to process matching exceptions from that group.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
try:
    async with asyncio.TaskGroup() as group:
        group.create_task(do_work())
        group.create_task(do_other_work())
except* OSError as errors:
    for error in errors.exceptions:
        print("I/O failure:", error)

Task cancellation is cooperative too. A task generally receives cancellation at an await point. If a coroutine needs cleanup, use try/finally; do not swallow cancellation unintentionally.

async def use_resource() -> None:
    resource = await acquire_resource()
    try:
        await perform_work(resource)
    finally:
        await resource.close()

Cleanup in a finally block should be designed to complete safely under cancellation. Avoid catching BaseException or suppressing CancelledError unless you have a deliberate cancellation policy; structured task management depends on cancellation being allowed to propagate.

When to create a task directly

asyncio.create_task(coro) schedules a coroutine to run concurrently and returns a task. Keep a reference to tasks you create, await them, and decide how their errors and cancellation are handled. Untracked background tasks can outlive the code that logically owns them, have exceptions that go unnoticed, or be cancelled when the program shuts down.

For a group of related tasks, prefer TaskGroup. Direct task creation remains useful when a task genuinely has a longer or separately managed lifetime, but the owner must still retain, await, cancel, and observe it deliberately.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Useful high-level asyncio APIs

Start with high-level APIs rather than manipulating event loops, futures, transports, or protocols. The latter are primarily useful when building frameworks or libraries that need lower-level control.

  • Network I/O: stream APIs such as asyncio.open_connection() and asyncio.start_server() support coroutine-based TCP clients and servers. For HTTP, use an async HTTP client library rather than expecting the standard library’s synchronous HTTP calls to become nonblocking.
  • Queues: asyncio.Queue lets producer and consumer coroutines exchange work without busy-waiting. A bounded queue can apply backpressure when producers outpace consumers.
  • Synchronization: asyncio.Lock, Event, Condition, and Semaphore coordinate tasks on an event loop. These are not general-purpose thread synchronization primitives.
  • Subprocesses: asyncio subprocess APIs allow a program to wait for child processes asynchronously. They are distinct from moving arbitrary CPU work into a process pool.
  • Timeouts: use timeout facilities such as asyncio.timeout() (Python 3.11+) to bound a region of asynchronous work. A timeout cancels the work; code must still clean up correctly.

Timeout example

import asyncio

async def main() -> None:
    try:
        async with asyncio.timeout(3):
            await perform_request()
    except TimeoutError:
        print("The operation exceeded three seconds")

asyncio.run(main())

A timeout is not a hard interruption of arbitrary synchronous code. If perform_request() blocks the event-loop thread, the loop cannot deliver cancellation promptly. Use an asynchronous operation or isolate blocking work.

Calling blocking work and crossing threads

When a blocking I/O function has no async equivalent, asyncio.to_thread() can run it in a worker thread so the event loop remains available. This is generally a way to avoid blocking during I/O, not a way to parallelize pure Python CPU-bound work.

import asyncio

async def main() -> None:
    result = await asyncio.to_thread(blocking_io_function, "argument")
    print(result)

asyncio.run(main())

Many asyncio objects are not thread-safe. If another operating-system thread needs to schedule work on a running loop, use the loop’s thread-safe scheduling APIs, such as loop.call_soon_threadsafe(callback, *args) for a callback or asyncio.run_coroutine_threadsafe(coro, loop) for a coroutine. Do not call ordinary loop scheduling methods from an unrelated thread.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Debug asyncio problems

Enable debug mode during development to help expose incorrect API use and slow callbacks. You can pass debug=True to asyncio.run(), or use the PYTHONASYNCIODEBUG environment variable. The event loop can report callbacks that take too long; such reports are a useful sign to inspect blocking calls or unexpectedly expensive work.

import asyncio

async def main() -> None:
    await do_work()

asyncio.run(main(), debug=True)

Debug mode can add overhead and is primarily a diagnostic aid. Treat slow-callback warnings as evidence that code is occupying the loop, not as a measure of total application performance.

Common errors and fixes

Symptom Likely cause Fix
RuntimeWarning: coroutine was never awaited A coroutine was created by calling an async def function but neither awaited nor scheduled. Use await function() from async code, or create and manage a task with asyncio.create_task() or a task group.
asyncio.run() cannot be called from a running event loop You called the script-level runner inside code that already runs an event loop, common in notebooks and async frameworks. In an async function, use await main(). Let the framework or notebook own its loop.
Other tasks appear frozen during a request or delay A synchronous blocking function is running on the event-loop thread. Replace it with an async API, use asyncio.to_thread() for suitable blocking I/O, or use an appropriate process-based approach for CPU work.
A task’s exception is not handled where expected The task may have been started without an owner awaiting it; a task group may report an exception group instead of a single exception. Await the task or use a task group, and handle grouped failures with except* where appropriate.
Cancellation skips cleanup or leaves work running Cancellation was swallowed, cleanup was omitted, or task lifetime was not managed. Use try/finally for cleanup, generally let CancelledError propagate, and keep tasks within a clear owner such as a task group.
“Non-thread-safe operation” or erratic behavior across threads An asyncio object or loop API was used from a thread other than the loop’s thread. Schedule callbacks with call_soon_threadsafe() or coroutines with run_coroutine_threadsafe().

Or skip the browser setup

If your async project needs website screenshots, ScreenshotNeo provides a screenshot API; it is a separate service, not an asyncio feature. One GET request returns an image or PDF. For example, this cURL request saves a WebP screenshot of Stripe:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

See the ScreenshotNeo API documentation for parameters and response details. Cookie banners are accepted and removed along with supported newsletter popups and chat widgets before capture; bot checks, blank pages, failed loads, timeouts, and cache hits are not billed. An MCP server provides screenshot tools for AI agents. The free plan includes 1,000 screenshots per month with no card, and paid plans start at $5 for 3,000 shots. Learn about ScreenshotNeo or sign up free.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Further reading

The official Python asyncio library reference describes the API families and their version-specific behavior. Consult the documentation for the Python release you deploy, particularly for newer task and timeout APIs; development-branch documentation can change before release.

Frequently Asked Questions

Can I use asyncio in a Jupyter notebook?

Yes. A notebook typically already has a running event loop, so await a coroutine directly in a cell instead of calling asyncio.run() there.

Does asyncio require multiple CPU cores?

No. Its concurrency model lets a single event-loop thread coordinate work that yields while waiting; that is different from executing CPU-bound Python code in parallel.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Leave a Reply

Your email address will not be published. Required fields are marked *

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

More from the Wire

  1. Shenzhen desk3 min
    HONOR Expands Beyond Smartphones With Humanoid Robot RevealHONOR said it unveiled its first humanoid robot at MWC 2026 and named shopping assistance, workplace inspections, and supportive companionship as intended uses. Later Robotics D1 claims and a reported…
  2. Cupertino desk5 min
    Apple Unveils AirPods Max 2: The Upgrade That Should Have Happened Years AgoAirPods Max 2 adds H2-powered audio features and Apple claims up to 1.5× more effective ANC, but its design, Smart Case, and 20-hour battery rating are unchanged. Wired lossless audio…
  3. Cupertino desk4 min
    Apple’s OLED Touch MacBooks Are Coming—but the Dynamic Island Is the Real GambleApple has not announced an OLED touchscreen MacBook, but reports point to high-end models arriving in late 2026 or early 2027. The reported Mac Dynamic Island could be useful, but…
Recommended PC Tool
Recommended PC Tool
Outdated Drivers Are Slowing You DownFree scan - exact matches
PC Slower Than It Used to Be?Free scan - under a minute

Two free Windows tools

One Free Minute Could Fix That PC

Before you go - each of these free tools takes about a minute and tackles what quietly slows a Windows PC down.

Special offer. View Outbyte info, uninstall instructions, EULA, and Privacy Policy.