Warm-up · Activity 1 of 7
Warm-up from the modules lesson: this file is run with python main.py. What does it print?
print(__name__)// I4.2 · ~35 min · Intermediate
After this lesson you can turn a script into a command-line program: it reads arguments and options with argparse, prints errors to stderr and ends with the right exit status.
You will be able to
Warm-up · Activity 1 of 7
print(__name__)Predict · Activity 2 of 7
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("name")
parser.add_argument("--times", type=int, default=1)
args = parser.parse_args(["Ada", "--times", "3"])
print(args.name * args.times)Practice · Activity 3 of 7
parser.add_argument("--verbose", action="____")Practice · Activity 4 of 7
import sys
sys.exit("disk full")Practice · Activity 5 of 7
Brain teaser · Activity 6 of 7
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--debug", type=bool)
args = parser.parse_args(["--debug", "False"])
print(args.debug)Apply · Activity 7 of 7
Check your work against this list
Read the worked example, then write the exercises. Your code runs in your browser or on your computer and is never uploaded.
Worked example
wordcount counts the words of a text, with an option --min-length and a flag -u. So that you can see several command lines at once, demo() runs main with argument lists and shows the exit status; stderr is sent to the output too. On your computer, replace the demo calls with sys.exit(main()) and run python main.py "the cat sat" --min-length 3.
main.py
import argparse
import shlex
import sys
from contextlib import redirect_stderr
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(prog="wordcount", description="Count the words in a text.")
parser.add_argument("text", help="the text to count")
parser.add_argument("--min-length", type=int, default=1, help="skip shorter words")
parser.add_argument("-u", "--upper", action="store_true", help="print the words in upper case")
args = parser.parse_args(argv) # None: read sys.argv[1:]
words = [word for word in args.text.split() if len(word) >= args.min_length]
if not words:
print("wordcount: no words left", file=sys.stderr)
return 1
shown = " ".join(words).upper() if args.upper else " ".join(words)
print(f"{len(words)} words: {shown}")
return 0
def demo(argv: list[str]) -> None:
"""Runs main as if typed in a terminal; stderr shows up in the output too."""
print(f"$ python main.py {shlex.join(argv)}".rstrip())
with redirect_stderr(sys.stdout):
try:
status = main(argv)
except SystemExit as stop: # argparse exits on --help and on bad input
status = stop.code if isinstance(stop.code, int) else 1
print("exit status", status)
if __name__ == "__main__":
# On your computer, replace these demos with: sys.exit(main())
demo(["the cat sat on the mat", "--min-length", "3"])
demo(["-u", "hello world"])
demo(["a b", "--min-length", "5"])
demo(["hello", "--min-length", "three"])
demo([])
demo(["--help"])
Run it with
python main.pyOutput
$ python main.py 'the cat sat on the mat' --min-length 3
5 words: the cat sat the mat
exit status 0
$ python main.py -u 'hello world'
2 words: HELLO WORLD
exit status 0
$ python main.py 'a b' --min-length 5
wordcount: no words left
exit status 1
$ python main.py hello --min-length three
usage: wordcount [-h] [--min-length MIN_LENGTH] [-u] text
wordcount: error: argument --min-length: invalid int value: 'three'
exit status 2
$ python main.py
usage: wordcount [-h] [--min-length MIN_LENGTH] [-u] text
wordcount: error: the following arguments are required: text
exit status 2
$ python main.py --help
usage: wordcount [-h] [--min-length MIN_LENGTH] [-u] text
Count the words in a text.
positional arguments:
text the text to count
options:
-h, --help show this help message and exit
--min-length MIN_LENGTH
skip shorter words
-u, --upper print the words in upper case
exit status 0Exercise 1 of 2
Complete main(argv) in greet.py (here main.py). It takes a name, an option --times (an int, default 1) and a flag --shout. It prints Hello, <name>! once per time, in capitals with --shout, and returns 0. A --times that is not a number must end with argparse's exit status 2. Run the tests, then try python main.py Ada --times 3 --shout on your computer (python3 on macOS and Linux).
This exercise needs Python on your computer (the browser version cannot run it). The files and commands are below.
parser.add_argument("--times", type=int, default=1) makes args.times an int that is 1 when left out.
action="store_true" makes --shout a flag; test it with if args.shout:.
Build the greeting once, call .upper() when shouting, and print it in for _ in range(args.times):.
One way to solve it. Yours can look different and still pass the checks.
import argparse
import sys
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(prog="greet")
parser.add_argument("name")
parser.add_argument("--times", type=int, default=1)
parser.add_argument("--shout", action="store_true")
args = parser.parse_args(argv)
greeting = f"Hello, {args.name}!"
if args.shout:
greeting = greeting.upper()
for _ in range(args.times):
print(greeting)
return 0
if __name__ == "__main__":
sys.exit(main())
Install Python 3.14 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.
main.py
import argparse
import sys
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(prog="greet")
parser.add_argument("name")
# Add --times (an int, default 1) and the flag --shout.
args = parser.parse_args(argv)
print(f"Hello, {args.name}!")
return 0
if __name__ == "__main__":
sys.exit(main())
test_main.py
import contextlib
import io
from main import main
def run(argv):
out = io.StringIO()
err = io.StringIO()
with contextlib.redirect_stdout(out), contextlib.redirect_stderr(err):
try:
status = main(argv)
except SystemExit as stop:
status = stop.code
return status, out.getvalue().splitlines(), err.getvalue()
def test_default():
"""Ada alone prints one greeting and returns 0"""
status, lines, err = run(["Ada"])
assert (status, lines) == (0, ["Hello, Ada!"]), f"main(['Ada']) returned {status!r} and printed {lines!r}; stderr: {err!r}"
def test_times():
"""--times 3 prints the greeting three times"""
status, lines, err = run(["Ada", "--times", "3"])
assert lines == ["Hello, Ada!"] * 3, f"with --times 3 the program printed {lines!r}; stderr: {err!r}"
def test_shout():
"""--shout prints in capitals, before or after the name"""
status, lines, err = run(["--shout", "Bo"])
assert lines == ["HELLO, BO!"], f"with --shout the program printed {lines!r}; stderr: {err!r}"
def test_bad_times():
"""--times many ends with exit status 2"""
status, lines, err = run(["Ada", "--times", "many"])
assert status == 2, f"with --times many the status was {status!r}, expected 2 (let type=int reject it)"
On macOS and Linux, type python3 wherever these commands say python, as in the first lesson.
Run the program:
python main.pyRun the checks (needs learnrun.py in the same folder):
python learnrun.py testDownload learnrun.pyExercise 2 of 2
main.py averages the numbers on its command line, with --max (default 100). Fix it: when a number is above the maximum, print average: 500.0 is above the maximum 100.0 (with the real values) to stderr, print nothing to stdout and return 1. Otherwise print the average with two decimals and return 0. Run as a program, the status must reach the shell: end with sys.exit(main()).
This exercise needs Python on your computer (the browser version cannot run it). The files and commands are below.
print(..., file=sys.stderr) sends a line to stderr instead of stdout.
Return 1 right after printing the error, so the average is never printed.
main() returns the status, but only sys.exit(main()) hands it to the shell.
One way to solve it. Yours can look different and still pass the checks.
import argparse
import sys
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(prog="average")
parser.add_argument("numbers", nargs="+", type=float)
parser.add_argument("--max", type=float, default=100.0)
args = parser.parse_args(argv)
for number in args.numbers:
if number > args.max:
print(f"average: {number} is above the maximum {args.max}", file=sys.stderr)
return 1
print(f"{sum(args.numbers) / len(args.numbers):.2f}")
return 0
if __name__ == "__main__":
sys.exit(main())
Install Python 3.14 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.
main.py
import argparse
import sys
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(prog="average")
parser.add_argument("numbers", nargs="+", type=float)
parser.add_argument("--max", type=float, default=100.0)
args = parser.parse_args(argv)
for number in args.numbers:
if number > args.max:
print(f"average: {number} is above the maximum {args.max}")
print(f"{sum(args.numbers) / len(args.numbers):.2f}")
return 0
if __name__ == "__main__":
main()
test_main.py
import contextlib
import io
import sys
from learnrun import run_main
from main import main
def run(argv):
out = io.StringIO()
err = io.StringIO()
with contextlib.redirect_stdout(out), contextlib.redirect_stderr(err):
try:
status = main(argv)
except SystemExit as stop:
status = stop.code
return status, out.getvalue(), err.getvalue()
def test_average():
"""2, 4 and 9 give 5.00 and status 0"""
status, out, err = run(["2", "4", "9"])
assert (status, out.strip()) == (0, "5.00"), f"main returned {status!r} and printed {out!r}"
def test_above_max():
"""500 is reported on stderr, with status 1 and nothing on stdout"""
status, out, err = run(["5", "500"])
assert status == 1, f"main returned {status!r}, expected 1"
assert out == "", f"stdout should be empty, but it holds {out!r}"
assert err.strip() == "average: 500.0 is above the maximum 100.0", f"stderr holds {err!r}"
def test_custom_max():
"""--max 10 rejects 11"""
status, out, err = run(["--max", "10", "5", "11"])
assert status == 1, f"with --max 10 and 11, main returned {status!r}, expected 1"
def test_not_a_number():
"""A word instead of a number ends with status 2"""
status, out, err = run(["five"])
assert status == 2, f"for five, the status was {status!r}, expected 2"
def test_status_reaches_the_shell():
"""Run as a program, python main.py 5 500 exits with status 1"""
saved = sys.argv
sys.argv = ["main.py", "5", "500"]
code = 0
try:
with contextlib.redirect_stderr(io.StringIO()):
run_main()
except SystemExit as stop:
code = stop.code
finally:
sys.argv = saved
assert code == 1, f"the program ended with status {code!r}, expected 1: call sys.exit(main())"
On macOS and Linux, type python3 wherever these commands say python, as in the first lesson.
Run the program:
python main.pyRun the checks (needs learnrun.py in the same folder):
python learnrun.py testDownload learnrun.pyimport argparse
parser = argparse.ArgumentParser()
parser.add_argument("count")
args = parser.parse_args(["3"])
print(args.count + 1)
What Python prints
TypeError: can only concatenate str (not "int") to strWhy, and the fix
Every command-line argument arrives as a string, and argparse keeps it one unless you give a type. Write add_argument("count", type=int). Then argparse converts it, and rejects a value like three with a clear error and exit status 2.
import argparse
parser = argparse.ArgumentParser(prog="wordcount")
parser.add_argument("--min-length", type=int, default=1)
args = parser.parse_args(["--min-length", "3"])
print(args.min-length)
What Python prints
AttributeError: 'Namespace' object has no attribute 'min'Why, and the fix
args.min-length is read as args.min minus length. argparse turns the dashes inside an option name into underscores, so the value is args.min_length.
import argparse
parser = argparse.ArgumentParser(prog="greet")
parser.add_argument("name")
args = parser.parse_args([])
print("Hello,", args.name)
What Python prints
greet: error: the following arguments are required: nameWhy, and the fix
A positional argument must be given. argparse prints the usage and this error to stderr and exits with status 2, so print is never reached. To make it optional, turn it into an option with a default, such as add_argument("--name", default="world").
Python in the browser: Pyodide 314.0.7, MPL-2.0. Licence and source
5 questions, no hints. Score 80% or more to complete the lesson.
Finish every activity above to unlock the exit ticket.