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

    Network data flow representing asynchronous context with Python contextvars
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python contextvars: Async Context State

    Learn Python contextvars to store task-local state, prevent asyncio leaks, copy contexts, propagate metadata, and restore values with tokens.

    Ler mais

    Tempo de leitura: 4 minutos
    11/08/2026
    Programming code representing operations as functions with Python operator
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python operator: Operations as Functions

    Learn Python operator to use operations as functions, sort fields, access items, call methods, and build clear functional pipelines.

    Ler mais

    Tempo de leitura: 4 minutos
    11/08/2026
    Three-dimensional alphabet representing Unicode normalization with Python unicodedata
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python unicodedata: Normalize Unicode

    Learn Python unicodedata to normalize Unicode, inspect names, categories, numeric values, combining marks, bidirectional classes, and width.

    Ler mais

    Tempo de leitura: 5 minutos
    11/08/2026
    Server network representing resource management with Python ExitStack
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python ExitStack: Manage Resources

    Learn Python ExitStack to manage dynamic files, connections, callbacks, and cleanup safely in predictable reverse order.

    Ler mais

    Tempo de leitura: 5 minutos
    11/08/2026
    Locked folder representing file types and permissions with Python stat
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python stat: File Types and Permissions

    Learn Python stat to interpret file types, permissions, links, timestamps, Windows attributes, and Unix flags safely and portably.

    Ler mais

    Tempo de leitura: 5 minutos
    10/08/2026
    Laptop with code representing automatic documentation with Python pydoc
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python pydoc: Automatic Documentation

    Learn Python pydoc to generate terminal help, HTML, search, and a local documentation server safely from docstrings.

    Ler mais

    Tempo de leitura: 6 minutos
    10/08/2026