Skip to main content

argparse and CLI Design

What You'll Learn

How to build professional command-line tools using Python's built-in argparse module.

Why argparse?

Instead of reading sys.argv manually:

# ❌ Fragile and no help text
import sys
filename = sys.argv[1]
verbose = "--verbose" in sys.argv

Use argparse:

# ✅ Clean, with automatic --help
import argparse
parser = argparse.ArgumentParser(description="Process a file")
parser.add_argument("filename")
parser.add_argument("--verbose", action="store_true")
args = parser.parse_args()

Running python3 script.py --help now prints:

usage: script.py [-h] [--verbose] filename

Process a file

positional arguments:
filename

options:
-h, --help show this help message and exit
--verbose

Positional Arguments

Positional arguments are required and matched by position:

import argparse

parser = argparse.ArgumentParser(description="Rename a file")
parser.add_argument("source", help="Source file path")
parser.add_argument("destination", help="Destination file path")
args = parser.parse_args()

print(f"Moving {args.source}{args.destination}")
python3 rename.py old.txt new.txt

Optional Arguments (Flags)

parser = argparse.ArgumentParser()

# Boolean flag (store_true/store_false)
parser.add_argument("--verbose", "-v", action="store_true",
help="Enable verbose output")

parser.add_argument("--dry-run", action="store_true",
help="Show what would be done without doing it")

# Argument with a value
parser.add_argument("--output", "-o", default="output.txt",
help="Output file (default: output.txt)")

parser.add_argument("--count", "-n", type=int, default=10,
help="Number of results (default: 10)")

# Required option (rare — prefer positional for required args)
parser.add_argument("--config", required=True,
help="Path to config file")

args = parser.parse_args()
print(args.verbose) # True or False
print(args.output) # "output.txt" or user's value
print(args.count) # 10 or user's value

Argument Types and Choices

from pathlib import Path

parser.add_argument("--level", type=int, choices=[1, 2, 3],
default=1, help="Verbosity level (1-3)")

parser.add_argument("--format", choices=["json", "csv", "yaml"],
default="json", help="Output format")

parser.add_argument("input", type=Path,
help="Input file path") # auto-converts to Path

args = parser.parse_args()
# args.input is already a Path object

Accepting Multiple Values (nargs)

# One or more values
parser.add_argument("files", nargs="+", help="Files to process")

# Zero or more values
parser.add_argument("--tags", nargs="*", default=[])

# Exactly N values
parser.add_argument("--range", nargs=2, type=int, metavar=("START", "END"))

args = parser.parse_args()
# args.files = ["a.txt", "b.txt"]
# args.range = [1, 100]

Subcommands (like git or docker)

import argparse

parser = argparse.ArgumentParser(description="User management tool")
subparsers = parser.add_subparsers(dest="command", required=True)

# 'create' subcommand
create = subparsers.add_parser("create", help="Create a new user")
create.add_argument("username", help="Username to create")
create.add_argument("--admin", action="store_true")

# 'delete' subcommand
delete = subparsers.add_parser("delete", help="Delete a user")
delete.add_argument("username")
delete.add_argument("--force", action="store_true")

# 'list' subcommand
list_cmd = subparsers.add_parser("list", help="List all users")
list_cmd.add_argument("--active-only", action="store_true")

args = parser.parse_args()

if args.command == "create":
print(f"Creating user: {args.username}, admin={args.admin}")
elif args.command == "delete":
print(f"Deleting user: {args.username}, force={args.force}")
elif args.command == "list":
print(f"Listing users, active_only={args.active_only}")

Usage:

python3 users.py create alice --admin
python3 users.py delete bob --force
python3 users.py list --active-only
python3 users.py --help
python3 users.py create --help

Full Example: A Real CLI Tool

#!/usr/bin/env python3
"""Count lines, words, and characters in files."""

import argparse
import sys
from pathlib import Path


def count_file(path: Path) -> dict:
try:
text = path.read_text(encoding="utf-8")
except (OSError, UnicodeDecodeError) as e:
print(f"Error reading {path}: {e}", file=sys.stderr)
return {}

return {
"lines": text.count("\n"),
"words": len(text.split()),
"chars": len(text),
"path": str(path),
}


def main() -> int:
parser = argparse.ArgumentParser(
description="Count lines, words, and chars in files"
)
parser.add_argument("files", nargs="+", type=Path, help="Files to count")
parser.add_argument("--lines", "-l", action="store_true",
help="Show line count only")
parser.add_argument("--words", "-w", action="store_true",
help="Show word count only")
args = parser.parse_args()

total = {"lines": 0, "words": 0, "chars": 0}
errors = 0

for path in args.files:
if not path.exists():
print(f"File not found: {path}", file=sys.stderr)
errors += 1
continue

counts = count_file(path)
if not counts:
errors += 1
continue

if args.lines:
print(f"{counts['lines']:>8} {path}")
elif args.words:
print(f"{counts['words']:>8} {path}")
else:
print(f"{counts['lines']:>8} {counts['words']:>8} {counts['chars']:>8} {path}")

for key in ("lines", "words", "chars"):
total[key] += counts.get(key, 0)

if len(args.files) > 1:
print(f"{total['lines']:>8} {total['words']:>8} {total['chars']:>8} total")

return 1 if errors else 0


if __name__ == "__main__":
sys.exit(main())

Common Mistakes

MistakeFix
Using positional for optional argsOptional args use -- prefix
No help= stringsAlways add help text to every argument
Forgetting type=intString by default — cast with type=
No if __name__ == "__main__":Always guard the entry point
Not validating args after parseValidate paths, ranges, combinations

Quick Reference

import argparse
import sys

parser = argparse.ArgumentParser(description="What this does")

# Positional (required)
parser.add_argument("name", help="Description")

# Optional flag
parser.add_argument("--verbose", "-v", action="store_true")
parser.add_argument("--dry-run", action="store_true")

# Optional with value
parser.add_argument("--output", "-o", default="out.txt")
parser.add_argument("--count", type=int, default=10)

# Choices
parser.add_argument("--format", choices=["json", "csv"])

# Multiple values
parser.add_argument("files", nargs="+", type=Path)

# Subcommands
sub = parser.add_subparsers(dest="command", required=True)
sub.add_parser("run")
sub.add_parser("stop")

args = parser.parse_args()

What's Next

Lesson 4: CSV and Tabular Files