# Advanced Python Coroutines: Best Practices for Efficient Asynchronous Programming

You must have used the functions in the Python program to perform a certain task. These functions in the Python program are known as subroutines. Subroutines follow a set pattern of execution like entering at one point(subroutine or function call) and exiting at another point(return statement).

Coroutines are similar to subroutines but unlike subroutines, coroutines can enter, exit, and resume at different points during the execution. Coroutines are used for cooperative multitasking which means the control is passed from one task to another to enable multiple tasks to run simultaneously.

## Introduction To Coroutines

**Coroutines** (generator-based coroutines) are a specialized version of generators and like them, they can be paused and resumed using the **yield** keyword at the time of execution.

[Generators](https://geekpython.in/python-generators-with-yield-statement) generate data, whereas coroutines can do both, generating and consuming data, with a slight difference in how the yield is used within coroutines. We can use **yield as an expression** (`value = yield`) within coroutines, which means that `yield` can both generate and consume values.

To justify the above point, consider the following example in which we created a function that exhibits the behavior of a coroutine.

```python
def cor_func(char):
    print(f"Searching for character: {char}")
    while True:
        data = yield
        if char in data:
            print("True")
        else:
            print("False")

value = cor_func("e")
```

The above code defines a coroutine function called `cor_func`, which searches for a parameter `char`. The function `cor_func` uses `while True` to run an infinite loop, and within the loop, `yield` is encountered, which is a halt in the execution and allows us to send data in the meantime. The caller's data is saved inside the variable `data`.

If the character `char` is present in the `data`, the function prints the message `True`, otherwise, the message `False` is printed.

We created the instance of the coroutine (`cor_func("e")`) and stored it inside the variable `value`.

```python
value.__next__()
value.send("hello")
value.send("GeekPython")
value.send("Geek")
```

The coroutine is started by calling `value.__next__()`. The coroutine function will execute until it reaches the `yield`, allowing us to send data.

We first sent the string `"hello"` using the `value.send("hello")` and the string will be checked if the character `"e"` is present in it, since there is `"e"` in `"hello"`, the output would be `True`. The same process will be repeated for `value.send("GeekPython")` and `value.send("Geek")` as well.

```bash
Searching for character: e
True
True
True
```

### Closing the Coroutine

The `close()` method, as the name implies, is used to close the coroutine, which means that no more values can be sent to the coroutine.

Consider the previous code, which we modified by adding the `value.close()` method.

```python
def cor_func(char):
    print(f"Searching for character: {char}")
    while True:
        data = yield
        if char in data:
            print("True")
        else:
            print("False")

value = cor_func("e")

value.__next__()
value.send("hello")
value.send("GeekPython")
value.close()
value.send("Geek")
```

We called the `close()` method on the coroutine, which will close the coroutine and prevent it from receiving further values. However, when we tried to send the string `"Geek"` to the coroutine, we got the following result.

```python
Searching for character: e
True
True
Traceback (most recent call last):
  ....
    value.send("Geek")
StopIteration
```

Since the coroutine had already been closed, the `send()` method that followed the `close()` method threw the `StopIteration` exception.

## Async Coroutine

We can define a coroutine function (`async def`) and pause the process until a specific task is completed by using the (`async`/`await`) keywords.

%[https://geekpython.in/asyncio-how-to-use-asyncawait-in-python] 

```python
import asyncio

async def coroutine_func():
    print("Coroutine started.")
    await asyncio.sleep(1)
    print("Coroutine ended.")

cor = coroutine_func()
print(cor)

----------
<coroutine object coroutine_func at 0x0000023A863280C0>
```

The `asyncio` module was imported, which allows us to write asynchronous code. Then we defined the asynchronous coroutine function (`coroutine_func()`), which prints a message and then waits one second before printing another message using `await asyncio.sleep(1)`.

We created a coroutine function instance and stored it in the variable `cor`. When we printed, the coroutine object was returned.

We can use the `asyncio.run()` function and pass in our coroutine object `cor` to run the above coroutine.

```python
asyncio.run(cor)
```

## Concurrency Using Coroutine

We can think of it as the ability to run multiple tasks concurrently in an overlapping manner. Let's understand with an example.

```python
import asyncio

# Task 1
async def read_file():
    with open("test.txt") as data:
        await asyncio.sleep(1)
        print(data.read())

# Task 2
async def write_file():
    with open("test.txt", "a") as data:
        await asyncio.sleep(1)
        data.write("\nIt will be fun.")

# Task 3
async def message():
    print("Hey,")
    await asyncio.sleep(1)
    print("Welcome aboard.")

# Entry point
async def main():
    await asyncio.gather(message(), write_file(), read_file())

if __name__ == "__main__":
    import time
    start = time.perf_counter()
    asyncio.run(main())
    elapsed = time.perf_counter() - start
    print(f"Tasks executed in {elapsed:0.1f} seconds.")
```

Three coroutine functions are defined in the preceding code.

The coroutine function `read_file()` opens the file `test.txt`, waits for one second, reads the content, and then prints it.

The coroutine function `write_file()` opens the file in append mode, waits for one second, writes the data, and then appends to the file.

The `message()` coroutine function prints one message, then waits for one second before printing another.

The coroutine function `main()` is defined to run those three coroutine functions concurrently using `asyncio.gather(message(), write_file(), read_file())`.

Inside the `if __name__ == "__main__":` block, we executed our `main()` coroutine function using `asyncio.run(main())` and we used the `time.perf_counter()` to measure the execution time.

```bash
Hey,
Welcome aboard.
We are currently learning coroutines.
It will be fun.
Tasks executed in 1.0 seconds.
```

The code took `1.0` seconds to execute all three coroutine functions which were due to 1 second delay in the coroutines. These coroutine functions were executed simultaneously.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1687452206338/1c4af836-18d2-4d4d-b963-9e4badc6961b.png align="center")

## Awaiting Coroutine

Coroutines are **awaitables** (objects that can be used in an `await` expression), so they can be awaited from other coroutines. Let's look at the example to get a grasp of it.

```python
import asyncio

async def read_file():
    with open("test.txt") as file:
        return file.read()

async def write_file():
    with open("test.txt", "a+") as file:
        file.write("\nLet's get started.")

    # Awaiting read_file() from write_file() coroutine
    print(await read_file())

asyncio.run(write_file())
```

When we run the above code with `asyncio.run(write_file())`, the coroutine function `write_file()` is called first, and it opens the `test.txt` file in append mode and appends the data. Then it proceeds until it encounters the `await read_file()`, which halts the execution of the `write_file()` coroutine function.

The execution flow then proceeds to the `read_file()` coroutine function, which reads the contents of the `test.txt` file.

When the execution flow returns, the content returned by `read_file()` is printed.

```bash
We are currently learning coroutines.
It will be fun.
Let's get started.
```

Inside the `test.txt` file, we can see that the string `"Let's get started"` has been appended to our existing data.

## Conclusion

Coroutines are very helpful in asynchronous programming in which multiple tasks run concurrently. We've seen how multiple coroutines are executed concurrently to save time.

Coroutines can enter, exit, and resume at different points during the execution. They are similar to generators but they have additional features such as support for cooperative multitasking, asynchronous programming, and more.

---

🏆**Other articles you might be interested in if you liked this one**

✅[How to use assert statements for debugging in Python](https://geekpython.in/python-assert)?

✅[How to manipulate paths using pathlib module in Python](https://geekpython.in/python-pathlib-module)?

✅[Different types of inheritances in Python classes](https://geekpython.in/class-inheritance-in-python).

✅[How to implement \_\_getitem\_\_, \_\_setitem\_\_ and \_\_delitem\_\_ in Python classes](https://geekpython.in/implement-getitem-setitem-and-delitem-in-python)?

✅[How to connect PostgreSQL with Python using psycopg2](https://geekpython.in/integrate-postgresql-database-in-python)?

✅[How to display images on the frontend using FastAPI](https://geekpython.in/displaying-images-on-the-frontend-using-fastapi)?

✅[Public, Protected, and Private access modifiers in Python](https://geekpython.in/access-modifiers-in-python).

---

**That's all for now**

**Keep coding✌✌**
