Skip to content

Getting Started

Your first CLI

An extremely simple clypi CLI can be built in a couple of lines:

cli.py
from clypi import Command

class Cli(Command):
    async def run(self):
        print(f"Hello, world!")

if __name__ == '__main__':
    cmd = Cli.parse()
    cmd.start()
python cli.py --helpUsage: cli

Adding positional arguments

Positional arguments are unnamed arguments provided by the user in a specific order. We only care about the position they are in (hence the name positional).

In this example, whatever the first argument to our program is will be passed in as name, and the second argument will be passed as age since they're defined in that order.

cli.py
from clypi import Command, Positional

class Cli(Command):
    name: Positional[str]
    age: Positional[int]

    async def run(self):
        print(f"Hello, {self.name}. You are {self.age}!")

if __name__ == '__main__':
    cmd = Cli.parse()
    cmd.start()
python cli.py --helpUsage: cli [NAME] [AGE]

┏━ Arguments ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ [NAME] ┃
┃ [AGE] ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛

If we run it:

python cli.py Daniel 22Hello, Daniel. You are 22!

Adding options

Options are named arguments, and they are usually optional. A user of your CLI must provide the name of the argument and the value(s) to assign to it.

cli.py
from clypi import Command, Positional

class Cli(Command):
    name: Positional[str]
    age: int | None = None

    async def run(self):
        print(f"Hello, {self.name}.")
        if self.age is not None:
            print(f"You are {self.age}!")

if __name__ == '__main__':
    cmd = Cli.parse()
    cmd.start()
python cli.py --helpUsage: cli [NAME] [OPTIONS]

┏━ Arguments ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ [NAME] ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛

┏━ Options ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ --age <AGE> ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛

If we run it with and without the --age flag, we get:

python cli.py DanielHello, Daniel.

python cli.py Daniel --age 22Hello, Daniel.
You are 22!

Adding documentation

As it stands right now, our CLI is a bit difficult to use. We can add documentation to help our users understand what our CLI is and how to use it.

To document the overall command we can use Python docstrings. To document each argument and option we can make user of clypi's arg helper.

cli.py
from clypi import Command, Positional, arg

class Cli(Command):
    """A very simple CLI"""

    name: Positional[str] = arg(help="Your name")
    age: int | None = arg(None, help="Your age in years")

    async def run(self):
        print(f"Hello, {self.name}.")
        if self.age is not None:
            print(f"You are {self.age}!")

if __name__ == '__main__':
    cmd = Cli.parse()
    cmd.start()
python cli.py --helpA very simple CLI

Usage: cli [NAME] [OPTIONS]

┏━ Arguments ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ [NAME] Your name ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛

┏━ Options ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ --age <AGE> Your age in years ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛

Adding subcommands

Our applications sometimes have multiple usecases. To better group each usecase and avoid having too many options and arguments, we can create subcommands. Subcommands allow the user to select which tool to use inside your CLI.

Creating subcommands is very easy. Just create more commands and then define a class-level attribute in your main CLI with the name subcommand:

cli.py
from clypi import Command, Positional, arg

class Greet(Command):
    """Say hi to someone"""
    name: Positional[str] = arg(help="Your name")

    async def run(self):
        print(f"Hello, {self.name}")

class Wave(Command):
    """Wave at someone"""
    name: Positional[str] = arg(help="Your name")

    async def run(self):
        print(f"👋 {self.name}")

class Cli(Command):
    """A very simple CLI"""
    subcommand: Greet | Wave

if __name__ == '__main__':
    cmd = Cli.parse()
    cmd.start()
python cli.py --helpA very simple CLI

Usage: cli COMMAND

┏━ Subcommands ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ greet Say hi to someone ┃
┃ wave Wave at someone ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛

python cli.py greet --helpSay hi to someone

Usage: cli greet [NAME]

┏━ Arguments ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ [NAME] Your name ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛

python cli.py wave Daniel👋 Daniel

Prompting for values

Sometimes we want to make arguments required (by not providing a default) but we don't want to force our users to pass in an argument directly. We might want to ask them in a more user-friendly way. For that, we can set up a prompt in case the user does not manually pass in a value:

cli.py
from clypi import Command, Positional, arg

class Cli(Command):
    """A very simple CLI"""
    name: Positional[str] = arg(prompt="What's your name?")

    async def run(self):
        print(f"Hello, {self.name}!")

if __name__ == '__main__':
    cmd = Cli.parse()
    cmd.start()
python cli.pyWhat's your name?: Daniel
Hello, Daniel!

python cli.py DanielHello, Daniel!

Built-in parsers

Clypi comes with built-in parsers for all common Python types. See the built-in types section in the API docs to find all supported types and validations. Most often, using a normal Python type will automatically load the right parser, but if you want more control or extra features you can use these directly:

from clypi import Command, arg
import clypi.parsers as cp

class MyCommand(Command):
    file: Path = arg(
        parser=cp.Path(exists=True),
    )

You can also create your own parser if there's a complex data type we do not support. Refer to the Custom parsers docs.

Argument groups

Sometimes you want to separate your options based on behaviors. For example, you might want to split up environment options from output options. For that, just define a group parameter in the args you want to group together:

cli.py
from typing import Literal
from clypi import Command, Positional, arg

class Cli(Command):
    """A very simple CLI"""

    # Output configs here
    format: Literal["json", "raw"] = arg("raw", help="The output format to use")
    verbose: bool = arg(False, help="Whether to show verbose output")

    # Cluster configs here...
    env: Literal["qa", "prod"] = arg(
        "qa",
        help="The environment to run in",
        group="environment",
    )
    cluster: Literal["default", "secondary"] = arg(
        "default",
        help="The cluster to run in",
        group="environment",
    )

if __name__ == '__main__':
    cmd = Cli.parse()
    cmd.start()

You can see they now get displayed in different groups:

python cli.py --helpA very simple CLI

Usage: cli [OPTIONS]

┏━ Options ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ --format <FORMAT> The output format to use {JSON|RAW} ┃
┃ --verbose Whether to show verbose output ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛

┏━ Environment options ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ --env <ENV> The environment to run in {QA|PROD} ┃
┃ --cluster <CLUSTER> The cluster to run in {DEFAULT|SECONDARY} ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛