El módulo selectors ofrece una interfaz de alto nivel para esperar eventos de entrada y salida en muchos descriptores. Se usa principalmente con sockets no bloqueantes. En lugar de crear una thread por conexión, un único loop puede reaccionar solamente cuando cada recurso está preparado para leer o escribir.
El módulo envuelve primitivas de bajo nivel como select, poll, epoll y kqueue. DefaultSelector elige automáticamente la implementación más eficiente disponible.
Cuándo usar selectors
Usa selectors cuando necesites control directo sobre un loop de eventos, implementar un protocolo sencillo o monitorizar muchos sockets sin un framework completo. Para aplicaciones asíncronas habituales, asyncio suele ser más productivo.
El módulo exige gestionar buffers, mensajes parciales, desconexiones, backpressure y plazos. Una operación bloqueante de base de datos o disco sigue bloqueando todo el loop.
Crear el selector por defecto
import selectors
with selectors.DefaultSelector() as selector:
print(type(selector).__name__)El context manager libera el recurso interno. Después de close(), el selector no puede reutilizarse.
Eventos de lectura y escritura
Las máscaras principales son EVENT_READ y EVENT_WRITE. Se combinan con OR bit a bit.
eventos = selectors.EVENT_READ | selectors.EVENT_WRITEPreparado para lectura también puede significar EOF o cierre del peer. Preparado para escritura indica que el sistema acepta alguna cantidad de datos, no necesariamente todo el buffer.
Registrar un socket servidor
import socket
import selectors
selector = selectors.DefaultSelector()
servidor = socket.socket()
servidor.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
servidor.bind(("127.0.0.1", 9000))
servidor.listen()
servidor.setblocking(False)
selector.register(
servidor,
selectors.EVENT_READ,
data={"tipo": "servidor"},
)register() asocia objeto, máscara y datos opcionales. Los datos pueden ser un callback, ID de sesión u objeto de estado.
Aceptar conexiones
def aceptar(sock, selector):
conexion, direccion = sock.accept()
conexion.setblocking(False)
estado = {
"direccion": direccion,
"entrada": bytearray(),
"salida": bytearray(),
}
selector.register(conexion, selectors.EVENT_READ, estado)El socket aceptado también debe ser no bloqueante. Elimínalo del selector antes de cerrarlo.
El loop de eventos
while True:
listos = selector.select(timeout=1.0)
for clave, mascara in listos:
if clave.fileobj is servidor:
aceptar(servidor, selector)
else:
procesar(clave, mascara, selector)select() devuelve pares de SelectorKey y máscara lista. La clave contiene objeto, descriptor, eventos solicitados y datos asociados.
Timeouts
Con timeout=None, la espera es indefinida. Un valor positivo limita segundos. Cero o negativo consulta sin bloquear.
Este timeout no sustituye el plazo del protocolo. Guarda la última actividad y desconecta clientes inactivos según la política de la aplicación.
Recibir sin bloquear
def recibir(sock, estado):
try:
datos = sock.recv(4096)
except BlockingIOError:
return
if datos:
estado["entrada"].extend(datos)
else:
raise ConnectionResetError("cliente cerrado")Incluso después de una notificación, trata BlockingIOError. Un resultado vacío normalmente indica cierre ordenado.
TCP entrega un flujo
Una llamada puede devolver media trama, varias tramas o cualquier división. El protocolo debe definir framing mediante delimitador, longitud prefijada o formato autodescriptivo.
def extraer_lineas(buffer):
lineas = []
while b"\n" in buffer:
linea, _, restante = buffer.partition(b"\n")
lineas.append(linea)
buffer[:] = restante
return lineasImpone un tamaño máximo para impedir consumo ilimitado de memoria.
Escritura parcial y backpressure
def enviar(sock, estado):
if not estado["salida"]:
return
try:
enviados = sock.send(estado["salida"])
except BlockingIOError:
return
del estado["salida"][:enviados]send() puede enviar menos bytes. Conserva el resto. Monitoriza EVENT_WRITE solo cuando existan datos pendientes; hacerlo siempre puede producir un loop ocupado.
Modificar intereses
def actualizar(sock, estado, selector):
eventos = selectors.EVENT_READ
if estado["salida"]:
eventos |= selectors.EVENT_WRITE
selector.modify(sock, eventos, estado)modify() es más eficiente que eliminar y registrar de nuevo.
Cerrar correctamente
def cerrar(sock, selector):
try:
selector.unregister(sock)
except KeyError:
pass
sock.close()Trata errores por conexión para que un cliente defectuoso no detenga todo el servidor.
Diferencias entre plataformas
En Windows, los selectores soportan principalmente sockets; pipes normales no funcionan igual que en Unix. En Unix pueden soportarse sockets, pipes, FIFOs y dispositivos especiales según la implementación.
DefaultSelector mejora portabilidad, pero no hace que todos los objetos sean seleccionables. El módulo no está disponible en WASI.
Objetos de sesión
Para protocolos mayores, usa una dataclass con buffers, dirección, plazos, estado del parser y métricas.
from dataclasses import dataclass, field
@dataclass
class Sesion:
direccion: tuple
entrada: bytearray = field(default_factory=bytearray)
salida: bytearray = field(default_factory=bytearray)
cerrando: bool = FalseMantén límites y no conserves secretos innecesarios.
Señales y resultados vacíos
Python moderno normalmente repite la espera cuando una señal interrumpe y el handler no genera excepción. Aun así, el loop debe aceptar una lista vacía y ejecutar mantenimiento periódico.
Evitar CPU alta
Un loop puede consumir un núcleo completo si siempre monitoriza escritura, usa timeout cero o gira tras un error. Registra solo eventos útiles y elige un timeout razonable.
Mide cantidad de eventos, tamaño de buffers, duración de callbacks y latencia del loop. Un callback lento retrasa todas las conexiones.
Mover trabajo bloqueante
El parsing pequeño puede quedarse en el loop. CPU intensiva, consultas bloqueantes y disco lento deben ejecutarse en otro worker. Devuelve resultados mediante una cola segura o descriptor de wakeup.
Ejemplo de procesamiento
def procesar(clave, mascara, selector):
sock = clave.fileobj
estado = clave.data
try:
if mascara & selectors.EVENT_READ:
recibir(sock, estado)
for linea in extraer_lineas(estado["entrada"]):
estado["salida"].extend(linea.upper() + b"\n")
if mascara & selectors.EVENT_WRITE:
enviar(sock, estado)
actualizar(sock, estado, selector)
except (ConnectionError, OSError):
cerrar(sock, selector)Producción requiere validación, límites, logs, cierre ordenado y cancelación.
selectors frente a asyncio
asyncio usa conceptos similares y puede utilizar selectores internamente. Elige selectors para control de bajo nivel. Elige asyncio para coroutines, tareas, streams y ecosistema asíncrono.
Errores frecuentes
- Olvidar
setblocking(False). - Cerrar antes de hacer unregister.
- Suponer que un recv devuelve una trama completa.
- Ignorar escrituras parciales.
- Monitorizar escritura permanentemente.
- Ejecutar trabajo bloqueante en el loop.
- Suponer soporte idéntico de pipes.
Buenas prácticas
- Usa
DefaultSelector. - Limita buffers por cliente.
- Registra escritura solo cuando sea necesaria.
- Trata errores por conexión.
- Implementa plazos del protocolo.
- Cierra sockets y selector de forma fiable.
- Prueba mensajes fragmentados, clientes lentos y desconexiones.
Guías relacionadas
Continúa con contextvars en Python, ExitStack en Python, shlex en Python, faulthandler en Python y platform en Python.
Consulta la documentación oficial de selectors y la documentación de socket.
Conclusión
selectors permite construir loops de I/O eficientes y portables con control directo sobre lectura, escritura y estado de conexión. La fiabilidad depende de buffers limitados, backpressure, plazos, errores por cliente y ausencia de operaciones bloqueantes.







