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 specified

Argument 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: