Every layer in one program
This capstone touches every layer of the course in a single program, with your code playing the role of the shell.
- Create a program by writing Python source code into a file on disk, from unit 7.
- Inspect the file by reading its metadata to prove it exists as bytes, from lesson 7-3.
- Run it as a child process with
subprocess, exactly the way the shell does, from lesson 3-3. - Capture what the child wrote to file descriptor 1, from lesson 8-2.
- Read its exit code and give a verdict, from lesson 3-3.
| Step | Layer exercised |
|---|---|
| 1 and 2 | filesystem, bytes and metadata |
| 3 | process creation |
| 4 | file descriptors and pipes |
| 5 | exit codes |
The working version below does all five steps in order, and each print lines up with one numbered step above.
The inspector
The full inspector: it writes a child program to disk, checks its size in bytes, runs it as a separate process, then reports the child's output and exit code.
sys.executable is used instead of a literal "python3" so the child runs under the exact same interpreter as the parent. capture_output=True is what redirects the child's file descriptor 1 into a pipe the parent can read, rather than letting it print straight to the terminal.
import os import subprocess import sys child_source = 'print("child reporting in")\n' with open("child.py", "w") as f: f.write(child_source) print("program on disk:", os.stat("child.py").st_size, "bytes") result = subprocess.run([sys.executable, "child.py"], capture_output=True, text=True) print("child said:", result.stdout.strip()) print("exit code:", result.returncode)
Output
program on disk: 28 bytes child said: child reporting in exit code: 0
Notes
- Until subprocess.run, child.py is a program (a file, doing nothing). During the run it is a process with its own PID and memory. Afterward only the exit code remains.
Inspecting a program that fails
The same five steps against a child that deliberately fails. bad.py contains only import sys and sys.exit(3), so it produces no output and exits with code 3.
import os import subprocess import sys bad_source = 'import sys\nsys.exit(3)\n' with open("bad.py", "w") as f: f.write(bad_source) print("program on disk:", os.stat("bad.py").st_size, "bytes") result = subprocess.run([sys.executable, "bad.py"], capture_output=True, text=True) print("exit code:", result.returncode) if result.returncode == 0: print("verdict: ok") else: print("verdict: failed")
Output
program on disk: 23 bytes exit code: 3 verdict: failed
The source is 23 bytes because 'import sys\n' is 11 and 'sys.exit(3)\n' is 12, and result.returncode holds the child's exit code, compared against 0 for the verdict.
This is the case that matters in practice. A non-zero exit code is how a failing process reports trouble to whoever started it, and it is what a shell stores in $?, what && checks before running the next command, and what CI reads to decide whether a build passed.
The parent survives the child's failure untouched and simply reads the number, which is the isolation from unit 3 paying off in the most ordinary possible way.
Where to go from here
You now hold the mental models that make the rest of a software career less mysterious.
- A debugger pauses a process the same way the scheduler does, and reads its stack frames, from unit 4.
- Docker containers are ordinary processes wearing OS-enforced isolation, not tiny virtual machines.
- Databases are careful choreography of buffers,
fsync, and locks, from units 5 and 7. - async and await are a user-space answer to the same problem the scheduler solves in unit 6, which is never wasting time waiting.
When something weird happens, the course's questions are the tool. Which process is it, whose memory is involved, who is waiting on what, which layer buffered it, and what did the exit code say.
| Symptom | The question that cracks it |
|---|---|
| output missing | which layer buffered it |
| service hung | who is waiting on what |
| memory climbing | what still holds a reference |
| build failing silently | what did the exit code say |
Those questions are the skill, and they transfer to every language and every stack, because the machine underneath does not change.
Why a crashing child cannot hurt the inspector
When the inspector's child process crashes badly, the parent keeps running fine and simply reads a non-zero exit code, because processes have isolated memory.
Process isolation from lessons 3-1 and 4-1 gives the child its own address space, so its death cannot corrupt the parent. The parent collects a non-zero exit code as in lesson 3-3 and carries on.
| Unit of concurrency | Shares memory | Survives a sibling crash |
|---|---|---|
| process | no | yes |
| thread | yes, the heap | no |
Threads would not enjoy this protection, which is exactly the trade from unit 5. That is also the reason supervisors, browsers, and CI runners are built out of child processes rather than threads: the isolation converts a crash into a number.