Argparse vs Click: Why Every Prototype Deserves a CLI

Reading time ~4 minutes

When you are building a new script or prototype, the temptation is to hardcode values and run it directly. You tell yourself you will add proper argument handling later. But later rarely comes, and you end up with a collection of scripts that require editing source code just to change a parameter.

Building a CLI from the start changes how you think about your code. It forces you to define clear interfaces, makes testing easier, and transforms throwaway scripts into reusable tools.

The Case for CLI First Development

Consider a typical prototype workflow. You write a script, hardcode some values, run it, tweak the values, run it again. Each iteration requires opening the file, finding the right variable, changing it, saving, and running. This friction adds up.

With a CLI, testing becomes:

python script.py --input data.csv --threshold 0.8
python script.py --input data.csv --threshold 0.5
python script.py --input other.csv --threshold 0.8 --verbose

No file editing. No hunting for magic numbers buried in the code. You can pipe output, run from cron jobs, integrate with shell scripts, and share with teammates who can use the tool without reading the source.

Argparse: The Standard Library Approach

Python includes argparse in the standard library. It works, it is well documented, and it requires no external dependencies.

import argparse

parser = argparse.ArgumentParser(description='Greet someone.')
parser.add_argument('--count', type=int, default=1)
parser.add_argument('--name', required=True)
args = parser.parse_args()

for _ in range(args.count):
    print(f"Hello, {args.name}!")

This gets the job done. You define arguments through method calls on the parser object, parse them, and access them as attributes. For simple scripts, argparse is perfectly adequate.

But notice the pattern: you are imperatively building up state (the parser), then separately using that state (the args). The argument definitions live far from where they are used, and the help text requires additional parameters.

Click: A Declarative Alternative

Click takes a different approach. Instead of building a parser object, you decorate your function:

import click

@click.command()
@click.option('--count', default=1, help='Number of greetings.')
@click.option('--name', prompt='Your name', help='The person to greet.')
def hello(count, name):
    """Simple program that greets NAME for a total of COUNT times."""
    for _ in range(count):
        click.echo(f"Hello, {name}!")

if __name__ == '__main__':
    hello()

The function signature matches the CLI interface. Arguments become function parameters. The docstring becomes the command help. This colocation makes the code more readable and maintainable.

Click also provides interactive prompting for free. If the user does not provide --name, Click will prompt for it. This small feature dramatically improves the user experience for interactive tools.

Direct Comparison

Aspect Argparse Click
Dependencies None (stdlib) External package
Style Imperative Declarative
Help text Separate argument Inline with decorator
Prompting Manual implementation Native support
Subcommands Subparsers (verbose) Groups (clean)
Testing Parse args manually CliRunner helper

For quick scripts where dependencies are a concern, argparse wins. For anything you plan to maintain or share, Click’s ergonomics pay off quickly.

Building a Production Grade CLI

Real tools need more than argument parsing. They need logging, configuration management, and pleasant terminal output. Here is a pattern that combines Click with modern Python libraries:

import click
from loguru import logger
from rich.console import Console
from pydantic_settings import BaseSettings
from dotenv import load_dotenv
from rich.logging import RichHandler

load_dotenv()

class Settings(BaseSettings):
    debug: bool = False
    model_config = {"env_file": ".env"}

settings = Settings()

logger.remove()
logger.add(
    RichHandler(rich_tracebacks=True),
    level="DEBUG" if settings.debug else "INFO"
)

console = Console()

@click.command()
@click.option("--debug", is_flag=True)
@click.version_option()
def main(debug: bool):
    settings = Settings(debug=debug)
    logger.debug("Debug mode ON")
    console.print("[bold magenta]My Awesome CLI[/]")

if __name__ == "__main__":
    main()

This gives you:

  • Pydantic Settings: Configuration from environment variables with type validation
  • Loguru: Structured logging with sensible defaults
  • Rich: Colored output, formatted tables, and beautiful tracebacks
  • Click: Clean argument parsing with automatic help generation

Better Terminal Output with Rich

When your CLI produces output, presentation matters. Rich makes it easy to add progress bars, tables, and formatted text:

from rich.console import Console
from rich.table import Table
from rich.progress import track

console = Console()

# Progress bar
for i in track(range(100), description="Processing..."):
    ...

# Table
table = Table(title="Results")
table.add_column("Name")
table.add_row("Alice")
console.print(table)

These small touches transform a script that dumps text to stdout into a tool that feels polished. Users trust tools that look professional.

The Prototyping Mindset

The goal is not to overengineer every script. The goal is to make CLI creation so effortless that it becomes your default. A Click decorator adds maybe three lines to your code. That tiny investment pays dividends when you need to:

  • Test with different parameters
  • Debug production issues with verbose flags
  • Share the tool with a colleague
  • Run the script in CI/CD
  • Come back to the project six months later

Getting Started

Here is a minimal setup using Poetry:

[project]
name = "mycli"
version = "0.1.0"
requires-python = ">=3.10"
dependencies = [
    "click (>=8.3.1,<9.0.0)",
    "loguru (>=0.7.3,<0.8.0)",
    "rich (>=14.2.0,<15.0.0)",
    "pydantic (>=2.12.5,<3.0.0)",
    "pydantic-settings (>=2.12.0,<3.0.0)"
]

Start with Click alone. Add Rich when you want better output. Add Pydantic when you need configuration. Add loguru when print statements are not enough. Each library solves a specific problem without unnecessary complexity.

Conclusion

The choice between argparse and Click is less important than the choice to build a CLI at all. Both work. Click requires a dependency but offers a better developer experience. Argparse is always available but requires more boilerplate.

What matters is treating your scripts as tools from day one. A CLI is an interface contract. It forces you to think about inputs and outputs. It makes your code testable and shareable. And when that prototype becomes a production system, you will be glad you started with a proper command line interface.

NORAI: Building Canada's Sovereign AI Platform

A grassroots effort to promote Canada's AI-for-science ecosystem, inspired by the US Department of Energy's Genesis Mission. Continue reading

Ngrok

Published on November 21, 2025