python
subprocess
communicate
exit-code
programming-tips

How to get exit code when using Python subprocess communicate method?

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

When you run a child process in Python, communicate() is used to read its output safely and wait for it to finish. The exit code is not returned directly by communicate(), but it is available immediately afterward through the process object's returncode attribute.

Basic Pattern with Popen

The normal sequence is:

  1. create the process with subprocess.Popen
  2. call communicate() to wait for completion and collect output
  3. read proc.returncode
python
1import subprocess
2
3proc = subprocess.Popen(
4    ["python3", "-c", "import sys; print('hello'); sys.exit(3)"],
5    stdout=subprocess.PIPE,
6    stderr=subprocess.PIPE,
7    text=True,
8)
9
10stdout, stderr = proc.communicate()
11
12print("stdout:", stdout.strip())
13print("stderr:", stderr.strip())
14print("exit code:", proc.returncode)

After communicate() returns, the process has finished, so proc.returncode contains the exit status. Before the process finishes, returncode is None.

Why communicate() Is Preferred

If you redirect both standard output and standard error to pipes, reading them manually can deadlock if one buffer fills up while you are waiting on the other. communicate() is the safe built-in API that drains the pipes and waits for termination in the intended order.

That is why the usual advice is not just "call wait() and then inspect returncode." If you also need the captured output, communicate() is the right companion method.

A Small Helper Function

In real code, subprocess handling is often repeated many times. Wrapping it in a helper keeps error handling and timeout behavior consistent.

python
1from dataclasses import dataclass
2import subprocess
3from typing import Sequence
4
5
6@dataclass
7class CommandResult:
8    exit_code: int
9    stdout: str
10    stderr: str
11
12
13def run_command(cmd: Sequence[str], timeout: float = 10.0) -> CommandResult:
14    proc = subprocess.Popen(
15        cmd,
16        stdout=subprocess.PIPE,
17        stderr=subprocess.PIPE,
18        text=True,
19    )
20
21    try:
22        stdout, stderr = proc.communicate(timeout=timeout)
23    except subprocess.TimeoutExpired:
24        proc.kill()
25        stdout, stderr = proc.communicate()
26        return CommandResult(124, stdout, "command timed out")
27
28    return CommandResult(proc.returncode, stdout, stderr)
29
30
31result = run_command(["python3", "-c", "print('ready')"])
32print(result)

The key idea is still the same: the exit code comes from proc.returncode, not from the tuple returned by communicate().

Using subprocess.run When You Do Not Need Popen

If all you want is to execute one command, wait, capture output, and inspect the exit code, subprocess.run is shorter and usually clearer.

python
1import subprocess
2
3completed = subprocess.run(
4    ["python3", "-c", "import sys; sys.exit(5)"],
5    capture_output=True,
6    text=True,
7)
8
9print(completed.returncode)

Under the hood, run handles the common pattern for you. Use Popen when you need finer control, such as streaming input, connecting multiple child processes, or interacting with the process before it exits.

Interpreting Nonzero Exit Codes

A nonzero exit code does not always mean the same thing. Each external program defines its own meanings. For one tool, 1 might mean validation errors were found. For another, 1 might mean a fatal runtime failure.

That means your application should not just check "zero or nonzero" if the command contract is more specific.

python
1result = run_command(["bash", "-lc", "exit 2"])
2
3if result.exit_code == 0:
4    print("success")
5elif result.exit_code == 2:
6    print("known validation status")
7else:
8    print("unexpected failure")

Treat the exit code as part of the API offered by the external command.

Common Pitfalls

  • Expecting communicate() itself to return the exit code.
  • Checking proc.returncode before the child process has actually exited.
  • Using wait() when you also need piped output, then risking deadlocks with manual reads.
  • Ignoring stderr, which often contains the only useful error message.
  • Using shell=True unnecessarily and making argument handling or security harder.

Summary

  • 'communicate() returns captured output, not the exit code.'
  • Read the exit status from proc.returncode after communicate() completes.
  • Use communicate() when output is piped because it avoids common deadlock patterns.
  • Wrap subprocess logic in a helper if your code runs many commands.
  • Prefer subprocess.run for the simple one-command case.

Related reading
Free course
Beginner
7 lessons
2 hours
Tackling System Design Interview Problems

A short course that equips you with the skills to approach system design interviews methodically.

Start the free course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

All Rights Reserved.