Argument Parsing with argparse
argparse reads what you type after the program name and turns it into Python variables. You describe the arguments your program expects (a title, a body, an optional author name), and argparse handles validation, error messages, and help text automatically.
Standard library argparse, no external dependencies. Type-safe argument parsing with auto-generated --help. This lesson covers ArgumentParser, add_argument for positional and optional args, type=, default=, and choices=. Subcommands come in Lesson 3.
In Lesson 1, James wrote a manual command dispatcher with sys.argv. Four commands, 40 lines of if/elif branches, no help text, no type checking. Every new command made it worse.
Emma opens a fresh file. "The standard library includes a module called argparse. You describe what arguments your program accepts. argparse parses them, validates them, and generates help text. No more manual index checking."
"Is it complicated to set up?"
"Four lines for a basic parser. Watch."
Your First ArgumentParser
Create a file called greet.py:
import argparse
def main() -> None:
parser = argparse.ArgumentParser(description="Greet someone by name")
parser.add_argument("name", help="The name to greet")
args = parser.parse_args()
print(f"Hello, {args.name}!")
if __name__ == "__main__":
main()
Run it:
uv run python greet.py Alice
Output:
Hello, Alice!
Four things happened:
ArgumentParser()created a parser objectadd_argument("name")told the parser to expect one positional argumentparse_args()readsys.argv, matched"Alice"to thenameargument, and returned aNamespaceobjectargs.nameaccessed the parsed value
What if you forget the argument?
uv run python greet.py
Output:
usage: greet.py [-h] name
greet.py: error: the following arguments are required: name
Compare that to the sys.argv version from Lesson 1, where forgetting an argument would either crash with an IndexError or print your hand-written error message. argparse tells the user exactly which argument is missing and shows the correct usage pattern.
Positional vs Optional Arguments
Arguments come in two flavors:
| Type | Syntax | Required? | Example |
|---|---|---|---|
| Positional | add_argument("title") | Yes (by default) | smartnotes add "Python Tips" |
| Optional | add_argument("--author") | No (by default) | smartnotes add "Tips" --author Emma |
The -- prefix is the signal. Arguments without -- are positional (required, matched by position). Arguments with -- are optional (matched by name).
Build a parser for the SmartNotes add command:
import argparse
def main() -> None:
parser = argparse.ArgumentParser(
description="Add a note to SmartNotes"
)
parser.add_argument("title", help="The note title")
parser.add_argument("body", help="The note body text")
parser.add_argument(
"--author",
default="Anonymous",
help="The note author (default: Anonymous)",
)
args = parser.parse_args()
print(f"Title: {args.title}")
print(f"Body: {args.body}")
print(f"Author: {args.author}")
if __name__ == "__main__":
main()
Run it with all three arguments:
uv run python add_note.py "Python Tips" "My first note" --author Emma
Output:
Title: Python Tips
Body: My first note
Author: Emma
Run it without the optional --author:
uv run python add_note.py "Python Tips" "My first note"
Output:
Title: Python Tips
Body: My first note
Author: Anonymous
The default="Anonymous" kicks in when --author is not provided. Positional arguments have no default; omitting them is an error:
uv run python add_note.py "Python Tips"
Output:
usage: add_note.py [-h] [--author AUTHOR] title body
add_note.py: error: the following arguments are required: body
argparse names the missing argument. No manual checking required.
Type Safety and Defaults
By default, every argument is a string. If you need a number, pass type=int:
import argparse
def main() -> None:
parser = argparse.ArgumentParser(description="List recent notes")
parser.add_argument(
"--limit",
type=int,
default=10,
help="Maximum notes to display (default: 10)",
)
parser.add_argument(
"--format",
choices=["json", "csv", "md"],
default="md",
help="Output format (default: md)",
)
args = parser.parse_args()
print(f"Limit: {args.limit} (type: {type(args.limit).__name__})")
print(f"Format: {args.format}")
if __name__ == "__main__":
main()
Run it normally:
uv run python list_notes.py --limit 5 --format json
Output:
Limit: 5 (type: int)
Format: json
The limit value is an int, not a string. argparse converted it. Now try passing a non-integer:
uv run python list_notes.py --limit abc
Output:
usage: list_notes.py [-h] [--limit LIMIT] [--format {json,csv,md}]
list_notes.py: error: argument --limit: invalid int value: 'abc'
And try an invalid format choice:
uv run python list_notes.py --format xml
Output:
usage: list_notes.py [-h] [--limit LIMIT] [--format {json,csv,md}]
list_notes.py: error: argument --format: invalid choice: 'xml' (choose from 'json', 'csv', 'md')
argparse rejects bad input with clear messages. Compare this to the sys.argv version: you would need to write every one of these checks manually.
| Feature | sys.argv (manual) | argparse |
|---|---|---|
| Type validation | You write int(sys.argv[n]) wrapped in try/except | type=int handles it |
| Allowed values | You write if value not in ["json","csv","md"] | choices=[...] handles it |
| Default values | You write value = sys.argv[n] if len(sys.argv) > n else "md" | default="md" handles it |
| Error messages | You write print("Invalid format") | Detailed message with usage pattern |
Auto-Generated Help
Every ArgumentParser gets --help for free:
uv run python add_note.py --help
Output:
usage: add_note.py [-h] [--author AUTHOR] title body
Add a note to SmartNotes
positional arguments:
title The note title
body The note body text
options:
-h, --help show this help message and exit
--author AUTHOR The note author (default: Anonymous)
The help text comes from three sources:
| Source | Where it appears |
|---|---|
description= in ArgumentParser() | Below the usage line |
help= in add_argument() | Next to each argument |
| Argument names and defaults | Automatically formatted |
You never write a help function. You describe your arguments with help= strings, and argparse assembles the help page. If you add a new argument, the help updates automatically.
Run the list command with --help:
uv run python list_notes.py --help
Output:
usage: list_notes.py [-h] [--limit LIMIT] [--format {json,csv,md}]
List recent notes
options:
-h, --help show this help message and exit
--limit LIMIT Maximum notes to display (default: 10)
--format {json,csv,md}
Output format (default: md)
Notice how {json,csv,md} appears automatically from the choices= parameter. Users can see their options without reading your code.
PRIMM-AI+ Practice: Predict the Parse
Predict [AI-FREE]
Press Shift+Tab to enter Plan Mode.
Given this parser:
parser = argparse.ArgumentParser()
parser.add_argument("action")
parser.add_argument("target")
parser.add_argument("--count", type=int, default=1)
parser.add_argument("--dry-run", action="store_true")
What does parse_args() return for each command? Write the attribute values on paper.
# Command A
uv run python tool.py deploy server --count 3
# Command B
uv run python tool.py deploy server --dry-run
# Command C
uv run python tool.py deploy
Rate your confidence from 1 to 5.
Check your predictions
# Command A
# args.action = "deploy", args.target = "server", args.count = 3, args.dry_run = False
# Command B
# args.action = "deploy", args.target = "server", args.count = 1, args.dry_run = True
# Command C
# Error: the following arguments are required: target
Two details to notice. First, --dry-run becomes args.dry_run (the hyphen turns into an underscore). Second, action="store_true" means the flag is False by default and True when present. No value needed after the flag.
Run
Press Shift+Tab to exit Plan Mode.
Create tool.py with the parser above, print all four attributes, and run each command. Compare to your predictions.
Investigate
Run /investigate @tool.py in Claude Code and ask: "What does action='store_true' do, and how is it different from type=bool? Why would type=bool give surprising results?"
The answer reveals a common argparse pitfall: type=bool converts the string "False" to True because bool("False") is True (any non-empty string is truthy). store_true avoids this entirely.
Modify
Add two new optional arguments to your parser:
--verbosewithaction="store_true"(a flag, no value)--outputwithtype=stranddefault="result.txt"(a string with a default)
Run --help and confirm both appear. Run the tool with and without each flag and verify the defaults.
Make [Mastery Gate]
Write a complete parser for the SmartNotes add command using /tdg in Claude Code:
The parser should accept:
title(positional, required)body(positional, required)--author(optional, default"Anonymous")--tags(optional, accepts multiple values withnargs="+", which means "one or more arguments collected into a list")
Write the stub, write tests that call parse_args() with different argument lists, generate the implementation, and verify. Your tests should cover: both positional args provided, --author overriding the default, --tags collecting multiple values, and missing required arguments raising SystemExit.
Try With AI
If Claude Code is not already running, open your terminal, navigate to your SmartNotes project folder, and type claude. If you need a refresher, Chapter 44 covers the setup.
Prompt 1: Connect to SmartNotes
Here is my argparse parser for the smartnotes add command:
[paste your parser from the Mastery Gate]
Now connect it to the actual SmartNotes package. When the
user runs the command, it should create a Note object and
save it using save_note from smartnotes.storage. Show me
the complete function with types.
What you're learning: A parser is only half the tool. The other half connects parsed arguments to your library's functions. You are practicing the library-to-command pattern: parse arguments, create domain objects, call library functions.
Prompt 2: Short Flags
My argparse parser uses --author and --tags as long flags.
How do I add short flags like -a for --author and -t for
--tags? Show me the add_argument syntax and explain what
happens if both -a and --author are used in the same command.
What you're learning: Professional CLI tools use both short (-a) and long (--author) flags. argparse supports this natively. Understanding flag conventions makes your tools feel familiar to users who already know Unix command-line patterns.
Prompt 3: Test Argument Parsing
/tdg
Use the TDG workflow to write a function build_add_parser() -> argparse.ArgumentParser that returns a configured parser for the add command. Write tests that exercise parse_args() with different argument combinations, including edge cases like empty titles and missing required arguments.
What you're learning: Testing argument parsers separately from business logic is a separation of concerns: the parser handles input validation, the business logic handles operations. You can test each independently.
James runs uv run python add_note.py --help and reads the auto-generated output. "Like the warehouse label printer," he says. "You describe the product: name, weight, destination, fragile flag. The printer makes the label. You do not design the label layout yourself."
Emma laughs. "That is a good analogy. You describe the interface. argparse builds the parser, the help text, and the error messages." She pauses, then frowns. "Actually, I misspoke earlier. I said argparse 'handles everything.' It does not handle subcommands out of the box. Not without one more piece."
"Subcommands?"
"Right now your tool is add_note.py. A single command. But SmartNotes needs add, search, list, and export. Four commands in one tool. That is what add_subparsers does. It is not hard, but it is a different pattern."
James looks at his parser code. Positional arguments, optional flags, type validation, auto-generated help. All of that for describing a single command. "So Lesson 3 is about combining multiple commands into one tool?"
"Exactly."