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
Jump to a chapter
Read the video transcript
Select a timestamp to watch that moment on YouTube.
- 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.
- 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'.
- 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.
- 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
- 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.
- 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.
- 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.
- 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
- 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
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)listEach command has its own parser.
02. Require a flag only on add
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']))){'command': 'add', 'name': 'Ada'}The add parser requires a name; list remains simple.
03. Catch an invalid invocation in a test
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)2A 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.
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.
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)list- Create the parent ArgumentParser.
- Register add and list.
- Inspect the command field after parsing list.
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.
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']))){'command': 'add', 'name': 'Ada'}- Create the add child parser.
- Require --name there.
- Check the parsed Namespace and test list separately.
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.
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)2- Parse an empty argv list.
- Catch the nonzero SystemExit in the test.
- 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 runNeed input()? Add one value per line
Your output appears here.
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.
Typical mistakes
Everyone meets these errors. See what causes them and how to fix them.
Putting every option on the parent parser
parser.add_argument('--name')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
parser.add_subparsers(dest='command')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.