Sling Academy
Home/Python/Python asyncio: How to control and communicate with subprocesses

Python asyncio: How to control and communicate with subprocesses

Last updated: February 12, 2024

Introduction

Python’s asyncio library is a cornerstone of writing efficient and highly concurrent code, especially in scenarios where IO-bound tasks predominate. With recent versions of Python, asyncio has become even more powerful, thanks to numerous enhancements and new features. One of the common use cases where asyncio shines is in the managing subprocesses – external programs that your Python code can start, interact with, and control independently of its main execution flow. In this tutorial, we will explore how to leverage asyncio in Python 3.11 (and higher) to control and communicate with subprocesses, showcasing the simplicity and power of asynchronous programming.

Understanding asyncio Subprocesses

In asyncio, subprocesses are managed by creating and interacting with Process objects. These objects provide a high-level API to asynchronously start, interact with, and terminate subprocesses. This can greatly improve the performance of your application when dealing with IO-bound tasks, such as reading from or writing to standard input and output of child processes, as these operations do not block the execution of your asyncio event loop.

Creating a Subprocess

Let’s start by creating a simple subprocess. We’ll use the asyncio.create_subprocess_exec function, which is designed to instantiate a subprocess executing a specified command. Here’s how you can do it:

import asyncio

async def run_subprocess(cmd):
    process = await asyncio.create_subprocess_exec(
        *cmd,
        stdout=asyncio.subprocess.PIPE,
        stderr=asyncio.subprocess.PIPE
    )

    stdout, stderr = await process.communicate()
    return stdout, stderr

async def main():
    cmd = ['ls', '-l', '/usr/bin']
    stdout, stderr = await run_subprocess(cmd)
    print(f'Standard Output: {stdout.decode()}', f'Standard Error: {stderr.decode()}')

asyncio.run(main())

In this example, we run the ls -l /usr/bin command, a common UNIX command to list directory contents, as a subprocess. The asyncio.create_subprocess_exec function is used to start the process, and its standard output and error are captured asynchronously. This means your Python script can proceed with other tasks while waiting for the subprocess to complete.

Communicating with the Subprocess

Communication with subprocesses involves sending data to their standard input (stdin) and reading from their standard output (stdout) and standard error (stderr). This is where asyncio’s power really shines, allowing for non-blocking communication channels. Here’s an example of how to send inputs to a process and read its outputs:

import asyncio

async def interact_with_process():
    process = await asyncio.create_subprocess_shell(
        'python3 interactive_script.py',
        stdin=asyncio.subprocess.PIPE,
        stdout=asyncio.subprocess.PIPE,
        stderr=asyncio.subprocess.PIPE
    )

    # Sending input to the subprocess.
    process.stdin.write(b'input data\n')
    await process.stdin.drain()

    # Reading subprocess output.
    stdout = await process.stdout.read()
    print(stdout.decode())

    await process.wait()

asyncio.run(interact_with_process())

This example demonstrates how you can start a Python script as a subprocess, send it an input string, and then read the response. This non-blocking approach is perfect for tasks that require interaction with external processes without halting the main program.

Monitoring and Controlling the Subprocess

Monitoring subprocesses to ensure they’re behaving as expected and controlling them (e.g., terminating them) is another crucial aspect of using asyncio with subprocesses. Here’s how you can terminate a subprocess:

import asyncio

async def terminate_process():
    process = await asyncio.create_subprocess_shell('some_long_running_command',
        stdin=asyncio.subprocess.PIPE,
        stdout=asyncio.subprocess.PIPE,
        stderr=asyncio.subprocess.PIPE
    )

    # Optionally, perform operations on the process here.

    process.terminate()
    await process.wait()

asyncio.run(terminate_process())

By calling process.terminate(), you send a termination signal to the subprocess. This is useful for ensuring that tasks do not run indefinitely or for stopping them based on certain conditions detected by your code.

Advanced Usage and Tips

As you become more familiar with using asyncio to manage subprocesses, you’ll discover more advanced techniques and best practices. For instance:

  • Always handle subprocess stdout and stderr asynchronously to prevent deadlocks.
  • Use asyncio.subprocess.create_subprocess_shell judiciously as it introduces a shell injection risk. Prefer create_subprocess_exec for better security.
  • Combine asyncio with other Python features like threading or multiprocessing for handling CPU-bound tasks concurrently with IO-bound tasks.

Conclusion

In summary, Python’s asyncio library offers a robust and efficient way to control and communicate with subprocesses. By integrating these asynchronous capabilities into your Python applications, you can achieve greater concurrency, better performance, and enhanced responsiveness. The examples provided in this guide offer a foundation, but the possibilities are vast and limited only by your imagination. Happy coding!

Next Article: Python asyncio: How to stop/kill a child process

Previous Article: A list of popular Python libraries that use asyncio

Series: Python Asynchronous Programming Tutorials

Python

You May Also Like

  • Python Warning: Secure coding is not enabled for restorable state
  • Python TypeError: write() argument must be str, not bytes
  • 4 ways to install Python modules on Windows without admin rights
  • Python TypeError: object of type ‘NoneType’ has no len()
  • Python: How to access command-line arguments (3 approaches)
  • Understanding ‘Never’ type in Python 3.11+ (5 examples)
  • Python: 3 Ways to Retrieve City/Country from IP Address
  • Using Type Aliases in Python: A Practical Guide (with Examples)
  • Python: Defining distinct types using NewType class
  • Using Optional Type in Python (explained with examples)
  • Python: How to Override Methods in Classes
  • Python: Define Generic Types for Lists of Nested Dictionaries
  • Python: Defining type for a list that can contain both numbers and strings
  • Using TypeGuard in Python (Python 3.10+)
  • Python: Using ‘NoReturn’ type with functions
  • Type Casting in Python: The Ultimate Guide (with Examples)
  • Python: Using type hints with class methods and properties
  • Python: Typing a function with default parameters
  • Python: Typing a function that can return multiple types