Parse CLI args with argparse
Learn how to parse command-line arguments with argparse in this step-by-step Python tutorial for progressive mastery.
Focus: parse command-line arguments with argparse
Ever written a script that only works when you hardcode a filename or a database URL inside the code? That's a maintenance nightmare for you and a wall for anyone else who wants to reuse your tool. The argparse module is Python's built-in solution for building professional, flexible command-line interfaces (CLIs) that let users pass arguments like --output results.csv or -v, making your scripts robust, shareable, and ready for automation.
The problem this lesson solves
Hardcoded values are the enemy of reusable code. Imagine a Python script that processes a specific file every time:
# data_processor.py — hardcoded path, terrible
import csv
with open('input.csv') as f:
reader = csv.reader(f)
for row in reader:
print(row)
This works only for input.csv. To change the file, you edit the script. To control verbosity, you add a global variable. To set an output path, you guess. This is fragile, unsharable, and completely broken in automation pipelines.
The pain: - Scripts are not composable — you cannot chain them. - You need separate scripts for every variation. - No help message — users must read your code.
Pro tip: Every script that outlives your keyboard should have a CLI.
argparseis the standard library tool for this job since Python 3.2.
Core concept / mental model
argparse turns your Python script into a function with named parameters — but from the command line. Think of it as a declarative specification for what your script accepts (flags, options, positional arguments) and a parser that automatically:
- Extracts values from
sys.argv - Converts strings to the desired type (int, float, file, etc.)
- Validates required vs. optional inputs
- Generates a help message (even docs!)
Key definitions:
- Positional argument: Required by position, usually a value the command acts on (e.g., filename).
- Optional argument / flag: Prefixed with -- or -, may be required or not (e.g., --verbose).
- Parser: The ArgumentParser object that holds the specification.
- Namespace: The parsed result — an object whose attributes are your argument names.
How it works step by step
Building a CLI with argparse is always a three-step dance:
1. Create the parser
import argparse
parser = argparse.ArgumentParser(
description='Process some integers.' # appears in --help
)
2. Add arguments
parser.add_argument('integers', metavar='N', type=int, nargs='+',
help='an integer for the accumulator')
parser.add_argument('--sum', dest='accumulate', action='store_const',
const=sum, default=max,
help='sum the integers (default: find the max)')
3. Parse and use the result
args = parser.parse_args()
print(args.accumulate(args.integers)) # args.integers is a list of ints
What happens under the hood:
1. sys.argv is inspected (e.g., ['program.py', '1', '2', '3', '--sum'])
2. Positional arguments are matched in order, with type coercion and error messages if needed.
3. Optional arguments are matched by prefix; --sum sets args.accumulate = sum.
4. A Namespace object is returned — you access values as attributes.
Hands-on walkthrough
Let's build a real script: a file stat tool that accepts a file, optionally output to JSON, and supports verbose mode.
Minimal example — greet the user
# greet.py
import argparse
parser = argparse.ArgumentParser(description='Greet someone.')
parser.add_argument('name', help='name of the person to greet')
parser.add_argument('--greeting', default='Hello',
help='custom greeting word (default: Hello)')
args = parser.parse_args()
print(f'{args.greeting}, {args.name}!')
Usage:
$ python greet.py Alice
Hello, Alice!
$ python greet.py Alice --greeting Hi
Hi, Alice!
$ python greet.py --help
usage: greet.py [-h] [--greeting GREETING] name
Greet someone.
positional arguments:
name name of the person to greet
optional arguments:
-h, --help show this help message and exit
--greeting GREETING custom greeting word (default: Hello)
File stat tool (advanced)
# filestat.py
import argparse
import os
parser = argparse.ArgumentParser(
description='Print file statistics.'
)
parser.add_argument('file', help='path to the file')
parser.add_argument('--json', action='store_true',
help='output as JSON')
parser.add_argument('--verbose', '-v', action='count', default=0,
help='increase verbosity level')
args = parser.parse_args()
stat = os.stat(args.file)
size = stat.st_size
modified = stat.st_mtime
if args.json:
import json
result = {"file": args.file, "size": size, "modified": modified}
print(json.dumps(result, indent=2))
else:
print(f'File: {args.file}')
print(f'Size: {size} bytes')
print(f'Modified: {modified}')
if args.verbose >= 2:
print(f'Inode: {stat.st_ino}')
print(f'Permissions: {oct(stat.st_mode)}')
Usage:
$ python filestat.py README.md
File: README.md
Size: 1234 bytes
Modified: 1691234567.89
$ python filestat.py README.md --json -vv
{
"file": "README.md",
"size": 1234,
"modified": 1691234567.89
}
Inode: 123456789
Permissions: 0o100644
Adding type checking and choices
# convert.py
import argparse
parser = argparse.ArgumentParser(description='Convert temperature units.')
parser.add_argument('temperature', type=float, help='temperature value')
parser.add_argument('--from', dest='from_unit', choices=['C', 'F', 'K'],
default='C', help='input unit (default: C)')
parser.add_argument('--to', dest='to_unit', choices=['C', 'F', 'K'],
default='F', help='output unit (default: F)')
args = parser.parse_args()
# conversion logic omitted for brevity
print(f"{args.temperature} {args.from_unit} -> {args.to_unit}")
Compare options / when to choose what
argparse is not your only CLI option. Here's how it stacks up against alternatives:
| Library | Pros | Cons | Best for |
|---|---|---|---|
| argparse | Built-in, no dependencies, full-featured, automatic help | Verbose syntax | Standard scripts, libraries, everything in the standard library |
| click | Decorator-based, composable, less boilerplate | External dependency | Complex CLIs with subcommands |
| typer | FastAPI-style, type hints, minimal code | External dependency, newer | Type-safe CLIs with Python 3.6+ |
| sys.argv | No imports, simple | Manual parsing, no help, error-prone | One-off prototypes |
When to choose argparse:
- You need no extra dependencies.
- Your script has 1–10 arguments.
- You want auto-generated --help.
- You work in a CI/CD environment where installing extra packages is undesirable.
Troubleshooting & edge cases
Common mistakes and how to fix them
1. Forgetting default when using nargs='?'
parser.add_argument('--verbose', nargs='?', const=1)
# If you run without --verbose, args.verbose is None, not 0.
# Fix: add default=0
2. Using type=bool — not what you think
parser.add_argument('--debug', type=bool)
# This converts any string to bool. Empty string is False, non-empty is True.
# Use action='store_true' for flags.
3. Positional arguments are always required unless nargs='?'
- If you want an optional positional, use nargs='?' and default.
4. Argument name conflicts with reserved attribute names
- If you store to args.type or args.help, Python gets angry. Use dest='...' to change the attribute name.
5. Parsing partial command lines (testing)
- Use parser.parse_args(['--verbose', '5']) in unit tests instead of mocking sys.argv.
What you learned & what's next
You now know how to parse command-line arguments with argparse: - The mental model of a parser as a declarative spec. - The three-step workflow: create → add → parse. - How to handle positional vs optional arguments. - Type coercion, choices, and flags. - Real-world patterns (file paths, verbosity, JSON output). - How argparse compares to other CLI libraries. - Debugging common pitfalls.
Next step: In the next lesson, you'll learn how to generate and parse configuration files (like JSON, YAML, or TOML), so your scripts can be configured without even typing arguments — perfect for production pipelines.
Practice recap
Mini exercise: Write a script called stats.py that accepts one positional argument (a file path) and two optional flags: --lines (count lines) and --words (count words). If both are given, print both statistics. Use action='store_true' for both flags. Name your script, run it against a test file, and verify --help output is clear.
Common mistakes
- Using
type=boolfor a flag — this converts any string to True; useaction='store_true'oraction='store_false'instead. - Forgetting to add
default=0when usingaction='count'— you'll getNoneinstead of zero on first use. - Mixing positional arguments (always required) with options — if you want an optional positional, use
nargs='?'anddefault. - Accidentally naming an argument that shadows an existing attribute on the Namespace (e.g.,
type,help) — usedestto rename.
Variations
- Use
action='append'to collect multiple flag values into a list (e.g.,--input a --input b). - Use
nargs='+'for one-or-more positional arguments,nargs='*'for zero-or-more. - Combine
argparsewith subparsers to build multi-command CLIs (e.g.,git commit,git push).
Real-world use cases
- A data pipeline script that takes input/output file paths and a
--verboseflag, used in a scheduled cron job. - A web scraping tool that accepts a search term (
--query) and an output format (--jsonor--csv). - A CI/CD deploy script that takes environment (
--env prod) and runs different steps based on optional flags.
Key takeaways
argparseturns command-line strings into Python objects with automatic help and error messages.- Always follow the three-step pattern: create a parser, add arguments, call
parse_args(). - Use
action='store_true'for boolean flags, nottype=bool. - Positional arguments are required by default; use
nargs='?'oroptionsfor optional ones. - Choose argparse over click/typer when you need zero dependencies or a simple, standard CLI.
- You can test argument parsing in isolation using
parser.parse_args(['--flag', 'value']).
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.