Command-Line Interfaces with argparse
Design predictable arguments, helpful failures, stable output, and testable exit behavior.
By the end, you’ll be able to…
- Build parsers with clear argument types and defaults.
- Separate usage errors from operational failures.
- Keep stdout machine-readable and diagnostics on stderr.
01The command line is a public API
A command-line tool has users and downstream programs. Argument names, defaults, output formats, and exit codes become part of its interface. Start with a short usage story and make the common path easy. Positional arguments identify required operands; named options change behavior. Avoid ambiguous abbreviations when a tool will be scripted, and write help text that explains units and valid values.
argparse handles tokenization of the argument list, required arguments, choices, help, and many usage errors. It reads sys.argv by default, while parse_args(argv) accepts an explicit list for tests. Build the parser in a function so tests can inspect parsing without launching a subprocess. Keep the actual domain calculation in another function that does not know about terminal output.
02Validate at the appropriate layer
An argument type function can convert a token and raise ArgumentTypeError with a useful explanation. Use it for local constraints such as a positive integer. Relationships between arguments often need a second validation step, while file contents and remote responses belong to their own boundary validators. Do not cram the whole application into a type callback merely because it runs during parsing.
The example sums nonnegative integers and offers a stable JSON output mode. It explicitly rejects negative values, uses allow_abbrev=False, and keeps main callable with an argument list. The demonstration supplies a fixed list so the code runs deterministically in the lesson. A real cli.py entry point should instead call raise SystemExit(main()) to consume the user’s command-line arguments.
03Make output and failures predictable
Use stdout for requested results and stderr for diagnostic messages. A JSON mode should emit one documented JSON value without progress chatter mixed into it. argparse normally prints usage errors to stderr and exits with status 2; help exits successfully. Choose an application failure code, commonly 1, for expected operational failures such as an unreadable input file.
Catch exceptions where the program can explain them meaningfully. Show a short message for expected input or I/O failures, while allowing unexpected programming errors to remain visible during development. Do not catch BaseException around the whole application: that also catches interruption and process-exit signals. Avoid echoing full untrusted payloads or secrets into diagnostics when a filename and record number suffice.
04Test behavior as a user observes it
Direct tests of main(argv) are fast and let you capture output streams. Add subprocess acceptance tests for installed entry points, actual exit codes, working directories, and file encoding. Use temporary directories rather than fixed filenames in the repository. Check invalid arguments, empty files, malformed rows, Unicode titles, and paths with spaces because those often expose boundary assumptions.
For destructive actions, offer a concrete preview or require an explicit user-controlled flag according to the product contract. For read-only reporting, avoid needless confirmations that break scripts. Keep defaults conservative and document stable output fields. Version a machine-readable format when consumers depend on it; silently renaming a JSON key can break automation even though the command still exits successfully.
The ideas worth keeping.
- Treat arguments, output, and exit codes as an interface.
- Keep parsing, domain logic, and presentation separate.
- Test both direct main calls and the installed command.
Make it yours.
Type and test your code right here in the interactive workspace. Run your solution, check the output, and compare with the worked guide when ready.
Validate a concurrency option
Create a parser with --workers defaulting to 4 and accepting only integers from 1 through 8. Verify the default and an explicit valid value.
Explore a worked solution
import argparsedef workers(text): try: value = int(text) except ValueError as error: raise argparse.ArgumentTypeError('workers must be an integer') from error if not 1 <= value <= 8: raise argparse.ArgumentTypeError('workers must be between 1 and 8') return valueparser = argparse.ArgumentParser(allow_abbrev=False)parser.add_argument('--workers', type=workers, default=4)assert parser.parse_args([]).workers == 4assert parser.parse_args(['--workers', '2']).workers == 2The argument type combines conversion with a local resource bound. A workload may still require a lower practical limit.
Prove a usage error
Require --format to be text or json. Capture stderr and verify an unsupported value raises SystemExit with code 2.
Explore a worked solution
import argparseimport contextlibimport ioparser = argparse.ArgumentParser(prog='demo')parser.add_argument('--format', choices=['text', 'json'], default='text')errors = io.StringIO()try: with contextlib.redirect_stderr(errors): parser.parse_args(['--format', 'xml'])except SystemExit as error: assert error.code == 2else: raise AssertionError('usage error accepted')assert 'invalid choice' in errors.getvalue()The test checks observable status and a meaningful diagnostic fragment instead of mirroring argparse’s entire version-dependent help formatting.
What stayed with you?
Check the ideas while they’re fresh. Your answers stay in this page and feedback appears as you go.
Validate a concurrency option
Create a parser with --workers defaulting to 4 and accepting only integers from 1 through 8. Verify the default and an explicit valid value.
[Process exited with code 0 - No output]
Keep exploring.
The official references behind this chapter, when you want a second angle or the full details.
One chapter closer.
Reading is a start. Trying it yourself makes the connection.