Setting up a CLI with Typer
I'd used argparse before. It's clunky - lots of boilerplate for even simple commands. Typer works differently. You write a normal function with type annotated parameters. Typer builds the CLI from the signature.
A basic command
Here's the init command as it shipped with v0.1.0:
from pathlib import Path
from typer import Typer
from typer import Argument as typer_argument
from typer import Option as typer_option
app = Typer(help="Scaffold a new project locally")
@app.command("init")
def init(
project_name: str = typer_argument(
..., help="Name of the new project"
),
path: Path = typer_option(
Path("."), help="Where to create the project"
),
) -> None:
"""Scaffold a new project locally."""
...
No parser setup. No add_argument calls. The function signature is the whole CLI definition.
Arguments vs options
Typer splits inputs into two kinds: arguments and options.
project_name uses typer_argument. Arguments are positional and required by default. Run scaffoldr init myproject, and myproject fills that slot. The ... as the first value tells Typer that this argument has no default - it must be provided.
path uses typer_option. Options use --flag value syntax, like scaffoldr init myproject --path ~/projects. Options always have a default, set by the first value passed to typer_option. Skip the flag, and it falls back to Path(".").
Type hints drive validation
Look at path: Path. Typer reads that type hint. It converts whatever the user types into a pathlib.Path object. No conversion needed. It arrives as the right type inside the function.
Booleans work the same way. For example:
protect: bool = typer_option(
True, help="Enable branch protection on main."
)
Typer creates --protect and --no-protect flags automatically. No extra code for that either.
Docstrings become help text
The docstring under the function - """Scaffold a new project locally.""" - isn't just for developers reading the code. Typer shows it when someone runs scaffoldr init --help. The help= argument on typer_argument and typer_option works the same way. It shows per-parameter help text.
The function signature and docstring are the only source of truth for the CLI's documentation.
Registering the entry point
Once the function is decorated with @app.command("init"), it needs to be wired into the installable CLI. In pyproject.toml:
[project.scripts]
scaffoldr = "scaffoldr.main:app"
This tells Python's packaging tools to create a scaffoldr executable. It runs the app object from scaffoldr/main.py. app is the Typer instance every command gets registered to.
What's next
With the CLI shell in place, the next problem was defining what gets created when someone runs scaffoldr init - the folder structure and files that make up a scaffolded project. In v0.1.0, this was hardcoded. That's covered in the next post.
