pytest-asyncio review
pytest-asyncio 1.4.0 lets pytest collect and execute `asyncio` coroutine tests and async fixtures. It creates event loops, separates fixture and test loop scopes, supports strict or automatic discovery, and exposes asyncio debug mode. The current release introduces `pytest_asyncio_loop_factories`, which can parametrize tests over named loop implementations, and deprecates replacing the `event_loop_policy` fixture. It also raises the pytest floor to 8.4, makes the unset fixture-loop-scope warning clearer, and closes a loop that could be leaked after synchronous code called `asyncio.run()` or unset the current loop.
pytest-asyncio 1.4.0 installed in 0.4 seconds and used 8 MB across 7 packages, with no audit findings in our sandbox, but its package metadata did not state a license. Use it for an asyncio-only pytest suite after pinning discovery and loop scopes; mismatched fixture and loop lifetimes are the main reason to leave it out.
We installed it
| Install | ✓ · 0.4s | 7 packages on disk · 8 MB |
| Import | ✓ | import pytest_asyncio in 0.84s · pure Python · py.typed · requires Python >=3.10 |
| Known vulns | 0 | (pip-audit) |
Answers from our run
Does pytest-asyncio install cleanly?
Yes. In a fresh container with an empty cache, pip install pytest-asyncio finished in 0.4s, leaving 7 packages and 8 MB on disk. pip-audit reported no known vulnerabilities.
What does pytest-asyncio need to run?
Python >=3.10, and nothing compiled: it is pure Python. In our run import pytest_asyncio succeeded in 0.84s, and the package ships py.typed for type checkers.
pytest-asyncio or anyio: which should you use?
anyio: Pick its pytest plugin when identical tests must cover asyncio and Trio backends. pytest-asyncio 1.4.0 installed in 0.4 seconds and used 8 MB across 7 packages, with no audit findings in our sandbox, but its package metadata did not state a license.
When should you not use pytest-asyncio?
The application is written for Trio. pytest-trio knows nursery and Trio cancellation behavior that this plugin does not.
Use it if
- An asyncio suite should keep pytest assertions, parametrization, fixtures, and reporting.
- Async resources need setup and teardown on an explicitly chosen loop lifetime.
- Tests require either function isolation or a shared class, module, package, or session loop.
- The same test must run against named event-loop factories such as uvloop through the 1.4 hook.
- The application is written for Trio. pytest-trio knows nursery and Trio cancellation behavior that this plugin does not.
- One test body must run on both asyncio and Trio. AnyIO's pytest integration is built around backend parametrization.
- Async methods live on `unittest.TestCase`. The project documents that class style as unsupported and points to `IsolatedAsyncioTestCase`.
- The environment is pinned below Python 3.10 or pytest 8.4, both required by release 1.4.0.
- The migration plan depends on overriding `event_loop` or `event_loop_policy`. Current configuration uses `loop_scope` and the new loop-factory hook.
Setup reality
We installed pytest-asyncio 1.4.0 in a fresh Python 3.12 Bookworm container in 0.4 seconds. Seven packages occupied 8 MB, import pytest_asyncio completed in 0.84 seconds, and pip-audit reported zero known vulnerabilities. Metadata lists 8 direct dependencies and Python 3.10 or newer. The distribution is pure Python and contains py.typed. Its installed metadata did not identify a license, so consumers with automated license gates must resolve that unknown rather than borrowing a value from the repository page.
Installation registers the plugin with pytest. Strict mode requires @pytest.mark.asyncio on coroutine tests and @pytest_asyncio.fixture on async fixtures. Auto mode claims async tests and fixtures without those decorators, which is convenient until another async plugin also tries to own them. Set asyncio_mode, asyncio_default_test_loop_scope, and asyncio_default_fixture_loop_scope in checked-in pytest configuration so collection does not depend on a developer's local defaults.
Pytest cache scope and asyncio loop scope are independent controls. A session fixture created on a function loop outlives the loop and can later raise a closed-loop or different-loop error. Give an async fixture a loop_scope at least as wide as its cache scope, then keep dependent tests on a compatible loop. Function scope isolates loop state. A session loop reduces setup cost but also allows pending tasks and loop mutations to survive between tests.
Release 1.4.0 replaces policy-fixture overrides with pytest_asyncio_loop_factories. A mapping with several factories parametrizes matching coroutine tests; marker arguments select named entries. Calling asyncio.run() or asyncio.set_event_loop(None) inside a test can still confuse code that reads global loop state, although 1.4.0 repairs plugin cleanup around those calls. The plugin does not take over async methods on unittest.TestCase, so use unittest.IsolatedAsyncioTestCase for that style.
Patterns
Run one coroutine test mark-async-test
import pytest
@pytest.mark.asyncio
async def test_fetch(client):
result = await client.fetch()
assert result.status == 200In strict mode, this marker gives the coroutine to pytest-asyncio. Without it, pytest may skip the function and only print a warning.
Claim async tests automatically configure-auto-mode
# pyproject.toml
[tool.pytest.ini_options]
asyncio_mode = 'auto'
asyncio_default_test_loop_scope = 'function'
asyncio_default_fixture_loop_scope = 'function'Auto mode claims every async test and fixture. Install it only where another plugin will not compete for the same collected functions.
Open and close an async resource define-async-fixture
import pytest_asyncio
@pytest_asyncio.fixture
async def client():
value = await connect()
yield value
await value.aclose()Strict discovery recognizes `pytest_asyncio.fixture`, not a bare pytest async fixture. Cleanup after `yield` executes on this fixture's loop.
Match fixture and loop lifetime share-session-resource
@pytest_asyncio.fixture(scope='session', loop_scope='session')
async def pool():
value = await create_pool()
yield value
await value.close()A session fixture cannot safely depend on a function loop that closes after one test. Match both lifetimes for loop-bound clients and pools.
Run a module on one loop share-module-loop
import pytest
pytestmark = pytest.mark.asyncio(loop_scope='module')
async def test_write(db):
await db.write('x')Every marked coroutine in this module uses one loop. Unfinished tasks or altered loop state can therefore affect the next test.
Create several coroutine cases parametrize-async-test
@pytest.mark.asyncio
@pytest.mark.parametrize('value', [1, 2, 3])
async def test_valid(value):
assert await validate(value)Pytest creates 3 test items here. With function loop scope, each item also receives a new event loop.
Test with uvloop through the new hook register-loop-factory
# conftest.py
import uvloop
def pytest_asyncio_loop_factories(config, item):
return {'uvloop': uvloop.new_event_loop}Version 1.4 uses this hook instead of an `event_loop_policy` override. Returning multiple names parametrizes eligible tests across factories.
Choose one configured factory select-loop-factory
@pytest.mark.asyncio(loop_factories=['uvloop'])
async def test_transport():
assert await check_transport()`uvloop` must be a key returned by `pytest_asyncio_loop_factories`. An unknown name cannot create a loop for the test.
Run with asyncio diagnostics enable-asyncio-debug
# pytest.ini
[pytest]
asyncio_debug = trueAsyncio debug checks can reveal un-awaited coroutines and slow callbacks. They add overhead, so CI timing may differ from a normal run.
Use the standard unittest async class test-unittest-coroutine
import unittest
class TestApi(unittest.IsolatedAsyncioTestCase):
async def test_get(self):
self.assertEqual(await get_value(), 42)pytest-asyncio excludes `unittest.TestCase` coroutine methods. `IsolatedAsyncioTestCase` creates and owns the loop for this class itself.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| anyio | PyPI | Pick its pytest plugin when identical tests must cover asyncio and Trio backends. |
| pytest-trio | PyPI | Pick it for native Trio fixtures, nurseries, cancellation, and clock control. |
| pytest-tornasync | PyPI | Keep it for an existing Tornado suite already written around that plugin. |
More testing guides
pytest · chai · vitest · jsdom · playwright · coverage · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

