What does selectors.DefaultSelector let one thread do that a blocking socket.recv() cannot?
answer
- One thread, many connections
- Ask the kernel who is actionable
- Register interest, then wait once
- select() returns key plus ready mask
- Alias for epoll or kqueue
basics
~20 sIt asks the kernel which of many registered sockets are ready right now, so one thread can serve thousands of connections. A blocking socket.recv() parks that thread on one connection until that single connection has data.
solid answer
~50 s`selectors.DefaultSelector` is a readiness multiplexer. You `register()` each socket with the events you care about - `selectors.EVENT_READ`, `selectors.EVENT_WRITE`, or both - plus an arbitrary `data` object, then call `select(timeout)`. The call blocks until at least one registered socket is ready (or the timeout expires) and returns a list of `(SelectorKey, mask)` pairs, where the key carries `fileobj`, `fd`, `events` and your `data`. One thread can therefore hold thousands of connections and touch only the handful that are actionable this pass, instead of dedicating a thread or a process to each. `DefaultSelector` is an alias chosen at import time for the most scalable backend the platform offers - the epoll-based one on Linux, the kqueue-based one on the BSDs and macOS, falling back to poll or select. Registered sockets must be in non-blocking mode, and readiness means *a call will not block*, not *a whole message has arrived*.
code
python · 15 linesimport selectors
import socket
a, b = socket.socketpair()
sel = selectors.DefaultSelector()
sel.register(a, selectors.EVENT_READ, data="left")
sel.register(b, selectors.EVENT_READ, data="right")
b.sendall(b"ping")
for key, mask in sel.select(timeout=1):
print(key.data, key.fileobj.recv(64))
sel.close()
a.close()
b.close()go deeper
Be ready to say in one sentence what a selector buys you: one thread watching many sockets by asking the kernel which are ready. Know that select() returns keys, not bytes, and that you still call recv yourself.
Explain the mechanics: register with an event mask, attach per-connection state as data, read the (SelectorKey, mask) pairs back, and unregister on close. Be able to say why blocking mode breaks the whole loop.
Expect to justify the model against thread-per-connection for a real workload, and to name where it stops helping - CPU-bound handlers, a single core, and message framing that the selector does nothing to solve.
Own the choice of concurrency shape for the service. Be able to argue when a hand-written readiness loop is the right thing at all, versus a framework loop or a thread pool, and what each costs in staffing and debuggability.
### The problem A socket in blocking mode owns the thread that calls it. `sock.recv(4096)` returns when bytes arrive, and until then the thread is parked in the kernel doing nothing. That is a perfectly good design for one connection, and it is why the simplest servers are written thread-per-connection: accept, hand the socket to a thread, let that thread block. The cost shows up at scale - every idle connection holds a thread, each with its own stack and its own scheduling weight, and the machine spends its time context-switching between threads that are almost all waiting. Readiness multiplexing inverts the arrangement. Instead of *one waiter per connection*, you have *one waiter for all connections*: the thread asks the kernel a single question - "of these N descriptors, which can I touch without blocking?" - and the kernel answers with the small subset that is actionable. ### The API ```python import selectors, socket sel = selectors.DefaultSelector() sel.register(conn, selectors.EVENT_READ, data=bytearray()) for key, mask in sel.select(timeout=1.0): if mask & selectors.EVENT_READ: chunk = key.fileobj.recv(4096) ``` Three pieces matter. **`register(fileobj, events, data=None)`** records an interest. `fileobj` is any object with a `fileno()` - a socket, a pipe end, anything the platform can poll. `events` is a bitmask of `selectors.EVENT_READ` and `selectors.EVENT_WRITE`. `data` is yours: the module never inspects it, and it is the intended place to hang per-connection state such as an input buffer, an output queue, or a callback. Without it you end up maintaining a side dictionary keyed by file descriptor, which is exactly the bookkeeping the `data` slot removes. **`select(timeout)`** is the blocking point of the whole program. It returns a list of `(SelectorKey, events)` tuples. The `selectors.SelectorKey` is a named tuple of `fileobj`, `fd`, `events` (what you registered) and `data` (what you attached); the second element of the tuple is the mask of what is ready *now*, which is a subset of what you registered. A `timeout` of `None` blocks indefinitely, `0` polls and returns immediately, and a number is an upper bound - the timeout is how a real loop makes room for timers and periodic work. **`modify()` and `unregister()`** change or drop an interest, and `get_map()` exposes the live registration table, which is a genuinely useful health metric. ### What DefaultSelector actually is `selectors.DefaultSelector` is not a class of its own; it is a name bound at import time to the most efficient implementation available on the platform. On Linux that is the epoll-backed selector, on the BSDs and macOS the kqueue-backed one, otherwise a poll-based one, and finally `selectors.SelectSelector`, which wraps `select.select`. The distinction is not cosmetic: the select- and poll-based backends hand the kernel the entire descriptor set on every call, so their cost grows with the number of *watched* descriptors, while epoll and kqueue keep the interest set in the kernel and report only the *ready* ones. The select backend additionally cannot exceed a compile-time descriptor-set size, commonly 1024 on Unix builds. Writing against `DefaultSelector` gets you the good backend on the platforms that have one without any conditional code. ### Readiness is a hint, not a delivery Two misreadings cause most bugs here. First, the selector never moves bytes. It tells you a call will not block; you still call `recv()` or `send()` yourself. A readable listening socket means `accept()` will not block; a readable connected socket means `recv()` will return promptly - possibly with `b""`, which is the peer's clean close and your cue to `unregister()` and `close()`. Second, readable does not mean *complete*. TCP is a byte stream with no message boundaries, so one readable event may deliver half a request, or two requests glued together. The loop's real job is to accumulate into the per-connection buffer you hung off `data` and parse out whole messages as they become available. For the same reason, registered sockets must be set non-blocking: readiness can be stale by the time you act on it, and a blocking `recv()` on a socket that turned out to have nothing would freeze every other connection in the process. ### What it costs The price of the model is control flow. Blocking code reads top to bottom; a readiness loop is a dispatcher over explicit per-connection state machines, because no connection may ever be allowed to stall the single thread. That is precisely the ergonomic problem `async`/`await` exists to solve - and the event loop underneath it is doing exactly what the loop above does. It is also worth being clear about the boundary: a readiness loop scales *waiting*, not computing. One thread still means one core, so CPU-heavy work inside the loop delays every connection registered with it.
- What is the data argument to a selector's register() call for?It is arbitrary per-registration state that the module stores and hands back as `SelectorKey.data` when that socket becomes ready. It is where the per-connection input buffer, output queue or handler callback belongs. Without it you keep a parallel dictionary keyed by file descriptor and have to keep the two structures in step, which is a common source of leaks when a connection is closed but its entry is not removed.
- Why must sockets registered with a selector be put in non-blocking mode?Readiness is a report about a moment that has already passed. A socket can be reported readable and still have nothing to give by the time you call `recv()` - a discarded segment, another consumer, or a spurious wake-up. In blocking mode that call parks the single thread and every other registered connection stops being served. Non-blocking mode turns the same situation into a `BlockingIOError` you can ignore and carry on.
A blocking recv() is a receptionist who walks to one office and waits outside its door. A selector is a switchboard: every line is wired in, and the lamp lights only on the lines that have someone talking.
saying these in an interview costs you the question
- Thinks the selector reads or writes the data for you
- Says readable means a complete message has arrived
- Believes each registered socket needs its own thread
- Leaves registered sockets in blocking mode
- Cannot say what select() actually returns