Of course! This is an excellent question that gets to the heart of high-performance network programming in Python. Let's break down the relationship between "Reactor," "Python," and "epoll."
The Big Picture: Why Bother?
Imagine a restaurant with one waiter. The waiter takes an order from Table 1, walks to the kitchen, waits for the food, brings it back, and then takes an order from Table 2. This is simple, but if the kitchen is slow, Tables 3, 4, and 5 are left waiting for a long time. This is the blocking I/O model.

Now, imagine a smart restaurant manager. The waiter takes an order from Table 1 and immediately tells the manager, "Kitchen for Table 1 is ready." Then the waiter takes an order from Table 2, "Kitchen for Table 2 is ready." The waiter keeps doing this for all tables. The manager then looks at his list and tells the waiter, "Go pick up the food for Table 1, it's ready." The waiter is never just standing idle waiting for the kitchen. This is the non-blocking I/O model, and it's incredibly efficient.
epoll is the manager's system call in Linux. It's the mechanism that tells the waiter (your Python program) which "kitchens" (network sockets) are ready.
Reactor is the overall design pattern or the "smart manager" logic. It's the code that uses epoll to manage multiple tasks without getting stuck on any single one.
The Reactor Pattern
The Reactor pattern is a concurrency design pattern for handling service requests that are delivered to a service handler by one or more inputs. The key idea is to have a single, central event loop that:

- Registers sources of events (like network sockets or files).
- Waits (or "blocks") until one or more of these sources become "ready" (e.g., a socket is ready for reading or writing).
- Dispatches the appropriate handler (a function or method) to process the event.
This pattern is the foundation of most high-performance network servers.
Analogy:
- Reactor: The Restaurant Manager.
- Event Loop: The Manager's list of tasks and his constant scanning of it.
- Handles (File Descriptors): The tables in the restaurant.
- Demultiplexer (epoll): The manager's "call bell" system. When a kitchen (OS) is ready, the bell rings.
- Event Handlers (Callbacks): The Waiters. They are assigned to a specific table and know exactly what to do when the manager tells them that table is ready.
Python and epoll
Python has had support for epoll for a long time through its select module, but the modern and much more common way to use it is through a third-party library called selectors.
selectors: The Modern, Cross-Platform Abstraction
The selectors module (standard in Python 3.4+) is a high-level interface for different polling mechanisms. It automatically chooses the best available one for your operating system:

- Linux: Uses
epoll(the most efficient). - macOS / BSD: Uses
kqueue(equivalent toepollin power and efficiency). - Windows / Other OS: Falls back to
select(less efficient, but widely supported).
This is fantastic because you can write cross-platform code without worrying about the underlying system call.
Key selectors classes:
selectors.DefaultSelector: The best choice. It picksepoll,kqueue, orselectautomatically.selectors.EpollSelector: Explicitly usesepoll(Linux only).selectors.SelectSelector: Explicitly usesselect(universal but slow).
How epoll Works (The Low-Level Gist)
epoll is a Linux-specific system call that provides a mechanism for monitoring multiple file descriptors (sockets are a type of file descriptor) to see if I/O is possible on any of them.
epoll_create(): Creates an "epoll instance," which is a kernel object that holds a list of file descriptors you want to monitor.epoll_ctl()(Control): You use this to add, modify, or delete file descriptors from the epoll instance's watch list. When you add a file descriptor, you specify which events you're interested in (e.g.,EPOLLINfor "ready to read,"EPOLLOUTfor "ready to write").epoll_wait()(Wait): This is the heart of the system call. Your program calls it and blocks. The kernel then checks all the file descriptors in the epoll instance's list. When one or more of them become ready (e.g., data arrives on a socket, making it ready to read),epoll_waitunblocks and returns a list of the ready file descriptors and the events that occurred.
This is far more efficient than older methods like select because:
- No "Linear Scan":
selecthas to check every single file descriptor you give it, every time it's called.epollmaintains an internal state in the kernel, so it only returns the ones that have actually changed state. - No File Descriptor Limit:
selectis often limited to 1024 file descriptors.epollcan handle many more (limited only by system memory).
Putting It All Together: A Code Example
Let's build a simple echo server using the Reactor pattern with Python's selectors module.
This server will:
- Create a listening socket.
- Register it with the
DefaultSelectorto listen for new connections (EPOLLIN). - Enter an event loop (
epoll_wait). - When a new connection arrives, it accepts it, registers the new client socket to listen for incoming data, and stops listening for new connections on the main socket.
- When a client sends data, it reads the data and echoes it back.
- If a client disconnects, it closes the socket and unregisters it.
import selectors
import socket
import types
# A simple function to accept a new connection
def accept_connection(sock):
conn, addr = sock.accept() # Should be ready to read
print(f"Accepted connection from {addr}")
conn.setblocking(False) # Set the new socket to non-blocking mode
# Create a data object to hold the connection information
data = types.SimpleNamespace(addr=addr, inb=b"", outb=b"")
# Register the new socket to read data from the client
# We use 'events | data' to associate data with the file descriptor
selector.register(conn, selectors.EVENT_READ, data=data)
# A simple function to service a client connection
def service_connection(key, mask):
sock = key.fileobj
data = key.data
if mask & selectors.EVENT_READ:
# The socket is ready for reading
try:
recv_data = sock.recv(1024) # Should be ready to read
if recv_data:
print(f"Received from {data.addr}: {recv_data.decode()}")
data.outb += recv_data # Echo back the received data
else:
# If recv_data is empty, the client has closed the connection
print(f"Closing connection to {data.addr}")
selector.unregister(sock)
sock.close()
except ConnectionResetError:
print(f"Closing connection to {data.addr} (reset by peer)")
selector.unregister(sock)
sock.close()
# --- Main Server Logic ---
# Create a TCP socket
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server_socket.bind(("127.0.0.1", 65432))
server_socket.listen()
server_socket.setblocking(False) # Set the server socket to non-blocking
# Create a selector instance
# On Linux, this will use epoll() under the hood
selector = selectors.DefaultSelector()
# Register the server socket to listen for incoming connections
# We associate it with the accept_connection function
selector.register(server_socket, selectors.EVENT_READ, data=None)
print("Server started on 127.0.0.1:65432")
# The main event loop (the Reactor)
try:
while True:
# The core of the Reactor: wait for events
# This blocks until one or more registered sockets are ready
events = selector.select(timeout=None) # timeout=None means block indefinitely
for key, mask in events:
if key.data is None:
# This is the server socket, ready for a new connection
accept_connection(key.fileobj)
else:
# This is a client socket, ready for I/O
service_connection(key, mask)
except KeyboardInterrupt:
print("Server shutting down.")
finally:
selector.close()
server_socket.close()
Summary Table
| Concept | Role | Analogy | Python Implementation |
|---|---|---|---|
| Reactor Pattern | The overall design pattern for an event |
