Build Your First CLI Tool
Build your first CLI tool with argparse — Python for DevOps automation.
Focus: build your first cli tool with argparse
You’ve written scripts that print output and take hardcoded values — but the moment you need to run them with different inputs, environments, or flags, you hit a wall. Manually editing variables, parsing sys.argv with fragile index checks, or writing your own --help text is a nightmare. This lesson shows you how to build your first CLI tool with argparse — the standard Python library module that turns a simple script into a professional, self-documenting command-line tool. By the end, you’ll be able to add arguments, flags, and helpful error messages in minutes, and you’ll have a reusable pattern for every DevOps automation script you write.
The problem this lesson solves
As a DevOps engineer, you run the same Python script over and over — maybe to check server health, parse a log file, or trigger a deployment. Each run requires different values: a hostname, a port, a debug flag. If your script hardcodes those values, you’re forced to edit the source every time. That’s slow, error-prone, and useless for anyone else on your team.
A real CLI tool accepts input as command-line arguments — like python deploy.py --env staging --dry-run. Without a solid argument parser, you might be tempted to use sys.argv directly:
import sys
# Fragile: positional order matters, no --help, no type checking
if len(sys.argv) > 2:
env = sys.argv[1]
dry_run = sys.argv[2] == "--dry-run"
else:
print("Usage: deploy.py <env> [--dry-run]")
sys.exit(1)
This approach breaks when arguments are reordered, missing, or typed incorrectly. You end up writing your own help text, validation, and error handling — that’s tedious and buggy. argparse solves all of this out of the box. It’s part of Python’s standard library, so you don’t need pip installs, and it gives you --help automatically, type validation, and clear error messages.
In this lesson, you’ll learn how to build your first CLI tool with argparse that would be immediately useful in a DevOps context — a config file inspector that accepts a path, an optional output format, and a verbose flag.
Core concept / mental model
Think of argparse as a concierge at the command-line entrance. Your script’s main logic is the hotel inside; argparse stands at the door, reads every word the user types, and hands you a clean dictionary of confirmed choices. It doesn’t just pass strings — it validates types, enforces required vs optional flags, and even generates a help menu when someone types --help.
The heart of argparse is the ArgumentParser object. You create one, register arguments on it, then call parse_args() to get a Namespace object. This namespace holds all the user input as attributes that you can access with dot notation. For example, args.env or args.verbose.
Here’s the mental model:
- Positional arguments are required pieces of input, like a filename or a hostname. They appear in order on the command line.
- Optional arguments are flags like
--verboseor--env staging. They start with one or two dashes and may take a value. - The
parse_args()call returns a single object holding every argument—no more indexingsys.argv.
This design makes your script declarative: you define what inputs you expect, and argparse handles the parsing and validation. Your code stays focused on the actual automation logic.
Pro tip:
argparseis imported asimport argparseand is part of the standard library. Once you learn it, you can script your mental model into every future tool.
How it works step by step
Let’s break down the process of building a CLI tool with argparse into five logical steps:
- Create the parser — instantiate
argparse.ArgumentParser(description="..."). This sets up the framework and will auto-generate--help. - Add arguments — use
parser.add_argument()for each positional or optional argument. You specify the name, type, help text, default, and whether it’s required. - Parse the arguments — call
args = parser.parse_args()inside yourmain()function. This does all the dirty work: readssys.argv, matches patterns, and reports errors. - Use the values — access them as attributes on the
argsnamespace. For example,args.config_path. - Run your logic — execute your automation code based on those values. If something goes wrong, argparse has already ensured the input is valid.
Here’s a minimal skeleton that shows the structure:
# cli_basics.py
import argparse
def main():
parser = argparse.ArgumentParser(description="A minimal CLI tool")
parser.add_argument("input", help="Path to input file")
parser.add_argument("--verbose", action="store_true", help="Show extra output")
args = parser.parse_args()
if args.verbose:
print(f"Processing {args.input}...")
# ... your logic
if __name__ == "__main__":
main()
If you run python cli_basics.py sample.txt, you get Processing sample.txt.... If you add --verbose, you see extra output. The key thing to remember: argparse turns raw strings into typed values via the type parameter (like type=int), and it enforces required arguments by default for positionals.
Hands-on walkthrough
Now let’s build your first CLI tool with argparse that actually does something a DevOps engineer would use daily. We’ll create a config file inspector that reads a YAML or JSON config and prints its contents. This could be used to verify a deployment config before applying it.
Step 1: Set up the parser and arguments
Create a file inspect_config.py with the following code:
# inspect_config.py
import argparse
import json
import sys
try:
import yaml
except ImportError:
yaml = None
def main():
parser = argparse.ArgumentParser(
description="Inspect a config file (JSON or YAML) for DevOps tasks."
)
parser.add_argument("config_path", help="Path to the config file (JSON or YAML)")
parser.add_argument("--format", choices=["json", "yaml", "pretty"], default="pretty",
help="Output format (default: pretty)")
parser.add_argument("-v", "--verbose", action="store_true",
help="Print extra debugging info")
args = parser.parse_args()
if args.verbose:
print(f"Reading config from {args.config_path}")
# Load the file (simplified for demo)
try:
with open(args.config_path) as f:
if args.config_path.endswith(".json"):
data = json.load(f)
elif yaml and args.config_path.endswith((".yaml", ".yml")):
data = yaml.safe_load(f)
else:
print("Unsupported file format. Use .json, .yaml, or .yml", file=sys.stderr)
sys.exit(1)
except FileNotFoundError:
print(f"Error: {args.config_path} not found", file=sys.stderr)
sys.exit(1)
except json.JSONDecodeError:
print(f"Error: {args.config_path} is not valid JSON", file=sys.stderr)
sys.exit(1)
if args.format == "json":
print(json.dumps(data, indent=2))
elif args.format == "yaml" and yaml:
print(yaml.dump(data))
else:
# simple pretty print fallback
print(json.dumps(data, indent=2))
if args.verbose:
print("Done.")
if __name__ == "__main__":
main()
Step 2: Run it with different arguments
Create a sample config file app.json:
{
"name": "api-server",
"port": 8080,
"debug": false
}
Now run the tool from your terminal:
python inspect_config.py app.json
python inspect_config.py app.json --format json
python inspect_config.py app.json --verbose
Expected output for the first command (assuming pretty fallback):
{
"name": "api-server",
"port": 8080,
"debug": false
}
And with --verbose, you’ll see the extra debug lines before and after the output.
Step 3: Test error handling
Run it with a missing file:
python inspect_config.py missing.json
You’ll get:
Error: missing.json not found
And argparse also provides its own error if you forget the positional argument:
python inspect_config.py
usage: inspect_config.py [-h] [--format {json,yaml,pretty}] [-v] config_path
inspect_config.py: error: the following arguments are required: config_path
That’s a clean, professional error message—no custom code needed.
Pro tip: Use
action="store_true"for boolean flags like--verbose. It automatically sets the attribute toTruewhen present,Falseotherwise. For flags that take a value, usetypeto convert strings to int, float, or callable.
Compare options / when to choose what
Argparse isn’t the only way to handle command-line inputs in Python. Here’s a quick comparison to help you decide when to use what:
| Library | Built-in? | When to choose |
|---|---|---|
sys.argv |
Yes | Extremely simple scripts with 1–2 fixed arguments, no help needed |
argparse |
Yes | Standard choice for any moderately complex CLI; automatic help, type checking, subcommands |
click |
No (third-party) | When you want a decorator-based approach and automatic nice-looking help; great for larger tools |
typer |
No (third-party) | Built on Click, uses type hints, very modern, great for fast development |
docopt |
No (third-party) | When you want the help text itself to define the parser (rare) |
For most DevOps scripts, argparse is the sweet spot. It’s already installed, stable, and powerful enough for 95% of use cases—like the config inspector we built. If you find yourself writing repetitive boilerplate in many tools, then Click or Typer might be worth the extra dependency. But start with argparse to keep your toolchain lean.
Troubleshooting & edge cases
Here are the most common pitfalls when you build your first CLI tool with argparse — and how to fix them.
-
My optional argument is required but I didn’t set
required=True. By default, optional arguments (starting with-) are not required. If you need them mandatory, passrequired=Truetoadd_argument(). For example:parser.add_argument("--env", required=True, help="Environment (staging/prod)"). -
I get
NameError: name 'args' is not defined. This happens when you try to accessargsoutside the function where it was parsed. Make sure you callargs = parser.parse_args()insidemain()and passargsto any helper functions. -
My integer argument is a string. Use
type=intinadd_argument()— otherwiseargs.portwill be a string like"8080", and comparisons likeif args.port > 1000will fail. Here’s a quick example:
parser.add_argument("--port", type=int, default=8080, help="Port number")
-
Help text doesn’t show up. argparse generates
-h/--helpautomatically, but if you define a custom-hflag yourself, it will override it. To avoid confusion, don’t use-hfor your own flags unless you also disable the default viaadd_help=False. -
I want subcommands like
git commit. Useadd_subparsers(). This is a more advanced feature but essential for multi-command tools. For now, just know it exists — it builds on the sameadd_argument()pattern. -
My script hangs waiting for input. If you accidentally use
input()inside the script because you’re expecting an interactive prompt, remember that argparse parses command-line arguments, not interactive input. If you need both, you’ll have to design your tool to accept either, but that’s beyond this lesson.
Pro tip: Always run your script with
--helpbefore deploying it to colleagues. It’s the fastest way to verify that your argument names and help text are clear.
What you learned & what's next
You’ve just built your first CLI tool with argparse! Let’s recap what you accomplished:
- You understood the pain of fragile
sys.argvparsing and why argparse is the standard solution. - You learned the core concept: create a parser, add arguments, parse them, and use the resulting namespace.
- You followed a step-by-step process to build a config inspector that reads JSON/YAML files with optional output formatting and a verbose flag.
- You compared argparse to alternatives like Click and Typer, and you know when to stick with the standard library.
- You debugged common edge cases like required flags, type conversion, and subcommands.
All of this directly supports the learning objective: explain the core idea of argparse and complete a practical exercise to use it.
Now that you can build CLI tools, the natural next lesson in this track is adding logging and error handling — because a CLI tool that prints to stdout is only half the story. In production, you’ll want structured logs, proper exit codes, and graceful handling of unexpected failures. That’s exactly what the next lesson covers. You’ll take your argparse tool to production-ready status.
The pattern you’ve learned here is a template you’ll reuse in every automation script: parse input cleanly, then execute your logic. With this skill, you’re ready to move on and build even more powerful DevOps tools.
Practice recap
Take the inspect_config.py script and extend it: add a --key KEY option that prints only that specific key from the config, and a --threads N integer option that (pretend to) validate the config in N parallel checks. Run it with --help to confirm your new arguments appear correctly. This will solidify your argparse skills and prepare you for the next lesson on logging.
Common mistakes
- Forgetting to set
type=intfor numeric arguments, leading to string comparisons and subtle bugs. - Not using
required=Truefor optional flags that are actually mandatory, causing the script to run with a default and fail later. - Overriding the built-in
-hhelp flag by defining your own-hargument, breaking--helpoutput. - Accessing parsed arguments outside the function where
parse_args()was called, resulting in undefined variable errors.
Variations
- Use
clickortyperfor decorator-based argument parsing with less boilerplate, especially for larger multi-command tools. - Implement subcommands with
add_subparsers()to create tools likegit commitorkubectl. - Use environment variables as fallback defaults for arguments, allowing configuration without CLI flags (e.g.,
os.environ.get("PORT", 8080)).
Real-world use cases
- A deployment script that accepts
--env stagingand--dry-runto validate configuration before applying changes. - A log analyzer that takes a log file path and
--level ERRORto filter and summarize errors for on-call engineers. - A database migration tool that uses
--databaseand--schemapositional arguments to run specific migration scripts.
Key takeaways
- Argparse is Python's standard-library CLI parser that generates
--help, validates input types, and returns a namespace of parsed values. - Always create the parser, add arguments, then call
parse_args()before running your core logic. - Optional flags (with
--) are not required by default; userequired=Truewhen you must have them. - Use
type=int,choices=[...], andaction='store_true'to enforce clean input handling. - For most DevOps scripts, argparse is the right choice — reserve Click/Typer for very large multi-command tools.
- When something fails, argparse gives clear error messages and exit codes — debug with
--helpfirst.
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.