Coroutines and Their Applications

In the previous post, we talked about generators. We described them as special kinds of iterators that allow two-way data transfer between the calling program and the generator itself. This property can be leveraged to implement coroutines.

Coroutines are code structures that can cooperatively pass control to one another to complete a shared task. The term coroutine (short for cooperative routine) comes from this collaborative behavior.

To make this kind of cooperation possible, coroutines have several unique features. They can suspend execution multiple times while preserving their internal state, transfer control to another block of code, and later resume execution exactly where they left off. In addition, they can receive data each time they resume, and send data back whenever they suspend.

Let’s illustrate how coroutines work using a simple example.
We’ll model a card game played by three players using a standard French deck of playing cards. Each player starts with five cards. The remaining deck is placed face up on the table.

Players take turns in a fixed order. The rule is that a player must play a card that either matches the suit of the top card on the table, or, if no such card is available, one with an equal or higher rank. If the player cannot play any card, they must draw the top card from the deck. The winner is the player who first runs out of cards.

Here’s one possible implementation of the game:

The first thing we do in the program is to create the 52 cards as namedtuple objects. Then we represent each player by a generator-based coroutine. To achieve this, we define a generator function that takes the player’s initial five cards as a list.

Inside the function body, the variable on the left-hand side of yield expression receives the top card from the deck on the table, while the right-hand side of yield holds the card the player decides to play (or None if no card can be played).

We then check whether the player has a playable card. If so, one is selected—preferably a card matching the suit of the table card. If none match by suit, we choose one based on value. If the player runs out of cards after discarding, this is signaled by raising a dedicated exception.

After shuffling the deck, we create the player coroutines and initialize them. Then the game loop begins:

  • We iterate over the list of players and send the top card on the table to the current player using the send() method.
  • If the player cannot play, they must keep the top card, which is then removed from the top of the deck.
  • If the player could play a card, it becomes the new top card on the table.
  • Once a player’s turn ends, they are placed at the end of the list to await their next turn.
  • When a player wins, we handle the exception and print the winner’s name. Then the game ends (exiting the loop).

Below you can see the output of a run.

Note that the game doesn’t always produce a winner; in such cases, execution does not stop. Of course, this could also be managed, and the program could be refined in many places, but such additional code would take the focus away from the primary goal, which was to demonstrate how coroutines work.

You can learn more about coroutines and other language constructs used in this example in the e-book Python Knowledge Building Step by Step.

Interested in the e-book Python Knowledge Building Step by Step: From the Basics to Your First Desktop Application?