How to implement a Coroutine in Python



A coroutine is like a generator in the sense that they both utilize the yield keyword. However, since the use of each is different, the use of yield within them is also different.

Generators are specifically used to facilitate the flow of iterators, and yielding in a generator just returns a value (or values) in an efficient way. How it's efficient is a topic I will not get into, in this article, to preserve the focus on coroutines.

To continue comparison, generators are often referred to as restricted coroutines. Even though both coroutines and generators can yield several times within the same function, coroutines are more flexible in the sense that they allow program flow to continue at any specific point, by identifying that point using yield. Generators continue flow based on the generator's caller. By upholding the flexibility coroutines offer, a wider range of potential applications becomes possible.

Before I give an example, there is just one more major difference between both and the usage of yield within them. This difference is in syntax. In generators, the syntax for yield is perhaps the more intuitive, being 'yield value', in which we ask the generator to produce a value. However, in coroutines, yield appears on the right-hand-side of an expression, as in 'value = yield', and it might not even actually generate a value. At first sight this may seem counter intuitive, so lets look at some examples to help us understand why it's used this way.

When we first want to start the coroutine, metaphorically speaking, we must activate it, to get it from the first state to the second state. Therefore, the first call to a coroutine must start the generator. To do that, you can either use function next( ) or the method .send(None), which are both called on the generator object, which is an object of the coroutine function.

The .send( ) method is primarily used to advance the coroutine from one yield to the next. In that context, the method posts data that then becomes the value of the yield inside the function. However, if None is passed into it, and the send method is applied to a coroutine which hasn't started execution, it's start the generator. Lets take a look at the example below.

def _coroExample():
    state = None
    print('first state -> second state')
    thirdState = yield state
    print('second state ->', thirdState)
    stillThirdState = yield state
    print('still second state ->', stillThirdState)
    fourthState = yield state
    print('coroutine overflow = fourth state', fourthState)


>>> coro = _coroExample()

>>> next(coro)
first state -> second state
>>> coroTwo = _coroExample()
>>> coroTwo.send(None)
first state -> second state
>>> coro.send(1)
('second state ->', 1)
>>> coro.send(2)
('still second state ->', 2)
>>> coro.send(3)
('coroutine overflow = fourth state', 3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration


To get a firm grasp of what each state is exactly, a coroutine must be in one of four possible states.


  1. 'GEN_CREATED' - Coroutine created, but not yet executed.
  2. 'GEN_RUNNING' - Coroutine is being currently being executed. 
  3. 'GEN_SUSPENDED' - Coroutine is currently suspended at yield.
  4. 'GEN_CLOSED' - Coroutine execution is complete. 
We can use getgeneratorstate from the inspect library to identify the state of the coroutine. Keep in mind this is only available in Python 3.


>>> from inspect import getgeneratorstate
>>> coro = _coroExample()
>>> getgeneratorstate(coro)
'GEN_CREATED'
>>> next(coro)
first state -> second state
>>> getgeneratorstate(coro)
'GEN_SUSPENDED'
>>> coro.send(1)
second state -> 1
>>> getgeneratorstate(coro)
'GEN_SUSPENDED'
>>> coro.send(2)
still second state -> 2
>>> getgeneratorstate(coro)
'GEN_SUSPENDED'
>>> coro.send(3)
coroutine overflow = fourth state 3
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration
>>> getgeneratorstate(coro)
'GEN_CLOSED'

>>>

Notice that each time we call next( ) or send( ) the interpreter executes up until the next yield. Take a look.


def _coroExample():
    state = None
    print('first state -> second state')
    thirdState = yield state ## 1 
    print('second state ->', thirdState)
    stillThirdState = yield state ## 2 
    print('still second state ->', stillThirdState)
    fourthState = yield state ## 3 
    print('coroutine overflow = fourth state', fourthState)

After calling next( ), the interpreter goes until ## 1, the phase ends in the yield expression, literally in the right hand side of the expression yield state but NOT thirdState. 

When we then call .send( ), the interpreter picks up on the same line of ## 1, when the value of the yield expression is assigned to a variable. The same systematic operation repeats until we get to 'GEN_CLOSED'.

Coroutine closing

To close a coroutine at any point, just use the .close( ) method. Which means you can easily use it in any of the examples we went through above. Another good example is with a function that has a while loop.


def listAppender():
    variable = None
    lst = []
    while True:
        element = yield variable
        lst.append(element)
        print(lst)

>>> lappender = listAppender()
>>> next(lappender)
>>> lappender.send(0)
[0]
>>> lappender.send(1)
[0, 1]
>>> lappender.send(2)
[0, 1, 2]
>>> lappender.close()
>>> lappender.send(3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration
>>>

As we expected, the function will keep accepting values and appending them until it's closed. However, once it is closed, we cannot use the send method on it anymore. A new object of the function must be created. 

Coroutine Exception Handling 

If an unhandled exception occurs in a coroutine, it terminates. Meaning you cannot continue using the coroutine object, doing so will raise a StopIteration exception. Let's use a string appender coroutine this time to demonstrate. 

def stringAppender():
    variable = None
    st = ''
    while True:
        element = yield variable
        st += element
        print(st)

>>> sapp = stringAppender()
>>> next(sapp)
>>> sapp.send('s')
s
>>> sapp.send('s')
ss
>>> sapp.send('s')
sss
>>> sapp.send(4)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "coro.py", line 20, in stringAppender
    st += element
TypeError: cannot concatenate 'str' and 'int' objects
>>> sapp.send('s')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

The error occurred because an attempt to append an int to a string was made. Furthermore, our failure to handle the exception killed the coroutine object. As you can see we were unable to use it after the TypeError, even if the argument supplied is valid. Lets take a look at a quick fix. 

def stringAppender():
    variable = None
    st = ''
    while True:
        try:
            element = yield variable
            st += element
        except TypeError as TE:
            st += str(element)
            print('Warning: Please provide string from now on')
        finally:
            print(st)

>>> sapp = stringAppender()
>>> next(sapp)
>>> sapp.send('s')
s
>>> sapp.send('s')
ss
>>> sapp.send(4)
Warning: Please provide string from now on
ss4
>>> sapp.send('s')
ss4s

We've included exception handling for any TypeErrors. Which means that if an integer is supplied, it will first be converted to string before appending. Keep in mind that not all values supplied can be converted to string, but you know now how to handle exceptions. Go on, modify for your application!