I pick Click over argparse almost every time now. The subcommand pattern is so clean that I can hand the resulting tool to a non-Python-ist and they can read the help and figure out what it does. This is the template I paste into every new admin script.

#!/usr/bin/env python3
"""Example multi-command CLI with shared options."""
from __future__ import annotations

import json
import sys
from pathlib import Path

import click


@click.group(context_settings={"help_option_names": ["-h", "--help"]})
@click.option("-v", "--verbose", count=True, help="Repeat for more output.")
@click.option("--config", type=click.Path(exists=True, path_type=Path),
              default=Path("~/.myclirc").expanduser())
@click.pass_context
def cli(ctx: click.Context, verbose: int, config: Path) -> None:
    """Do a thing with several subcommands."""
    ctx.obj = {"verbose": verbose, "config": config}


@cli.command()
@click.argument("name")
@click.pass_obj
def greet(obj, name: str) -> None:
    """Say hello."""
    if obj["verbose"]:
        click.echo(f"(config: {obj['config']})", err=True)
    click.echo(f"hello, {name}")


@cli.command(name="dump-config")
@click.pass_obj
def dump_config(obj) -> None:
    """Print the resolved config as JSON."""
    click.echo(json.dumps(
        {"verbose": obj["verbose"], "config": str(obj["config"])},
        indent=2,
    ))


if __name__ == "__main__":
    sys.exit(cli())

ctx.obj is the right place to stash anything shared. click.Path(path_type=Path) returns pathlib.Path directly. See also /snippets/python-csv-stream-large/.