LESSON 65 · DEEPER PYTHON32 min

Build a Small CLI with argparse

Define a repeatable argument parser for a report command and validate its parsed options.

Before you start: Functions · Exceptions · Conditions

Example check: Worked and guided code samples are checked against CPython 3.13. Practice uses Pyodide 0.29.5 (Python 3.13) in your browser. How we check lessons

Video lesson: Build a Small CLI with argparse

The preview is stored on this site. YouTube loads only when you press Play. Watch on YouTube

Read the video transcript

Select a timestamp to watch that moment on YouTube.

  1. 00:00

    A useful command-line tool needs to understand options, defaults, and mistakes. A user may ask for JSON output and a limit of five rows. The shell passes those tokens as strings; it does not give our Python function a ready-made integer or validated format. We could manually scan a list and convert values, but that quickly becomes fragile when options arrive in a different order or someone misspells one. Python's standard-library argparse gives us a parser that describes the command's grammar. In this lesson, we will run a deliberately unsupported format, read the error, build a small report parser, then test defaults, conversions, and a positive-number rule.

  2. 00:44

    The browser console cannot rely on a local shell's arguments, so its practical task passes an explicit list like `['--format', 'json', '--limit', '5']`. A real script can use the same parser with process arguments at its entry point. We keep parsing separate from any file or network work so the examples run the same way on every machine. Think of the parser as the boundary between untrusted text supplied by a person and the typed values the rest of the report program is allowed to use. The parser refuses xml because we declared only text and json. In a normal terminal, argparse would also print a usage line and an invalid-choice explanation, then exit with status two.

  3. 01:31

    This demonstration catches the exit and redirects its usage text only to make the output stable for our recording. The important behavior remains: a wrong option is an error. Quietly changing xml to text would mislead the user into thinking their request was honored. Choices handle this kind of syntax-level restriction. We also want an integer limit, so we can declare `type=int`; argparse will reject a token like “many.” But it will happily parse zero or negative two, because both are valid integers. The application still needs its own rule that a report limit must be positive.

  4. 02:11

    Keep those layers separate: argparse converts and checks the command grammar, then our function validates the domain meaning. This separation makes error handling and testing much clearer. Nothing has crashed because zero is a perfectly valid integer token. `type=int` answered only the conversion question: can these characters become an int? It did not know that a zero-row report is meaningless for our example. We need a second check after parsing. A reusable `parse_report_args(argv)` function can raise ValueError with a specific message when limit is below one. A full command-line entry point might call `parser.error` instead, which displays usage and exits; the reusable core can let the caller choose how to present the failure.

  5. 03:00

    We should also decide whether unknown flags are accepted. Here they are not. `parse_args` rejects them, catching typos like `--limt`. `parse_known_args` has a real use when forwarding leftovers to another program, but silently discarding unknown tokens in this tool would hide mistakes. Finally, the browser task must pass an explicit argv list. Calling `parse_args()` with no argument would read whatever arguments launched the Python process, which is unrelated to the sample command and makes tests unreliable. The parser creates named options from the supplied tokens. The format choice is limited to text or json, with text as the default.

  6. 03:43

    The limit token is converted to int and defaults to ten. We then apply the positive-number rule and return a small tuple that the report-writing code can use. The first call gives an explicit format and limit; the second shows both defaults. Each call builds its own parser here for clarity, although a larger application could construct the parser once and reuse it. The order of named options is not important: `--limit 5 --format json` yields the same result. Keep help enabled in a normal command-line program so a user can request `--help`; add_help=False only keeps this exercise's output focused.

  7. 04:24

    Also think about where errors are displayed. argparse's own errors usually print to stderr and exit; our added ValueError should be turned into a clear user-facing message by the entry point, not swallowed and replaced with a mysterious empty report. The assertions cover defaults, a different option order, an invalid positive range, and a format outside choices. They test the parser function without writing a report file or reading any shell-specific state. That makes the exercise repeatable in the browser and locally. If a hidden check fails, inspect which layer owns the rule. Argparse already converts a supplied integer and rejects an unknown format.

  8. 05:08

    Your code must still reject zero and negatives and return the parsed values rather than placeholders. A full CLI needs more than this core: help text, a useful exit message, actual report generation, and tests for its output. We have deliberately isolated the part that turns tokens into trusted options. The same approach works for batch utilities and maintenance scripts: describe the command grammar explicitly, validate domain ranges after parsing, and keep command-line input separate from the work the command performs. Try the solved examples on the matching PythonLessonLab page, then complete the checked function and quiz before extending it with another option of your own.

  9. 05:52

    A useful next test is a misspelled flag such as `--limt`; let the parser reject it so the user can correct the command rather than wondering why the report ignored their request.

Understand the concept

A command-line interface turns text tokens into structured options. Python's argparse creates help text, supports defaults, converts types, and rejects choices that are not allowed. A script usually calls parser.parse_args() to read its process arguments. In the browser console, there is no meaningful user shell command line, so this lesson passes an explicit list such as ['--format', 'json', '--limit', '5']. The same parser works in a local script when given sys.argv[1:].

Define each option once: --format may choose text or json, while --limit converts to int and defaults to ten. argparse catches a non-integer like 'many' at parsing time. Type conversion alone does not enforce a positive number; zero is still an int. For this reusable function, raise ValueError after parsing if limit is below one. A production CLI can instead call parser.error to show usage and exit with a nonzero status.

The parser is configured with add_help=False in the exercise so the sample stays focused on the two options and deterministic in a browser. A normal CLI should keep help enabled and use a descriptive prog value. parse_args rejects unknown arguments rather than silently discarding them. If you need to pass through unknown options to another program, that is a different contract and parse_known_args may be appropriate. Do not silently accept a misspelled option.

The checked task implements parse_report_args(argv). It returns a two-item tuple (format, limit) and never reads the browser process's arguments. Hidden checks cover defaults, reversed option order, invalid formats, and nonpositive limits. Separate parsing from the report-writing function so tests can reason about options without touching files or network services.

  • ArgumentParser defines a command-line grammar
  • Explicit argv lists make parsing deterministic in a browser
  • type conversion does not replace domain validation
  • choices and unknown-argument errors catch input mistakes

See it step by step

Read the code, predict the output, then compare it with the result.

01. Parse an explicit list

PYTHON
import argparse

parser = argparse.ArgumentParser(add_help=False)
parser.add_argument("--name", default="world")
options = parser.parse_args(["--name", "Ada"])
print(options.name)
EXPECTED OUTPUT
Ada

Explicit argv makes the example independent of the host process's arguments.

02. Convert an integer and use a default

PYTHON
import argparse

parser = argparse.ArgumentParser(add_help=False)
parser.add_argument("--count", type=int, default=2)
print(parser.parse_args([]).count)
print(parser.parse_args(["--count", "4"]).count)
EXPECTED OUTPUT
2
4

The default applies when the flag is absent; type=int converts the supplied string.

03. Validate a domain rule after parsing

PYTHON
import argparse

parser = argparse.ArgumentParser(add_help=False)
parser.add_argument("--limit", type=int, default=10)
value = parser.parse_args(["--limit", "0"]).limit
try:
    if value < 1:
        raise ValueError("limit must be positive")
except ValueError as error:
    print(error)
EXPECTED OUTPUT
limit must be positive

Zero parses as an integer but violates the report command's rule.

A closer look

Follow the reasoning, inspect each result, then try the suggested changes in the console below.

01 / 03

Turn a list of tokens into named options

A command line is a sequence of strings, not a dictionary of already typed values. argparse supplies a grammar for those strings and returns an object with named fields. In a browser lesson, pass a literal list to parse_args so the example does not depend on the process that launched the Python runtime.

The first parser gives --format a default and a restricted set of choices. A missing flag becomes text; an explicit json becomes json. A real script can call parse_args without an argument to read its process command line, but this exercise keeps input visible and repeatable. That makes both explanations and tests much easier to verify.

PYTHON
import argparse

parser = argparse.ArgumentParser(prog="report", add_help=False)
parser.add_argument("--format", choices=("text", "json"), default="text")
print(parser.parse_args([]).format)
print(parser.parse_args(["--format", "json"]).format)
EXPECTED OUTPUT
text
json
Follow the reasoning
  1. Read the default when no flag is supplied.
  2. Pass an explicit option list and observe the parsed attribute.
02 / 03

Separate type conversion from a positive-number rule

Adding `type=int` turns a token like '5' into an integer. It also accepts zero and negative numbers because those are valid integers. The report command, however, needs a positive item limit. Put that domain check immediately after parsing and raise a clear ValueError when the value is below one.

Keep the parser and validation in a function that receives argv. It can be called repeatedly with independent lists, so a test can cover defaults, a supplied value, and an invalid boundary without touching the shell. A normal command-line entry point might convert the ValueError into parser.error for a usage message. This reusable core leaves that presentation decision to the caller.

PYTHON
import argparse

def parse_report_args(argv):
    parser = argparse.ArgumentParser(prog="report", add_help=False)
    parser.add_argument("--format", choices=("text", "json"), default="text")
    parser.add_argument("--limit", type=int, default=10)
    options = parser.parse_args(argv)
    if options.limit < 1:
        raise ValueError("limit must be positive")
    return options.format, options.limit

print(parse_report_args(["--limit", "5", "--format", "json"]))
try:
    parse_report_args(["--limit", "0"])
except ValueError as error:
    print(error)
EXPECTED OUTPUT
('json', 5)
limit must be positive
Follow the reasoning
  1. Notice that option order does not change the parsed result.
  2. Confirm zero parses as int but fails the separate application rule.
03 / 03

Treat unsupported options as errors

choices helps catch a mistyped format before report generation starts. parse_args also rejects unknown flag names. Do not quietly switch to parse_known_args and discard the leftovers unless the command intentionally forwards arguments to another tool. Silent acceptance can make a user believe a flag took effect when it did not.

The local demonstration suppresses argparse's usage text only so the expected output remains stable; it still catches SystemExit and prints the exception type. In a real CLI, keep help enabled and let argparse display usage. Tests should focus on supported options, documented defaults, and a few representative failure cases rather than asserting every character of a platform-specific error message.

PYTHON
import argparse
import contextlib
import io

parser = argparse.ArgumentParser(prog="report", add_help=False)
parser.add_argument("--format", choices=("text", "json"), default="text")
with contextlib.redirect_stderr(io.StringIO()):
    try:
        parser.parse_args(["--format", "xml"])
    except SystemExit as error:
        print(type(error).__name__, error.code)
print(parser.parse_args(["--format", "json"]).format)
EXPECTED OUTPUT
SystemExit 2
json
Follow the reasoning
  1. See an unsupported format fail instead of silently falling back.
  2. Run a supported value afterward to confirm the parser is reusable.

Try it in Python

Edit the example and run it. Python starts in your browser the first time you click Run.

Python console

Ready to run
Need input()? Add one value per line
Ctrl / ⌘ + Enter to run
OUTPUT
Your output appears here.
Your turnComplete parse_report_args(argv): after parsing, raise ValueError('limit must be positive') for limit below 1 and otherwise return (options.format, options.limit). Never read sys.argv. The prepared example should print ('json', 5) and ('text', 10).
Need a hint?

Use `if options.limit < 1: raise ValueError('limit must be positive')`, then `return (options.format, options.limit)`. argparse already handles the listed choices and integer conversion.

Complete the task and select Check task to verify your code.

    Quick quiz

    Three questions. You can change your answers and try again.

    01.Why pass an explicit list to parse_args in the browser?

    02.Does type=int alone reject a limit of zero?

    03.What does choices=('text','json') provide?

    Typical mistakes

    Everyone meets these errors. See what causes them and how to fix them.

    Reading ambient argv in a browser task

    COMMON MISTAKE
    options = parser.parse_args()
    FIX
    options = parser.parse_args(argv)

    What happens: The result depends on the process running the example, not the supplied exercise input.
    Inject tokens for deterministic practice and testing.

    Assuming integer conversion enforces positivity

    COMMON MISTAKE
    parser.add_argument('--limit', type=int)
    # assume 0 is rejected
    FIX
    if options.limit < 1: raise ValueError('limit must be positive')

    What happens: Zero and negative integers parse successfully.
    Validate business rules after syntax conversion.

    Ignoring unknown options

    COMMON MISTAKE
    options, extra = parser.parse_known_args(argv)
    # silently discard extra
    FIX
    options = parser.parse_args(argv)

    What happens: A misspelled flag can disappear without warning.
    For this command, unknown arguments should be errors.

    Browse this lesson’s problem fixes

    Sources and further reading