Skip to content

python-stdlib/selectors: Add selectors module based on select.poll. - #1168

Open
pavelrevak wants to merge 1 commit into
micropython:masterfrom
pavelrevak:selectors
Open

pavelrevak wants to merge 1 commit into
micropython:masterfrom
pavelrevak:selectors

Conversation

@pavelrevak

@pavelrevak pavelrevak commented Oct 2, 2026 •

Copy link
Copy Markdown

Summary

This adds a CPython compatible
selectors module to
python-stdlib, implemented as a thin layer over MicroPython's
select.poll(). It lets code written for CPython's high-level I/O
multiplexing API run unchanged on MicroPython, on the unix port as well as
on bare-metal ports. See micropython/micropython#1550 for earlier
discussion about a consistent polling API and CPython's selectors.

It provides EVENT_READ, EVENT_WRITE, SelectorKey, BaseSelector,
PollSelector and DefaultSelector with register(), unregister(),
modify(), select(), close(), get_key(), get_map() and context
manager support. Exceptions follow CPython (ValueError, KeyError,
RuntimeError).

Differences from CPython (documented in the package README.md):

  • File objects are tracked by identity rather than by file descriptor,
    because sockets on bare-metal ports have no fileno().
    SelectorKey.fd is the fileno() when available, otherwise -1.
  • get_map() returns a plain dict keyed by file object.
  • Only PollSelector is provided, DefaultSelector is an alias for it.

This depends on micropython/micropython#19741. Without it, registering an
object that select.poll() rejects leaves the poll object in a broken
state, and a later select() crashes. Normal use with valid objects such
as sockets is not affected.

Testing

  • Added python-stdlib/selectors/test_selectors.py (14 tests using
    loopback TCP sockets) and added the package to tools/ci.sh. One test
    covers a failed registration and needs extmod/modselect: Fix crashes after a failed register() and pollfds realloc. micropython#19741.
  • Tests pass on the unix port (MicroPython master with the fix, macOS),
    and 20 repeated runs showed no flakiness.
  • The same tests pass against CPython's stdlib selectors module
    (CPython 3.14), which confirms the behaviour matches CPython.
  • The example from README.md was run as an echo server on the unix port.
  • manifestfile.py --compile builds the package.
  • The module is already used successfully in my projects on ESP32-xx,
    for example together with my uhttp-server or uhttp-client.

Trade-offs and Alternatives

This is a new optional package, so firmware size is unchanged. The
compiled selectors.mpy is 1670 bytes.

An alternative would be to implement selectors in C. The
performance-critical part, waiting for and collecting events, is already
done in C by ipoll(); the Python layer only maps event masks and keeps
the registered keys. As a micropython-lib package it adds no code size to
firmware for anyone who doesn't use it.

Generative AI

I used generative AI tools when creating this PR, but a human has checked the
code and is responsible for the code and the description above.

This adds a CPython compatible `selectors` module implemented as a thin
layer over MicroPython's `select.poll()`.  It provides EVENT_READ,
EVENT_WRITE, SelectorKey, BaseSelector, PollSelector and DefaultSelector
with register(), unregister(), modify(), select(), close(), get_key(),
get_map() and context manager support.

File objects are tracked by identity rather than by file descriptor,
because sockets on bare-metal ports have no fileno().  SelectorKey.fd is
the fileno() when available, otherwise -1.

The tests also pass against CPython's stdlib selectors module.

Signed-off-by: Pavel Revak <pavelrevak@gmail.com>
@pavelrevak

Copy link
Copy Markdown
Author

The CI failure is expected: test_select_after_invalid_register segfaults
because it needs the fix from micropython/micropython#19741. CI builds the
unix port from MicroPython master, which doesn't include that fix yet.

The test registers an object that select.poll() rejects and then calls
select(). Without the fix, the failed poll.register() leaves a NULL
entry in the poll map and the next poll() dereferences it. With #19741
applied, all 14 tests pass.

I'll re-run CI once #19741 is merged.

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.

1 participant