Code of the Day
BeginnerUtility Thinking

Building a CLI tool

Put argparse, stdin/stdout, and exit codes together into a complete, working command-line utility.

UtilitiesBeginner12 min read
By the end of this lesson you will be able to:
  • 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 /stdout, argparse, and 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:

  1. Parse arguments — use argparse to define what the tool accepts.
  2. Read input — from stdin, from a named file, or from the argument itself.
  3. Do the work — one focused computation.
  4. Write output — data to stdout, diagnostics to stderr.
  5. 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:

Python — editable, runs in your browser

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.

Finished reading? Mark it complete to track your progress.

On this page