How can I run an external command asynchronously from Python?
Master System Design with Codemia
Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.
Introduction
Running an external command asynchronously in Python means starting a child process without blocking the rest of your application. The right API depends on the rest of your program: subprocess.Popen fits normal synchronous code, while asyncio.create_subprocess_exec is the right choice when your application already uses an event loop.
The important distinction is not only how you start the process, but how you handle output, timeouts, and cleanup. A child process that starts in the background but is never monitored can still hang, fill buffers, or become an orphaned process.
Use subprocess.Popen in Synchronous Programs
If your program is otherwise synchronous, subprocess.Popen is the standard tool. It starts the child process immediately and returns a process handle that you can poll, wait on, or terminate later.
This is asynchronous in the sense that the parent keeps running while the child is still working. The child is not tied to an event loop. It is just another operating-system process.
Capture Output Safely
If you need stdout or stderr, attach pipes and use communicate(). That method reads the streams safely and waits for the child to finish:
The main reason to prefer communicate() over ad hoc reads is deadlock avoidance. If the child writes enough output to fill a pipe and the parent is not draining it correctly, both sides can stall.
Use asyncio in Async Applications
If your application already uses async and await, do not mix in blocking subprocess waits. Use the asyncio subprocess API so the event loop can continue serving other tasks:
This is the cleanest approach in web servers, async task runners, or CLI tools that already depend on asyncio.
Run Multiple Commands Concurrently
Once you are in an async design, running several external commands in parallel becomes straightforward:
This does not turn the external commands into Python coroutines. It simply lets the parent Python process manage multiple child processes without blocking on one at a time.
Handle Timeouts and Cleanup
External commands can hang, so you should treat timeout handling as part of the design, not an optional add-on.
With subprocess, a timeout looks like this:
With asyncio, use asyncio.wait_for:
Killing and waiting is important. It ensures the child really exits and prevents process leaks.
Common Pitfalls
The biggest mistake is calling a blocking method immediately after starting the child and still describing the design as asynchronous. If you call .wait() right away, you are blocking again.
Another common issue is using shell=True for convenience even when you already have clean argument values. Prefer argument lists unless you truly need shell syntax such as pipes or wildcard expansion.
Developers also mix blocking subprocess code into asyncio applications and then wonder why the event loop freezes. If the application is async, the subprocess handling needs to be async as well.
Finally, never ignore the child after starting it. Check the return code, read the streams, and clean up on timeouts or cancellation.
Summary
- Use
subprocess.Popenfor background processes in synchronous Python code. - Use
asyncio.create_subprocess_execwhen the rest of the application is async. - Prefer
communicate()for safe stdout and stderr handling. - Add timeout and cleanup logic so hung processes do not accumulate.
- Avoid
shell=Trueunless shell features are actually required.

