Argparse handles basic CLI parsing, but its advanced features enable professional-grade command-line tools.

Subcommands

Build Git-style CLIs with subcommands:

import argparse
 
def cmd_init(args):
    print(f"Initializing project: {args.name}")
 
def cmd_build(args):
    print(f"Building with {'debug' if args.debug else 'release'} mode")
 
def cmd_deploy(args):
    print(f"Deploying to {args.env}")
 
parser = argparse.ArgumentParser(prog='myapp')
subparsers = parser.add_subparsers(dest='command', help='Commands')
 
# Init command
init_parser = subparsers.add_parser('init', help='Initialize project')
init_parser.add_argument('name', help='Project name')
init_parser.set_defaults(func=cmd_init)
 
# Build command
build_parser = subparsers.add_parser('build', help='Build project')
build_parser.add_argument('--debug', action='store_true')
build_parser.set_defaults(func=cmd_build)
 
# Deploy command
deploy_parser = subparsers.add_parser('deploy', help='Deploy project')
deploy_parser.add_argument('--env', choices=['dev', 'staging', 'prod'], 
                           default='dev')
deploy_parser.set_defaults(func=cmd_deploy)
 
args = parser.parse_args()
if hasattr(args, 'func'):
    args.func(args)
else:
    parser.print_help()

Custom Types with Validation

import argparse
from pathlib import Path
 
def valid_file(path: str) -> Path:
    """Validate file exists."""
    p = Path(path)
    if not p.is_file():
        raise argparse.ArgumentTypeError(f"File not found: {path}")
    return p
 
def valid_dir(path: str) -> Path:
    """Validate directory exists."""
    p = Path(path)
    if not p.is_dir():
        raise argparse.ArgumentTypeError(f"Directory not found: {path}")
    return p
 
def port_number(value: str) -> int:
    """Validate port number range."""
    port = int(value)
    if not 1 <= port <= 65535:
        raise argparse.ArgumentTypeError(f"Port must be 1-65535: {port}")
    return port
 
def positive_int(value: str) -> int:
    """Validate positive integer."""
    n = int(value)
    if n <= 0:
        raise argparse.ArgumentTypeError(f"Must be positive: {n}")
    return n
 
parser = argparse.ArgumentParser()
parser.add_argument('--config', type=valid_file, required=True)
parser.add_argument('--output', type=valid_dir, default='.')
parser.add_argument('--port', type=port_number, default=8080)
parser.add_argument('--workers', type=positive_int, default=4)

Environment Variable Defaults

import argparse
import os
 
class EnvDefault(argparse.Action):
    """Use environment variable as default."""
    def __init__(self, envvar, required=True, default=None, **kwargs):
        if envvar in os.environ:
            default = os.environ[envvar]
        if required and default:
            required = False
        super().__init__(default=default, required=required, **kwargs)
    
    def __call__(self, parser, namespace, values, option_string=None):
        setattr(namespace, self.dest, values)
 
parser = argparse.ArgumentParser()
parser.add_argument(
    '--api-key',
    action=EnvDefault,
    envvar='API_KEY',
    help='API key (or set API_KEY env var)'
)
parser.add_argument(
    '--database-url',
    action=EnvDefault,
    envvar='DATABASE_URL',
    help='Database URL (or set DATABASE_URL env var)'
)

Config File Integration

import argparse
import json
from pathlib import Path
 
def load_config(config_path: str) -> dict:
    """Load JSON config file."""
    return json.loads(Path(config_path).read_text())
 
parser = argparse.ArgumentParser()
parser.add_argument('--config', type=str, help='Config file path')
parser.add_argument('--host', default='localhost')
parser.add_argument('--port', type=int, default=8080)
parser.add_argument('--debug', action='store_true')
 
# Parse args, then override with config
args = parser.parse_args()
 
if args.config:
    config = load_config(args.config)
    # Config values override defaults but not CLI args
    parser.set_defaults(**config)
    args = parser.parse_args()

Mutually Exclusive Arguments

import argparse
 
parser = argparse.ArgumentParser()
 
# Only one of these can be specified
output_group = parser.add_mutually_exclusive_group()
output_group.add_argument('--json', action='store_true', help='JSON output')
output_group.add_argument('--csv', action='store_true', help='CSV output')
output_group.add_argument('--table', action='store_true', help='Table output')
 
# Require at least one
auth_group = parser.add_mutually_exclusive_group(required=True)
auth_group.add_argument('--token', help='Auth token')
auth_group.add_argument('--user', help='Username (prompts for password)')

Argument Groups

Organize help output:

import argparse
 
parser = argparse.ArgumentParser(description='Web server configuration')
 
# Server options
server_group = parser.add_argument_group('Server Options')
server_group.add_argument('--host', default='0.0.0.0')
server_group.add_argument('--port', type=int, default=8080)
server_group.add_argument('--workers', type=int, default=4)
 
# Database options
db_group = parser.add_argument_group('Database Options')
db_group.add_argument('--db-host', default='localhost')
db_group.add_argument('--db-port', type=int, default=5432)
db_group.add_argument('--db-name', required=True)
 
# Logging options
log_group = parser.add_argument_group('Logging Options')
log_group.add_argument('--log-level', choices=['DEBUG', 'INFO', 'WARNING', 'ERROR'])
log_group.add_argument('--log-file', type=str)

Nargs Patterns

import argparse
 
parser = argparse.ArgumentParser()
 
# Exactly N arguments
parser.add_argument('--point', nargs=2, type=float, metavar=('X', 'Y'))
 
# Zero or more
parser.add_argument('--files', nargs='*', default=[])
 
# One or more
parser.add_argument('--tags', nargs='+')
 
# Optional (0 or 1)
parser.add_argument('--config', nargs='?', const='config.json', default=None)
 
# Remaining arguments
parser.add_argument('commands', nargs=argparse.REMAINDER)
 
args = parser.parse_args(['--point', '1.5', '2.5', '--files', 'a.txt', 'b.txt'])
print(args.point)  # [1.5, 2.5]
print(args.files)  # ['a.txt', 'b.txt']

Custom Actions

import argparse
 
class CountAction(argparse.Action):
    """Count occurrences: -v, -vv, -vvv"""
    def __call__(self, parser, namespace, values, option_string=None):
        count = getattr(namespace, self.dest, 0) or 0
        setattr(namespace, self.dest, count + 1)
 
class AppendKeyValue(argparse.Action):
    """Parse key=value pairs into dict."""
    def __call__(self, parser, namespace, values, option_string=None):
        d = getattr(namespace, self.dest, None) or {}
        key, value = values.split('=', 1)
        d[key] = value
        setattr(namespace, self.dest, d)
 
parser = argparse.ArgumentParser()
parser.add_argument('-v', '--verbose', nargs=0, action=CountAction, default=0)
parser.add_argument('-D', '--define', action=AppendKeyValue, default={})
 
args = parser.parse_args(['-vvv', '-D', 'env=prod', '-D', 'debug=true'])
print(args.verbose)  # 3
print(args.define)   # {'env': 'prod', 'debug': 'true'}

Nested Subcommands

import argparse
 
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(dest='command')
 
# db command with its own subcommands
db_parser = subparsers.add_parser('db', help='Database operations')
db_subparsers = db_parser.add_subparsers(dest='db_command')
 
db_migrate = db_subparsers.add_parser('migrate', help='Run migrations')
db_migrate.add_argument('--dry-run', action='store_true')
 
db_seed = db_subparsers.add_parser('seed', help='Seed database')
db_seed.add_argument('--file', required=True)
 
# Usage: myapp db migrate --dry-run
# Usage: myapp db seed --file data.json

Help Formatting

import argparse
 
class CustomFormatter(argparse.RawDescriptionHelpFormatter,
                      argparse.ArgumentDefaultsHelpFormatter):
    """Combined formatter for raw description and defaults."""
    pass
 
parser = argparse.ArgumentParser(
    description='''
My Application
==============
 
A powerful command-line tool for doing things.
 
Examples:
  myapp --input file.txt
  myapp --verbose --output result.json
    ''',
    formatter_class=CustomFormatter,
    epilog='For more info, visit https://example.com'
)
 
parser.add_argument('--input', help='Input file')
parser.add_argument('--output', default='output.txt', help='Output file')

Version Flag

import argparse
 
parser = argparse.ArgumentParser()
parser.add_argument(
    '--version',
    action='version',
    version='%(prog)s 1.0.0'
)

Complete CLI Example

import argparse
import sys
from pathlib import Path
 
def main():
    parser = argparse.ArgumentParser(
        prog='mytool',
        description='Process data files'
    )
    
    parser.add_argument('--version', action='version', version='1.0.0')
    parser.add_argument('-v', '--verbose', action='count', default=0)
    
    subparsers = parser.add_subparsers(dest='command', required=True)
    
    # Process command
    process = subparsers.add_parser('process', help='Process files')
    process.add_argument('input', type=Path, help='Input file')
    process.add_argument('-o', '--output', type=Path, default=Path('output.json'))
    process.add_argument('--format', choices=['json', 'csv'], default='json')
    
    # Validate command
    validate = subparsers.add_parser('validate', help='Validate files')
    validate.add_argument('files', nargs='+', type=Path)
    validate.add_argument('--strict', action='store_true')
    
    args = parser.parse_args()
    
    if args.verbose >= 2:
        print(f"Debug: args = {args}")
    
    if args.command == 'process':
        print(f"Processing {args.input} -> {args.output}")
    elif args.command == 'validate':
        print(f"Validating {len(args.files)} files")
 
if __name__ == '__main__':
    main()

Argparse scales from simple scripts to complex CLIs. Master these patterns to build tools that feel professional and intuitive.

React to this post: