Imagine ordering a burger and receiving an order number. You can find a table while the kitchen works; the number lets you collect the burger when it is ready. A future plays a similar role in a program: it is a placeholder returned by an asynchronous call while the result is still unknown. The eventual outcome may be a result or an error. section 12.6
Returning a future lets the caller continue instead of waiting for the computation to finish. When the result becomes available, it can be placed in the future, allowing waiting code to continue. That makes a future both a proxy for a result and a way to synchronize independent work. The names promise, delay, and deferred describe roughly similar ideas, though their meanings vary across languages. section 12.6
# Chapter 12/future_burger.py
from __future__ import annotations
import typing as T
from collections import deque
from random import randint
Result = T.Any
Burger = Result
Coroutine = T.Callable[[], 'Future']
class Future:
def __init__(self) -> None:
self.done = False
self.coroutine = None
self.result = None
def set_coroutine(self, coroutine: Coroutine) -> None: #1
self.coroutine = coroutine #1
def set_result(self, result: Result) -> None: #2
self.done = True #2
self.result = result #2
def __iter__(self) -> Future:
return self
def __next__(self) -> Result: #3
if not self.done: #3
raise StopIteration #3
return self.result #3
class EventLoop:
def __init__(self) -> None:
self.tasks: T.Deque[Coroutine] = deque()
def add_coroutine(self, coroutine: Coroutine) -> None:
self.tasks.append(coroutine)
def run_coroutine(self, task: T.Callable) -> None:
future = task() #4
future.set_coroutine(task) #4
try: #4
next(future) #4
if not future.done: #4
future.set_coroutine(task) #4
self.add_coroutine(task) #4
except StopIteration: #4
return #4
def run_forever(self) -> None:
while self.tasks:
self.run_coroutine(self.tasks.popleft())
def cook(on_done: T.Callable[[Burger], None]) -> None: #5
burger: str = f"Burger #{randint(1, 10)}" #5
print(f"{burger} is cooked!") #5
on_done(burger) #5
def cashier(burger: Burger, on_done: T.Callable[[Burger], None]) -> None: #6
print("Burger is ready for pick up!") #6
on_done(burger) #6
def order_burger() -> Future:
order = Future() #7
def on_cook_done(burger: Burger) -> None:
cashier(burger, on_cashier_done)
def on_cashier_done(burger: Burger) -> None:
print(f"{burger}? That's me! Mmmmmm!")
order.set_result(burger)
cook(on_cook_done) #8
return order
if __name__ == "__main__":
event_loop = EventLoop()
event_loop.add_coroutine(order_burger)
event_loop.run_forever()
A small Python Future starts unfinished, holds a coroutine and a result, and marks itself done when set_result is called. section 12.6
In a cooperative server, a nonblocking socket operation returns a future rather than making the server wait for data. The event loop tracks tasks that are ready and tasks waiting for I/O. When a socket event occurs, it runs the appropriate callback and can resume the waiting task with the operation’s result. This only works cooperatively: the loop cannot interrupt a task that never hands back control. section 12.8
# Chapter 12/asynchronous_pizza/async_socket.py
from __future__ import annotations
import select
import typing as T
import socket
from future import Future
Data = bytes
class AsyncSocket:
def __init__(self, sock: socket.socket):
self._sock = sock
self._sock.setblocking(False)
def recv(self, bufsize: int) -> Future:
future = Future()
def handle_yield(loop, task) -> None:
try:
data = self._sock.recv(bufsize)
loop.add_ready(task, data)
except BlockingIOError:
loop.register_event(self._sock, select.POLLIN, future, task)
future.set_coroutine(handle_yield)
return future
def send(self, data: Data) -> Future:
future = Future()
def handle_yield(loop, task):
try:
nsent = self._sock.send(data)
loop.add_ready(task, nsent)
except BlockingIOError:
loop.register_event(self._sock, select.POLLOUT, future, task)
future.set_coroutine(handle_yield)
return future
def accept(self) -> Future:
future = Future()
def handle_yield(loop, task):
try:
r = self._sock.accept()
loop.add_ready(task, r)
except BlockingIOError:
loop.register_event(self._sock, select.POLLIN, future, task)
future.set_coroutine(handle_yield)
return future
def close(self) -> Future:
future = Future()
def handle_yield(*args):
self._sock.close()
future.set_coroutine(handle_yield)
return future
def __getattr__(self, name: str) -> T.Any:
return getattr(self._sock, name)
The AsyncSocket listing shows recv creating a Future and registering interest in a readable socket when receiving would block. section 12.8
Keeping the pizza counter open
Suppose cooking a pizza takes a long time. If the server cooks it inside a cooperative task, other customers must wait. Instead, it can send the work to a thread pool and return a future, leaving the server free to take orders. When cooking finishes, a notification lets the result be supplied. This demonstration is not production-ready: its exception handling is insufficient, and its event loop advances only on socket events. section 12.10







