The command line is where a lot of real work happens. If you have ever found yourself re-running the same Python script over and over, changing a filename or a threshold by editing the code each time, you are ready to learn argparse. It is Python's standard-library module for building friendly, professional command-line interfaces, and it ships with every Python installation, so there is nothing extra to install.
In this tutorial you will learn how to turn a plain script into a proper CLI tool with arguments, flags, defaults, subcommands, and helpful error messages. Everything is built with examples you can run immediately.
Why argparse instead of manual parsing?
Before argparse existed, many developers parsed arguments by hand:
import sys
def main():
args = sys.argv[1:]
if not args:
print("usage: script.py FILENAME")
sys.exit(1)
filename = args[0]
print(f"Processing {filename}")
That works for exactly one case. The moment you need optional flags, typed values, a --version flag, or validation, hand-rolled parsing becomes fragile. argparse gives you all of this out of the box:
- Automatic
-h/--helptext - Type conversion (
int,float, file paths) - Default values
- Choices and validation
- Subcommands, like
git commitandgit push
Your first parser
Here is the simplest possible version:
import argparse
parser = argparse.ArgumentParser(description="Process a data file.")
parser.add_argument("filename", help="path to the input file")
args = parser.parse_args()
print(f"Input file: {args.filename}")
Save it as demo.py and run it:
$ python demo.py sales.csv
Input file: sales.csv
$ python demo.py
usage: demo.py [-h] filename
demo.py: error: the following arguments are required: filename
$ python demo.py -h
usage: demo.py [-h] filename
Process a data file.
positional arguments:
filename path to the input file
You got help text, a clear error, and a non-zero exit code for free. Notice the argument name becomes an attribute automatically: --filename becomes args.filename.
Positional vs. optional arguments
Arguments come in two flavors:
| Kind | Example | Required? | Access |
|---|---|---|---|
| Positional | parser.add_argument("filename") |
Yes, unless nargs="?"
|
args.filename |
| Optional flag | parser.add_argument("--verbose") |
No |
args.verbose (True/False) |
| Optional with value | parser.add_argument("--limit", type=int) |
No |
args.limit or default |
| Short flag | parser.add_argument("-o", "--output") |
No | args.output |
A common pattern is an input file plus optional tuning flags:
parser.add_argument("input", help="input CSV file")
parser.add_argument("-o", "--output", help="write result to this file")
parser.add_argument("-n", "--limit", type=int, default=0,
help="only process the first N rows (0 = all)")
parser.add_argument("-v", "--verbose", action="store_true",
help="print extra detail while working")
action="store_true" is how you make a simple on/off switch: if the user passes -v, args.verbose is True, otherwise False.
Type conversion and validation
By default argparse treats everything as a string. Passing type=int converts the value and rejects bad input automatically:
$ python demo.py data.csv --limit abc
usage: demo.py [-h] input [-o OUTPUT] [-n LIMIT] [-v]
demo.py: error: argument -n/--limit: invalid int value: 'abc'
You can also restrict values with choices:
parser.add_argument("--mode", choices=["fast", "safe", "test"],
default="safe", help="processing mode")
And enforce sensible ranges with a small helper function:
def positive_int(value):
try:
ivalue = int(value)
except ValueError:
raise argparse.ArgumentTypeError(f"{value!r} is not an integer")
if ivalue <= 0:
raise argparse.ArgumentTypeError(f"{value!r} must be positive")
return ivalue
parser.add_argument("--workers", type=positive_int, default=1)
When your validation raises ArgumentTypeError, argparse formats the message consistently with its own errors. Your users get one uniform style of feedback instead of a confusing traceback.
Reading files safely
A nice trick is type=argparse.FileType("r"). It opens the file for you and produces a clear error if the file does not exist:
parser.add_argument("input", type=argparse.FileType("r"),
help="input CSV file")
args = parser.parse_args()
for line in args.input:
print(line.strip())
Because FileType checks the file immediately, a typo in the filename fails fast with a clean message instead of halfway through your processing loop.
Using defaults so the tool works out of the box
Users appreciate a tool that runs with zero arguments. Give every optional flag a sensible default:
parser.add_argument("--output", default="report.txt")
parser.add_argument("--encoding", default="utf-8")
parser.add_argument("--max-errors", type=int, default=10)
Now python tool.py data.csv is a complete, working command. Flags exist for the moments when the default is not what the user needs. This pattern matches how polished tools like git and curl behave: simple by default, configurable when required.
Subcommands for multi-purpose tools
Once a script grows past one job, split it into subcommands. This is the structure behind git commit, pip install, and npm run:
parser = argparse.ArgumentParser(description="CSV toolbox")
sub = parser.add_subparsers(dest="command", required=True,
help="available commands")
# validate subcommand
p_validate = sub.add_parser("validate", help="check a CSV for issues")
p_validate.add_argument("input", help="CSV file to check")
# summarize subcommand
p_summary = sub.add_parser("summary", help="print column statistics")
p_summary.add_argument("input", help="CSV file to analyze")
p_summary.add_argument("--column", default=None, help="column to focus on")
args = parser.parse_args()
if args.command == "validate":
print(f"Validating {args.input}")
elif args.command == "summary":
print(f"Summarizing {args.input}, column={args.column}")
Each subcommand gets its own help screen:
$ python tool.py summary -h
usage: tool.py summary [-h] [--column COLUMN] input
...
With required=True on add_subparsers, running tool.py with no command prints a useful error telling the user which commands exist.
A complete example: CSV column counter
Here is everything combined into one small, practical tool that counts rows per value in a chosen CSV column:
import argparse
import csv
from collections import Counter
def positive_int(value):
ivalue = int(value)
if ivalue <= 0:
raise argparse.ArgumentTypeError("must be positive")
return ivalue
def main():
parser = argparse.ArgumentParser(
description="Count occurrences of each value in a CSV column.")
parser.add_argument("input", type=argparse.FileType("r"),
help="input CSV file")
parser.add_argument("--column", required=True,
help="column name to count")
parser.add_argument("--top", type=positive_int, default=10,
help="show only the top N values (default: 10)")
parser.add_argument("-v", "--verbose", action="store_true",
help="print row counts while reading")
args = parser.parse_args()
reader = csv.DictReader(args.input)
if args.column not in reader.fieldnames:
parser.error(f"column {args.column!r} not found "
f"(available: {', '.join(reader.fieldnames)})")
counts = Counter()
total = 0
for row in reader:
counts[row[args.column]] += 1
total += 1
if args.verbose and total % 1000 == 0:
print(f"... {total} rows read")
print(f"\nTotal rows: {total}")
print(f"Distinct values in {args.column!r}: {len(counts)}\n")
for value, count in counts.most_common(args.top):
print(f"{count:8d} {value}")
if __name__ == "__main__":
main()
Run it against any CSV file:
$ python count_column.py orders.csv --column country --top 3
Total rows: 1250
Distinct values in 'country': 27
412 Germany
389 France
205 Spain
If the user names a column that does not exist, parser.error exits with a clear message and exit code 2, which is exactly what shell scripts expect when they test for failure.
Testing a parser-based tool
Because parsing is separated from processing, you can test the pieces. argparse even lets you feed in fake arguments instead of the real sys.argv:
def parse_args(argv=None):
parser = argparse.ArgumentParser()
parser.add_argument("input")
parser.add_argument("--limit", type=int, default=0)
return parser.parse_args(argv)
def test_parse_args():
args = parse_args(["data.csv", "--limit", "5"])
assert args.input == "data.csv"
assert args.limit == 5
def test_default_limit():
args = parse_args(["data.csv"])
assert args.limit == 0
Keeping parse_args separate from main is the single most useful habit for testable command-line tools. You can now exercise every flag combination without ever spawning a subprocess.
Practical tips summary
- Use
type=int(or a small validator function) instead of casting strings later. - Give every optional argument a sensible default so the tool runs with a bare command.
- Separate
parse_args(argv=None)from the processing logic so it is testable. - Use
argparse.FileTypefor file arguments to get clean errors early. - Use subcommands once a script handles more than one job.
- Check
choicesandparser.error()for validation that reads naturally. - The
-hflag is automatic; write help strings that complete the sentence "the value for this argument is ...".
Next steps
Try converting one of your existing scripts: take the values you currently edit at the top of the file, and promote them to command-line arguments with defaults. You will immediately get a tool you can hand to a colleague or call from a shell script, and you will never edit a filename inside the code again. argparse is one of those standard-library modules that quietly makes everything around it more professional, and it is a great foundation before reaching for heavier frameworks when your interface eventually grows beyond what a single parser can express.
Top comments (0)