Build Your First CLI Tool

Build your first CLI tool with argparse — Python for DevOps automation.

Focus: build your first cli tool with argparse

Sponsored

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 --verbose or --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 indexing sys.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: argparse is imported as import argparse and 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:

  1. Create the parser — instantiate argparse.ArgumentParser(description="..."). This sets up the framework and will auto-generate --help.
  2. 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.
  3. Parse the arguments — call args = parser.parse_args() inside your main() function. This does all the dirty work: reads sys.argv, matches patterns, and reports errors.
  4. Use the values — access them as attributes on the args namespace. For example, args.config_path.
  5. 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 to True when present, False otherwise. For flags that take a value, use type to 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.

  1. 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, pass required=True to add_argument(). For example: parser.add_argument("--env", required=True, help="Environment (staging/prod)").

  2. I get NameError: name 'args' is not defined. This happens when you try to access args outside the function where it was parsed. Make sure you call args = parser.parse_args() inside main() and pass args to any helper functions.

  3. My integer argument is a string. Use type=int in add_argument() — otherwise args.port will be a string like "8080", and comparisons like if args.port > 1000 will fail. Here’s a quick example:

parser.add_argument("--port", type=int, default=8080, help="Port number")
  1. Help text doesn’t show up. argparse generates -h/--help automatically, but if you define a custom -h flag yourself, it will override it. To avoid confusion, don’t use -h for your own flags unless you also disable the default via add_help=False.

  2. I want subcommands like git commit. Use add_subparsers(). This is a more advanced feature but essential for multi-command tools. For now, just know it exists — it builds on the same add_argument() pattern.

  3. 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 --help before 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.argv parsing 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=int for numeric arguments, leading to string comparisons and subtle bugs.
  • Not using required=True for optional flags that are actually mandatory, causing the script to run with a default and fail later.
  • Overriding the built-in -h help flag by defining your own -h argument, breaking --help output.
  • Accessing parsed arguments outside the function where parse_args() was called, resulting in undefined variable errors.

Variations

  1. Use click or typer for decorator-based argument parsing with less boilerplate, especially for larger multi-command tools.
  2. Implement subcommands with add_subparsers() to create tools like git commit or kubectl.
  3. 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 staging and --dry-run to validate configuration before applying changes.
  • A log analyzer that takes a log file path and --level ERROR to filter and summarize errors for on-call engineers.
  • A database migration tool that uses --database and --schema positional 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; use required=True when you must have them.
  • Use type=int, choices=[...], and action='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 --help first.

Sponsored

Sponsored

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.

Related tutorials, quizzes, and articles for this topic.