Skip to content

asyncio: allow port=None in create_server when a host is given - #16219

Open
ekanshul wants to merge 1 commit into
python:mainfrom
ekanshul:asyncio-create-server-port-none
Open

asyncio: allow port=None in create_server when a host is given#16219
ekanshul wants to merge 1 commit into
python:mainfrom
ekanshul:asyncio-create-server-port-none

Conversation

@ekanshul

Copy link
Copy Markdown

Fixes #16157.

create_server hands port to getaddrinfo, which accepts None, so loop.create_server(proto, "localhost", None) works at runtime. The stub rejected it because the non-sock overload declared port: int. This widens it to port: int | None = None in the six create_server overloads (three version gates each in base_events.pyi and events.pyi).

create_connection also has port: int = ..., but I left it alone, since its port is not optional in the same way.

What the runtime actually does

I probed every combination on 3.14.2 rather than going from the docs, since the report and the docs disagree in one place:

call runtime stub before stub after
create_server(P) ValueError: Neither host/port nor sock were specified accepted accepted
create_server(P, "localhost") OK accepted accepted
create_server(P, "localhost", 8080) OK accepted accepted
create_server(P, "localhost", None) OK rejected accepted
create_server(P, None, None) ValueError accepted accepted
create_server(P, None, 8080) OK (binds all interfaces) accepted accepted
create_server(P, sock=s) OK accepted accepted
create_server(P, "localhost", sock=s) ValueError: host/port and sock can not be specified at the same time rejected rejected

Worth noting that the first line of the issue's example, await loop.create_server(asyncio.Protocol), is listed there as succeeding but actually raises ValueError.

On the third overload

@srittau suggested a third overload plus dropping the default on port in the existing one. I could not write it exactly as sketched: host has a default and port does not, so host: str | Sequence[str] | None = None, port: int is a syntax error, and dropping host's default to fix that makes the overload require host, which rejects create_server(P, port=8080) (row 6 above, valid at runtime).

Tightening the stub so the two ValueError rows are rejected too therefore needs four overloads rather than three, across all six sites. That is a much larger diff and a real design call, so I have kept this PR to the reported bug, which it fixes with no new false positives. Happy to add the stricter version in this PR or a follow-up if you would like it.

Tests

Added stdlib/@tests/test_cases/asyncio/check_create_server.py, per the note in the issue that tests seemed useful here. It covers both AbstractEventLoop and BaseEventLoop, and the two ValueError combinations are pinned with # type: ignore.

Verified locally:

  • Test case fails on main (4 errors, on exactly the two port=None lines) and passes with the change.
  • mypy --strict on the test case passes for --python-version 3.10 through 3.14, covering all three version gates.
  • pyright passes with pyrightconfig.testcases.json, so both # type: ignores are necessary under reportUnnecessaryTypeIgnoreComment.
  • stubtest clean on asyncio.base_events and asyncio.events on 3.13 and 3.14.
  • pyright clean on the two modified stubs, plus check_typeshed_structure.py, black, flake8 with flake8-pyi, and the test-case ruff selection.

create_server passes port straight to getaddrinfo, which accepts None,
so create_server(proto, "localhost", None) works at runtime but the stub
rejected it because the non-sock overload declared port: int.

Widen that parameter to int | None in the six create_server overloads
(three version gates each in base_events.pyi and events.pyi) and add
test cases covering the host/port/sock combinations.

create_connection also has port: int = ..., but it is left alone here:
its port is not optional in the same way.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

Diff from mypy_primer, showing the effect of this PR on open source code:

websockets (https://github.com/aaugustin/websockets)
+ src/websockets/asyncio/server.py:672: error: Unused "type: ignore" comment  [unused-ignore]

aiohttp (https://github.com/aio-libs/aiohttp)
- aiohttp/web_runner.py:60:13: error: Argument 3 to "create_server" of "AbstractEventLoop" has incompatible type "int | None"; expected "int"  [arg-type]

@ekanshul

Copy link
Copy Markdown
Author

The mypy_primer run is a good confirmation of this one, so worth spelling out what the two lines are.

aiohttp loses a real false positive. web_runner.py passes a port of type int | None straight through, and today that is an error:

- aiohttp/web_runner.py:60:13: error: Argument 3 to "create_server" of "AbstractEventLoop" has incompatible type "int | None"; expected "int"

websockets is the same bug, already worked around in the source. The new "unused type: ignore" is that workaround becoming unnecessary:

# mypy cannot tell that kwargs must provide sock when port is None.
return await loop.create_server(protocol_factory, host, port, **kwargs)  # type: ignore[arg-type]

So both entries point the same way: one project was eating the error, the other had suppressed it by hand.

Worth noting for the second one that the comment there describes the stricter behaviour discussed in the issue, sock being required when port is None. This PR does not implement that, so their # type: ignore becomes redundant rather than wrong. If you would rather I go the four-overload route so that constraint is actually enforced, say the word and I will extend this PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

BaseEventLoop.create_server overload doesn't allow passing host with port=None

1 participant