Every script eventually needs arguments. Here's how to handle them properly.

Basic Usage

import argparse
 
parser = argparse.ArgumentParser(description="Process some files")
parser.add_argument("filename", help="File to process")
parser.add_argument("-v", "--verbose", action="store_true", help="Verbose output")
 
args = parser.parse_args()
 
print(f"Processing {args.filename}")
if args.verbose:
    print("Verbose mode enabled")

Usage:

python script.py data.txt
python script.py data.txt --verbose
python script.py --help

Positional vs Optional Arguments

# Positional - required, no dash
parser.add_argument("input", help="Input file")
parser.add_argument("output", help="Output file")
 
# Optional - has dash, not required by default
parser.add_argument("-n", "--number", type=int, help="Number of items")
parser.add_argument("--config", help="Config file path")

Usage:

python script.py input.txt output.txt
python script.py input.txt output.txt -n 10 --config prod.yaml

Argument Types

# String (default)
parser.add_argument("--name", type=str)
 
# Integer
parser.add_argument("-n", "--count", type=int)
 
# Float
parser.add_argument("--threshold", type=float)
 
# File
parser.add_argument("--input", type=argparse.FileType("r"))
parser.add_argument("--output", type=argparse.FileType("w"))

Custom type:

def positive_int(value):
    ivalue = int(value)
    if ivalue <= 0:
        raise argparse.ArgumentTypeError(f"{value} is not a positive integer")
    return ivalue
 
parser.add_argument("--count", type=positive_int)

Default Values

parser.add_argument("--port", type=int, default=8080)
parser.add_argument("--host", default="localhost")
parser.add_argument("--debug", action="store_true")  # Default is False

Required Optional Arguments

parser.add_argument("--api-key", required=True, help="API key (required)")

Choices

parser.add_argument(
    "--format",
    choices=["json", "csv", "xml"],
    default="json",
    help="Output format"
)
 
parser.add_argument(
    "--level",
    type=int,
    choices=range(1, 6),
    help="Level 1-5"
)

Boolean Flags

# Flag - presence means True
parser.add_argument("-v", "--verbose", action="store_true")
parser.add_argument("-q", "--quiet", action="store_true")
 
# Store False when present
parser.add_argument("--no-cache", action="store_false", dest="cache")

Multiple Values

# Fixed number
parser.add_argument("--point", nargs=2, type=float, metavar=("X", "Y"))
 
# Zero or more
parser.add_argument("--tags", nargs="*")
 
# One or more
parser.add_argument("files", nargs="+")
 
# Optional positional
parser.add_argument("output", nargs="?", default="out.txt")

Subcommands

For git-style CLIs:

parser = argparse.ArgumentParser(prog="myapp")
subparsers = parser.add_subparsers(dest="command", required=True)
 
# 'init' command
init_parser = subparsers.add_parser("init", help="Initialize project")
init_parser.add_argument("--template", default="basic")
 
# 'run' command
run_parser = subparsers.add_parser("run", help="Run the project")
run_parser.add_argument("--port", type=int, default=8000)
 
args = parser.parse_args()
 
if args.command == "init":
    print(f"Initializing with template: {args.template}")
elif args.command == "run":
    print(f"Running on port {args.port}")

Usage:

myapp init --template advanced
myapp run --port 3000

Help Text

parser = argparse.ArgumentParser(
    prog="myapp",
    description="My awesome application",
    epilog="Example: myapp --config prod.yaml input.txt"
)
 
parser.add_argument(
    "-c", "--config",
    metavar="FILE",
    help="Configuration file (default: %(default)s)",
    default="config.yaml"
)

Generated help:

usage: myapp [-h] [-c FILE] ...

My awesome application

options:
  -h, --help            show this help message and exit
  -c FILE, --config FILE
                        Configuration file (default: config.yaml)

Example: myapp --config prod.yaml input.txt

Argument Groups

parser = argparse.ArgumentParser()
 
# Group related arguments
server_group = parser.add_argument_group("Server options")
server_group.add_argument("--host", default="localhost")
server_group.add_argument("--port", type=int, default=8080)
 
auth_group = parser.add_argument_group("Authentication")
auth_group.add_argument("--username")
auth_group.add_argument("--password")

Mutually Exclusive

group = parser.add_mutually_exclusive_group()
group.add_argument("-v", "--verbose", action="store_true")
group.add_argument("-q", "--quiet", action="store_true")
# Can't use both --verbose and --quiet

Complete Example

#!/usr/bin/env python3
import argparse
import sys
 
def main():
    parser = argparse.ArgumentParser(
        description="Process data files",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Examples:
  %(prog)s input.csv
  %(prog)s input.csv -o output.json --format json
  %(prog)s input.csv --verbose --dry-run
        """
    )
    
    parser.add_argument("input", help="Input file")
    parser.add_argument("-o", "--output", help="Output file")
    parser.add_argument(
        "-f", "--format",
        choices=["csv", "json", "xml"],
        default="csv",
        help="Output format (default: %(default)s)"
    )
    parser.add_argument("-v", "--verbose", action="store_true")
    parser.add_argument("--dry-run", action="store_true")
    
    args = parser.parse_args()
    
    if args.verbose:
        print(f"Processing {args.input}")
    
    # Do the work...
    
    return 0
 
if __name__ == "__main__":
    sys.exit(main())

Alternatives

Click - decorator-based, more Pythonic:

import click
 
@click.command()
@click.argument("filename")
@click.option("--verbose", is_flag=True)
def cli(filename, verbose):
    click.echo(f"Processing {filename}")

Typer - Click + type hints:

import typer
 
def main(filename: str, verbose: bool = False):
    print(f"Processing {filename}")
 
typer.run(main)

Use argparse for simple scripts, Click/Typer for complex CLIs.

React to this post: