Skip to main content

Argument Parsing with argparse

If you're new to programming

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.

If you've coded before

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:

  1. ArgumentParser() created a parser object
  2. add_argument("name") told the parser to expect one positional argument
  3. parse_args() read sys.argv, matched "Alice" to the name argument, and returned a Namespace object
  4. args.name accessed 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:

TypeSyntaxRequired?Example
Positionaladd_argument("title")Yes (by default)smartnotes add "Python Tips"
Optionaladd_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.

Featuresys.argv (manual)argparse
Type validationYou write int(sys.argv[n]) wrapped in try/excepttype=int handles it
Allowed valuesYou write if value not in ["json","csv","md"]choices=[...] handles it
Default valuesYou write value = sys.argv[n] if len(sys.argv) > n else "md"default="md" handles it
Error messagesYou 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:

SourceWhere it appears
description= in ArgumentParser()Below the usage line
help= in add_argument()Next to each argument
Argument names and defaultsAutomatically 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:

  1. --verbose with action="store_true" (a flag, no value)
  2. --output with type=str and default="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 with nargs="+", 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

Opening Claude Code

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."