Bite code!

Share this post

Parameters, options and flags for Python scripts

www.bitecode.dev

Parameters, options and flags for Python scripts

Theory and practice

Jul 4, 2023
5
Share this post

Parameters, options and flags for Python scripts

www.bitecode.dev
11
Share

Summary

While there are various ways of making a script configurable in Python, argparse is a very popular option. Unfortunately, it's also a cryptic one.

This article provides ready to copy/paste snippets for the common use cases, and explains how they work for when you will ask ChatGPT about it.

In short:

import argparse

parser = argparse.ArgumentParser(
    description="Description of the program"
)

parser.add_argument(
    "positional_argument", 
    type=str, 
    help="Required first command parameter"
)

parser.add_argument(
    "-f",
    "--flag",
    action="store_true",
    help="Will be either True or False",
)

parser.add_argument(
    "-o",
    "--option,
    help="Attach a value to the name 'option'",
)

args = parser.parse_args()

print(
  args.positional_argument,
  args.flag,
  args.option
)

I put that here for me as well, as ChatGPT is blocked by some of my clients, and I'm lazy.

This is where ChatGPT is awesome

The more you dev, the more you need to make scripts, and therefore, make them configurable. Now, for project maintenance, I use doit, which I wrote about a few weeks before.

However, for the rest, I still do that the old fashion way, which in Python means 90% of the time using argparse or click.

argparse is in the stdlib, so this is what people will use when they don't want to pip install stuff.

But argparse doc and API suck.

They are so bad I have a permanent file with an argparse snippets I used to copy/paste from, every time I wrote a script because I could never remember or find out quickly how to use it.

Well, that's what I used to do. Now I ask ChatGPT, it's amazing for these kinds of things, and never gets tired of repeating.

Yet, ChatGPT has a limitation: you need to know what to ask, and understand the answer to be able to use it.

This article will help you do just that.

Creating an argparse parser

Since we don't want to process sys.argv manually, we are going to use a parser. Let's create a script "hello_wizard.py":

import argparse
parser = argparse.ArgumentParser(description="Hogwash reveal")
args = parser.parse_args()

If we now run:

python hello_wizard.py

It will appear to do nothing.

But it will in fact do many things:

  • Generate documentation for the script.

  • Define a -h and a --help flags to display the documentation.

  • Analyze anything you might write after python hello_wizard.py.

  • Match it to existing parameters, options and flags.

  • The run the code associated to those parameters, options and flags.

For now, this means you can type python hello_wizard.py -h or python hello_wizard.py --help and it will display the documentation:

$ python hello_wizard.py --help
usage: hello_wizard.py [-h]

Hogwash reveal

options:
  -h, --help  show this help message and exit

Positional parameters

Positional parameters are parameters that are recognized depending of where they are.

E.G: if you do mkdir my_project, it will create a directory "my_project" where you are. mkdir is the command, but my_project is a positional parameter. The command knows it's the directory to create because it's the first thing that comes after it.

Let's add one to our script, your name:

import argparse

parser = argparse.ArgumentParser(description="Hogwash reveal")

# Define the positional parameter.
parser.add_argument("name", type=str, help="Your name")

# This finds if "name" has been passed to your script or not
args = parser.parse_args()

# All found values are attached to args as attributes
print(f"You're a hazard {args.name}")

If we use -h again, can see it's now part of the doc:

$ python hello_wizard.py -h
usage: hello_wizard.py [-h] name

Hogwash reveal

positional arguments:
  name        Your name

options:
  -h, --help  show this help message and exit

If you run the script without any parameters, it gives you an error message:

$ python hello_wizard.py
usage: hello_wizard.py [-h] name
hello_wizard.py: error: the following arguments are required: name

But if we pass a name, it will print it back:

$ python hello_wizard.py Mary
You're a hazard Mary

Positional parameters are often required, but we can add optional ones.

Let's add a second one:

import argparse

parser = argparse.ArgumentParser(description="Hogwash reveal")

parser.add_argument("name", type=str, help="Your name")

# The order matters: only a value coming after "name" will be
# considered the value "age" in the command line, because 
# the add_argument() for "name" came before this one.
parser.add_argument("age", type=int, nargs="?", help="Your age")

args = parser.parse_args()

if args.age is not None:
    print(f"Now that you are {args.age}, I can reveal your true nature")

print(f"You're a hazard {args.name}")

The script will work as before, if I just pass the name. But now, optionally, you can pass the age. In this case we get:

python hello_wizard.py Mary 19
Now that you are 19, I can reveal your true nature
You're a hazard Mary

The optionality comes from nargs="?". This tells how many times this argument is expected. It's a very obscure notation:

  • ? means the argument is expected zero or one time.

  • + means it's expected one or more times.

  • * means zero, one or more.

It's similar to regexes, so of course, it's as clear as mud.

We also changed type to pass int. If we pass an age that is not a number, it will give us an error:

$ python hello_wizard.py Mary old
usage: hello_wizard.py [-h] name [age]
script.py: error: argument age: invalid int value: 'old'

And of course, all that will be reported in the help:

$ python hello_wizard.py -h
usage: hello_wizard.py [-h] name [age]

Hogwash reveal

positional arguments:
  name        Your name
  age         Your age

options:
  -h, --help  show this help message and exit

Note that age is noted "[age]". Using brackets is the convention to say it's optional.

Flags

A flag is a parameter with a name, and is either on or off. It is found, not by position, but by this name.

E.G: -h and --help are flags.

Flags can have a short form, with one dash and one letter (like -h), or a long form with two dashes and several letters (like --help).

Some commands use a different convention (tar has short flags without dash, find has long options with a single dash, some Windows use using slashes instead of dashes).

But the world has generally agreed on using this convention nowadays.

Let's add one flag:

import argparse

parser = argparse.ArgumentParser(description="Hogwash reveal")

parser.add_argument("name", type=str, help="Your name")

parser.add_argument("age", type=int, nargs="?", help="Your age")

# Flag position doesn't matter.
# The can omit either the long or the short form, but you 
# need at least one
parser.add_argument(
    "-m",
    "--is-main-character",
    action="store_true",
    help="The person is the main character",
)

args = parser.parse_args()

if args.age is not None:
    print(f"Now that you are {args.age}, I can reveal your true nature")
print(f"You're a hazard {args.name}")

# Dashes can't be used in python variable names, 
# so the attribute is named using underscores.
# If you have only a short form, this would use it instead.
if args.is_main_character:
    print("Also, here's a pile of cash")

You'll note that we do everything using add_argument, which makes the whole thing confusing. You really have to know argparse to understand which one does what.

The help message gains an "options" section:

python hello_wizard.py -h
usage: hello_wizard.py [-h] [-m] name [age]

Hogwash reveal

positional arguments:
  name        Your name
  age         Your age

options:
  -h, --help            show this help message and exit
  -m, --is-main-character
                        The person is the main character

It's because flags are a specific case of options, which we will see later on.

And of course, you can now use your flag:

$ python hello_wizard.py Mary --is-main-character
You're a hazard Mary
Also, here's a pile of cash

$ python hello_wizard.py Mary -m 19
usage: hello_wizard.py [-h] [-m] name [age]
hello_wizard.py: error: unrecognized arguments: 19

$ python hello_wizard.py --is-main-character Mary  19
Now that you are 19, I can reveal your true nature
You're a hazard Mary
Also, here's a pile of cash

Options

An option is a parameter with a name and a value. Like a flag, it is found, not by position, but by this name. But unlike a flag, an option has a value attached to it. In fact, a flag is just a special option.

The Python command has the -c option, which let you execute code directly from the cmd. E.G:

python -c "print('Wingarda Leviosum')"
Wingarda Leviosum

The option print('Wingarda Leviosum') is the value that comes with -c, and you can't use -c without passing any value, that would make no sense.

We can add an option to our script to make everything uppercase or lowercase:

import argparse


def speak(*texts, situation=None):
    text = " ".join(texts)
    if situation == "sequel":
        print(text.upper())
    elif situation == "parsertongue":
        print(text.lower())
    else:
        print(text)


parser = argparse.ArgumentParser(description="Hogwash reveal")

parser.add_argument("name", type=str, help="Your name")

parser.add_argument("age", type=int, nargs="?", help="Your age")

parser.add_argument(
    "-m",
    "--is-main-character",
    action="store_true",
    help="The person is the main character",
)

parser.add_argument(
    "-s",
    "--situation,
    help="Decide if the script scream or whisper",
)


args = parser.parse_args()

situation = args.situation

if args.age is not None:
    speak(
        f"Now that you are {args.age}, I can reveal your true nature",
        situation=situation,
    )
speak(f"You're a hazard {args.name}", situation=situation)

if args.is_main_character:
    speak("Also, here's a pile of cash", situation=situation)

And you got what you need:

$ python hello_wizard.py Mary --situation sequel
YOU'RE A HAZARD MARY
$ python hello_wizard.py Mary --situation parsertongue
you're a hazard mary

Some script will use an = sign to separate the option’s name from its value, like this: --situation=parsertongue. argparse accepts both forms.

Organizing your script

Putting everything like is at the root of the script is fine for a short story of code. If you think you are going full novel, then a better organization will make the whole thing more manageable:

import argparse


def speak(*texts, situation=None):
    text = " ".join(texts)
    if situation == "sequel":
        print(text.upper())
    elif situation == "parsertongue":
        print(text.lower())
    else:
        print(text)


# Parsing the arguments is separated into its own function
# This means the rest of the code doesn't need to know 
# where the data comes from
def parse_args():
    parser = argparse.ArgumentParser(description="Hogwash reveal")

    parser.add_argument("name", type=str, help="Your name")

    parser.add_argument("age", type=int, nargs="?", help="Your age")

    parser.add_argument(
        "-m",
        "--is-main-character",
        action="store_true",
        help="The person is the main character",
    )

    parser.add_argument(
        "-s",
        "--situation",
        help="Decide if the script scream or whisper",
    )

    return parser.parse_args()


# This function is now simpler, and simply accepts regular parameters
# It's easier to understand, and to test.
def hello(name, age=None, situation=None, is_main_character=False):
    if age is not None:
        speak(
            f"Now that you are {age}, I can reveal your true nature",
            situation=situation,
        )
    speak(f"You're a hazard {name}", situation=situation)

    if is_main_character:
        speak("Also, here's a pile of cash", situation=situation)


# We allow the script to run as such, but also to be imported
if __name__ == "__main__":
    args = parse_args()
    hello(args.name, args.age, args.situation, args.is_main_character)

Of course, this can be improved a lot. We can sculpt the functions into something with docs and types. We can add sub-commands and arguments validation. And also get data from environment variables or a configuration file.

For another article, maybe.

python subscribe.py reader -y -f

5
Share this post

Parameters, options and flags for Python scripts

www.bitecode.dev
11
Share
Previous
Next
11 Comments
Share this discussion

Parameters, options and flags for Python scripts

www.bitecode.dev
bjkeefe
Jul 6

Thanks for the deets. I have to say, though, that after a bit of early thrashing with argparse, as a Python n00b, I have come to think ... it's fine. Like you, I have a "frags" file with boilerplate for argparse, and that, plus the experience gained from that early thrashing, pretty much means that using argparse is a solved problem, from my perspective.

I will admit that, up until recently, I still had some vague feelings of dissatisfaction, but the more I thought about it, the more I realized, well, what more do you want? Yes, the few lines of code involved with setting up argparse are not the most beautiful thing ever seen in a text editor, but (1) they work, and (2) as you suggest, there's no reason why this chunk of code couldn't live in a separate function. On that second point, though, I don't see much advantage. What is gained, a cleaner __main__ or main()?

Ultimately, it came down to my realizing that writing scripts/programs that support cmdline args is just not that big a deal. The universe of possibilities is just not that vast. The most complicated situation I have yet come up with involves one switch, one option, and an arbitrary number of positional arguments (e.g., filenames, so the script will work with something like `py myscript.py *.txt`.

I looked at click a couple of times, a while ago, when argparse was frustrating me, and I don't remember why, but I do remember being thoroughly unmotivated to learn it, based on a reading of docs. I'm not harshing on it -- if it works for other people, that's all good. I'm just saying that handling cmdline args is a fairly limited problem, and that for me, argparse does the job.

Expand full comment
Reply
Share
1 reply by Bite Code!
Thomas
Jul 5·edited Jul 5

Hey this pretty cool. We use both argparse and click at my place of work. I looked around very quickly and couldn't find a nice way to introduce type hints to argparse. Do you have any experience with that?

Expand full comment
Reply
Share
8 replies by Bite Code! and others
9 more comments...
Top
New
Community

No posts

Ready for more?

© 2023 Bite Code!
Privacy ∙ Terms ∙ Collection notice
Start WritingGet the app
Substack is the home for great writing