Design a CLI with argparse
Learn to design a simple CLI application using Python's argparse module — a practical lesson for building command-line interfaces step by step.
Focus: design a simple cli application with argparse
Ever built a Python script that does something useful, only to realize it's locked to hardcoded values? You change a filename, edit the script, rerun — and repeat. That's a productivity killer. The fix is a proper command-line interface (CLI) — and Python's argparse module makes it dead simple. In this lesson, you'll learn to design a simple CLI application with argparse, moving from rigid scripts to flexible, professional tools that users (including future you) will love to run from the terminal.
The problem this lesson solves
Without a CLI argument parser, your Python scripts are inflexible. To change an input file, debug level, or output directory, users must edit the source code. That's error-prone, breaks automation, and makes your tool impossible to integrate into shell pipelines. argparse solves this by giving you a structured way to accept command-line arguments, flags, and options — automatically generating help messages, handling type conversion, and providing meaningful error messages when users mistype something.
Core concept / mental model
Think of argparse as a menu planner for your script. You define what ingredients (arguments) your script expects: required items (positional arguments), optional seasonings (optional arguments with flags like -o), and switches (boolean flags like --verbose). When a user runs your script from the terminal, argparse reads the command line, validates it against your menu, and returns a namespace object with all the parsed values. No manual parsing of sys.argv — ever.
Key terms
- Positional argument: an argument that must be provided in order (e.g., a filename).
- Optional argument: an argument introduced by a flag (e.g.,
-o output.txt). - Namespace: a simple object whose attributes hold the parsed values.
- Parser: the
ArgumentParserinstance that defines the interface.
Pro tip:
argparseis part of Python's standard library — no pip install needed. It works out of the box in Python 3.2+.
How it works step by step
1. Import and create a parser
Start by importing argparse and creating an ArgumentParser object. Give it a description — this shows up in the --help output.
import argparse
parser = argparse.ArgumentParser(description='Process some integers.')
2. Add arguments
Use .add_argument() to define each argument. The first argument is the name or flag. For positional arguments, just give a name (e.g., 'filenames'). For optional ones, use flag prefixes like - or -- (e.g., '--output', '-v').
parser.add_argument('filenames', nargs='+', help='Input files to process')
parser.add_argument('--output', '-o', help='Output file name')
parser.add_argument('--verbose', '-v', action='store_true', help='Increase output verbosity')
nargs='+'means one or more positional values.action='store_true'storesTrueif the flag is present,Falseotherwise.
3. Parse the command line
Call .parse_args() — it reads sys.argv automatically and returns a namespace.
args = parser.parse_args()
Now args.filenames, args.output, and args.verbose are available.
4. Use the values
Access parsed values as attributes of args and write your script logic.
if args.verbose:
print('Verbose mode enabled')
print(f"Processing files: {args.filenames}")
print(f"Output to: {args.output or 'stdout'}")
That's the skeleton. Now let's build a real tool.
Hands-on walkthrough
Example 1: A file duplication tool (basic)
Create a script dup.py that copies a file to a new name.
import argparse
import shutil
import os
parser = argparse.ArgumentParser(description='Duplicate a file with an optional new name.')
parser.add_argument('source', help='Source file to copy')
parser.add_argument('dest', nargs='?', default=None, help='Destination filename (default: source name with _copy)')
args = parser.parse_args()
dest = args.dest or f"{os.path.splitext(args.source)[0]}_copy{os.path.splitext(args.source)[1]}"
try:
shutil.copy2(args.source, dest)
print(f"Copied {args.source} to {dest}")
except FileNotFoundError:
print(f"Error: Source file '{args.source}' not found.")
exit(1)
Run it:
$ python dup.py sample.txt
Copied sample.txt to sample_copy.txt
$ python dup.py sample.txt backup.txt
Copied sample.txt to backup.txt
$ python dup.py missing.txt
Error: Source file 'missing.txt' not found.
Notice:
nargs='?'makesdestoptional.default=Nonelets us set a sensible default.
Example 2: Log file analyzer with options
A script that scans a log file for error counts, with optional verbosity and output control.
import argparse
import re
def main():
parser = argparse.ArgumentParser(description='Analyze log files for error counts.')
parser.add_argument('logfile', help='Path to the log file')
parser.add_argument('--pattern', '-p', default='ERROR|Error|error', help='Regex pattern to count (default: ERROR)')
parser.add_argument('--output', '-o', help='Save results to this file')
parser.add_argument('--verbose', '-v', action='store_true', help='Print summary during processing')
args = parser.parse_args()
try:
with open(args.logfile, 'r') as f:
content = f.read()
matches = re.findall(args.pattern, content)
count = len(matches)
output_lines = f"File: {args.logfile}\nPattern: {args.pattern}\nMatches: {count}\n"
if args.verbose:
print(output_lines)
if args.output:
with open(args.output, 'w') as out:
out.write(output_lines)
except FileNotFoundError:
print("Error: Log file not found.")
exit(1)
if __name__ == '__main__':
main()
Run and test:
$ echo -e "INFO: start\nERROR: timeout\nINFO: done" > demo.log
$ python log_analyzer.py demo.log -v -o results.txt
File: demo.log
Pattern: ERROR|Error|error
Matches: 1
$ cat results.txt
File: demo.log
Pattern: ERROR|Error|error
Matches: 1
Example 3: Calculator with subcommands
For tools that do multiple things (like git), use subparsers.
import argparse
def main():
parser = argparse.ArgumentParser(description='Simple calculator with subcommands.')
subparsers = parser.add_subparsers(dest='command', required=True)
# Add subcommand
add_parser = subparsers.add_parser('add', help='Add two numbers')
add_parser.add_argument('x', type=float, help='First number')
add_parser.add_argument('y', type=float, help='Second number')
# Subtract subcommand
sub_parser = subparsers.add_parser('sub', help='Subtract two numbers')
sub_parser.add_argument('x', type=float, help='First number')
sub_parser.add_argument('y', type=float, help='Second number')
args = parser.parse_args()
if args.command == 'add':
print(args.x + args.y)
elif args.command == 'sub':
print(args.x - args.y)
if __name__ == '__main__':
main()
$ python calc.py add 3 4
7.0
$ python calc.py sub 10 2.5
7.5
$ python calc.py --help
usage: calc.py [-h] {add,sub} ...
Simple calculator with subcommands.
positional arguments:
{add,sub} available subcommands
add Add two numbers
sub Subtract two numbers
Compare options / when to choose what
| Approach | Use case | Pros | Cons |
|---|---|---|---|
argparse |
Standard CLI tools (most Python scripts) | Built-in, powerful, help generation, subcommands | Verbose for very simple needs |
click (3rd party) |
Modern, decorator-based CLIs | Concise, auto-aware of types, nested commands | Extra dependency (pip install click) |
sys.argv |
Quick one-liners or scripts with ≤2 arguments | No imports, trivial setup | No help, no type conversion, messy |
typer (3rd party) |
Type-hint-driven CLIs | Pythonic, zero boilerplate, async support | Requires third-party library |
Recommendation: Start with
argparse— it's always available, battle-tested, and covers 90% of CLI needs. Switch toclickortyperwhen you hit complex nested commands or want a more declarative style.
Troubleshooting & edge cases
- Unrecognized arguments: If you misspell a flag, argparse prints an error and exits with a usage message. Always check the help output.
bash $ python dup.py --verbos usage: dup.py [-h] [-o OUTPUT] source [dest] dup.py: error: unrecognized arguments: --verbos - Missing required positional arguments: Forgetting to provide a positional argument triggers an error.
bash $ python dup.py usage: dup.py [-h] [-o OUTPUT] source [dest] dup.py: error: the following arguments are required: source - Type conversion: Use
type=intortype=floatto auto-convert. If conversion fails, argparse gives a clear error.python parser.add_argument('count', type=int, help='Number of iterations') - Conflicting flags: Don't use the same short flag (
-o) for two different arguments. If needed, define unique flags. - Subcommand default: If you set
dest='command'withrequired=True, users must provide a subcommand — you can control error messages withset_defaults(func=...).
What you learned & what's next
You now know how to design a simple CLI application with argparse — from basic positional arguments to optional flags, subcommands, and user-friendly help. You've seen how argparse eliminates manual parsing, adds validation, and makes your scripts feel like real command-line tools.
You can now: - Explain the core idea behind argparse — defining a menu and parsing arguments. - Complete a practical exercise that adds arguments, handles errors, and produces a usable CLI.
Next step: Move on to "Create a Python package for distribution" — where you'll learn to bundle your CLI script into an installable package so anyone can run it with a single pip install. Keep that argparse-based tool handy; you'll use it as your package!
Practice recap
Mini-exercise: Build a CLI tool called greeter.py that accepts a positional name and an optional --greeting (default "Hello") and --count (default 1, type int). Print the greeting count times. Then add a --shout flag that uppercases the message. Test it with python greeter.py Alice --greeting "Hi" --count 3 --shout.
Common mistakes
- Omitting
type=causes all arguments to be strings — you must explicitly settype=intfor numeric values. - Using
--flagwithoutaction='store_true'leads to errors — argparse expects a value after a flag unlessactionis set. - Forgetting to call
parser.parse_args()— nothing happens until you parse; you'll get a namespace object, not usable variables. - Mixing positional and optional arguments incorrectly — positional order matters, but optional can appear anywhere.
- Ignoring the default
Namespaceobject — attempting to accessargs.undefined_argsilently returnsNoneinstead of raising an error.
Variations
- Use
nargs='*'for zero or more positional arguments — unlikenargs='+', empty lists are allowed. - Add
required=Trueto make an optional flag mandatory — useful for forcing explicit input without using positional arguments. - Leverage
add_subparsers()for multi-command tools (like Git) where each subcommand has its own arguments.
Real-world use cases
- A log analysis tool that takes a log file and optional regex pattern, outputting error counts to stdout or a file.
- A backup script accepting source and destination paths, with a
--compressflag that enables tarball creation. - A database migration CLI with subcommands like 'upgrade', 'downgrade', and 'status', each with specific flags.
Key takeaways
- argparse handles command-line argument parsing, validation, and help generation automatically.
- Positional arguments are required by default; optional arguments use flag prefixes like
-oor--output. - Use
type=int,action='store_true', andnargsto control parsing behavior precisely. - Subparsers let you build multi-command interfaces (like Git) with independent argument sets per command.
- Always call
parser.parse_args()to trigger parsing — without it, your script ignores the command line. - Choose
argparsefor standard CLIs because it's built-in, powerful, and well-documented.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.