The selectors module provides a high-level interface for waiting on input and output readiness across many file descriptors. It is most commonly used with non-blocking sockets. Instead of allocating one thread for every connection, one event loop can react only when each resource is ready to read or write.
The module wraps lower-level primitives such as select, poll, epoll, and kqueue. DefaultSelector automatically selects the most efficient implementation available on the current platform.
When selectors is appropriate
Use selectors when you need direct control over an event loop, must integrate a small custom protocol, or want to understand how non-blocking servers work. For most application-level asynchronous code, asyncio is more productive because it already provides tasks, streams, cancellation, and timeout helpers.
The module does not remove the need to manage buffers, partial messages, disconnections, backpressure, and protocol deadlines. A blocking database query or disk operation will still block the entire loop.
Create the default selector
import selectors
with selectors.DefaultSelector() as selector:
print(type(selector).__name__)The context manager closes the underlying selector resource. A closed selector cannot be reused.
Read and write readiness
The two primary masks are EVENT_READ and EVENT_WRITE. Combine them with bitwise OR.
events = selectors.EVENT_READ | selectors.EVENT_WRITEReadable does not always mean application data is available; it can also signal EOF or a closed peer. Writable means the operating system can accept some data, not necessarily the complete buffer.
Register a listening socket
import socket
import selectors
selector = selectors.DefaultSelector()
server = socket.socket()
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(("127.0.0.1", 9000))
server.listen()
server.setblocking(False)
selector.register(
server,
selectors.EVENT_READ,
data={"kind": "server"},
)register() associates a file object, an event mask, and optional opaque data. The data can be a callback, session identifier, or per-connection state object.
Accept connections
def accept_connection(sock, selector):
connection, address = sock.accept()
connection.setblocking(False)
state = {
"address": address,
"input": bytearray(),
"output": bytearray(),
}
selector.register(connection, selectors.EVENT_READ, state)The accepted socket must also be non-blocking. Unregister each connection before closing it.
The event loop
while True:
ready = selector.select(timeout=1.0)
for key, mask in ready:
if key.fileobj is server:
accept_connection(server, selector)
else:
process_connection(key, mask, selector)select() returns pairs of SelectorKey and ready-event masks. A key contains the registered object, descriptor, requested events, and attached data.
Timeout behavior
With timeout=None, the call waits indefinitely. A positive value limits the wait in seconds. Zero or a negative value performs a non-blocking readiness check.
A selector timeout is not a protocol timeout. Track the last activity time separately and disconnect idle clients according to application policy.
Read without blocking
def receive(sock, state):
try:
data = sock.recv(4096)
except BlockingIOError:
return
if data:
state["input"].extend(data)
else:
raise ConnectionResetError("peer closed")Even after readiness notification, handle BlockingIOError because state can change before the operation. An empty receive result normally means an orderly peer shutdown.
TCP messages can be partial
TCP is a byte stream. One receive call may return half a message, multiple messages, or any other segmentation. Define framing with a delimiter, length prefix, or self-describing format.
def extract_lines(buffer):
lines = []
while b"\n" in buffer:
line, _, remaining = buffer.partition(b"\n")
lines.append(line)
buffer[:] = remaining
return linesSet a maximum buffer size so a client cannot send endless data without a delimiter and exhaust memory.
Partial writes and backpressure
def send_pending(sock, state):
if not state["output"]:
return
try:
sent = sock.send(state["output"])
except BlockingIOError:
return
del state["output"][:sent]send() may write fewer bytes than requested. Keep the remainder. Monitor EVENT_WRITE only while output is pending; sockets are often writable, so permanent monitoring can create a busy loop.
Modify registered interests
def update_interest(sock, state, selector):
events = selectors.EVENT_READ
if state["output"]:
events |= selectors.EVENT_WRITE
selector.modify(sock, events, state)modify() updates the mask and data more efficiently than unregistering and registering again.
Close a connection correctly
def close_connection(sock, selector):
try:
selector.unregister(sock)
except KeyError:
pass
sock.close()Handle client errors individually so one broken connection does not stop the whole server.
Platform differences
On Windows, selector support is primarily for sockets; ordinary pipes are not supported like they are on Unix. Unix implementations may support sockets, pipes, FIFOs, and special devices depending on the underlying primitive.
DefaultSelector improves portability but cannot make every file object selectable on every operating system. The module is unavailable on WASI.
Session objects
For larger protocols, use a dataclass to store buffers, address, deadlines, parser state, and metrics.
from dataclasses import dataclass, field
@dataclass
class Session:
address: tuple
input: bytearray = field(default_factory=bytearray)
output: bytearray = field(default_factory=bytearray)
closing: bool = FalseKeep state bounded and avoid retaining secrets longer than necessary.
Signals and empty results
Modern Python normally retries selector waits after a signal when the signal handler does not raise. The loop should still accept an empty ready list and run periodic maintenance.
Avoiding high CPU usage
A loop may consume a full core if it always monitors write readiness, uses a zero timeout continuously, or spins after an error. Register only useful events and choose a reasonable timeout.
Measure ready-event volume, buffer sizes, callback duration, and loop latency. A slow callback delays every connection handled by the same loop.
Move blocking work elsewhere
Small parsing operations belong in the loop. CPU-heavy work, blocking database calls, and slow file access should run in another worker. Return results through a thread-safe queue or wakeup descriptor.
Processing example
def process_connection(key, mask, selector):
sock = key.fileobj
state = key.data
try:
if mask & selectors.EVENT_READ:
receive(sock, state)
for line in extract_lines(state["input"]):
state["output"].extend(line.upper() + b"\n")
if mask & selectors.EVENT_WRITE:
send_pending(sock, state)
update_interest(sock, state, selector)
except (ConnectionError, OSError):
close_connection(sock, selector)A production implementation also needs protocol validation, limits, structured logging, graceful shutdown, and cancellation.
selectors versus asyncio
asyncio is based on similar readiness concepts and may use selectors internally. Choose selectors for low-level control or educational protocol work. Choose asyncio for coroutines, tasks, streams, and a broader asynchronous ecosystem.
Common mistakes
- Forgetting
setblocking(False). - Closing a socket before unregistering it.
- Assuming one receive call returns one full message.
- Ignoring partial writes.
- Watching write readiness all the time.
- Running blocking work in the event loop.
- Assuming pipe support is identical on Windows and Unix.
Recommended practices
- Use
DefaultSelector. - Bound all per-client buffers.
- Register write events only when output is pending.
- Handle errors per connection.
- Implement application-level deadlines.
- Close sockets and selectors reliably.
- Test fragmented messages, slow clients, and disconnects.
Related guides
Continue with Python contextvars, Python ExitStack, Python shlex, Python faulthandler, and Python platform.
See the official selectors documentation and the socket documentation.
Conclusion
selectors enables efficient, portable non-blocking I/O loops with direct control over read, write, and connection state. Reliability depends on bounded buffers, backpressure, deadlines, per-client error handling, and keeping blocking work outside the event loop.







