LESSON 154 · PYTHON TOOLS30 min

Python argparse Subcommands with add_subparsers

Give add and list commands separate arguments and reject a missing or incomplete command.

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: Python argparse Subcommands with add_subparsers

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 command line with distinct verbs is easier to explain when each verb owns its own arguments. argparse.ArgumentParser.add_subparsers creates child parsers for commands such as add and list. The add command may require --name, while list should accept no name. A single positional action plus a global --name flag cannot express those different requirements cleanly. A parser with add and list subcommands models two distinct operations. add_subparsers returns an object on which each add_parser call registers a verb. dest='command' stores the selected name in the parsed Namespace, and required=True makes an omitted verb a usage error.

  2. 00:44

    Think of the parser as a small tree. The parent recognizes the application name and chooses a verb. The add child then parses the fields needed to create a note. The list child has a different grammar and does not ask for a name. That structure becomes more valuable as a command-line program grows, because the help output for each verb can focus on its own options. Call add_subparsers(dest='command', required=True), then use add_parser for each verb. Add --name with required=True only to the add parser. The parsed Namespace contains command='add' and name='Ada' for add --name Ada; list yields command='list'.

  3. 01:27

    The browser task tests parse_args with explicit argument lists, so it does not depend on the process's actual sys.argv. This first small example passes ['list'] explicitly, so you can run it in the browser without needing a terminal command line. In a local program, argparse normally reads sys.argv[1:] when parse_args is called without a list. Explicit inputs are easier to test and demonstrate. The add child parser owns --name, with required=True. list has no such option. This expresses the actual command grammar: notes add --name Ada is valid, while notes list is valid without a name.

  4. 02:08

    A global --name option would make the relationship between verb and option ambiguous. argparse raises SystemExit after it prints usage for malformed command lines. That is normal for a CLI but surprising inside a reusable function or browser exercise. Keep the parser builder separate from execution; tests can call it with known lists and catch SystemExit for invalid cases. In a real script, parse once at the entry point and dispatch to a handler or use set_defaults(func=...). The returned Namespace contains both command and name for the add invocation. Downstream logic can dispatch on command or assign a handler with

  5. 02:50

    set_defaults(func=...). Keep parsing and side effects separate so an invalid command fails before a file or database changes. The earlier argparse lesson introduces ordinary flags and type conversion. This page answers the longer-tail question of command-specific grammars. Help output, aliases and nested subcommands can be added later, but each extra option should earn its place in the user interface. Keep `list` usable without `--name`, and test that an unknown verb or `add` with no name fails rather than guessing what the user intended. argparse writes a usage diagnostic to stderr and raises SystemExit for a bad invocation.

  6. 03:35

    The example redirects stderr and catches the exception so the browser lesson continues. An exit code of 2 represents a parse error in this demonstration; do not confuse it with a successful command result. Test a missing verb, an unknown verb, add without --name, and list with an unexpected --name. Those tests show that child parsers have genuinely different rules. Avoid catching SystemExit around your entire production application and pretending every failure is a recoverable parse error. Replace the shared command and --name arguments with required subparsers. Build add and list commands.

  7. 04:15

    Add must require --name; list must not accept it. The prepared run should print {'command': 'add', 'name': 'Ada'}. Missing command, unknown command, and add without a name must fail with SystemExit. If you accidentally put required --name on the parent, list becomes impossible without a meaningless name. If you put optional --name on the parent, add can run without the value it needs. Both are design mistakes rather than a Python syntax error. The child parser expresses the correct constraint where it belongs. In the test, print the Namespace so you can inspect which attributes exist for each command.

  8. 04:55

    On a real command line, notes add --name Ada is a sequence of three arguments after the program name. The shell has already split them before argparse sees them. In a unit test or the browser exercise, pass the same sequence explicitly to parse_args. That keeps the test independent of whichever arguments launched the test runner. Error cases are expected to raise SystemExit; catch it only inside the test harness. At PythonLessonLab, work through the matching lesson and run the checked task. Explain the output before changing the code, then make one small change and rerun the boundary case. The result matters more than memorizing a function name: you should know what it

  9. 05:38

    accepts, what it returns, and which failure it prevents.

Understand the concept

A command line with distinct verbs is easier to explain when each verb owns its own arguments. argparse.ArgumentParser.add_subparsers creates child parsers for commands such as add and list. The add command may require --name, while list should accept no name. A single positional action plus a global --name flag cannot express those different requirements cleanly.

Call add_subparsers(dest='command', required=True), then use add_parser for each verb. Add --name with required=True only to the add parser. The parsed Namespace contains command='add' and name='Ada' for add --name Ada; list yields command='list'. The browser task tests parse_args with explicit argument lists, so it does not depend on the process's actual sys.argv.

argparse raises SystemExit after it prints usage for malformed command lines. That is normal for a CLI but surprising inside a reusable function or browser exercise. Keep the parser builder separate from execution; tests can call it with known lists and catch SystemExit for invalid cases. In a real script, parse once at the entry point and dispatch to a handler or use set_defaults(func=...).

The earlier argparse lesson introduces ordinary flags and type conversion. This page answers the longer-tail question of command-specific grammars. Help output, aliases and nested subcommands can be added later, but each extra option should earn its place in the user interface. Keep `list` usable without `--name`, and test that an unknown verb or `add` with no name fails rather than guessing what the user intended.

  • add_subparsers defines command-specific grammar
  • required=True rejects a missing verb
  • Each child parser owns its own options
  • parse_args raises SystemExit for invalid CLI input

See it step by step

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

01. Create two command parsers

PYTHON
import argparse
p=argparse.ArgumentParser(prog='notes')
s=p.add_subparsers(dest='command',required=True)
s.add_parser('add'); s.add_parser('list')
print(p.parse_args(['list']).command)
EXPECTED OUTPUT
list

Each command has its own parser.

02. Require a flag only on add

PYTHON
import argparse
p=argparse.ArgumentParser(prog='notes')
s=p.add_subparsers(dest='command',required=True)
a=s.add_parser('add'); a.add_argument('--name',required=True)
s.add_parser('list')
print(vars(p.parse_args(['add','--name','Ada'])))
EXPECTED OUTPUT
{'command': 'add', 'name': 'Ada'}

The add parser requires a name; list remains simple.

03. Catch an invalid invocation in a test

PYTHON
import argparse,contextlib,io
p=argparse.ArgumentParser(prog='notes')
s=p.add_subparsers(dest='command',required=True)
s.add_parser('list')
with contextlib.redirect_stderr(io.StringIO()):
    try: p.parse_args([])
    except SystemExit as exc: print(exc.code)
EXPECTED OUTPUT
2

A CLI parse error raises SystemExit with a nonzero code.

A closer look

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

01 / 03

Make command verbs explicit

A parser with add and list subcommands models two distinct operations. add_subparsers returns an object on which each add_parser call registers a verb. dest='command' stores the selected name in the parsed Namespace, and required=True makes an omitted verb a usage error.

This first small example passes ['list'] explicitly, so you can run it in the browser without needing a terminal command line. In a local program, argparse normally reads sys.argv[1:] when parse_args is called without a list. Explicit inputs are easier to test and demonstrate.

PYTHON
import argparse
p=argparse.ArgumentParser(prog='notes')
s=p.add_subparsers(dest='command',required=True)
s.add_parser('add'); s.add_parser('list')
print(p.parse_args(['list']).command)
EXPECTED OUTPUT
list
Follow the reasoning
  1. Create the parent ArgumentParser.
  2. Register add and list.
  3. Inspect the command field after parsing list.
02 / 03

Require --name only for add

The add child parser owns --name, with required=True. list has no such option. This expresses the actual command grammar: notes add --name Ada is valid, while notes list is valid without a name. A global --name option would make the relationship between verb and option ambiguous.

The returned Namespace contains both command and name for the add invocation. Downstream logic can dispatch on command or assign a handler with set_defaults(func=...). Keep parsing and side effects separate so an invalid command fails before a file or database changes.

PYTHON
import argparse
p=argparse.ArgumentParser(prog='notes')
s=p.add_subparsers(dest='command',required=True)
a=s.add_parser('add'); a.add_argument('--name',required=True)
s.add_parser('list')
print(vars(p.parse_args(['add','--name','Ada'])))
EXPECTED OUTPUT
{'command': 'add', 'name': 'Ada'}
Follow the reasoning
  1. Create the add child parser.
  2. Require --name there.
  3. Check the parsed Namespace and test list separately.
03 / 03

Test the invalid path without exiting the lesson

argparse writes a usage diagnostic to stderr and raises SystemExit for a bad invocation. The example redirects stderr and catches the exception so the browser lesson continues. An exit code of 2 represents a parse error in this demonstration; do not confuse it with a successful command result.

Test a missing verb, an unknown verb, add without --name, and list with an unexpected --name. Those tests show that child parsers have genuinely different rules. Avoid catching SystemExit around your entire production application and pretending every failure is a recoverable parse error.

PYTHON
import argparse,contextlib,io
p=argparse.ArgumentParser(prog='notes')
s=p.add_subparsers(dest='command',required=True)
s.add_parser('list')
with contextlib.redirect_stderr(io.StringIO()):
    try: p.parse_args([])
    except SystemExit as exc: print(exc.code)
EXPECTED OUTPUT
2
Follow the reasoning
  1. Parse an empty argv list.
  2. Catch the nonzero SystemExit in the test.
  3. Repeat for command-specific invalid forms.

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 turnReplace the shared command and --name arguments with required subparsers. Build add and list commands. Add must require --name; list must not accept it. The prepared run should print {'command': 'add', 'name': 'Ada'}. Missing command, unknown command, and add without a name must fail with SystemExit.
Need a hint?

Use subs=parser.add_subparsers(dest='command',required=True); add=subs.add_parser('add'); add.add_argument('--name',required=True); subs.add_parser('list').

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

    Quick quiz

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

    01.Where should add-only --name be defined?

    02.What does required=True on add_subparsers require?

    03.What does argparse normally do for an invalid command line?

    Typical mistakes

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

    Putting every option on the parent parser

    COMMON MISTAKE
    parser.add_argument('--name')
    FIX
    add_parser.add_argument('--name', required=True)

    What happens: list accepts an irrelevant name and add does not require it.
    Command-specific arguments belong on that child parser.

    Making the command optional

    COMMON MISTAKE
    parser.add_subparsers(dest='command')
    FIX
    parser.add_subparsers(dest='command', required=True)

    What happens: Running notes with no verb can yield command=None.
    Reject incomplete invocations at the grammar boundary.

    Browse this lesson’s problem fixes

    Sources and further reading