I wrote a script to clean up old log files. Worked great on my machine. Shared it with the team. Within a day I had three people asking "how do I change the folder path?" and one person who accidentally deleted the wrong directory because they edited the script incorrectly.

That's when I learned the difference between a script and a tool. A script solves your problem. A tool solves everyone's problem without them needing to touch the code.

Stop Editing Variables

We've all written scripts like this:

# ⚠️ CHANGE THESE BEFORE RUNNING
INPUT_FILE = "data.csv"
OUTPUT_DIR = "/tmp/reports"
VERBOSE = True

It works, but it's fragile. Users have to open the source code to configure it. They might break something. You can't easily run it in a CI pipeline with different settings. And if someone forgets to change the path back, they might overwrite your production data.

A real CLI tool takes configuration as command-line arguments. No source code editing required.

argparse in 5 Minutes

Python's argparse module is built-in and handles everything: parsing arguments, type conversion, and generating help text automatically.

import argparse

def main():
    parser = argparse.ArgumentParser(
        description="Process CSV files and generate reports."
    )
    
    # Required positional argument
    parser.add_argument("filename", help="Path to input CSV")
    
    # Optional argument with default
    parser.add_argument("-o", "--output", default="output.txt",
                        help="Output path (default: output.txt)")
    
    # Boolean flag
    parser.add_argument("-v", "--verbose", action="store_true",
                        help="Enable verbose output")
    
    args = parser.parse_args()
    
    # Now use args.filename, args.output, args.verbose
    process_file(args.filename, args.output, args.verbose)

if __name__ == "__main__":
    main()

Run python tool.py --help and you get a beautiful help message for free:

usage: tool.py [-h] [-o OUTPUT] [-v] filename

Process CSV files and generate reports.

positional arguments:
  filename              Path to input CSV

optional arguments:
  -h, --help            show this help message and exit
  -o OUTPUT, --output OUTPUT
                        Output path (default: output.txt)
  -v, --verbose         Enable verbose output

No documentation to write. It's generated from your code.

Keep Logic Separate

Notice I separated main() from process_file(). This matters:

Why? Because now I can import and test process_file() directly, without dealing with command-line arguments. I can also call it from another script if needed.

Supporting Pipes

Unix tools chain together: cat file.txt | grep error | wc -l. Your Python tool can play along.

import sys
import argparse

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("filename", nargs="?", help="Input file (or pipe)")
    args = parser.parse_args()
    
    if args.filename:
        with open(args.filename) as f:
            content = f.read()
    elif not sys.stdin.isatty():
        # Reading from pipe
        content = sys.stdin.read()
    else:
        parser.error("Provide a filename or pipe data in")
    
    # Process content...
    print(f"Got {len(content)} characters")

Now you can do cat data.txt | python tool.py or python tool.py data.txt. Both work.

Exit Codes and Error Output

Two rules for CLI tools:

  1. Exit with 0 on success, non-zero on failure
  2. Normal output goes to stdout, errors go to stderr

This matters for scripting. Someone might write:

python tool.py data.csv > results.txt

If errors go to stdout, they end up in results.txt mixed with the data. If they go to stderr, they show in the terminal while clean data goes to the file.

import sys

def error_exit(message):
    print(f"Error: {message}", file=sys.stderr)
    sys.exit(1)

# Usage
if not is_valid:
    error_exit("Invalid configuration")

Quick Checklist

Before sharing a script with others:

It's maybe 20 extra lines of code. But now your teammates can actually use the thing without asking you questions every day.

← Back to Python Articles

Back to Home