Command-Line Tools with Python: Building Small Utilities That Stick
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:
main()handles CLI stuff—argument parsing, printing, exit codesprocess_file()does the actual work with plain Python types
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:
- Exit with 0 on success, non-zero on failure
- 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:
- Use argparse for all configuration (no hardcoded paths)
- Add
help="..."to every argument - Separate CLI code from business logic
- Use proper exit codes
- Send errors to stderr
It's maybe 20 extra lines of code. But now your teammates can actually use the thing without asking you questions every day.