← Debugging Guides

阅读中文版本

Python subprocess.CalledProcessError: returned non-zero exit status

Diagnose subprocess.CalledProcessError by capturing stderr, checking exit codes, and using check=True only where failure should abort.

intermediatedebuggingpythonsubprocess

Symptoms

  • subprocess.CalledProcessError: Command ... returned non-zero exit status 1
  • The exception message does not show what the child printed
  • A shell command works manually but fails from Python

Possible causes

  • The child process genuinely failed and check=True raised
  • Environment or PATH differs from your interactive shell
  • Arguments are passed as one string instead of a list
  • The working directory or shell differs, changing relative paths

Diagnosis steps

Capture and print stderr on failure

subprocess.run(cmd, capture_output=True, text=True)

Interpretation

The child message usually names the real problem that the traceback hides.

Run with a shell to compare behaviour

subprocess.run(cmd, shell=True)

Interpretation

If it passes only with shell=True, quoting or PATH is the difference.

Print the resolved environment and cwd

print(os.getcwd()); print(os.environ["PATH"])

Interpretation

Differences from your terminal explain missing binaries and relative-path errors.

Root cause

CalledProcessError means the child exited non-zero and check=True was set. The exception hides the child output by default, so the real cause lives in the child stderr — often a missing binary, a wrong working directory, or argument quoting.

Fix

Use capture_output=True with check=True, then surface stderr in the exception. Pass arguments as a list rather than a shell string, set cwd and env explicitly, and only use check=True where a failure should stop the caller. Log the full command and stderr for diagnosis.

Verify the fix

Re-run the command and confirm exit status 0 with the expected stdout. Add a test that asserts subprocess.run(..., check=True) succeeds for a known-good invocation.

Symptoms

subprocess.CalledProcessError fires with a message that omits what the child printed. The same command works when you run it by hand, which suggests environment or argument differences.

Diagnosis

Capture output so the child’s own error becomes visible:

try:
    subprocess.run(['make', 'build'], capture_output=True, text=True, check=True)
except subprocess.CalledProcessError as exc:
    print('rc', exc.returncode)
    print(exc.stderr)

Compare os.getcwd() and os.environ['PATH'] with your terminal.

Fix

Pass a list, set the environment and cwd explicitly, and surface stderr:

result = subprocess.run(
    ['git', 'rev-parse', 'HEAD'],
    capture_output=True, text=True, check=True,
    cwd=repo_path, env=clean_env,
)
print(result.stdout)

Verification

Re-run and confirm exit status 0 plus expected stdout. A unit test invoking a known-good command with check=True guards against regressions.

Distinguishing Cause from Symptom

The traceback names the command but not the reason, so the first job is to surface stderr. A non-zero exit can mean the tool genuinely rejected the input, or that the process could not even start because a binary was missing from PATH. Running with shell=True resolves most missing-binary and quoting surprises, but it also expands shell syntax that a list form never would, so treat it as a diagnostic step rather than the final design. Once stderr is visible, pin the working directory and environment explicitly and the failure usually reduces to a single missing precondition.