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 code | Means |
|---|---|
| 0 | success, as in zero errors |
| anything else | failure, 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.
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 failedA 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.
| Code | Common meaning |
|---|---|
| 0 | success |
| 1 | a general failure |
| 2 | misuse, 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 2result.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 sees | Verdict |
|---|---|
| exit code 0 | green, continue the pipeline |
| exit code 1 | red, 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.