Python argparse: Build CLI Tools in 10 Minutes
๐ Free: AI Publishing Checklist โ 7 steps in Python ยท Full pipeline: germy5.gumroad.com/l/xhxkzz (pay what you want, min $9.99)
The Problem with sys.argv[1]
You've been there. You write a quick script, hardcode a filename, then immediately need to change it. So you reach for sys.argv:
import sys
filename = sys.argv[1]
count = int(sys.argv[2])
This works โ until it doesn't. Run it without arguments and you get an IndexError. Pass a string where you expected an integer and it crashes. There's no help text, no validation, no defaults. Anyone else who picks up your script has to read the source code to know how to run it.
argparse solves all of this. It's in the standard library, requires no installation, and turns your script into a proper CLI tool in minutes.
The Basics: ArgumentParser
Every argparse script starts with a parser:
import argparse
parser = argparse.ArgumentParser(
description="My CLI tool โ does useful things."
)
args = parser.parse_args()
That one call to parse_args() handles everything: reading sys.argv, validating inputs, and printing help when the user passes --help.
Positional Arguments
Positional arguments are 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 Arguments (--flag and -f)
Optional arguments use -- prefix and can have short aliases:
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 Validation: No More Manual Casting
Instead of int(sys.argv[1]) wrapped in a try/except, let argparse handle it:
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 passes --count hello, argparse prints a clean error message and exits โ no stack trace, no confusion.
Required Arguments, nargs, and Lists
Required Optional Arguments
parser.add_argument("--title", required=True, help="Article title (required)")
Accepting Multiple Values
# One or more values: --tags python beginner tutorial
parser.add_argument("--tags", nargs="+", help="One or more tags")
# Zero or more values: --tags (empty is fine)
parser.add_argument("--tags", nargs="*", help="Zero or more tags")
The result is a Python list you can iterate directly:
args = parser.parse_args()
for tag in args.tags:
print(tag)
Boolean Flags: store_true and store_false
Boolean flags don't take a value โ their presence or absence is the value:
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
Subcommands: One Tool, Many Commands
Real CLI tools like git, docker, and pip use subcommands. add_subparsers() gives you the same structure.
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")
Now 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")
Complete Example: Publish Queue CLI
Here's a working CLI for managing an article publish queue โ the same pattern used in the full pipeline.
#!/usr/bin/env python3
"""
publish_queue.py โ CLI for managing the article publish queue.
Usage: python publish_queue.py <command> [options]
"""
import argparse
import json
import logging
import sys
from pathlib import Path
QUEUE_FILE = Path("queue.json")
def load_queue() -> list[dict]:
if not QUEUE_FILE.exists():
return []
return json.loads(QUEUE_FILE.read_text())
def save_queue(queue: list[dict]) -> None:
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] "
print(f"{i}. {status} {article['title']} ({', '.join(article.get('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 build_parser() -> argparse.ArgumentParser:
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:
parser = build_parser()
args = parser.parse_args()
logging.basicConfig(
level=logging.DEBUG if args.verbose else logging.INFO,
format="%(levelname)s: %(message)s",
)
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
Running It
# 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 next (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 updated queue with debug logging
python publish_queue.py list --verbose
Key Patterns to Remember
| Pattern | When to use it |
|---|---|
type=int / type=float
|
Any numeric input |
choices=[...] |
Fixed set of valid values |
required=True |
Mandatory optional args |
nargs="+" / nargs="*"
|
Lists of values |
action="store_true" |
Boolean flags |
add_subparsers() |
Multi-command tools |
set_defaults(func=...) |
Dispatch to subcommand functions |
What You Get for Free
Every argparse-based script automatically has:
-
--help/-hโ generated from yourhelp=strings - Type validation โ with clear error messages, no tracebacks
- Default values โ documented in the help output
- Usage line โ auto-generated from your argument definitions
No third-party libraries. No pip install. Just the standard library.
The publish queue CLI in the full pipeline uses argparse for --list, --add, and --publish: germy5.gumroad.com/l/xhxkzz โ pay what you want, min $9.99.
Top comments (0)