DEV Community

Cover image for 5 Python CLI Patterns I Use in Every Project (With Real Code)
sam jha
sam jha

Posted on

5 Python CLI Patterns I Use in Every Project (With Real Code)

After building Folder Intelligence — a Python CLI that reads file contents to organize them — I've refined a set of patterns I now use in every project. These aren't fancy abstractions. They're practical building blocks that make CLIs feel professional and maintainable.

Here are the 5 patterns, with real code from my projects.


1. Rich Progress Bars Instead of print()

The first thing users notice about your CLI is feedback. Bare print() calls feel amateurish. The rich library makes it trivial to add beautiful progress output.

from rich.progress import Progress, SpinnerColumn, TextColumn
from rich.console import Console

console = Console()

def process_files(files):
    with Progress(
        SpinnerColumn(),
        TextColumn("[progress.description]{task.description}"),
        transient=True,
    ) as progress:
        task = progress.add_task("Processing files...", total=len(files))
        for f in files:
            do_something(f)
            progress.advance(task)
    console.print("[bold green]Done![/bold green]")
Enter fullscreen mode Exit fullscreen mode

This pattern shows real-time progress without flooding the terminal. The transient=True clears the bar when done, leaving a clean output.


2. --dry-run Mode From Day One

Every CLI that modifies files, databases, or APIs should have a dry-run mode. It builds trust with users and makes testing 10x easier.

import click

@click.command()
@click.option('--dry-run', is_flag=True, help='Preview changes without applying them')
def organize(dry_run):
    for file in get_files():
        new_name = generate_name(file)
        if dry_run:
            click.echo(f"Would rename: {file} -> {new_name}")
        else:
            file.rename(new_name)
            click.echo(f"Renamed: {file} -> {new_name}")
Enter fullscreen mode Exit fullscreen mode

This is the single most-requested feature after releasing Folder Intelligence. Ship it on day one.


3. JSON Audit Logs for Every Destructive Operation

If your CLI moves, renames, or deletes anything, write an audit log. Users need a way to review (and potentially undo) what happened.

import json
from datetime import datetime
from pathlib import Path

def write_audit_log(operations: list[dict], output_dir: Path):
    timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
    log_file = output_dir / f"audit_{timestamp}.json"

    with open(log_file, 'w') as f:
        json.dump({
            "timestamp": timestamp,
            "operations": operations,
            "total": len(operations)
        }, f, indent=2)

    return log_file

# Usage
ops = []
for old, new in renames:
    ops.append({"action": "rename", "from": str(old), "to": str(new)})
    old.rename(new)

log = write_audit_log(ops, output_dir)
print(f"Audit log written: {log}")
Enter fullscreen mode Exit fullscreen mode

This pattern has saved me multiple times during testing. JSON is readable, diffable, and easy to parse for an undo script later.


4. Graceful Error Handling with Context

Nothing is more frustrating than a CLI that throws a raw traceback at you. Catch errors where they happen and give context about what failed.

from rich.console import Console

console = Console()

def process_file(filepath):
    try:
        content = extract_text(filepath)
        new_name = generate_name(content)
        return new_name
    except PermissionError:
        console.print(f"[yellow]Skipped {filepath.name}: no read permission[/yellow]")
        return None
    except Exception as e:
        console.print(f"[red]Error processing {filepath.name}: {e}[/red]")
        return None
Enter fullscreen mode Exit fullscreen mode

The key is: never crash the whole pipeline because one file failed. Log it, skip it, continue.


5. Configuration via Both CLI Args and a Config File

Power users want to set defaults without typing flags every time. The pattern I use: CLI args override config file, which overrides built-in defaults.

import click
import json
from pathlib import Path

def load_config() -> dict:
    config_path = Path.home() / '.folder-intelligence' / 'config.json'
    if config_path.exists():
        return json.loads(config_path.read_text())
    return {}

@click.command()
@click.option('--output-dir', default=None, help='Output directory')
@click.pass_context
def run(ctx, output_dir):
    config = load_config()
    # CLI arg > config file > default
    final_output = output_dir or config.get('output_dir') or './organized'
    process(final_output)
Enter fullscreen mode Exit fullscreen mode

This makes your CLI usable for both quick one-offs and automated pipelines.


Putting It All Together

These patterns are all used in Folder Intelligence — a CLI that reads PDFs, Word docs, and images (via OCR) and organizes them based on their actual content, not just filenames.

If you've found a CLI pattern that you use everywhere, drop it in the comments — I'm always looking to improve the toolbox.

GitHub: https://github.com/SGajjar24/folder-intelligence

Top comments (0)