Terminate a Python Subprocess with terminate() or kill()

Quick Answer

Call terminate() to request that a Popen child stop, then call wait() to collect its exit code. If it does not exit within a timeout, call kill() and wait again. Always manage pipes and timeouts so the parent does not hang.

Python subprocess lifecycle showing poll wait terminate kill and exit code collection
Check, stop, and collect a subprocess deliberately: poll(), terminate() or kill(), then wait() for the exit code.

subprocess.Popen.terminate() asks a child process to stop. In Python, it is commonly used when a command runs too long, a background worker is no longer needed, or a parent process must clean up children before exiting.

The safe pattern is to start the process, request termination, wait for a short period, and then call kill() only if the child does not exit. That sequence gives the child a chance to close files, flush output, and release resources before a forced stop.

terminate() and kill() are not the same. terminate() is the graceful first step. kill() is the fallback for a process that ignores or cannot handle the termination request. Always pair either call with wait() or communicate() so the parent collects the final return code.

Do not treat termination as a fire-and-forget operation. The parent process still needs to release handles, collect pipe data, and confirm that the child has actually stopped. Otherwise, cleanup code can leave background processes or blocked pipes behind.

The official Popen.terminate documentation, Popen.kill documentation, Popen.wait documentation, TimeoutExpired documentation, and signal module documentation are the primary references.

Terminate A Child Process

Use Popen when you need to manage a running child process. The example below starts another Python process, requests termination, and waits for it to exit.

import subprocess
import sys
import time

process = subprocess.Popen(
    [sys.executable, "-c", "import time; time.sleep(60)"]
)

time.sleep(1)
process.terminate()
return_code = process.wait(timeout=5)

print(return_code)

The return code may be platform-specific when a process exits because of a signal. The important point is that the parent called wait() and did not leave the child process unmanaged. For Unix process creation before termination and supervision, see the parent-child lifecycle in Python Forking with os.fork() Guide.

If you need to make a decision based on the return code, log the platform and command context as well. A signal-based stop can look different from a normal program exit, and that difference matters in monitoring or retry code.

Use kill() As A Fallback

A child process may not exit after terminate(). Use a timeout and call kill() only when the graceful request does not finish in time.

import subprocess
import sys

process = subprocess.Popen(
    [sys.executable, "-c", "import time; time.sleep(60)"]
)

process.terminate()

try:
    process.wait(timeout=3)
except subprocess.TimeoutExpired:
    process.kill()
    process.wait()

This pattern prevents a stuck child from living forever, while still giving it a chance to shut down cleanly first.

Clean Up After communicate() Timeout

If you use communicate() with a timeout, handle TimeoutExpired, terminate or kill the process, and then call communicate() again to collect buffered output.

import subprocess
import sys

process = subprocess.Popen(
    [sys.executable, "-c", "import time; time.sleep(60)"],
    stdout=subprocess.PIPE,
    stderr=subprocess.PIPE,
    text=True,
)

try:
    output, errors = process.communicate(timeout=2)
except subprocess.TimeoutExpired:
    process.terminate()
    output, errors = process.communicate(timeout=5)

print(process.returncode)

Calling communicate() again after termination lets Python drain pipes and update the process state.

This is especially important for commands that produce output. Killing a process without reading the pipes can discard useful error text and make the next debugging step harder.

Wrap Termination In A Helper

For repeated use, put the terminate-wait-kill sequence in one helper so every subprocess is cleaned up the same way.

import subprocess

def stop_process(process, timeout=5):
    if process.poll() is not None:
        return process.returncode

    process.terminate()
    try:
        return process.wait(timeout=timeout)
    except subprocess.TimeoutExpired:
        process.kill()
        return process.wait()

poll() checks whether the process has already exited. That avoids sending a termination request to a process that is no longer running.

Stop A Process Group On Unix

Some commands start child processes of their own. On Unix-like systems, start a new session and signal the process group when you need to stop the whole group.

import os
import signal
import subprocess
import sys

process = subprocess.Popen(
    [sys.executable, "-c", "import time; time.sleep(60)"],
    start_new_session=True,
)

os.killpg(process.pid, signal.SIGTERM)
process.wait(timeout=5)

This is useful for tools that spawn helper processes. Process-group handling is platform-specific, so keep it behind a small function if your code runs on multiple operating systems.

Use sys.executable For Python Children

When the child command is another Python process, call sys.executable instead of hard-coding python. That keeps the child aligned with the interpreter that launched the parent.

import subprocess
import sys

command = [
    sys.executable,
    "-c",
    "print('child started')",
]

completed = subprocess.run(command, check=True, capture_output=True, text=True)
print(completed.stdout.strip())

This avoids many path problems in virtual environments, scheduled jobs, CI, and hosting platforms.

Fix Checklist

Use terminate() when you want a graceful stop. Follow it with wait() and a timeout. Use kill() only when the child does not exit after the graceful request.

When pipes are open, prefer communicate() so output is collected safely. If a timeout occurs, terminate the child, collect the remaining output, and then inspect the return code.

For long-running tools that spawn their own children, decide whether you need to stop just the direct child or the full process group. Keep that choice explicit, because process behavior differs across operating systems.

Use short timeouts for interactive commands and longer timeouts for tools that need time to close files or write final output. The right timeout depends on what the child process is doing.

Use a Timeout Before Escalating

A reliable subprocess shutdown has three parts: request termination, wait for the child, and escalate if it does not exit. terminate() and kill() have platform-specific behavior, so treat them as process-control signals rather than application-level cleanup.

import subprocess

process = subprocess.Popen(["python", "worker.py"])
try:
    process.wait(timeout=10)
except subprocess.TimeoutExpired:
    process.terminate()
    try:
        process.wait(timeout=3)
    except subprocess.TimeoutExpired:
        process.kill()
        process.wait()

Use poll() when you want a non-blocking status check; it returns None while the child is still running. If you capture standard output or error, prefer communicate(timeout=...) so pipe buffers are drained and the child cannot block because the parent stopped reading.

Terminate Is Not Application Cleanup

A forced process stop may prevent the child from completing its own cleanup or writing a final result. If the worker supports a graceful shutdown protocol, send that application-level signal first, then use terminate() or kill() only as a fallback. On Windows, terminate() uses the platform termination API and is closer to a forced stop than a Unix signal-based graceful request.

Frequently Asked Questions

How do I terminate a Python subprocess?

Keep the Popen object, call process.terminate(), and then call process.wait() to collect the exit code.

What is the difference between terminate() and kill()?

terminate() requests that the child stop, while kill() is the stronger fallback used when the child does not exit in time. Exact behavior depends on the operating system.

How do I prevent wait() from hanging forever?

Pass a timeout to wait() or communicate(). Catch subprocess.TimeoutExpired, terminate the child, and escalate to kill() if needed.

Why should I call wait() after terminate() or kill()?

wait() reaps the child and returns its exit code. Skipping it can leave process resources unmanaged and makes shutdown status unclear.

Subscribe
Notify of
guest
0 Comments
Oldest
Newest Most Voted