CLI Arguments

Let's see how to configure CLI arguments with typer.Argument().

Optional CLI arguments

We said before that by default:

  • CLI options are optional
  • CLI arguments are required

Again, that's how they work by default, and that's the convention in many CLI programs and systems.

But you can change that.

In fact, it's very common to have optional CLI arguments, it's way more common than having required CLI options.

As an example of how it could be useful, let's see how the ls CLI program works.

// If you just type
$ ls

// ls will "list" the files and directories in the current directory
typer  tests  README.md  LICENSE

// But it also receives an optional CLI argument
$ ls ./tests/

// And then ls will list the files and directories inside of that directory from the CLI argument
__init__.py  test_tutorial

An alternative CLI argument declaration

In the First Steps you saw how to add a CLI argument:

import typer


def main(name: str):
    typer.echo(f"Hello {name}")


if __name__ == "__main__":
    typer.run(main)

Now let's see an alternative way to create the same CLI argument:

import typer


def main(name: str = typer.Argument(...)):
    typer.echo(f"Hello {name}")


if __name__ == "__main__":
    typer.run(main)

Before you had this function parameter:

name: str

And because name didn't have any default value it would be a required parameter for the Python function, in Python terms.

Typer does the same and makes it a required CLI argument.

And then we changed it to:

name: str = typer.Argument(...)

But now as typer.Argument() is the "default value" of the function's parameter, it would mean that "it is no longer required" (in Python terms).

As we no longer have the Python function default value (or its absence) to tell if something is required or not and what is the default value, the first parameter to typer.Argument() serves the same purpose of defining that default value, or making it required.

To make it required, we pass ... as the first function argument passed to typer.Argument(...).

Info

If you hadn't seen that ... before: it is a a special single value, it is part of Python and is called "Ellipsis".

All we did there achieves the same thing as before, a required CLI argument:

$ python main.py

Usage: main.py [OPTIONS] NAME
Try "main.py --help" for help.

Error: Missing argument "NAME".

It's still not very useful, but it works correctly.

And being able to declare a required CLI argument using name: str = typer.Argument(...) that works exactly the same as name: str will come handy later.

Make an optional CLI argument

Now, finally what we came for, an optional CLI argument.

To make a CLI argument optional, use typer.Argument() and pass a different "default" as the first parameter to typer.Argument(), for example None:

import typer


def main(name: str = typer.Argument(None)):
    if name is None:
        typer.echo("Hello World!")
    else:
        typer.echo(f"Hello {name}")


if __name__ == "__main__":
    typer.run(main)

Now we have:

name: str = typer.Argument(None)

Because we are using typer.Argument() Typer will know that this is a CLI argument (no matter if required or optional).

And because the first parameter passed to typer.Argument(None) (the new "default" value) is None, Typer knows that this is an optional CLI argument, if no value is provided when calling it in the command line, it will have that default value of None.

Check the help:

// First check the help
$ python main.py --help

Usage: main.py [OPTIONS] [NAME]

Options:
  --install-completion  Install completion for the current shell.
  --show-completion     Show completion for the current shell, to copy it or customize the installation.
  --help                Show this message and exit.

Tip

Notice that NAME is still a CLI argument, it's shown up there in the "Usage: main.py ...".

Also notice that now [NAME] has brackets ("[" and "]") around (before it was just NAME) to denote that it's optional, not required.

Now run it and test it:

// With no CLI argument
$ python main.py

Hello World!

// With one optional CLI argument
$ python main.py Camila

Hello Camila

Tip

Notice that "Camila" here is an optional CLI argument, not a CLI option, because we didn't use something like "--name Camila", we just passed "Camila" directly to the program.

An optional CLI argument with a default

We can also make a CLI argument have a default value other than None:

import typer


def main(name: str = typer.Argument("Wade Wilson")):
    typer.echo(f"Hello {name}")


if __name__ == "__main__":
    typer.run(main)

And test it:

// With no optional CLI argument
$ python main.py

Hello Wade Wilson

// With one CLI argument
$ python main.py Camila

Hello Camila

About CLI arguments help

CLI arguments are commonly used for the most necessary things in a program.

They are normally required and, when present, they are normally the main subject of whatever the command is doing.

For that reason, Typer (actually Click underneath) doesn't attempt to automatically document CLI arguments.

And you should document them as part of the CLI app documentation, normally in a docstring.

Check the last example from the First Steps:

import typer


def main(name: str, lastname: str = "", formal: bool = False):
    """
    Say hi to NAME, optionally with a --lastname.

    If --formal is used, say hi very formally.
    """
    if formal:
        typer.echo(f"Good day Ms. {name} {lastname}.")
    else:
        typer.echo(f"Hello {name} {lastname}")


if __name__ == "__main__":
    typer.run(main)

Here the CLI argument NAME is documented as part of the help text.

You should document your CLI arguments the same way.

Other uses

typer.Argument() has several other users. For data validation, to enable other features, etc.

But you will see about that later in the docs.