Course outline · 0% complete

0/28 lessons0%

Course overview →

Birth, death, and exit codes

lesson 3-3 · ~11 min · 9/28

The process lifecycle

A process is born when its parent asks the OS to create it, lives through the running, ready, and waiting states from lesson 3-1, and dies when it finishes or crashes. At death it leaves one last message for its parent, the exit code, a small integer.

The convention is universal:

Exit codeMeans
0success, as in zero errors
anything elsefailure, and the number can say why

Every tool you will use in a career respects this: shell scripts, git, test runners, and CI pipelines. CI stands for continuous integration, a server that automatically runs a project's tests and checks on every change a developer pushes. When a CI pipeline marks a build red, it is literally reading an exit code.

In Python you can create a child process with the subprocess module and inspect its exit code, which is what the rest of this lesson does.

parentchild runsits own memory and PIDchild exitscreatesfinishesexit code: 0 for success, anything else for failure
A process is created by its parent and reports one small number back on the way out.

Launching a child and reading its exit code

The parent starts a child Python process, captures what it printed, and reads the code it exited with.

import subprocess
import sys

child_code = "print('hello from the child')"
result = subprocess.run([sys.executable, "-c", child_code],
                        capture_output=True, text=True)
print(result.stdout, end="")
print("child exit code:", result.returncode)

Output

hello from the child
child exit code: 0

sys.executable is the path to the Python currently running, which is the reliable way to launch a matching child rather than hoping python3 means the same thing on every machine.

The child ran to the end without error, so the OS reports exit code 0 to the parent. Note that the parent had to ask for that number, since a dead process leaves its exit code behind for the parent to collect.

A crashing child seen from a healthy parent

The child divides by zero, and the parent reads a non-zero code without crashing itself.

import subprocess
import sys

result = subprocess.run([sys.executable, "-c", "1 / 0"],
                        capture_output=True, text=True)
print("exit code:", result.returncode)
if result.returncode == 0:
    print("child succeeded")
else:
    print("child failed")

Output

exit code: 1
child failed

A Python process that dies from an unhandled exception exits with code 1. The traceback went to the child's error stream and was captured rather than printed.

The parent is a separate process with separate memory, so the crash cannot touch it. That containment is the reason supervisors, web servers, and CI runners are built as parents that launch children: a failure becomes a number to handle rather than an outage.

Choosing your own exit code

A program can pick its exit code with sys.exit(n). Well-behaved command-line tools use this to report what kind of failure happened, so scripts can react without parsing any output text.

In the shell, $? holds the exit code of the last command, and && runs the next command only if the previous one exited 0:

python3 deploy_checks.py && python3 deploy.py

If the checks exit non-zero, the deploy never runs.

CodeCommon meaning
0success
1a general failure
2misuse, such as bad arguments

Those specific numbers are conventions rather than rules, and only 0 is truly universal. That one convention, success is zero, powers most automation on Earth.

Branching on two children's exit codes

One child exits 0, the other exits 2, and the parent reports each.

import subprocess
import sys

good = subprocess.run([sys.executable, "-c", "import sys; sys.exit(0)"])
bad = subprocess.run([sys.executable, "-c", "import sys; sys.exit(2)"])

for name, result in [("good", good), ("bad", bad)]:
    if result.returncode == 0:
        print(name, "-> ok")
    else:
        print(name, "-> failed with code", result.returncode)

Output

good -> ok
bad -> failed with code 2

result.returncode holds the child's exit code, and the only test that matters is whether it equals 0. The specific value 2 is printed rather than interpreted, because its meaning belongs to the tool that chose it.

This loop is a miniature version of what a build script does. Run several steps as children, treat zero as success, and report the first non-zero code upward so the caller can react the same way.

Why a red build follows from exit code 1

A CI pipeline whose test suite exits with code 1 marks the build red, because any non-zero exit code means failure by convention.

Exit code 0 is the universal handshake for success. Test runners deliberately exit non-zero when any test fails, precisely so CI systems, shell scripts, and && chains can react without reading a single line of output.

What CI seesVerdict
exit code 0green, continue the pipeline
exit code 1red, stop and report

The consequence is practical. A script that swallows a failure and returns 0 anyway will produce green builds over broken code, which is why wrapping a command in a way that discards its exit code is a genuinely dangerous habit.