pyguides

selectors module

Overview

The selectors module provides a high-level interface for I/O multiplexing — the ability to monitor many sockets or file objects simultaneously and react when they become readable or writable. It sits above the select module and automatically picks the most efficient implementation available on your platform (epoll on Linux, kqueue on macOS/BSD, poll on older systems).

The key abstraction is BaseSelector and its concrete implementations. You register file objects with a selector, then call select() which blocks until at least one registered object is ready for I/O.

Available Selector Classes

ClassUnderlying primitiveBest for
DefaultSelectorPlatform’s best optionMost users — auto-selects the right implementation
SelectSelectorselect.select()Portable; works everywhere but slower
PollSelectorselect.poll()Linux; better scalability than select
EpollSelectorselect.epoll()Linux only; best performance on Linux
KqueueSelectorselect.kqueue()macOS/BSD; handles many connections efficiently
DevpollSelectorselect.devpoll()Solaris-derived systems

For almost all cases, use DefaultSelector:

import selectors

sel = selectors.DefaultSelector()

Event Constants

Two bitwise constants define what to wait for:

import selectors

selectors.EVENT_READ  # file descriptor has data to read
selectors.EVENT_WRITE  # file descriptor is ready to accept data

You can combine them with | (bitwise OR):

events = selectors.EVENT_READ | selectors.EVENT_WRITE

Registering and Unregistering

register(fileobj, events, data=None)

Register a file object (socket, file descriptor integer, or any object with a fileno() method) to be monitored:

import selectors
import socket

sel = selectors.DefaultSelector()

server = socket.socket()
server.bind(("localhost", 5000))
server.listen(100)
server.setblocking(False)

sel.register(server, selectors.EVENT_READ, data=accept)
# Returns a SelectorKey namedtuple

The data parameter attaches arbitrary opaque data to the registration — typically a callback function.

unregister(fileobj)

Stop monitoring a file object. The file object must be unregistered before closing, otherwise the behavior is undefined:

key = sel.unregister(client_socket)
# key is the SelectorKey that was associated with this socket

The select() Method

Block until one or more registered file objects are ready, or the timeout expires:

events = sel.select(timeout=None)
# events: list of (SelectorKey, event_mask) tuples
  • timeout=None — block indefinitely until something is ready
  • timeout=0 — non-blocking; check what’s ready right now
  • timeout=5.0 — wait up to 5 seconds

The return value is a list of (key, mask) tuples:

for key, mask in sel.select():
    callback = key.data       # the data you attached during register()
    fileobj = key.fileobj     # the socket or file object
    events = mask             # EVENT_READ and/or EVENT_WRITE that are ready
    callback(fileobj, mask)

The SelectorKey Namedtuple

Every registration returns a SelectorKey with four fields:

key.fileobj   # the registered file object or fd
key.fd        # the underlying file descriptor (int)
key.events    # bitmask of events being monitored
key.data      # the opaque data you attached (e.g., a callback)

Modifying Registration

modify(fileobj, events, data=None)

Change the events or data for a registered file object, without unregistering and re-registering:

key = sel.modify(client, selectors.EVENT_READ, new_callback)

This is equivalent to unregister() + register(), but often implemented more efficiently.

Complete Echo Server Example

import selectors
import socket

sel = selectors.DefaultSelector()

def accept(sock, mask):
    conn, addr = sock.accept()
    print(f"accepted {conn} from {addr}")
    conn.setblocking(False)
    # Register the new connection for read events, with read as callback
    sel.register(conn, selectors.EVENT_READ, read)

def read(conn, mask):
    data = conn.recv(1024)
    if data:
        print(f"echoing {data!r} to {conn}")
        conn.sendall(data)
    else:
        print(f"closing {conn}")
        sel.unregister(conn)
        conn.close()

sock = socket.socket()
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(("localhost", 12345))
sock.listen(100)
sock.setblocking(False)
sel.register(sock, selectors.EVENT_READ, accept)

print("Echo server running on localhost:12345")
while True:
    events = sel.select()
    for key, mask in events:
        callback = key.data
        callback(key.fileobj, mask)

Run this and connect with nc localhost 12345 — type anything and it echoes back.

Closing the Selector

Always close the selector when done to free resources:

with selectors.DefaultSelector() as sel:
    # ... use the selector
# selector is automatically closed on exit from the with block

Or explicitly:

sel.close()

Common Use Cases

Event-driven TCP servers. Instead of threading or spawning a process per connection, a single thread loops over select() and dispatches to callbacks. This scales to thousands of concurrent connections.

IRC bots, chat servers, lightweight network tools. The selector loop pattern is the foundation of any non-blocking, single-threaded networking code.

Mixing socket and pipe I/O. Register both sockets and pipes (on Unix) with the same selector to wait on all I/O sources uniformly.

Gotchas

Unregister before closing. If you close a file object while it’s still registered, the behavior is undefined — may crash or produce spurious events. Always unregister() first.

Different primitives behave differently. epoll and kqueue are level-triggered, while select on Windows only works with sockets (not pipes). DefaultSelector hides these differences, but if you need specific behavior, check which implementation is in use:

import selectors

sel = selectors.DefaultSelector()
print(type(sel).__name__)  # e.g., EpollSelector, KqueueSelector

Callback runs on the selector thread. The select() loop is single-threaded. If a callback blocks, no other file objects get served. For CPU-intensive work, hand off to a thread pool:

from concurrent.futures import ThreadPoolExecutor

executor = ThreadPoolExecutor(max_workers=4)

def read(conn, mask):
    data = conn.recv(1024)
    if data:
        # Offload CPU work to a thread pool
        executor.submit(process_andRespond, conn, data)

Windows limitations. On Windows, selectors only supports sockets — not pipes, FIFOs, or regular files. On Unix, all of these work.

Data attached is per-registration, not per-file-object. If you register the same socket twice (not typical), the second registration will raise KeyError. Use modify() to update existing registrations.

See Also