Python selectors: Non-Blocking I/O

Published on: August 11, 2026
Reading time: 5 minutes
Network connections representing non-blocking I/O with Python selectors

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_WRITE

Readable 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 lines

Set 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 = False

Keep 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.
  • 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.

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.

Share:

Facebook
WhatsApp
Twitter
LinkedIn

Article content

    Related articles

    Developer configuring a Python HTTPS server and TLS certificate
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    HTTPSServer: Build a Local HTTPS Server in Python

    Learn Python HTTPSServer for local HTTPS services, TLS certificates, threaded handling, testing, and practical security limits.

    Ler mais

    Tempo de leitura: 4 minutos
    08/10/2026
    Protected files representing secure TAR extraction with Python
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    tarfile extraction_filter: Extract TAR Safely

    Learn Python tarfile extraction_filter for safer TAR extraction, path validation, links, permissions, and resource limits.

    Ler mais

    Tempo de leitura: 5 minutos
    08/10/2026
    Python code showing warnings controlled with catch_warnings
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    catch_warnings: Capture Python Warnings in Tests

    Learn Python catch_warnings to capture, test, and control warnings with focused filters and safe temporary scopes.

    Ler mais

    Tempo de leitura: 5 minutos
    07/10/2026
    Python code representing persistent pickle references
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    pickle persistent_id: Serialize External References

    Learn Python pickle persistent_id for stable external references, validation, security, performance, and long-term compatibility.

    Ler mais

    Tempo de leitura: 5 minutos
    07/10/2026
    Binary code representing Python buffers and memory views
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    memoryview.count: Count Values Without Buffer Copies

    Learn Python memoryview.count to count bytes and values in buffers without copies, with formats, limits, and practical safety.

    Ler mais

    Tempo de leitura: 5 minutos
    06/10/2026
    Python code and type annotations
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    annotationlib: Avoid Circular Imports in Annotations

    Learn Python annotationlib to inspect deferred annotations, avoid circular imports, and build safer runtime tooling.

    Ler mais

    Tempo de leitura: 5 minutos
    06/10/2026