The argparse module turns scripts into proper command-line tools. Here's how to use it well.
Basic Structure
import argparse
parser = argparse.ArgumentParser(
description="Process some files.",
epilog="Example: %(prog)s -v input.txt output.txt"
)
# Positional argument
parser.add_argument("input", help="Input file path")
# Optional argument
parser.add_argument("-o", "--output", help="Output file path")
# Flag (boolean)
parser.add_argument("-v", "--verbose", action="store_true")
args = parser.parse_args()
print(args.input, args.output, args.verbose)Argument Types
# Integer
parser.add_argument("-n", "--count", type=int, default=10)
# Float
parser.add_argument("--threshold", type=float, default=0.5)
# File (opens automatically)
parser.add_argument("--config", type=argparse.FileType("r"))
# Choices
parser.add_argument(
"--format",
choices=["json", "csv", "yaml"],
default="json"
)
# Multiple values
parser.add_argument("files", nargs="+", help="One or more files")
parser.add_argument("--tags", nargs="*", help="Zero or more tags")Nargs Options
# Exactly N arguments
parser.add_argument("--point", nargs=2, type=float, metavar=("X", "Y"))
# Zero or one (optional positional)
parser.add_argument("output", nargs="?", default="out.txt")
# Zero or more
parser.add_argument("files", nargs="*")
# One or more
parser.add_argument("files", nargs="+")
# Remainder (everything after)
parser.add_argument("command", nargs=argparse.REMAINDER)Actions
# Store value (default)
parser.add_argument("--name", action="store")
# Boolean flags
parser.add_argument("--verbose", action="store_true")
parser.add_argument("--no-color", action="store_false", dest="color")
# Count occurrences
parser.add_argument("-v", action="count", default=0)
# -v = 1, -vv = 2, -vvv = 3
# Append to list
parser.add_argument("--include", action="append")
# --include a --include b -> ['a', 'b']
# Store constant
parser.add_argument("--debug", action="store_const", const=logging.DEBUG)
# Version
parser.add_argument("--version", action="version", version="%(prog)s 1.0.0")Subcommands
parser = argparse.ArgumentParser(prog="git")
subparsers = parser.add_subparsers(dest="command", required=True)
# 'clone' subcommand
clone_parser = subparsers.add_parser("clone", help="Clone a repository")
clone_parser.add_argument("url")
clone_parser.add_argument("--depth", type=int)
# 'commit' subcommand
commit_parser = subparsers.add_parser("commit", help="Record changes")
commit_parser.add_argument("-m", "--message", required=True)
commit_parser.add_argument("-a", "--all", action="store_true")
args = parser.parse_args()
if args.command == "clone":
print(f"Cloning {args.url}")
elif args.command == "commit":
print(f"Commit: {args.message}")Custom Types
import argparse
from pathlib import Path
def positive_int(value):
"""Custom type that only accepts positive integers."""
ivalue = int(value)
if ivalue <= 0:
raise argparse.ArgumentTypeError(f"{value} is not positive")
return ivalue
def valid_path(value):
"""Custom type that validates path exists."""
path = Path(value)
if not path.exists():
raise argparse.ArgumentTypeError(f"{value} does not exist")
return path
def date_type(value):
"""Parse date string."""
from datetime import datetime
try:
return datetime.strptime(value, "%Y-%m-%d").date()
except ValueError:
raise argparse.ArgumentTypeError(f"Invalid date: {value}")
parser.add_argument("--count", type=positive_int)
parser.add_argument("--file", type=valid_path)
parser.add_argument("--date", type=date_type)Mutually Exclusive Groups
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument("--json", action="store_true")
group.add_argument("--csv", action="store_true")
group.add_argument("--yaml", action="store_true")
# Only one can be specifiedArgument Groups
parser = argparse.ArgumentParser()
# Logical grouping in help text
input_group = parser.add_argument_group("Input options")
input_group.add_argument("--input", "-i")
input_group.add_argument("--encoding")
output_group = parser.add_argument_group("Output options")
output_group.add_argument("--output", "-o")
output_group.add_argument("--format")Default Values from Environment
import os
parser.add_argument(
"--api-key",
default=os.environ.get("API_KEY"),
help="API key (default: $API_KEY)"
)
parser.add_argument(
"--config",
default=os.environ.get("CONFIG_PATH", "~/.config/app.yaml"),
)Help Formatting
parser = argparse.ArgumentParser(
prog="mytool",
description="A useful tool.",
epilog="For more info, see https://example.com",
formatter_class=argparse.RawDescriptionHelpFormatter,
)
# Custom metavar
parser.add_argument(
"--output",
metavar="FILE",
help="Output file"
)
# Hide from help
parser.add_argument("--debug", help=argparse.SUPPRESS)Parent Parsers
# Shared options
parent = argparse.ArgumentParser(add_help=False)
parent.add_argument("--verbose", "-v", action="store_true")
parent.add_argument("--config", type=Path)
# Child parsers inherit options
parser_a = argparse.ArgumentParser(parents=[parent])
parser_a.add_argument("input")
parser_b = argparse.ArgumentParser(parents=[parent])
parser_b.add_argument("--mode")Config File Integration
import argparse
import json
from pathlib import Path
def load_config(config_path):
if config_path and Path(config_path).exists():
return json.loads(Path(config_path).read_text())
return {}
parser = argparse.ArgumentParser()
parser.add_argument("--config", type=Path)
parser.add_argument("--name")
parser.add_argument("--count", type=int)
# Parse config first
args, remaining = parser.parse_known_args()
config = load_config(args.config)
# Set defaults from config
parser.set_defaults(**config)
# Parse again with config defaults
args = parser.parse_args(remaining)Complete Example
#!/usr/bin/env python3
"""File processing tool."""
import argparse
import sys
from pathlib import Path
def create_parser():
parser = argparse.ArgumentParser(
description="Process and transform files.",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
parser.add_argument(
"files",
nargs="+",
type=Path,
help="Files to process"
)
parser.add_argument(
"-o", "--output",
type=Path,
default=Path("output"),
help="Output directory"
)
parser.add_argument(
"-f", "--format",
choices=["json", "csv", "yaml"],
default="json",
help="Output format"
)
parser.add_argument(
"-v", "--verbose",
action="count",
default=0,
help="Increase verbosity (-v, -vv, -vvv)"
)
parser.add_argument(
"--dry-run",
action="store_true",
help="Show what would be done"
)
parser.add_argument(
"--version",
action="version",
version="%(prog)s 1.0.0"
)
return parser
def main():
parser = create_parser()
args = parser.parse_args()
if args.verbose >= 2:
print(f"Arguments: {args}")
for file in args.files:
if not file.exists():
print(f"Error: {file} not found", file=sys.stderr)
sys.exit(1)
if args.verbose:
print(f"Processing: {file}")
if not args.dry_run:
# Process the file
pass
return 0
if __name__ == "__main__":
sys.exit(main())argparse handles the boring parts of CLI building—parsing, validation, help generation—so you can focus on what your tool actually does.
React to this post: