Use default=os.getenv("VAR_NAME") to read from environment. This lets CI/CD pass values via env vars without explicit flags. Users can override with flags. Best of both worlds: defaults from env, explicit control via flags.
Building CLI Tools with argparse
Transform Python scripts into professional command-line tools with subcommands, help text, validation, and user-friendly argument parsing—tools that feel like kubectl, docker, and aws cli.
🧒 Simple Explanation (ELI5)
When you run `docker run --name myapp -e VAR=value myimage`, docker parses all those flags and options. argparse does that for your Python script. Instead of `my_script.py arg1 arg2`, you get `my_script.py deploy --app nginx --replicas 5 --force`. argparse handles parsing, validation, and auto-generates --help text so users know what is available.
🔧 Why Do We Need argparse?
- User experience: scripts feel professional, with --help, validation, helpful error messages.
- Reusability: other team members use your script without reading source code.
- Automation: CI/CD systems call your script with different flags for different environments.
- Documentation: argparse auto-generates help from descriptions—no separate docs needed.
- Validation: enforce types, choices, required arguments before your business logic runs.
⚙️ Technical Explanation
ArgumentParser: main class responsible for parsing. add_argument(): define each argument/flag. parse_args(): parse sys.argv and return Namespace object with values. Subparsers: support commands like `git commit` or `kubectl apply`—different subcommands with different args.
⌨️ argparse Patterns
import argparse
import os
# ===== SIMPLE ARGUMENT PARSER =====
parser = argparse.ArgumentParser(
description="Deploy application to Kubernetes cluster",
epilog="Examples:\n python deploy.py --app nginx --replicas 3\n python deploy.py --app web --env prod"
)
# Positional argument (required)
parser.add_argument("action", help="Action to perform: deploy, scale, restart")
# Optional argument
parser.add_argument("--app", required=True, help="Application name")
parser.add_argument("--replicas", type=int, default=1, help="Number of replicas (default: 1)")
parser.add_argument("--env", choices=["dev", "staging", "prod"], default="dev", help="Environment")
# Boolean flag
parser.add_argument("--force", action="store_true", help="Force deployment even if running")
parser.add_argument("--verbose", "-v", action="store_true", help="Verbose output")
# Parse arguments
args = parser.parse_args()
print(f"Action: {args.action}")
print(f"App: {args.app}")
print(f"Replicas: {args.replicas}")
print(f"Force: {args.force}")
# ===== ADVANCED: SUBCOMMANDS =====
# Like: deploy.py deploy --app nginx vs deploy.py scale --app nginx --replicas 5
main_parser = argparse.ArgumentParser(description="Deployment tool")
subparsers = main_parser.add_subparsers(dest="command", help="Command to execute")
# deploy command
deploy_parser = subparsers.add_parser("deploy", help="Deploy application")
deploy_parser.add_argument("--app", required=True)
deploy_parser.add_argument("--version", required=True)
deploy_parser.add_argument("--env", choices=["dev", "prod"], default="dev")
# scale command
scale_parser = subparsers.add_parser("scale", help="Scale deployment")
scale_parser.add_argument("--app", required=True)
scale_parser.add_argument("--replicas", type=int, required=True)
# status command
status_parser = subparsers.add_parser("status", help="Get deployment status")
status_parser.add_argument("--app", required=True)
# Parse
args = main_parser.parse_args()
if args.command == "deploy":
print(f"Deploying {args.app} version {args.version} to {args.env}")
elif args.command == "scale":
print(f"Scaling {args.app} to {args.replicas} replicas")
elif args.command == "status":
print(f"Checking status of {args.app}")
else:
main_parser.print_help()
# ===== ENVIRONMENT VARIABLE DEFAULTS =====
# Let environment variables provide defaults, but allow flag override
parser = argparse.ArgumentParser()
parser.add_argument(
"--kubeconfig",
default=os.getenv("KUBECONFIG", "~/.kube/config"),
help="Path to kubeconfig file"
)
parser.add_argument(
"--namespace",
default=os.getenv("NAMESPACE", "default"),
help="Kubernetes namespace"
)
args = parser.parse_args()
print(f"Kubeconfig: {args.kubeconfig}")
print(f"Namespace: {args.namespace}")
# ===== TYPE VALIDATION =====
parser = argparse.ArgumentParser()
# Integer type
parser.add_argument("--port", type=int, help="Port number")
# Custom type validation
def positive_int(value):
"""Validate that value is a positive integer."""
try:
ivalue = int(value)
if ivalue <= 0:
raise ValueError(f"{ivalue} is not positive")
return ivalue
except ValueError as e:
raise argparse.ArgumentTypeError(f"Invalid positive integer: {e}")
parser.add_argument("--replicas", type=positive_int, help="Replicas (must be > 0)")
# Parse with validation
try:
args = parser.parse_args(["--port", "8080", "--replicas", "3"])
print(f"Port: {args.port}, Replicas: {args.replicas}")
except argparse.ArgumentTypeError as e:
print(f"Validation error: {e}")
# ===== MUTUALLY EXCLUSIVE ARGUMENTS =====
parser = argparse.ArgumentParser()
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument("--file", help="Read config from file")
group.add_argument("--inline", help="Provide config as JSON string")
# User must provide either --file or --inline, but not both
# ===== ARGUMENT GROUPS =====
parser = argparse.ArgumentParser()
basic_args = parser.add_argument_group("basic options")
basic_args.add_argument("--app", required=True, help="App name")
basic_args.add_argument("--version", required=True, help="Version")
advanced_args = parser.add_argument_group("advanced options")
advanced_args.add_argument("--timeout", type=int, default=300, help="Timeout in seconds")
advanced_args.add_argument("--retries", type=int, default=3, help="Number of retries")
args = parser.parse_args()
# ===== REAL-WORLD EXAMPLE: COMPREHENSIVE CLI TOOL =====
def create_deployment_cli():
"""Create a deployment management CLI."""
parser = argparse.ArgumentParser(
description="Manage Kubernetes deployments",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
python deploy_cli.py deploy --app web --version 2.0 --replicas 5
python deploy_cli.py scale --app web --replicas 10
python deploy_cli.py status --app web --namespace production
python deploy_cli.py health --app web --check readiness
"""
)
# Global options
parser.add_argument(
"--kubeconfig",
default=os.getenv("KUBECONFIG"),
help="Path to kubeconfig"
)
parser.add_argument(
"-v", "--verbose",
action="store_true",
help="Verbose output"
)
# Subcommands
subparsers = parser.add_subparsers(dest="command", required=True)
# deploy command
deploy = subparsers.add_parser("deploy", help="Deploy or update deployment")
deploy.add_argument("--app", required=True, help="Application name")
deploy.add_argument("--version", required=True, help="Image version")
deploy.add_argument("--replicas", type=int, default=3, help="Number of replicas")
deploy.add_argument("--namespace", default="default", help="Kubernetes namespace")
deploy.add_argument("--force", action="store_true", help="Force redeployment")
# scale command
scale = subparsers.add_parser("scale", help="Scale deployment")
scale.add_argument("--app", required=True)
scale.add_argument("--replicas", type=positive_int, required=True)
scale.add_argument("--namespace", default="default")
# status command
status = subparsers.add_parser("status", help="Check deployment status")
status.add_argument("--app", required=True)
status.add_argument("--namespace", default="default")
# health command
health = subparsers.add_parser("health", help="Check pod health")
health.add_argument("--app", required=True)
health.add_argument("--check", choices=["readiness", "liveness"], default="readiness")
return parser
# Usage
def positive_int(value):
try:
ivalue = int(value)
if ivalue <= 0:
raise ValueError(f"{ivalue} is not positive")
return ivalue
except ValueError as e:
raise argparse.ArgumentTypeError(f"Invalid positive int: {e}")
parser = create_deployment_cli()
args = parser.parse_args()
print(f"Command: {args.command}")
if args.command == "deploy":
print(f"Deploying {args.app}:{args.version} with {args.replicas} replicas")
elif args.command == "scale":
print(f"Scaling {args.app} to {args.replicas} replicas")
# ===== PARSE FROM LIST (NOT sys.argv) =====
# Useful for testing
test_args = ["deploy", "--app", "nginx", "--version", "1.20", "--replicas", "5"]
args = parser.parse_args(test_args)
# ===== NAMESPACE OBJECT =====
# parse_args() returns a Namespace object
# Access values as attributes
print(args.app) # 'nginx'
print(args.version) # '1.20'
print(args.replicas) # 5
# Convert to dict if needed
args_dict = vars(args) # {'app': 'nginx', 'version': '1.20', ...}
💼 Example (Real-world Use Case)
A DevOps team builds a tool: `deploy-cli.py deploy --app api --version 2.1 --env prod --replicas 5`. The tool parses arguments, validates (version format, replicas > 0), reads kubeconfig from KUBECONFIG env var (or use --kubeconfig to override), then deploys. Users run `deploy-cli.py --help` to discover options. CI/CD runs `deploy-cli.py deploy --app ... --env prod --force`.
🧪 Hands-on
- Create a simple CLI with positional args and optional flags.
- Add subcommands: create, delete, status (like git or kubectl).
- Implement custom type validation for an argument.
- Use environment variables as defaults for arguments.
- Add mutually exclusive argument groups (--file vs --inline).
Build a CLI tool that: (1) accepts command (deploy, rollback, status), (2) for deploy: app name, version, replicas, (3) validates replicas > 0, (4) shows --help explaining usage, (5) parses from environment variables when available, (6) accepts flag overrides. Run: tool.py deploy --app web --version 2.0 --help
🐛 Debugging Scenario
Problem: argparse shows "unrecognized arguments" error but you are sure you passed the right flag.
- Cause: argument defined after another subcommand, wrong flag name (--replca instead of --replica), or arg belongs to subcommand but passed to main parser.
- Diagnose: run with --help to see available arguments. Check exact spelling. Ensure flags go to right command (after subcommand name, not before).
- Fix: reorganize argument definitions if needed. Use exact flag names. For subcommands, global flags go before subcommand name, subcommand-specific flags go after.
🎯 Interview Questions
Beginner
Positional arguments are required and specified by position (first arg, second arg). Optional arguments use flags (--flag value) and can have defaults/be omitted. Example: `script.py action --option value` has "action" as positional, "--option value" as optional.
Subparsers let you create commands like `git commit` vs `git push`. Each subcommand has its own arguments. The main program routes to the correct subparser based on the command name. Example: deploy deploy ... vs deploy scale ... use different subparsers.
Provide help text to each add_argument() call, and description/epilog to the parser. Use RawDescriptionHelpFormatter to preserve formatting in epilog (examples). Good --help is better documentation than a README—users discover features by reading help.
Scenario-based
Use add_mutually_exclusive_group(required=True). Add both --config and --json to the group. argparse will enforce exactly one is provided and show error if both/neither given.
🌐 Real-world Usage
Every DevOps tool—kubectl, docker, aws cli, terraform—uses CLI argument parsing internally. Helm, Ansible, CI/CD platforms all use similar patterns. DevOps teams build custom tools on top of argparse. Professional tools have professional CLIs built with argparse.
📝 Summary
argparse parses command-line arguments into a Namespace object. Define positional args (required by position) and optional args (--flags). add_argument() accepts type validation (int, custom functions), choices, default values. Use subparsers for multi-command tools (like kubectl). Environment variables provide defaults; flags override. Provide help text for auto-generated --help. Validate arguments early (custom type functions) before business logic. These patterns create professional CLI tools that teams love to use and that integrate cleanly into CI/CD pipelines.