CoreMesh

Python Subprocess Timeout: Examples That Kill Hung Commands

How to use subprocess.run with timeout, catch TimeoutExpired, read partial output, and stop hung commands, with copy-paste examples.

Python Subprocess Timeout: Examples That Kill Hung Commands

Pass timeout= (in seconds) to subprocess.run() and wrap the call in try/except subprocess.TimeoutExpired. If the command runs too long, Python kills the child process, waits for it to finish, then raises the exception.

python
import subprocess

try:
    result = subprocess.run(["ls", "-l"], capture_output=True, text=True, timeout=5)
    print(result.stdout)
except subprocess.TimeoutExpired:
    print("command timed out")

The default is timeout=None, which means wait forever. That's why a script calling a stuck tool hangs until someone notices. Always set a timeout on commands you don't control.

Python subprocess run timeout: the minimal example#

The signature of run() includes timeout=None and capture_output=False. A realistic call combines the options you usually want together:

python
import subprocess

try:
    result = subprocess.run(
        ["mycmd", "myarg"],
        capture_output=True,
        text=True,
        timeout=30,
        check=True,
    )
except subprocess.TimeoutExpired as e:
    print(f"timed out after {e.timeout}s: {e.cmd}")
except subprocess.CalledProcessError as e:
    print(f"exit code {e.returncode}: {e.stderr}")
except FileNotFoundError:
    print("mycmd is not installed or not on PATH")
else:
    print(result.stdout)

What each option does:

  • capture_output=True sets both stdout and stderr to PIPE. You can't pass stdout or stderr alongside it.
  • text=True gives you strings instead of bytes on the result.
  • check=True raises CalledProcessError when the exit code is non-zero.
  • timeout=30 is passed internally to Popen.communicate().

I'd pass the command as a list, as in the examples above, and leave shell=False unless you truly need shell features. The reason shows up in the gotchas below.

What TimeoutExpired gives you#

TimeoutExpired is a subclass of SubprocessError, raised when a timeout expires while waiting for a child process. It carries these attributes:

Attribute Meaning
cmd The command used to spawn the child
timeout The timeout in seconds
output Captured stdout, if captured by run() or check_output(); otherwise None
stdout Alias for output
stderr Captured stderr, if captured by run(); otherwise None

Two details trip people up. First, output and stderr are always bytes when anything was captured, even if you passed text=True. Second, they may stay None instead of b'' when nothing was observed. So decode defensively:

python
import subprocess

def as_text(data):
    return data.decode(errors="replace") if data else ""

try:
    subprocess.run(["mycmd"], capture_output=True, text=True, timeout=10)
except subprocess.TimeoutExpired as e:
    print("partial stdout:", as_text(e.stdout))
    print("partial stderr:", as_text(e.stderr))

This is handy for logging what a command printed before it hung.

Python subprocess timeout kill: what actually gets killed#

The docs say that when the timeout expires, "the child process will be killed and waited for." Only after the child has terminated is TimeoutExpired re-raised. So with run() you don't need to call kill() yourself, and you don't leave a zombie behind.

The key word is child. It's the process Python started. The docs make no promise about processes that child started in turn. That matters in one common case: shell=True. There the child is the shell, and the real work may run under it. If you need guarantees about a whole tree of processes, test your specific command on your platform, and read the Popen documentation for process-group options. On Windows, the docs list CREATE_NEW_PROCESS_GROUP as the creationflags value needed to use os.kill() on the subprocess.

My advice: avoid shell=True for anything with a timeout. Run the actual program directly so the process you time out is the process doing the work.

Python Popen timeout: wait and communicate#

Use Popen directly when you need to do something while the command runs. The docs name two methods that accept a timeout: Popen.wait() and Popen.communicate(). Both raise TimeoutExpired when it elapses.

Unlike run(), nothing documented here kills the child for you when you call these directly. You decide what happens next:

python
import subprocess

proc = subprocess.Popen(
    ["mycmd"],
    stdout=subprocess.PIPE,
    stderr=subprocess.PIPE,
    text=True,
)
try:
    out, err = proc.communicate(timeout=15)
except subprocess.TimeoutExpired:
    proc.kill()
    out, err = proc.communicate()
    print("killed after timeout")

The second communicate() call after kill() collects what's left and reaps the process. Skip it and you leave the process unreaped and the pipes unread.

If you find yourself writing this pattern in every script, wrap it in one helper and reuse it. Or just use run(), which already does it.

check_output timeout and other helpers#

check_output() accepts timeout too, and the docs list it next to run() as a source of captured output on TimeoutExpired. It also raises CalledProcessError on a non-zero exit, like run(check=True).

python
import subprocess

try:
    out = subprocess.check_output(["mycmd", "myarg"], timeout=10)
except subprocess.TimeoutExpired as e:
    print("timed out:", e.cmd)
except subprocess.CalledProcessError as e:
    print("failed:", e.returncode)

The docs recommend run() for every use case it can handle, so I'd use run() in new code. It gives you a CompletedProcess with args, returncode, stdout and stderr, and you can call check_returncode() yourself if you'd rather not use check=True.

The legacy getoutput() and getstatusoutput() take no timeout parameter at all in their signatures, and they run through a shell. If you need a timeout, don't use them.

Python subprocess timeout not working: common mistakes#

When the timeout seems ignored, check these first.

1. You're timing out a shell, not your program. With shell=True, the process Python kills is the shell. Switch to a list of arguments and shell=False, as in the examples above.

2. The timeout is tiny. The docs warn that very small values, a few milliseconds, can produce almost immediate TimeoutExpired because process creation and scheduling take time. If everything times out, raise the value.

3. You expect it to fire during process creation. The initial creation can't be interrupted on many platform APIs. You may not see the exception until at least as long as creation takes.

4. You used Popen and never killed it. Popen.wait(timeout=...) just raises. The child keeps running unless you stop it yourself, as in the Popen example above.

5. You caught the wrong exception. A timeout raises TimeoutExpired, not CalledProcessError. Both inherit from SubprocessError, so a handler for that base class catches either, but you'll lose the ability to tell them apart. Put the specific handlers first.

6. You assumed text=True applies to the exception. It doesn't. e.stdout and e.stderr are bytes.

7. You used the wrong unit. The timeout is in seconds. A float like 0.5 is fine; 500 is more than eight minutes.

Retrying and logging timeouts#

A timeout is often transient: a slow disk, a busy server. A small retry loop keeps the handling in one place:

python
import subprocess

def run_with_retry(cmd, timeout, attempts=3):
    for n in range(1, attempts + 1):
        try:
            return subprocess.run(
                cmd, capture_output=True, text=True,
                timeout=timeout, check=True,
            )
        except subprocess.TimeoutExpired:
            print(f"attempt {n} timed out after {timeout}s")
    raise RuntimeError(f"{cmd} timed out {attempts} times")

Only retry commands that are safe to run twice. A command that half-completed a write before being killed isn't. That's a design question the timeout can't answer for you.

For reference, the full behavior is documented in the subprocess module docs, including the "Timeout Behavior" notes.

Quick reference checklist#

  • Default timeout is None (wait forever). Set one explicitly.
  • subprocess.run(cmd, capture_output=True, text=True, timeout=N, check=True) covers most cases.
  • run() kills and waits for the child, then re-raises TimeoutExpired.
  • Catch TimeoutExpired before CalledProcessError, and handle FileNotFoundError for a missing program.
  • Read e.cmd, e.timeout, e.stdout, e.stderr; the output is bytes or None.
  • With Popen, call communicate(timeout=...) or wait(timeout=...), then kill and collect output yourself.
  • Avoid shell=True when you need the timeout to stop the real work.
  • Don't use tiny timeouts; process creation takes time.
  • Prefer run() over check_output() in new code.

The official reference is short and worth reading once end to end.

Quick answers

What is the default timeout for subprocess.run?

The default is timeout=None, so run() waits as long as the command takes. Set a number of seconds to limit it.

Does subprocess.run kill the process on timeout?

Yes. The docs say the child process is killed and waited for, and then TimeoutExpired is re-raised. Processes that the child itself started aren't covered by that statement, which is one reason to avoid shell=True.

Why is my Popen timeout not killing the command?

Popen.wait() and Popen.communicate() only raise TimeoutExpired when time runs out. You need to stop the process yourself in the exception handler, then call communicate() again to collect output.

Can I get the output of a command that timed out?

If you captured it, read e.stdout and e.stderr on the TimeoutExpired exception. They're bytes even with text=True, and they may be None if nothing was observed.

Did this solve it?

One tap. No account.

Saved you an hour? Send a tip.

USDC or ETH on Base, Ethereum, Arbitrum and more. Keeps the notes free and the examples running.

Base

Network

0xc783bc5c3fa14cEC3f09A22284bC91C5AF0d59e1
Open in wallet · 5 USDC

On a computer? Scan the code with your phone wallet, or copy the address.

Pick the network your wallet or exchange uses. USDC works on all six; ETH on Base, Ethereum, Arbitrum and Optimism. Don't send on Tron or Solana: this address doesn't exist there. Tips are optional and unlock nothing; every guide stays free.

Discussion

Start the discussion

`code` and ``` blocks work

No account needed. Comments are read before they show up, usually the same day. Your email is never asked for.

Indra Wijaya

Builds and runs AI agents, bots and content pipelines, and writes down what works and what broke.

About · Email · RSS

Keep reading

New notes, no spam.

One post at a time, only when there's something worth reading. Follow in any feed reader.

Subscribe via RSS