It failed, but the exit code was 0
Exit code 0 is a promise
Summary
An operations tool reports its result through the exit code, speaks to people through standard error, and hands machine-readable results to other programs through standard output. The moment you mix these three channels, a script starts lying quietly inside cron and pipelines.
Why this matters
At three in the morning, a script that checks the backup directory ran. The backup had not arrived, yet the next step went ahead anyway. The script had printed print("backup missing!") and simply ended. Python gives an exit code of 0 to any program that finishes without an exception. Both cron and CI look only at that 0. Nobody read the sentence that appeared on the screen.
This course covers Python because the job market asks for it most often. In a count taken on 2026-09-11 across 13 Greenhouse boards and We Work Remotely (959 engineering job titles), Python appeared in 445 postings (46.4%), first among all technology keywords, and counting only six Korean companies it still came first with 131 postings (46.1%). But what those postings want is not syntax. Behind phrases like "write operations automation scripts" and "develop internal tools" lies the discipline of building a program that others can trust and run. The first part of that discipline is the exit code.
How it works
The documentation for sys.exit() states the convention this way: the integer 0 means a normal exit and any other value means an abnormal exit, and Unix programs use 2 for command-line syntax errors and 1 for any other error. If you pass a non-integer object (a string, for example), that object is printed to standard error and the exit code becomes 1. So the tools in this course keep three promises.
| Exit code | Meaning | Who handles it |
|---|---|---|
| 0 | The check passed | The next step proceeds |
| 1 | The check failed (the tool works, the target has a problem) | The alerting and retry policy decides |
| 2 | Tool error (bad arguments, a path that does not exist) | A person has to fix the tool |
It is no accident that this table has the same shape as the promise made by grep (0 for a match, 1 for no match, 2 for an error). argparse follows the same convention: ArgumentParser.error() prints a usage message to standard error and finishes with exit code 2. If you parse arguments by hand, you have to reimplement this promise every time, and you usually forget part of it.
Logging behaves as the logging HOWTO specifies. With no configuration, the default level is WARNING, the destination is standard error (sys.stderr), and the default format is 심각도:로거 이름:메시지, that is, severity, then logger name, then message, separated by colons. You change the level and the format with basicConfig(level=..., format=...). What matters here is that the destination is standard error. print() goes to standard output. So if you print diagnostic messages with print(), a pipeline such as tool | jq breaks, and if you emit the result through logging, the result never reaches the pipe.
import argparse, logging, sys
log = logging.getLogger("dircheck")
def main(argv=None) -> int:
p = argparse.ArgumentParser(prog="dircheck")
p.add_argument("path")
p.add_argument("-v", "--verbose", action="store_true")
args = p.parse_args(argv) # 잘못된 인자면 여기서 2 로 끝난다
logging.basicConfig(level=logging.DEBUG if args.verbose else logging.INFO,
format="%(levelname)s %(name)s: %(message)s")
log.debug("checking %s", args.path) # 표준 오류
print("files=3 ok=true") # 표준 출력 — 기계가 읽는다
return 0 # 종료 코드는 main 이 돌려준다
if __name__ == "__main__":
sys.exit(main())
The last two lines are the shape this course stresses. main(argv) returns an integer, and sys.exit() is called only once, at the very bottom of the file. That way a later module can import this tool and test it; if the program ran the moment the module was loaded, testing would be impossible.
What happens to exceptions? An uncaught exception prints a traceback to standard error and ends with exit code 1. Judged by exit code alone, it cannot be told apart from "check failed". So for tool errors (a missing path, no permission), catch OSError and turn it into one line of message plus exit code 2. A traceback is information for the person who built the tool, but noise for the person who got an alert at dawn.
What it looks like in the field
The most common accident is "a failure that ends like a success". Putting python3 check.py || true in the middle of a pipeline, a check tool that does not return an exit code, and swallowing every exception with try: ... except Exception: print(e) and ending with 0 all produce the same result. The second most common problem is standard output pollution. The tool emits JSON, but a progress message such as "connecting..." gets mixed into standard output and json.loads breaks. The third is a tool with no information that appears only when you add -v, so during an outage someone has to "rerun it and add some prints". If you do not build in log levels when you first write the tool, they will never exist.
What you will do in the next lab
You build the backup directory check tool dircheck.py from scratch. Starting from a skeleton where --help works, you add the 0, 1, and 2 exit code promise, print a one-line message instead of a traceback on tool errors, send DEBUG logs only to standard error with -v, and emit machine-readable results to standard output with --json. Finally you refine it into the shape where main(argv) returns an integer, so that importing the file does not run the tool.