Beyond basic argument parsing, argparse offers powerful features for building professional CLI tools.
Subcommands
import argparse
parser = argparse.ArgumentParser(prog='git')
subparsers = parser.add_subparsers(dest='command', help='commands')
# git clone
clone_parser = subparsers.add_parser('clone', help='Clone a repository')
clone_parser.add_argument('url')
clone_parser.add_argument('--depth', type=int)
# git commit
commit_parser = subparsers.add_parser('commit', help='Commit changes')
commit_parser.add_argument('-m', '--message', required=True)
commit_parser.add_argument('-a', '--all', action='store_true')
# git push
push_parser = subparsers.add_parser('push', help='Push changes')
push_parser.add_argument('remote', nargs='?', default='origin')
push_parser.add_argument('branch', nargs='?')
args = parser.parse_args()
if args.command == 'clone':
print(f"Cloning {args.url}")
elif args.command == 'commit':
print(f"Committing: {args.message}")Custom Types
import argparse
from pathlib import Path
from datetime import datetime
def valid_date(s):
try:
return datetime.strptime(s, "%Y-%m-%d")
except ValueError:
raise argparse.ArgumentTypeError(f"Invalid date: {s}")
def existing_file(s):
path = Path(s)
if not path.exists():
raise argparse.ArgumentTypeError(f"File not found: {s}")
return path
def port_number(s):
port = int(s)
if not 1 <= port <= 65535:
raise argparse.ArgumentTypeError(f"Invalid port: {s}")
return port
parser = argparse.ArgumentParser()
parser.add_argument('--date', type=valid_date)
parser.add_argument('--file', type=existing_file)
parser.add_argument('--port', type=port_number, default=8080)Choices with Custom Display
import argparse
class LogLevel:
DEBUG = 10
INFO = 20
WARNING = 30
ERROR = 40
@classmethod
def from_string(cls, s):
levels = {'debug': cls.DEBUG, 'info': cls.INFO,
'warning': cls.WARNING, 'error': cls.ERROR}
if s.lower() not in levels:
raise argparse.ArgumentTypeError(
f"Invalid level. Choose from: {', '.join(levels)}"
)
return levels[s.lower()]
parser = argparse.ArgumentParser()
parser.add_argument(
'--log-level',
type=LogLevel.from_string,
default=LogLevel.INFO,
metavar='LEVEL',
help='Logging level (debug, info, warning, error)'
)Argument Groups
import argparse
parser = argparse.ArgumentParser()
# Group related arguments
auth_group = parser.add_argument_group('authentication')
auth_group.add_argument('--user', '-u')
auth_group.add_argument('--password', '-p')
auth_group.add_argument('--token')
output_group = parser.add_argument_group('output options')
output_group.add_argument('--format', choices=['json', 'csv', 'table'])
output_group.add_argument('--output', '-o', type=argparse.FileType('w'))Mutually Exclusive Arguments
import argparse
parser = argparse.ArgumentParser()
# Only one of these can be used
group = parser.add_mutually_exclusive_group()
group.add_argument('--verbose', '-v', action='store_true')
group.add_argument('--quiet', '-q', action='store_true')
# Required mutual exclusion
auth = parser.add_mutually_exclusive_group(required=True)
auth.add_argument('--user', help='Username for auth')
auth.add_argument('--token', help='API token')Parent Parsers
Share common arguments between subcommands:
import argparse
# Common arguments
parent = argparse.ArgumentParser(add_help=False)
parent.add_argument('--verbose', '-v', action='store_true')
parent.add_argument('--config', '-c', default='config.yml')
# Main parser
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers()
# Inherit from parent
sub1 = subparsers.add_parser('serve', parents=[parent])
sub1.add_argument('--port', type=int, default=8000)
sub2 = subparsers.add_parser('deploy', parents=[parent])
sub2.add_argument('--target', required=True)FileType Arguments
import argparse
parser = argparse.ArgumentParser()
# Input file (or stdin with -)
parser.add_argument('input', type=argparse.FileType('r'), default='-')
# Output file (or stdout with -)
parser.add_argument('--output', '-o', type=argparse.FileType('w'), default='-')
args = parser.parse_args()
# Read and write
data = args.input.read()
args.output.write(data.upper())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='*')
# One or more
parser.add_argument('--tags', nargs='+')
# Optional (0 or 1)
parser.add_argument('--config', nargs='?', const='default.yml')
# Remaining arguments
parser.add_argument('command', nargs=argparse.REMAINDER)Action Classes
import argparse
# Count occurrences
parser.add_argument('-v', '--verbose', 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=True)
# Custom action
class UpperAction(argparse.Action):
def __call__(self, parser, namespace, values, option_string=None):
setattr(namespace, self.dest, values.upper())
parser.add_argument('--name', action=UpperAction)Environment Variable Defaults
import argparse
import os
parser = argparse.ArgumentParser()
parser.add_argument(
'--api-key',
default=os.environ.get('API_KEY'),
help='API key (or set API_KEY env var)'
)
parser.add_argument(
'--port',
type=int,
default=int(os.environ.get('PORT', 8000)),
help='Port (default: $PORT or 8000)'
)Config File Integration
import argparse
import json
from pathlib import Path
def load_config(path):
if path and Path(path).exists():
return json.loads(Path(path).read_text())
return {}
parser = argparse.ArgumentParser()
parser.add_argument('--config', '-c', help='Config file')
parser.add_argument('--host', default='localhost')
parser.add_argument('--port', type=int, default=8000)
# Parse known args first to get config
args, remaining = parser.parse_known_args()
# Load config and set as defaults
if args.config:
config = load_config(args.config)
parser.set_defaults(**config)
# Re-parse with config defaults
args = parser.parse_args()Professional Help Formatting
import argparse
parser = argparse.ArgumentParser(
prog='mytool',
description='A professional CLI tool',
epilog='Examples:\n mytool --verbose process file.txt',
formatter_class=argparse.RawDescriptionHelpFormatter
)
# Argument with detailed help
parser.add_argument(
'--output-format',
choices=['json', 'csv', 'table'],
default='table',
metavar='FORMAT',
help='Output format: json, csv, or table (default: %(default)s)'
)Version Flag
import argparse
parser = argparse.ArgumentParser()
parser.add_argument(
'--version', '-V',
action='version',
version='%(prog)s 1.0.0'
)Complete Example
#!/usr/bin/env python3
import argparse
import sys
def create_parser():
parser = argparse.ArgumentParser(
prog='deploy',
description='Deploy applications to servers'
)
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)
# deploy push
push = subparsers.add_parser('push', help='Push to server')
push.add_argument('target', help='Server to deploy to')
push.add_argument('--branch', '-b', default='main')
push.add_argument('--dry-run', action='store_true')
# deploy status
status = subparsers.add_parser('status', help='Check status')
status.add_argument('--all', '-a', action='store_true')
return parser
def main():
parser = create_parser()
args = parser.parse_args()
if args.command == 'push':
print(f"Deploying {args.branch} to {args.target}")
elif args.command == 'status':
print("Checking status...")
if __name__ == '__main__':
main()Summary
Advanced argparse patterns:
- Subcommands:
add_subparsers()for git-style CLIs - Custom types: Validation with
type=functions - Groups:
add_argument_group()andadd_mutually_exclusive_group() - Parent parsers: Share common arguments
- FileType: Handle stdin/stdout elegantly
- Actions:
count,append, custom actions
Build professional CLIs that users actually enjoy.
React to this post: