Command-line Apps with argparse
Many useful programs have no windows or buttons. You run them in a terminal and type instructions after the name: git commit -m "fix", pip install requests, python -m venv .venv. The words after the program name are called command-line arguments, and a program that reads them is a command-line app (CLI app). They are quick to build, easy to automate, and perfect for scripts that rename files, process CSV reports or back up folders.
In this lesson you will read arguments by hand with sys.argv, then use the standard argparse module to build tools with options, automatic help, clear error messages and sub-commands.
The raw way: sys.argv
Python puts the words typed on the command line into the list sys.argv. Item 0 is the script name, and the rest are the arguments, always as strings.
import sys
print(sys.argv)
name = sys.argv[1]
times = int(sys.argv[2])
for _ in range(times):
print(f"Namaste, {name}!")python greet.py Priya 2['greet.py', 'Priya', '2'] Namaste, Priya! Namaste, Priya!
This works until someone forgets an argument (IndexError: list index out of range), types two instead of 2 (ValueError), or asks how to use the program. Handling all of that by hand takes a lot of code. argparse does it for you.
Your first ArgumentParser
You describe the arguments your program accepts, and argparse reads sys.argv, converts the values, and returns them as attributes of a Namespace object.
parse_args() normally reads the real command line. You can also pass it a list of words, which is how you test a parser, and how every runnable example in this lesson works in the browser.
import argparse
parser = argparse.ArgumentParser(prog="fare", description="Work out an auto-rickshaw fare.")
parser.add_argument("km", type=float, help="distance in kilometres")
parser.add_argument("--night", action="store_true", help="add the night charge")
args = parser.parse_args(["7.5"])
print(args)
print(args.km * 15, args.night)
args = parser.parse_args(["12", "--night"])
fare = args.km * 15 * (1.5 if args.night else 1)
print(f"Fare: ₹{fare:.0f}")Namespace(km=7.5, night=False) 112.5 False Fare: ₹270
"km"has no dashes, so it is a positional argument: it is required, and its meaning comes from its position.type=floatconverts the text"7.5"to a number. Any function that takes a string works as a type."--night"starts with dashes, so it is an optional argument.action="store_true"makes it a flag:Trueif typed,Falseif not.
Options, defaults and choices
Most real tools combine several kinds of options. These are the settings you will use most:
| Setting | Effect | Example |
|---|---|---|
default= | Value when the option is not given | default=1 |
choices= | Only these values are allowed | choices=["Delhi", "Agra"] |
nargs="+" | One or more values, collected in a list | --names Aarav Isha |
nargs="?" | An optional positional argument | |
required=True | The option must be given (use sparingly) | |
"-n", "--seats" | A short and a long name for one option | -n 2 or --seats 2 |
dest= | A different attribute name | dest="count" |
A dash inside an option name becomes an underscore in the Namespace, so --from-city is read as args.from_city.
import argparse
parser = argparse.ArgumentParser(prog="bus", description="Book bus seats from Jaipur.")
parser.add_argument("city", choices=["Delhi", "Agra", "Udaipur"], help="where you are going")
parser.add_argument("-n", "--seats", type=int, default=1, help="number of seats (default: 1)")
parser.add_argument("--names", nargs="+", default=[], help="passenger names")
parser.add_argument("--ac", action="store_true", help="book an AC bus")
print(parser.parse_args(["Agra"]))
print(parser.parse_args(["Delhi", "-n", "2", "--names", "Rohan", "Meera", "--ac"]))
print()
parser.print_help()Namespace(city='Agra', seats=1, names=[], ac=False)
Namespace(city='Delhi', seats=2, names=['Rohan', 'Meera'], ac=True)
usage: bus [-h] [-n SEATS] [--names NAMES [NAMES ...]] [--ac]
{Delhi,Agra,Udaipur}
Book bus seats from Jaipur.
positional arguments:
{Delhi,Agra,Udaipur} where you are going
options:
-h, --help show this help message and exit
-n, --seats SEATS number of seats (default: 1)
--names NAMES [NAMES ...]
passenger names
--ac book an AC busYou never wrote that help text. argparse built it from your help= strings, and users see it by typing --help or -h. Options can appear in any order on the command line.
Helpful errors and custom types
When the input is wrong, argparse prints a short usage line and a clear error to the error stream, then stops the program with exit code 2. An exit code is a number a program returns to the terminal: 0 means success and anything else means failure, which scripts and other tools can check. In the browser editor we catch the SystemExit exception so the example keeps running.
For rules that type=int cannot express, write your own type function. Raise argparse.ArgumentTypeError and argparse turns your message into a proper error.
import argparse
def positive_int(text):
value = int(text)
if value < 1:
raise argparse.ArgumentTypeError(f"must be at least 1, got {value}")
return value
parser = argparse.ArgumentParser(prog="bus")
parser.add_argument("city", choices=["Delhi", "Agra", "Udaipur"])
parser.add_argument("-n", "--seats", type=positive_int, default=1)
for words in (["Pune"], ["Agra", "--seats", "0"], ["Agra", "--seats", "two"]):
try:
parser.parse_args(words)
except SystemExit as stop:
print("Exit code:", stop.code)usage: bus [-h] [-n SEATS] {Delhi,Agra,Udaipur}
bus: error: argument city: invalid choice: 'Pune' (choose from 'Delhi', 'Agra', 'Udaipur')
Exit code: 2
usage: bus [-h] [-n SEATS] {Delhi,Agra,Udaipur}
bus: error: argument -n/--seats: must be at least 1, got 0
Exit code: 2
usage: bus [-h] [-n SEATS] {Delhi,Agra,Udaipur}
bus: error: argument -n/--seats: invalid positive_int value: 'two'
Exit code: 2If your type function raises ValueError (as int("two") does), argparse reports "invalid ... value" using the function's name, so give type functions clear names.
Sub-commands
Bigger tools have sub-commands, each with its own arguments, like git add and git commit. Create them with add_subparsers(). A neat pattern is set_defaults(func=...): each sub-command stores the function that should handle it, and the main code simply calls args.func(args).
import argparse
notes = ["Buy chalk for class 7", "Call Farhan about the fest"]
def cmd_list(args):
for number, note in enumerate(notes, start=1):
print(number, note)
def cmd_add(args):
text = " ".join(args.words)
notes.append(text.upper() if args.urgent else text)
print("Added note", len(notes))
parser = argparse.ArgumentParser(prog="notes")
commands = parser.add_subparsers(dest="command", required=True)
list_cmd = commands.add_parser("list", help="show all notes")
list_cmd.set_defaults(func=cmd_list)
add_cmd = commands.add_parser("add", help="add a note")
add_cmd.add_argument("words", nargs="+")
add_cmd.add_argument("--urgent", action="store_true")
add_cmd.set_defaults(func=cmd_add)
for words in (["add", "Print", "exam", "timetable", "--urgent"], ["list"]):
args = parser.parse_args(words)
args.func(args)Added note 3 1 Buy chalk for class 7 2 Call Farhan about the fest 3 PRINT EXAM TIMETABLE
required=True on the sub-parsers means that running notes with no command shows an error instead of doing nothing. The Project: Expense Tracker builds a complete tool with this structure.
A complete script
Real CLI scripts usually follow one shape: a main(argv=None) function that parses arguments and returns an exit code. Passing None makes parse_args() read the real command line, while tests can pass a list.
import argparse
def build_parser():
parser = argparse.ArgumentParser(prog="bus", description="Book bus seats from Jaipur.")
parser.add_argument("city", choices=["Delhi", "Agra", "Udaipur"], help="where you are going")
parser.add_argument("-n", "--seats", type=int, default=1, help="number of seats (default: 1)")
parser.add_argument("--ac", action="store_true", help="book an AC bus")
return parser
def main(argv=None):
args = build_parser().parse_args(argv)
fare = {"Delhi": 650, "Agra": 480, "Udaipur": 720}[args.city]
if args.ac:
fare += 250
print(f"{args.seats} seat(s) to {args.city}: ₹{fare * args.seats}")
return 0
if __name__ == "__main__":
raise SystemExit(main())python bus.py Udaipur -n 2 --ac2 seat(s) to Udaipur: ₹1940
raise SystemExit(main()) passes the return value to the terminal as the exit code. When you later package the tool (see Packaging with pyproject.toml), the same main() becomes a command users can run without typing python.
Third-party options: click and typer
argparse is always available, which makes it the safe default. Two popular packages from PyPI offer a different style:
- click builds commands with decorators such as
@click.command()and@click.option(). - typer reads your function's type hints and builds the whole interface from them.
import typer
def main(city: str, seats: int = 1, ac: bool = False):
"""Book bus seats to CITY."""
kind = "AC" if ac else "non-AC"
print(f"Booked {seats} {kind} seat(s) to {city}")
if __name__ == "__main__":
typer.run(main)python book.py Jaipur --seats 2 --acBooked 2 AC seat(s) to Jaipur
Install it with python -m pip install typer inside a virtual environment. For a small script, argparse is plenty; for a large tool with many commands, click or typer can save typing.
Common mistakes
- Forgetting that arguments are strings. Without
type=int,args.seats * 2repeats the text:"22". - Using
type=bool.bool("False")isTrue. Useaction="store_true"for flags. - Making options required. If a value is always needed, make it positional instead of
required=True. - Testing by typing in the terminal every time. Pass a list to
parse_args()in tests. - Calling
parse_args()at import time. Keep it insidemain()so other code can import your module without side effects.
Key points
sys.argvholds the raw words, butargparseadds conversion, validation, errors and--help.- Positional arguments are required; names starting with
--are optional. type=,default=,choices=,nargs=andaction="store_true"cover most needs.- Bad input prints a usage line and an error, then exits with code 2; custom types raise
ArgumentTypeError. add_subparsers()withset_defaults(func=...)builds git-style sub-commands.- Wrap everything in
main(argv=None); consider click or typer for large tools.
Exercise
Write a parser for a tool called marks that takes one or more whole-number marks as positional arguments, an optional --pass-mark (whole number, default 40) and a --show-failed flag. Test it with parse_args(["78", "35", "91", "22", "--show-failed"]). Print the average and the number of passes, and when the flag is on, also print the failed marks.
Show solution
import argparse
parser = argparse.ArgumentParser(prog="marks", description="Summarise exam marks.")
parser.add_argument("marks", type=int, nargs="+", help="marks to summarise")
parser.add_argument("--pass-mark", type=int, default=40)
parser.add_argument("--show-failed", action="store_true")
args = parser.parse_args(["78", "35", "91", "22", "--show-failed"])
passed = [m for m in args.marks if m >= args.pass_mark]
print(f"Average: {sum(args.marks) / len(args.marks):.1f}")
print("Passed:", len(passed))
if args.show_failed:
print("Failed:", [m for m in args.marks if m < args.pass_mark])Average: 56.5 Passed: 2 Failed: [35, 22]
nargs="+" collects all the marks into a list, and type=int converts each one. The option --pass-mark is read as args.pass_mark, with an underscore.
Quick Quiz
1. What is in sys.argv[0]?
Item 0 is the script name; the user's arguments start at item 1.
2. How do you make an option that is True when typed and False otherwise?
store_true creates a flag; type=bool would turn any non-empty text into True.
3. What does argparse do when the user gives an invalid choice?
argparse stops the program early with a helpful message and exit status 2.
4. How is the option --pass-mark read from the Namespace?
Dashes in option names become underscores in the attribute name.
Learn Python with a mentor
Free lessons get you started. Our Python Programming training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
