Imagine ordering pizza by requesting a callback instead of waiting on the phone. You can do something else until the call comes. An event loop plays the coordinating role in a program: it waits for events, finds the callback registered for each one, runs it, and then looks for more work. section 11.2 section 11.3
A simple loop keeps arriving events in a queue and handles them one after another. An event handler can even register a new event. This differs from repeatedly checking whether something has happened: the loop waits for work rather than busy-waiting. Running handlers in turn is a form of concurrency, but it does not mean their code runs at the same time. section 11.3 section 11.5
From queue to pizza server
A small Python loop queues two knock events; each produces a who event. The loop handles both knocks, then both replies. A network pizza server uses the same coordinating idea with sockets: it registers callbacks for accepting connections, reading, and writing. Instead of repeatedly checking every client, it blocks in select until the operating system reports a socket ready for an operation, then invokes its callback. One thread can thereby serve multiple clients. section 11.3 section 11.5
# Chapter 11/event_loop.py
from collections import deque
from time import sleep
import typing as T
class Event: #1
def __init__(self, name: str, action: T.Callable[..., None],
next_event: T.Optional[Event] = None) -> None:
self.name = name
self._action = action
self._next_event = next_event
def execute_action(self) -> None:
self._action(self)
if self._next_event:
event_loop.register_event(self._next_event)
class EventLoop:
def __init__(self) -> None:
self._events: deque[Event] = deque() #2
def register_event(self, event: Event) -> None:
self._events.append(event) #3
def run_forever(self) -> None:
print(f"Queue running with {len(self._events)} events")
while True: #4
try: #4
event = self._events.popleft() #4
except IndexError: #4
continue #4
event.execute_action() #4
def knock(event: Event) -> None:
print(event.name)
sleep(1)
def who(event: Event) -> None:
print(event.name)
sleep(1)
if __name__ == "__main__":
event_loop = EventLoop()
replying = Event("Who's there?", who)
knocking = Event("Knock-knock", knock, replying)
for _ in range(2): #5
event_loop.register_event(knocking) #5
event_loop.run_forever() #6
The Python listing shows the event queue, registration method, and loop behind the knock-and-reply example. section 11.3
The rule is simple but important: a handler must give control back. A blocking operation holds up the only event-loop thread, delaying every other event. Splitting an application among callbacks can also make its flow harder to read and debug, particularly when callbacks nest. section 12.1 section 11.2
Coroutines offer another way to share that thread. A coroutine pauses at yield, letting the loop run another ready task; the loop resumes it when its awaited work completes. A cooperative loop tracks ready tasks and sockets awaiting I/O, calling select when nothing is ready. It cannot interrupt a running coroutine: that task must yield. For CPU-heavy work, a thread pool can work alongside the loop and notify it when a result is ready. Neither approach makes blocking inside the loop harmless. section 12.4 section 12.8 section 12.10










