Build a DevOps CLI with argparse
Create a DevOps CLI with argparse — Python for DevOps automation tutorial, lesson 32.
Focus: create a devops cli with argparse
You've just SSH'd into a box at 2 AM, and the runbook tells you to grep a log file, restart a service, and tail the output — but you're tired of memorizing the exact flags every time. That's the pain: DevOps life is a stream of repetitive, error-prone shell commands. A custom CLI fixes that by turning your operational knowledge into a single, documented, repeatable tool. In this lesson, you'll create a DevOps CLI with argparse — Python's built-in command-line parser — so your teammates (and future you) can run deploy --env prod --dry-run instead of a fragile five-step process.
The problem this lesson solves
Every day as a DevOps engineer, you execute the same sequences: check health endpoints, tail logs, restart services, roll back deployments. Doing this with raw shell commands leads to three problems:
- Memory overload — you can't remember if it's
--namespaceor-nfor every tool. - Human error — one wrong flag and you've restarted production instead of staging.
- No guardrails — no validation, no
--dry-run, no help text.
A well-built CLI gives you:
- Consistency — one command for a multi-step process.
- Safety — required arguments, type checking, and dry-run modes.
- Discoverability —
--helpshows exactly what the tool does.
This lesson shows you how to create a DevOps CLI with argparse that you'll actually use in your daily automation.
Core concept / mental model
argparse is Python's built-in module for creating user-friendly command-line interfaces. Think of it as the front desk of your script: it greets the user, takes their request (arguments), validates it against a list of rules you define, and hands the validated data to your backend functions.
A CLI has three layers:
- Parser — reads
sys.argvand defines what arguments are allowed. - Validation — argparse enforces required vs optional, types, choices, and counters.
- Action — your Python function that performs the DevOps task using the parsed arguments.
Here's the mental model in a single image (in words):
User input (--env prod --dry-run) → argparse parser (rules) → Namespace object → your function
The Namespace is a simple object with attributes named after your arguments. That's it — clean, predictable, and testable.
How it works step by step
Creating a CLI with argparse follows a repeatable recipe. Master these five steps and you can build a CLI for any operation.
Step 1: Create the parser
Instantiate ArgumentParser with a description — this becomes the help text.
Step 2: Add arguments
For every piece of input the user needs, add an argument. Decide:
- Positional (e.g.,
service_name) — required, no flag. - Optional (e.g.,
--env) — flag, may have a default. - Type —
int,str,float, or even a custom function. - Choices — restrict to a list of allowed values (e.g.,
['dev', 'staging', 'prod']). - Action —
store_truefor flags like--verboseor--dry-run. - Count — for counting occurrences like
-vfor verbosity.
Step 3: Parse arguments
Call parser.parse_args() which reads sys.argv, validates, and returns a Namespace.
Step 4: Use the arguments
Pass the namespace (or its attributes) to your business logic functions.
Step 5: Handle errors gracefully
argparse automatically prints usage and exits with code 2 when validation fails. For extra polish, catch SystemExit in tests.
Hands-on walkthrough
Let's build a real-world svcctl CLI — a service controller that checks health, restarts, and rolls back a deployment. It uses the concepts above in a practical way.
Example 1: Minimal health-check CLI
import argparse
import json
def check_health(service: str, env: str) -> dict:
# Simulated health check
return {"service": service, "env": env, "status": "OK"}
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Check service health")
parser.add_argument("service", help="Service name (e.g., web-api)")
parser.add_argument("--env", choices=["dev", "staging", "prod"], default="dev",
help="Environment to check")
args = parser.parse_args()
result = check_health(args.service, args.env)
print(json.dumps(result, indent=2))
Run it:
$ python healthcheck.py web-api --env prod
{
"service": "web-api",
"env": "prod",
"status": "OK"
}
Example 2: Add --verbosity with count and --dry-run
import argparse
import logging
def restart_service(service: str, env: str, dry_run: bool) -> None:
if dry_run:
logging.info("[DRY RUN] Would restart %s in %s", service, env)
return
# Real restart logic
logging.info("Restarting %s in %s...", service, env)
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Restart a service")
parser.add_argument("service")
parser.add_argument("--env", choices=["dev", "staging", "prod"], default="dev")
parser.add_argument("-v", "--verbose", action="count", default=0,
help="Increase verbosity (use -vv for debug)")
parser.add_argument("--dry-run", action="store_true",
help="Show what would happen without doing it")
args = parser.parse_args()
if args.verbose >= 2:
logging.basicConfig(level=logging.DEBUG)
elif args.verbose == 1:
logging.basicConfig(level=logging.INFO)
else:
logging.basicConfig(level=logging.WARNING)
restart_service(args.service, args.env, args.dry_run)
Output:
$ python restart.py web-api --env prod --dry-run -vv
DEBUG:root:[DRY RUN] Would restart web-api in prod
INFO:root:[DRY RUN] Would restart web-api in prod
Example 3: Deploy command with --version and subcommands
Many DevOps tools (like kubectl) use subcommands. Here's a simple version:
import argparse
def deploy(args):
print(f"Deploying {args.app} to {args.env} (dry_run={args.dry_run})")
def rollback(args):
print(f"Rolling back {args.app} to version {args.version}")
def main():
parser = argparse.ArgumentParser(description="Deployment CLI")
subparsers = parser.add_subparsers(dest="command", required=True)
deploy_parser = subparsers.add_parser("deploy", help="Deploy an app")
deploy_parser.add_argument("app")
deploy_parser.add_argument("--env", default="dev")
deploy_parser.add_argument("--dry-run", action="store_true")
deploy_parser.set_defaults(func=deploy)
rollback_parser = subparsers.add_parser("rollback", help="Rollback an app")
rollback_parser.add_argument("app")
rollback_parser.add_argument("--version", required=True)
rollback_parser.set_defaults(func=rollback)
args = parser.parse_args()
args.func(args)
if __name__ == "__main__":
main()
Usage:
$ python deploy.py deploy web-api --env prod --dry-run
Deploying web-api to prod (dry_run=True)
$ python deploy.py rollback web-api --version 1.2.3
Rolling back web-api to version 1.2.3
Example 4: Reading config from a JSON file (bonus)
import argparse
import json
def load_config(path):
with open(path) as f:
return json.load(f)
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--config", required=True, help="Path to config JSON")
args = parser.parse_args()
config = load_config(args.config)
print(f"Loaded config: {config}")
Compare options / when to choose what
argparse is not the only game in town. Here's how it stacks up:
| Tool | Pros | Cons | Best for |
|---|---|---|---|
| argparse | Built-in, no deps, stable, powerful | Verbose for complex UIs | Standard CLI tools, learning, enterprise scripting |
| Click | Decorator-based, chainable, auto help | Extra dependency | Interactive CLI tools with commands and groups |
| Typer | Type hints, auto completion, modern | Requires Python 3.6+, extra deps | Fast API-style CLIs, modern codebases |
| Fire | Zero-config, turns any function into CLI | Less control, magic behavior | Quick scripts for experimentation |
| docopt | Uses docstring as spec | Can get messy | Simple CLIs where docs are already written |
For a DevOps tool that must be robust and deployable on minimal systems, argparse is often the smartest default. It's already in the standard library, so no extra requirements.txt entries — a blessing when you're shipping a single script to a locked-down server.
Troubleshooting & edge cases
Here are the common pitfalls when you create a DevOps CLI with argparse, and how to fix them.
1. error: the following arguments are required: service
You forgot to provide a positional argument. Check your add_argument calls and the run command. If you intended it to be optional, add nargs='?' or make it a flag like --service.
2. Choices validation fails with a confusing list
If you see invalid choice: 'prod ' — that's a space in the input. Sanitize input with .strip() or use type=str.lower to normalize case.
3. --verbose not incrementing
If you use action='store_true', you can't count. Use action='count' and a default of 0. Also, remember that -vv only works if you define -v as the short flag.
4. Exiting code 2 on bad input
argparse calls sys.exit(2). If you're running in a shell script, this is correct. But in unit tests, you'll need to catch SystemExit. Wrap your parse call in a function so it's testable.
5. Subparser required error
If you use subcommands, declare required=True (Python 3.7+) or check args.command manually to avoid silent no-ops.
What you learned & what's next
You now know how to create a DevOps CLI with argparse: parse arguments, validate choices, support flags, and even build subcommand-based tools. You practiced with health checks, restarts, and deployments with --dry-run and verbosity. These skills directly enable you to replace half a page of handwritten runbook instructions with a single, safe, self-documenting command.
Now you're ready to move to the next lesson in the Python for DevOps automation track, where you'll wrap these CLI patterns into a full automation library — combining argparse with config loading, logging, and error handling to build production-grade tools. Start there to take your CLI from useful to indispensable.
Practice recap
Your turn: Extend the restart.py example to include a --timeout option (in seconds) and a --force flag. Make --force required for production (--env prod) to simulate a safety gate. Run it with --help to see your new options, then test with and without --dry-run. This solidifies the pattern of adding real-world guardrails to your own CLI.
Common mistakes
- Forgetting to call
parse_args()— your script runs but ignores all user input. - Using
store_truefor verbosity instead ofcount— you can't do-vv. - Not specifying
choicesfor environment names, leading to typos likeprdo. - Hard-coding
sys.argvparsing instead of letting argparse handle--helpand errors.
Variations
- Use
clickortyperfor a more modern decorator-based CLI with auto-completion. - Add subcommands for multi-tool CLIs (like
kubectldoes) usingadd_subparsers. - Support config file merging with
--configin addition to CLI flags.
Real-world use cases
- A deployment script that accepts
--env prod --dry-runto safely test releases before applying. - A log-tailer that takes
--serviceand--linesto fetch the last N lines from a remote host via SSH. - A backup tool that runs with
--schedule daily --retention 30and pushes archives to S3.
Key takeaways
- argparse turns your repetitive shell commands into a single, documented CLI.
- Use
add_argumentwithchoices,type,action, andcountto build safe, flexible interfaces. - Subcommands let you build multi-tool CLIs (deploy, rollback, status) in one script.
--dry-runis a critical safety feature for any DevOps command that changes state.- Always handle
SystemExitin tests when callingparse_args()to avoid failing your test suite. - Start with argparse; switch to click/typer only when you need advanced features.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.