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.jsonHelp 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: