Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/1614.deprecated.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The ``unused_tcp_port``, ``unused_tcp_port_factory``, ``unused_udp_port``, and ``unused_udp_port_factory`` fixtures are deprecated because the returned port can be claimed by another process before the test binds it. Bind to port 0 and read the assigned port from the server instead.
80 changes: 80 additions & 0 deletions docs/how-to-guides/avoid_unused_port_fixtures.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
Avoid the unused port fixtures

@seifertm seifertm Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This document lists calls to various asyncio functions, but it doesn't contain any information or recommendation on what current users of the deprecated fixtures should do or which functionality they should use instead. Can you make a proposal?

On another note, why do you think a how-to guide is warranted for the deprecation?

==============================

The ``unused_tcp_port``, ``unused_tcp_port_factory``, ``unused_udp_port``, and
``unused_udp_port_factory`` fixtures are deprecated because the port they
return can be claimed by another process before the test binds it.

Bind to port 0 instead and read the port assigned by the operating system.

Migration
---------

Replace the fixtures with the patterns below:

- ``unused_tcp_port`` / ``unused_udp_port``: bind the server or datagram
endpoint to port 0 and read the port assigned by the operating system.
The examples below show the full pattern for TCP and UDP.
- ``unused_tcp_port_factory`` / ``unused_udp_port_factory``: create each
server or datagram endpoint on port 0 and read its assigned port. If the
port must be passed to another process before the server starts, bind the
socket in the test and pass the listening socket (or use a
``socketpair``/pipe) instead of a bare port number; a bare port can always
be claimed before the other process binds it.

The deprecated fixtures remain available and keep working, but they emit a
``DeprecationWarning``. The warning and this guide describe the same
migration; the guide exists because the fixtures are public API and the
warning alone does not show the replacement pattern.

TCP
---

.. code-block:: python

import asyncio

import pytest


async def handler(reader, writer):
writer.close()


@pytest.mark.asyncio
async def test_server():
server = await asyncio.start_server(handler, host="127.0.0.1", port=0)
port = server.sockets[0].getsockname()[1]
try:
reader, writer = await asyncio.open_connection("127.0.0.1", port)
writer.close()
await writer.wait_closed()
finally:
server.close()
await server.wait_closed()

UDP
---

.. code-block:: python

import asyncio

import pytest


class Protocol(asyncio.DatagramProtocol):
pass


@pytest.mark.asyncio
async def test_datagram_endpoint():
loop = asyncio.get_running_loop()
transport, _ = await loop.create_datagram_endpoint(
Protocol, local_addr=("127.0.0.1", 0)
)
port = transport.get_extra_info("sockname")[1]
try:
transport.sendto(b"ping", ("127.0.0.1", port))
finally:
transport.close()
1 change: 1 addition & 0 deletions docs/how-to-guides/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,6 @@ How-To Guides
parametrize_with_asyncio
uvloop
test_item_is_async
avoid_unused_port_fixtures

This section of the documentation provides code snippets and recipes to accomplish specific tasks with pytest-asyncio.
6 changes: 6 additions & 0 deletions docs/reference/fixtures/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@
Fixtures
========

.. warning::

The unused port fixtures are deprecated because the returned port can be
claimed before the test binds it. See
:doc:`../../how-to-guides/avoid_unused_port_fixtures`.

unused_tcp_port
===============
Finds and yields a single unused TCP port on the localhost interface. Useful for
Expand Down
14 changes: 14 additions & 0 deletions pytest_asyncio/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -1073,19 +1073,32 @@ def _unused_port(socket_type: int) -> int:
return sock.getsockname()[1]


def _warn_deprecated_port_fixture(name: str) -> None:
warnings.warn(
f"The {name} fixture is deprecated because its port can be "
"claimed before the test binds it. Bind to port 0 and read the "
"assigned port from the server instead.",
DeprecationWarning,
stacklevel=2,
)


@pytest.fixture
def unused_tcp_port() -> int:
_warn_deprecated_port_fixture("unused_tcp_port")
return _unused_port(socket.SOCK_STREAM)


@pytest.fixture
def unused_udp_port() -> int:
_warn_deprecated_port_fixture("unused_udp_port")
return _unused_port(socket.SOCK_DGRAM)


@pytest.fixture(scope="session")
def unused_tcp_port_factory() -> Callable[[], int]:
"""A factory function, producing different unused TCP ports."""
_warn_deprecated_port_fixture("unused_tcp_port_factory")
produced = set()

def factory():
Expand All @@ -1105,6 +1118,7 @@ def factory():
@pytest.fixture(scope="session")
def unused_udp_port_factory() -> Callable[[], int]:
"""A factory function, producing different unused UDP ports."""
_warn_deprecated_port_fixture("unused_udp_port_factory")
produced = set()

def factory():
Expand Down
33 changes: 33 additions & 0 deletions tests/test_port_factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,18 @@

from textwrap import dedent

import pytest
from pytest import Pytester

import pytest_asyncio.plugin

pytestmark = pytest.mark.filterwarnings(
"ignore:The unused_.* fixture is deprecated:DeprecationWarning"
)


def test_unused_tcp_port_selects_unused_port(pytester: Pytester):
pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function")
pytester.makepyfile(dedent("""\
import asyncio

Expand All @@ -31,9 +37,17 @@ async def closer(_, writer):
await server1.wait_closed()
"""))

result = pytester.runpytest("-W", "ignore:The unused_:DeprecationWarning")
result.assert_outcomes(passed=1)


def test_unused_udp_port_selects_unused_port(pytester: Pytester):
pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function")
pytester.makepyfile(dedent("""\
import asyncio

import pytest

@pytest.mark.asyncio
async def test_unused_udp_port_fixture(unused_udp_port):
class Closer:
Expand All @@ -60,9 +74,17 @@ def connection_lost(self, *arg, **kwd):
transport1.abort()
"""))

result = pytester.runpytest("-W", "ignore:The unused_:DeprecationWarning")
result.assert_outcomes(passed=1)


def test_unused_tcp_port_factory_selects_unused_port(pytester: Pytester):
pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function")
pytester.makepyfile(dedent("""\
import asyncio

import pytest

@pytest.mark.asyncio
async def test_unused_port_factory_fixture(unused_tcp_port_factory):
async def closer(_, writer):
Expand Down Expand Up @@ -96,9 +118,17 @@ async def closer(_, writer):
await server3.wait_closed()
"""))

result = pytester.runpytest("-W", "ignore:The unused_:DeprecationWarning")
result.assert_outcomes(passed=1)


def test_unused_udp_port_factory_selects_unused_port(pytester: Pytester):
pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function")
pytester.makepyfile(dedent("""\
import asyncio

import pytest

@pytest.mark.asyncio
async def test_unused_udp_port_factory_fixture(unused_udp_port_factory):
class Closer:
Expand Down Expand Up @@ -144,6 +174,9 @@ def connection_lost(self, *arg, **kwd):
transport3.abort()
"""))

result = pytester.runpytest("-W", "ignore:The unused_:DeprecationWarning")
result.assert_outcomes(passed=1)


def test_unused_port_factory_duplicate(unused_tcp_port_factory, monkeypatch):
"""Test correct avoidance of duplicate ports."""
Expand Down
33 changes: 33 additions & 0 deletions tests/test_port_fixture_deprecation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from __future__ import annotations

from textwrap import dedent

import pytest
from pytest import Pytester


@pytest.mark.parametrize(
"fixture_name",
[
"unused_tcp_port",
"unused_udp_port",
"unused_tcp_port_factory",
"unused_udp_port_factory",
],
)
def test_port_fixtures_are_deprecated(pytester: Pytester, fixture_name: str) -> None:
pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function")
pytester.makepyfile(dedent(f"""\
import pytest

def test_deprecated(request):
with pytest.warns(
DeprecationWarning,
match="The {fixture_name} fixture is deprecated",
):
request.getfixturevalue("{fixture_name}")
"""))

result = pytester.runpytest()

result.assert_outcomes(passed=1)
Loading