Python argparse: Build CLI Tools in 10 Minutes

Published: (May 9, 2026 at 07:20 PM EDT)
8 min read
Source: Dev.to

Source: Dev.to

๐ŸŽ Free: AI Publishing Checklist โ€” 7 steps in Python
Full pipeline: (pay what you want, minโ€ฏ$9.99)

The Problem

You write a quick script, hardโ€‘code a filename, then immediately need to change it.
You reach for sys.argv:

import sys

filename = sys.argv[1]
count = int(sys.argv[2])

It worksโ€ฆ until it doesnโ€™t:

  • Run it without arguments โ†’ IndexError.
  • Pass a nonโ€‘integer where an integer is expected โ†’ crash.
  • No help text, no validation, no defaults.

Anyone else who picks up your script has to read the source to know how to run it.

Why argparse?

argparse (standard library) solves all of this.
A single call to parse_args() handles:

  • Reading sys.argv
  • Validating inputs
  • Printing help (--help)

Basic Boilerplate

import argparse

parser = argparse.ArgumentParser(
    description="My CLI tool โ€” does useful things."
)
args = parser.parse_args()

Positional Arguments

Required and identified by position, not name:

parser.add_argument("filename", help="Path to the input file")
parser.add_argument("count", help="Number of items to process")

Optional (Flag) Arguments

Use -- prefix; short aliases are optional:

parser.add_argument(
    "--output", "-o",
    help="Output file path",
    default="output.txt"
)
parser.add_argument(
    "--verbose", "-v",
    help="Enable verbose logging",
    action="store_true"
)

Type Conversion & Choices

Let argparse do the conversion and validation:

parser.add_argument(
    "--count", type=int, default=10,
    help="Number of items"
)
parser.add_argument(
    "--rate", type=float, default=1.5,
    help="Processing rate"
)
parser.add_argument(
    "--format",
    choices=["json", "csv", "txt"],
    default="json",
    help="Output format"
)

If a user runs --count hello, argparse prints a clean error and exitsโ€”no stack trace.

nargs and Lists

ExampleMeaning
parser.add_argument("--title", required=True, help="Article title (required)")Required optional argument
parser.add_argument("--tags", nargs="+", help="One or more tags")One or more values (+)
parser.add_argument("--tags", nargs="*", help="Zero or more tags")Zero or more values (*)

Result is a Python list you can iterate directly:

args = parser.parse_args()
for tag in args.tags:
    print(tag)

Boolean Flags (store_true / store_false)

parser.add_argument(
    "--dry-run",
    action="store_true",
    help="Simulate without writing"
)
parser.add_argument(
    "--no-color",
    action="store_false",
    dest="color",
    help="Disable color output"
)

Usage

python publish.py --dry-run   # args.dry_run is True
python publish.py             # args.dry_run is False
python publish.py --no-color # args.color is False

Subโ€‘commands (like git, docker, pip)

parser = argparse.ArgumentParser(description="Publish queue manager")
subparsers = parser.add_subparsers(dest="command", required=True)

# `publish` subcommand
publish_parser = subparsers.add_parser(
    "publish",
    help="Publish the next article in queue"
)
publish_parser.add_argument(
    "--dry-run",
    action="store_true",
    help="Simulate without publishing"
)

# `list` subcommand
list_parser = subparsers.add_parser(
    "list",
    help="Show the publish queue"
)
list_parser.add_argument(
    "--format",
    choices=["table", "json"],
    default="table"
)

args.command tells you which subcommand was chosen, and each subcommand has its own arguments.

The --verbose / -v Pattern

A common pattern is using --verbose to set the logging level at runtime:

import argparse
import logging

parser = argparse.ArgumentParser()
parser.add_argument(
    "--verbose", "-v",
    action="store_true",
    help="Enable debug logging"
)
args = parser.parse_args()

logging.basicConfig(
    level=logging.DEBUG if args.verbose else logging.INFO,
    format="%(levelname)s: %(message)s"
)

log = logging.getLogger(__name__)
log.info("Starting...")
log.debug("This only shows with --verbose")

Full Example: Article Publish Queue CLI

#!/usr/bin/env python3
"""
publish_queue.py โ€” CLI for managing the article publish queue.
Usage: python publish_queue.py  [options]
"""

import argparse
import json
import logging
import sys
from pathlib import Path

QUEUE_FILE = Path("queue.json")

def load_queue() -> list[dict]:
    """Load the queue from JSON; return empty list if file missing."""
    if not QUEUE_FILE.exists():
        return []
    return json.loads(QUEUE_FILE.read_text())

def save_queue(queue: list[dict]) -> None:
    """Write the queue back to disk."""
    QUEUE_FILE.write_text(json.dumps(queue, indent=2))

def cmd_list(args: argparse.Namespace) -> None:
    queue = load_queue()
    if not queue:
        print("Queue is empty.")
        return
    for i, article in enumerate(queue, 1):
        status = "[published]" if article.get("published") else "[pending]  "
        tags = ", ".join(article.get("tags", []))
        print(f"{i}. {status} {article['title']} ({tags})")

def cmd_add(args: argparse.Namespace) -> None:
    queue = load_queue()
    article = {
        "title": args.title,
        "tags": args.tags or [],
        "published": False,
    }
    queue.append(article)
    save_queue(queue)
    logging.info("Added: %s", args.title)
    print(f"Added '{args.title}' to queue. Total: {len(queue)} articles.")

def cmd_publish(args: argparse.Namespace) -> None:
    queue = load_queue()
    pending = [a for a in queue if not a.get("published")]
    if not pending:
        print("No pending articles.")
        return
    next_article = pending[0]
    if args.dry_run:
        print(f"[DRY RUN] Would publish: {next_article['title']}")
        return
    next_article["published"] = True
    save_queue(queue)
    print(f"Published: {next_article['title']}")
    logging.info("Published: %s", next_article["title"])

def main() -> None:
    parser = argparse.ArgumentParser(
        description="Publish queue manager"
    )
    subparsers = parser.add_subparsers(dest="command", required=True)

    # add subcommand
    add_parser = subparsers.add_parser(
        "add",
        help="Add a new article to the queue"
    )
    add_parser.add_argument(
        "--title",
        required=True,
        help="Article title (required)"
    )
    add_parser.add_argument(
        "--tags",
        nargs="*",
        help="Zero or more tags"
    )
    add_parser.set_defaults(func=cmd_add)

    # list subcommand
    list_parser = subparsers.add_parser(
        "list",
        help="Show the publish queue"
    )
    list_parser.set_defaults(func=cmd_list)

    # publish subcommand
    publish_parser = subparsers.add_parser(
        "publish",
        help="Publish the next pending article"
    )
    publish_parser.add_argument(
        "--dry-run",
        action="store_true",
        help="Simulate without publishing"
    )
    publish_parser.set_defaults(func=cmd_publish)

    # global verbose flag
    parser.add_argument(
        "--verbose", "-v",
        action="store_true",
        help="Enable debug logging"
    )

    args = parser.parse_args()

    # configure logging *after* parsing arguments
    logging.basicConfig(
        level=logging.DEBUG if args.verbose else logging.INFO,
        format="%(levelname)s: %(message)s"
    )

    # dispatch to the chosen subcommand
    args.func(args)

if __name__ == "__main__":
    main()

Run the script with:

python publish_queue.py add --title "My First Article" --tags python tutorial
python publish_queue.py list
python publish_queue.py publish --dry-run
python publish_queue.py publish -v

TL;DR

  • argparse turns a oneโ€‘liner script into a polished CLI.
  • Define positional and optional arguments, use type conversion, choices, and nargs for lists.
  • Use store_true / store_false for flags.
  • Leverage subโ€‘parsers for gitโ€‘style subโ€‘commands.
  • Add a --verbose flag to toggle logging.

Give it a tryโ€”your scripts will be more robust, selfโ€‘documenting, and userโ€‘friendly in minutes!

publish_queue.py โ€“ Argumentโ€‘Parser based CLI

import argparse
import logging

# Example logging call used by the script
logging.info("Published: %s", next_article["title"])

def build_parser() -> argparse.ArgumentParser:
    """Create the topโ€‘level argument parser."""
    parser = argparse.ArgumentParser(
        prog="publish_queue",
        description="Manage your article publish queue.",
    )
    parser.add_argument(
        "--verbose", "-v",
        action="store_true",
        help="Enable debug logging",
    )

    subparsers = parser.add_subparsers(dest="command", required=True)

    # โ”€โ”€ list โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
    list_parser = subparsers.add_parser("list", help="Show the publish queue")
    list_parser.set_defaults(func=cmd_list)

    # โ”€โ”€ add โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
    add_parser = subparsers.add_parser("add", help="Add an article to the queue")
    add_parser.add_argument("--title", required=True, help="Article title")
    add_parser.add_argument("--tags", nargs="*", help="Tags for the article")
    add_parser.set_defaults(func=cmd_add)

    # โ”€โ”€ publish โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
    publish_parser = subparsers.add_parser(
        "publish", help="Publish the next pending article"
    )
    publish_parser.add_argument(
        "--dry-run", action="store_true", help="Simulate without writing"
    )
    publish_parser.set_defaults(func=cmd_publish)

    return parser

def main() -> None:
    """Entry point for the CLI."""
    parser = build_parser()
    args = parser.parse_args()

    logging.basicConfig(
        level=logging.DEBUG if args.verbose else logging.INFO,
        format="%(levelname)s: %(message)s",
    )

    # Dispatch to the appropriate subโ€‘command function
    args.func(args)

if __name__ == "__main__":
    main()

--help Output

$ python publish_queue.py --help
usage: publish_queue [-h] [--verbose] {list,add,publish} ...

Manage your article publish queue.

positional arguments:
  {list,add,publish}
    list              Show the publish queue
    add               Add an article to the queue
    publish           Publish the next pending article

options:
  -h, --help          show this help message and exit
  --verbose, -v       Enable debug logging
$ python publish_queue.py add --help
usage: publish_queue add [-h] --title TITLE [--tags [TAGS ...]]

options:
  -h, --help           show this help message and exit
  --title TITLE        Article title
  --tags [TAGS ...]    Tags for the article

Example Workflow

# Add articles to the queue
python publish_queue.py add --title "Python argparse guide" \
    --tags python beginners tutorial
python publish_queue.py add --title "Automate your workflow" \
    --tags python automation

# List the queue
python publish_queue.py list
# 1. [pending]   Python argparse guide (python, beginners, tutorial)
# 2. [pending]   Automate your workflow (python, automation)

# Publish the next article (dryโ€‘run first)
python publish_queue.py publish --dry-run
# [DRY RUN] Would publish: Python argparse guide

python publish_queue.py publish
# Published: Python argparse guide

# Check the updated queue with debug logging
python publish_queue.py list --verbose

Argparse Pattern Cheatโ€‘Sheet

PatternWhen to use it
type=int / type=floatAny numeric input
choices=[...]Fixed set of valid values
required=TrueMandatory optional args
nargs="+" / nargs="*"Lists of values
action="store_true"Boolean flags
add_subparsers()Multiโ€‘command tools
set_defaults(func=โ€ฆ)Dispatch to subโ€‘command functions

What argparse gives you automatically

  • -h/--help โ€“ generated from help= strings
  • Type validation โ€“ clear error messages, no tracebacks
  • Default values โ€“ documented in the help output
  • Usage line โ€“ autoโ€‘generated from the argument definitions

No thirdโ€‘party libraries required. Everything works with the Python standard library.

Further Reading & Resources

  • The publishโ€‘queue CLI in the full pipeline โ€“ germy5.gumroad.com/l/xhxkzz (pay what you want, minโ€ฏ$9.99)
  • Your First Automated Python Script That Validates and Runs Itself
  • Python logging: Stop Using print() in Your Automation Scripts
  • How to Schedule Python Scripts with Cron: A Beginnerโ€™s Complete Guide
0 views
Back to Blog

Related posts

Read more ยป

str() vs repr() vs print() in Python

Overview When learning Python you encounter three builtโ€‘in utilities that often look similar: - str - repr - print At first they may seem to do the same thingโ€”...