What you'll learn
- Turn a script into a command that takes positional arguments and flags
- Get
--helpoutput, type conversion, defaults, and validation for free - Use choices, repeatable flags, and boolean switches
- Structure a script so the parser, the logic, and the entry point stay separate
Concept
A script that only works with values hard-coded at the top is a script you edit every time you run it. A command reads its inputs from the command line: organize photos/ --by year --dry-run. Python exposes the raw words in sys.argv, but parsing them by hand means writing help text, converting types, and reporting mistakes yourself. argparse does all of that from a declaration of what the command accepts.

You build an ArgumentParser, call add_argument once per input, then parse_args() returns an object with one attribute per argument. Positional arguments are required and identified by order. Options start with dashes, --by or -v, are optional unless you say otherwise, and become attributes with dashes turned to underscores. type=int converts and rejects non-numbers, default= fills in absent options, choices= restricts values, action="store_true" makes a flag that is False unless present, and action="append" collects a flag given several times into a list. help= on each argument builds the --help page.
add_argument(...) |
User types | You get |
|---|---|---|
"folder" |
photos/ |
args.folder == "photos/" |
"--by", choices=["year", "type"], default="type" |
--by year |
args.by == "year" |
"--limit", type=int, default=10 |
--limit 5 |
args.limit == 5 |
"--dry-run", action="store_true" |
--dry-run |
args.dry_run is True |
"-t", "--tag", action="append" |
-t a -t b |
args.tag == ["a", "b"] |
Structure matters as much as parsing. Put the parser in a function that builds and returns it, the real work in functions that take plain values, and a main(argv=None) that connects them, called under the main guard. The logic then never touches argparse, so it can be imported and tested with ordinary arguments, and main can be tested by passing a list of strings instead of relying on the real command line. Exit with sys.exit(1) or a parser.error(...) when something is wrong, so shells and CI can tell success from failure.

Code
A complete small command, saved as a file for the later blocks to use. The parser is built in one function, the work happens in another, and main takes an optional argument list so it can be driven from Python as well as the shell.
# organize.py
import argparse
import sys
def build_parser():
parser = argparse.ArgumentParser(prog="organize", description="Sort files in a folder into subfolders.")
parser.add_argument("folder", help="folder to organize")
parser.add_argument("--by", choices=["year", "type"], default="type", help="grouping rule (default: type)")
parser.add_argument("--limit", type=int, default=100, help="max files to move")
parser.add_argument("--dry-run", action="store_true", help="show what would happen, move nothing")
parser.add_argument("-t", "--tag", action="append", default=[], help="tag to add; repeatable")
return parser
def organize(folder, by, limit, dry_run, tags):
verb = "Would move" if dry_run else "Moving"
return f"{verb} up to {limit} files in {folder} by {by}, tags={tags}"
def main(argv=None):
args = build_parser().parse_args(argv)
if args.limit <= 0:
build_parser().error("--limit must be positive")
print(organize(args.folder, args.by, args.limit, args.dry_run, args.tag))
return 0
if __name__ == "__main__":
sys.exit(main())
Driving the command from Python with argument lists, exactly as a test would. Defaults, conversion, a repeated flag, and a boolean switch all show up on the args object.
# drive.py
from organize import build_parser, main
args = build_parser().parse_args(["photos", "--by", "year", "-t", "trip", "-t", "2026", "--dry-run"])
print(args)
main(["photos"])
main(["docs", "--limit", "5", "--by", "year"])
Namespace(folder='photos', by='year', limit=100, dry_run=True, tag=['trip', '2026'])
Moving up to 100 files in photos by type, tags=[]
Moving up to 5 files in docs by year, tags=[]The help page is generated from the declarations. Running with --help prints it and exits with code 0.
# show_help.sh
python organize.py --help
usage: organize [-h] [--by {year,type}] [--limit LIMIT] [--dry-run] [-t TAG]
folder
Sort files in a folder into subfolders.
positional arguments:
folder folder to organize
options:
-h, --help show this help message and exit
--by {year,type} grouping rule (default: type)
--limit LIMIT max files to move
--dry-run show what would happen, move nothing
-t, --tag TAG tag to add; repeatableValidation is free. A bad choice and a non-integer are rejected with a usage line and exit code 2, before any of your code runs.
# bad_input.sh
python organize.py photos --by size
echo "exit code: $?"
python organize.py photos --limit ten
usage: organize [-h] [--by {year,type}] [--limit LIMIT] [--dry-run] [-t TAG]
folder
organize: error: argument --by: invalid choice: 'size' (choose from 'year', 'type')
exit code: 2
usage: organize [-h] [--by {year,type}] [--limit LIMIT] [--dry-run] [-t TAG]
folder
organize: error: argument --limit: invalid int value: 'ten'Try it yourself
Create wordcount.py, a command that takes one or more file paths (nargs="+"), a --top N option with a default of 5, and a --ignore-case switch. It should print the most common words across all the files using Counter. Write two small text files, run the command with and without the flags, then run it with --help and with a missing file to see argparse's messages.
Common mistakes
- Parsing
sys.argvby hand. The moment you need help text or a second flag,argparsehas already paid for itself. - Mixing parsing and logic. If the function that does the work reads
args.folder, it cannot be reused or tested without a parser; pass plain values. - Forgetting the exit code. A command that prints an error and returns 0 looks successful to every script that calls it; use
parser.errororsys.exit(1).
What's next
ArgumentParserplusadd_argumentdeclares positionals, options, types, defaults, choices, switches, and repeatable flags;parse_argsreturns them as attributes.--help, type conversion, and validation come free, with exit code 2 on bad input.- Keep the parser, the logic, and
main(argv=None)separate so everything can be tested from Python.
Your scripts are real commands now. Next they will run other programs and manage files on disk, the automation half of scripting: subprocess, shutil, and os.