A Python Script Template with Sub-commands (and Type Hints)

2021-10-15 Now there are two of them!

Earlier this week I shared my Python script template. Here’s an extended version with sub-command support, and an example script.

The Template

Voilà:

#!/usr/bin/env python
from __future__ import annotations

import argparse
from collections.abc import Sequence


def main(argv: Sequence[str] | None = None) -> int:
    parser = argparse.ArgumentParser()
    subparsers = parser.add_subparsers(dest="command", required=True)

    # Add sub-commands
    subcommand1_parser = subparsers.add_parser("subcommand1", help="...")
    subcommand1_parser.add_argument("string")

    args = parser.parse_args(argv)

    if args.command == "subcommand1":
        return subcommand1(args.string)
    else:
        # Unreachable
        raise NotImplementedError(
            f"Command {args.command} does not exist.",
        )


def subcommand1(string: str) -> int:
    # Implement behaviour

    return 0


if __name__ == "__main__":
    raise SystemExit(main())

This works with Python 3.7+.

Here’s what’s different from vanilla template:

Por Exemplo

Here we have two sub-commands, debug and hello:

#!/usr/bin/env python
from __future__ import annotations

import argparse
import sys
from collections.abc import Sequence


def main(argv: Sequence[str] | None = None) -> int:
    parser = argparse.ArgumentParser()
    subparsers = parser.add_subparsers(dest="command", required=True)

    subparsers.add_parser("debug", help="Show debug information.")

    hello_parser = subparsers.add_parser("hello", help="Say hello.")
    hello_parser.add_argument("name", help="Who to greet.")

    args = parser.parse_args(argv)

    if args.command == "debug":
        return debug()
    elif args.command == "hello":
        return hello(name=args.name)
    else:
        raise NotImplementedError(
            f"Command {args.command} does not exist.",
        )


def debug() -> int:
    print(f"Python version {sys.version}")
    return 0


def hello(name: str) -> int:
    print(f"Hello {name}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

Cool... let’s try it out.

We can ask for help and argparse obliges:

$ ./example.py -h
usage: example.py [-h] {debug,hello} ...

positional arguments:
  {debug,hello}
    debug        Show debug information.
    hello        Say hello.

optional arguments:
  -h, --help     show this help message and exit

We can also get help for sub-commands:

$ ./example.py hello --help
usage: example.py hello [-h] name

positional arguments:
  name        Who to greet.

optional arguments:
  -h, --help  show this help message and exit

The debug sub-command is straightforward:

$ ./example.py debug
Python version 3.9.7 (default, Sep  1 2021, 11:36:38)
[Clang 12.0.5 (clang-1205.0.22.11)]

And hello works as expected:

$ ./example.py hello reader
Hello reader

Very cool!

Fin

Go and create some great tools,

—Adam


🤩 Django-Related Sales for Black Friday Week 🤩


Subscribe via RSS, Twitter, or email:

One summary email a week, no spam, I pinky promise.

Related posts:

Tags: mypy, python