Building a CLI tool
Put argparse, stdin/stdout, and exit codes together into a complete, working command-line utility.
- Structure a utility with argparse, a main() function, and sys.exit()
- Count words and lines from a text input
- Support a --verbose flag to show additional detail
- Exit non-zero when input is empty
You have seen stdin/stdout, argparse, and exit codes as separate ideas. Now they go together. A complete CLI tool is not much more than these three pieces wired up correctly — but the wiring matters.
The structure of a utility
Every utility in this track follows the same shape:
- Parse arguments — use
argparseto define what the tool accepts. - Read input — from stdin, from a named file, or from the argument itself.
- Do the work — one focused computation.
- Write output — data to stdout, diagnostics to stderr.
- Exit cleanly — 0 for success, non-zero for failure.
That structure is not accidental. It matches the composability contract from the
earlier lesson, and it makes the tool easy to test: you can call main() with
known input and check the output without running a subprocess.
Building a word counter
Here is a complete word-counting utility. Read it carefully — every line is doing something from a lesson you have already seen:
Try editing the Runnable: change parse_args([]) to parse_args(["--verbose"])
and run it again. The unique word count appears because args.verbose is now
True.
The if __name__ == '__main__': guard belongs in a real .py file — it
prevents main() from running when the module is imported as a library. The
browser runner executes the whole file directly, so it is omitted here, but
you would write it in a real utility:
if __name__ == '__main__':
main()Why main() is a function
Putting all logic inside main() instead of at the module's top level has two
benefits:
- Testability. A test can import the module and call
main()or individual helpers without side effects firing at import time. - Clarity. The entry point is obvious. Anyone reading the file knows exactly where execution starts.
Handling the empty-input case
The if not text.strip() check above is the exit-code lesson in action. An empty
input cannot produce useful output. Rather than printing lines: 0, words: 0 and
exiting 0 — which tells a pipeline "everything is fine" — the tool writes an error
to stderr and exits 1. The caller can then decide what to do with the failure.
Where to go next
Next: the lab — build a utility from scratch, applying everything in this module.