Skip to content

Quickstart Guide

This guide is structured as a tutorial that will walk you through creating command-line applications with clap-py.

It was adapted from the excellent tutorial for clap-rs using Claude 4.0 Sonnet in Cursor (and some manual cleaning by hand). Most text is lifted verbatim.

Quick Start

You can create an application declaratively with a class and some decorators.

Here is a preview of the type of application you can make:

from pathlib import Path
from typing import Optional

import clap
from clap import ArgAction, arg, long, short


@clap.subcommand
class Test:
    """Does testing things."""

    list_flag: bool = arg(short, long="list")
    """Lists test values."""


@clap.command(version="1.0")
class Cli(clap.Parser):
    """A simple to use, efficient, and full-featured Command Line Argument Parser."""

    command: Optional[Test]

    name: Optional[str]
    """Optional name to operate on."""
    config: Optional[Path] = arg(short, long, value_name="FILE")
    """Sets a custom config file."""
    debug: int = arg(short, long, action=ArgAction.Count)
    """Turn debugging information on."""


def main():
    cli = Cli.parse()

    # You can check the value provided by positional arguments, or option arguments
    if cli.name:
        print(f"Value for name: {cli.name}")

    if cli.config:
        print(f"Value for config: {cli.config}")

    # You can see how many times a particular flag or argument occurred
    # Note, only flags can have multiple occurrences
    match cli.debug:
        case 0:
            print("Debug mode is off")
        case 1:
            print("Debug mode is kind of on")
        case 2:
            print("Debug mode is on")
        case _:
            print("Don't be crazy")

    # You can check for the existence of subcommands, and if found use their
    # matches just as you would the top level cmd
    match cli.command:
        case Test(list_flag):
            if list_flag:
                print("Printing testing lists...")
            else:
                print("Not printing testing lists...")
        case None: ...

    # Continued program logic goes here...


if __name__ == "__main__":
    main()
adityasz@github:clap-py$ python docs/docs/quickstart/01_quick.py --help
A simple to use, efficient, and full-featured Command Line Argument Parser

Usage: 01_quick.py [OPTIONS] [NAME] [COMMAND]

Commands:
  test  Does testing things

Arguments:
  [NAME]  Optional name to operate on

Options:
  -c, --config <FILE>  Sets a custom config file
  -d, --debug...       Turn debugging information on [default: 0]
  -h, --help           Print help
  -V, --version        Print version

By default, the program does nothing:

adityasz@github:clap-py$ python docs/docs/quickstart/01_quick.py
Debug mode is off

But you can mix and match the various features:

adityasz@github:clap-py$ python docs/docs/quickstart/01_quick.py -dd test
Debug mode is on
Not printing testing lists...

See also:

  • The tests for more usage examples.
  • The examples for more application-focused examples.

Configuring the Parser

You use the @clap.command decorator to start building a parser.

import clap
from clap import arg, long


@clap.command(name="MyApp", version="1.0")
class Cli(clap.Parser):
    """Does awesome things."""

    two: str = arg(long)
    one: str = arg(long)


def main():
    cli = Cli.parse()

    print(f"two: {cli.two}")
    print(f"one: {cli.one}")


if __name__ == "__main__":
    main()
adityasz@github:clap-py$ python docs/docs/quickstart/02_apps.py --help
Does awesome things

Usage: MyApp --two <TWO> --one <ONE>

Options:
      --two <TWO>
      --one <ONE>
  -h, --help       Print help
  -V, --version    Print version
adityasz@github:clap-py$ python docs/docs/quickstart/02_apps.py --version
MyApp 1.0

Adding Arguments

  1. Positionals
  2. Options
  3. Flags
  4. Optional
  5. Defaults
  6. Subcommands

Arguments are inferred from the fields of your class.

Positionals

By default, class fields define positional arguments:

import clap


@clap.command(version="1.0")
class Cli(clap.Parser):
    name: str


def main():
    cli = Cli.parse()
    print(f"name: {cli.name}")


if __name__ == "__main__":
    main()
adityasz@github:clap-py$ python docs/docs/quickstart/03_03_positional.py --help
Usage: 03_03_positional.py <NAME>

Arguments:
  <NAME>

Options:
  -h, --help     Print help
  -V, --version  Print version
adityasz@github:clap-py$ python docs/docs/quickstart/03_03_positional.py bob
name: bob

Note that the default ArgAction is Set. To accept multiple values, use a list type:

import clap


@clap.command(version="1.0")
class Cli(clap.Parser):
    name: list[str]


def main():
    cli = Cli.parse()
    print(f"name: {cli.name}")


if __name__ == "__main__":
    main()
adityasz@github:clap-py$ python docs/docs/quickstart/03_03_positional_mult.py --help
Usage: 03_03_positional_mult.py [<NAME>...]

Arguments:
  [<NAME>...]

Options:
  -h, --help     Print help
  -V, --version  Print version
adityasz@github:clap-py$ python docs/docs/quickstart/03_03_positional_mult.py
name: []
adityasz@github:clap-py$ python docs/docs/quickstart/03_03_positional_mult.py bob
name: ['bob']
adityasz@github:clap-py$ python docs/docs/quickstart/03_03_positional_mult.py bob john
name: ['bob', 'john']

Options

You can name your arguments with a flag:

  • Intent of the value is clearer
  • Order doesn't matter

To specify the flags for an argument, you can use arg() on a field:

  • To automatically generate flags, short and long can be used: arg(short, long).
    • arg(short=True, long=True) can also be used.
  • To specify the flags manually:
    • arg(short="n", long="name").

Note

arg() takes up to two positional-only paramters of type AutoFlag, and short and long are aliases for AutoFlag.Short and AutoFlag.Long. These are used to automatically generate flags.

It also takes keyword-only arguments named short and long. These are used for manually specifying the flags.

import clap
from clap import arg, long, short


@clap.command(version="1.0")
class Cli(clap.Parser):
    name: str = arg(short, long)


def main():
    cli = Cli.parse()
    print(f"name: {cli.name}")


if __name__ == "__main__":
    main()
adityasz@github:clap-py$ python docs/docs/quickstart/03_02_option.py --help
Usage: 03_02_option.py --name <NAME>

Options:
  -n, --name <NAME>
  -h, --help         Print help
  -V, --version      Print version
adityasz@github:clap-py$ python docs/docs/quickstart/03_02_option.py --name bob
name: bob
adityasz@github:clap-py$ python docs/docs/quickstart/03_02_option.py --name=bob
name: bob
adityasz@github:clap-py$ python docs/docs/quickstart/03_02_option.py -n bob
name: bob
adityasz@github:clap-py$ python docs/docs/quickstart/03_02_option.py -n=bob
name: bob
adityasz@github:clap-py$ python docs/docs/quickstart/03_02_option.py -nbob
name: bob

Note that the default ArgAction is Set. To accept multiple occurrences, override the action with Append via list:

import clap
from clap import arg, long, short


@clap.command(version="1.0")
class Cli(clap.Parser):
    name: list[str] = arg(short, long)


def main():
    cli = Cli.parse()
    print(f"name: {cli.name}")


if __name__ == "__main__":
    main()
adityasz@github:clap-py$ python docs/docs/quickstart/03_02_option_mult.py --help
Usage: 03_02_option_mult.py [OPTIONS]

Options:
  -n, --name <NAME>  [default: []]
  -h, --help         Print help
  -V, --version      Print version
adityasz@github:clap-py$ python docs/docs/quickstart/03_02_option_mult.py
name: []
adityasz@github:clap-py$ python docs/docs/quickstart/03_02_option_mult.py --name bob
name: ['bob']
adityasz@github:clap-py$ python docs/docs/quickstart/03_02_option_mult.py --name bob --name john
name: ['bob', 'john']
adityasz@github:clap-py$ python docs/docs/quickstart/03_02_option_mult.py --name bob --name=john -n tom -n=chris -nsteve
name: ['bob', 'john', 'tom', 'chris', 'steve']

Flags

Flags can also be switches that can be on/off:

import clap
from clap import arg, long, short


@clap.command(version="1.0")
class Cli(clap.Parser):
    verbose: bool = arg(short, long)


def main():
    cli = Cli.parse()
    print(f"verbose: {cli.verbose}")


if __name__ == "__main__":
    main()
adityasz@github:clap-py$ python docs/docs/quickstart/03_01_flag_bool.py --help
Usage: 03_01_flag_bool.py [OPTIONS]

Options:
  -v, --verbose
  -h, --help     Print help
  -V, --version  Print version
adityasz@github:clap-py$ python docs/docs/quickstart/03_01_flag_bool.py
verbose: False
adityasz@github:clap-py$ python docs/docs/quickstart/03_01_flag_bool.py --verbose
verbose: True

Note that the default ArgAction for a bool field is SetTrue. To accept multiple flags, override the action with Count:

import clap
from clap import ArgAction, arg, long, short


@clap.command(version="1.0")
class Cli(clap.Parser):
    verbose: int = arg(short, long, action=ArgAction.Count)


def main():
    cli = Cli.parse()
    print(f"verbose: {cli.verbose}")


if __name__ == "__main__":
    main()
adityasz@github:clap-py$ python docs/docs/quickstart/03_01_flag_count.py --help
Usage: 03_01_flag_count.py [OPTIONS]

Options:
  -v, --verbose...  [default: 0]
  -h, --help        Print help
  -V, --version     Print version
adityasz@github:clap-py$ python docs/docs/quickstart/03_01_flag_count.py
verbose: 0
adityasz@github:clap-py$ python docs/docs/quickstart/03_01_flag_count.py --verbose
verbose: 1
adityasz@github:clap-py$ python docs/docs/quickstart/03_01_flag_count.py --verbose --verbose
verbose: 2

Optional

By default, arguments are assumed to be required. To make an argument optional, wrap the field's type in Optional:

from typing import Optional

import clap


@clap.command(version="1.0")
class Cli(clap.Parser):
    name: Optional[str]


def main():
    cli = Cli.parse()
    print(f"name: {cli.name}")


if __name__ == "__main__":
    main()
adityasz@github:clap-py$ python docs/docs/quickstart/03_06_optional.py --help
Usage: 03_06_optional.py [NAME]

Arguments:
  [NAME]

Options:
  -h, --help     Print help
  -V, --version  Print version
adityasz@github:clap-py$ python docs/docs/quickstart/03_06_optional.py
name: None
adityasz@github:clap-py$ python docs/docs/quickstart/03_06_optional.py bob
name: bob

Defaults

We've previously showed that arguments can be required or optional. When optional, you work with an Optional and can use or or provide a default value. Alternatively, you can set default_value.

import clap
from clap import arg


@clap.command(version="1.0")
class Cli(clap.Parser):
    port: int = arg(default_value=2020)


def main():
    cli = Cli.parse()
    print(f"port: {cli.port}")


if __name__ == "__main__":
    main()
adityasz@github:clap-py$ python docs/docs/quickstart/03_05_default_values.py --help
Usage: 03_05_default_values.py [PORT]

Arguments:
  [PORT]  [default: 2020]

Options:
  -h, --help     Print help
  -V, --version  Print version
adityasz@github:clap-py$ python docs/docs/quickstart/03_05_default_values.py
port: 2020
adityasz@github:clap-py$ python docs/docs/quickstart/03_05_default_values.py 22
port: 22

Subcommands

Subcommands are created with @clap.subcommand and added via a type annotation to a field that will contain the parsed result. If there are multiple subcommands, use Union. Each instance of a subcommand can have its own version, author(s), arguments, and even its own subcommands.

from typing import Optional

import clap


@clap.subcommand
class Add:
    """Adds files to myapp."""

    name: Optional[str]


@clap.command(version="1.0", propagate_version=True)
class Cli(clap.Parser):
    command: Add


def main():
    cli = Cli.parse()

    # You can check for the existence of subcommands, and if found use their
    # matches just as you would the top level cmd
    match cli.command:
        case Add(name):
            print(f"'myapp add' was used, name is: {name}")


if __name__ == "__main__":
    main()
adityasz@github:clap-py$ python docs/docs/quickstart/03_04_subcommands.py --help
Usage: 03_04_subcommands.py <COMMAND>

Commands:
  add  Adds files to myapp

Options:
  -h, --help     Print help
  -V, --version  Print version
adityasz@github:clap-py$ python docs/docs/quickstart/03_04_subcommands.py add --help
Adds files to myapp

Usage: 03_04_subcommands.py add [NAME]

Arguments:
  [NAME]

Options:
  -h, --help     Print help
  -V, --version  Print version
adityasz@github:clap-py$ python docs/docs/quickstart/03_04_subcommands.py add bob
'myapp add' was used, name is: bob

To make a subcommand optional, wrap it in an Optional (e.g. command: Optional[Add]).

Since we specified propagate_version=True, the --version flag is available in all subcommands:

adityasz@github:clap-py$ python docs/docs/quickstart/03_04_subcommands.py --version
03_04_subcommands.py 1.0
adityasz@github:clap-py$ python docs/docs/quickstart/03_04_subcommands.py add --version
add 1.0

Validation

  1. Enumerated values
  2. Argument Relations
  3. Custom Validation

An appropriate default parser/validator will be selected for the field's type.

Enumerated values

For arguments with specific values you want to test for, you can use Python's Enum. This allows you to specify the valid values for that argument. If the user does not use one of those specific values, they will receive a graceful exit with error message informing them of the mistake, and what the possible valid values are.

from enum import Enum, auto

import clap


class Mode(Enum):
    """TODO: Help strings are not yet printed for enum values in the long help output.

    See TODOs in README.md.
    """

    Fast = auto()
    """Run swiftly."""
    Slow = auto()
    """Crawl slowly but steadily.

    This paragraph is ignored because there is no long help text for possible values.
    """


@clap.command(version="1.0")
class Cli(clap.Parser):
    mode: Mode
    """What mode to run the program in."""


def main():
    cli = Cli.parse()

    match cli.mode:
        case Mode.Fast:
            print("Hare")
        case Mode.Slow:
            print("Tortoise")


if __name__ == "__main__":
    main()
adityasz@github:clap-py$ python docs/docs/quickstart/04_01_enum.py --help
Usage: 04_01_enum.py <MODE>

Arguments:
  <MODE>
          What mode to run the program in

          Possible values:
          - fast: Run swiftly
          - slow: Crawl slowly but steadily

Options:
  -h, --help     Print help
  -V, --version  Print version
adityasz@github:clap-py$ python docs/docs/quickstart/04_01_enum.py -h
Usage: 04_01_enum.py <MODE>

Arguments:
  <MODE>  What mode to run the program in [possible values: fast, slow]

Options:
  -h, --help     Print help
  -V, --version  Print version
adityasz@github:clap-py$ python docs/docs/quickstart/04_01_enum.py fast
Hare
adityasz@github:clap-py$ python docs/docs/quickstart/04_01_enum.py slow
Tortoise

Argument Relations

Note

Advanced argument relations and dependencies like requires and conflicts_with are not yet implemented in clap-py. You can use Group and MutexGroup for basic grouping and mutual exclusion.

Custom Validation

As a last resort, you can create custom validation logic in your application after parsing:

import sys
from typing import Optional

import clap
from clap import arg, long, short


@clap.command(version="1.0")
class Cli(clap.Parser):
    input_file: Optional[str]
    """Some regular input."""

    set_ver: Optional[str] = arg(long, value_name="VER")
    """Set version manually."""

    major: bool = arg(long)
    """Auto inc major."""

    minor: bool = arg(long)
    """Auto inc minor."""

    patch: bool = arg(long)
    """Auto inc patch."""

    spec_in: Optional[str] = arg(long)
    """Some special input argument."""

    config: Optional[str] = arg(short)


def main():
    cli = Cli.parse()

    # Let's assume the old version 1.2.3
    major = 1
    minor = 2
    patch = 3

    # See if --set-ver was used to set the version manually
    if cli.set_ver is not None:
        if cli.major or cli.minor or cli.patch:
            print("error: Can't do relative and absolute version change", file=sys.stderr)
            sys.exit(1)
        version = cli.set_ver
    else:
        # Increment the one requested (in a real program, we'd reset the lower numbers)
        flags_set = [cli.major, cli.minor, cli.patch]
        if sum(flags_set) != 1:
            print("error: Can only modify one version field", file=sys.stderr)
            sys.exit(1)

        if cli.major:
            major += 1
        elif cli.minor:
            minor += 1
        elif cli.patch:
            patch += 1
        else:
            print(
                "error: Must specify one of --set-ver, --major, --minor, or --patch",
                file=sys.stderr,
            )
            sys.exit(1)

        version = f"{major}.{minor}.{patch}"

    print(f"Version: {version}")

    # Check for usage of -c
    if cli.config is not None:
        input_file = cli.input_file or cli.spec_in
        if input_file is None:
            print(
                "error: INPUT_FILE or --spec-in is required when using --config", file=sys.stderr
            )
            sys.exit(1)
        print(f"Doing work using input {input_file} and config {cli.config}")


if __name__ == "__main__":
    main()
adityasz@github:clap-py$ python docs/docs/quickstart/04_04_custom.py --help
Usage: 04_04_custom.py [OPTIONS] [INPUT_FILE]

Arguments:
  [INPUT_FILE]  Some regular input

Options:
      --set-ver <VER>      Set version manually
      --major              Auto inc major
      --minor              Auto inc minor
      --patch              Auto inc patch
      --spec-in <SPEC_IN>  Some special input argument
  -c <CONFIG>
  -h, --help               Print help
  -V, --version            Print version
adityasz@github:clap-py$ python docs/docs/quickstart/04_04_custom.py --major
Version: 2.2.3
adityasz@github:clap-py$ python docs/docs/quickstart/04_04_custom.py --major -c config.toml --spec-in input.txt
Version: 2.2.3
Doing work using input input.txt and config config.toml

Docstrings

import clap

@clap.command
class Cli(clap.Parser):
    """This is the short about (without the trailing period).

    Any subsequent paragraphs are ignored in the short about. The long about
    contains the entire docstring.
    """

    input: str
    """Help messages are generated in a similar way.
    Ellipsis are kept in the short help...

    Paragraphs are separated by at least two newlines.
    """

Docstrings are processed just like clap-rs.

Help Output

See ArgAction.Help, ArgAction.HelpLong, and ArgAction.HelpShort.

A custom template can be used, and styles can be customized using Styles.

Here's the help output for examples/typst.py:

adityasz@github:clap-py$ python examples/typst.py --help
Usage: typst [OPTIONS] <COMMAND>

Commands:
  watch  Watches an input file and recompiles on changes [aliases: w]
  init   Initializes a new project from a template

Options:
      --cert <CERT>
          Path to a custom CA certificate to use when making network requests

      --color <COLOR>
          Whether to use color. When set to `auto` if the terminal to supports it

          Possible values:
          - auto:   Enables colored output only when the output is going to a terminal or TTY
          - always: Enables colored output regardless of whether or not the output is going to a
                    terminal/TTY
          - never:  Disables colored output no matter if the output is going to a terminal/TTY, or
                    not

          [default: auto]

  -h, --help
          Print help
adityasz@github:clap-py$ python examples/typst.py watch -h
Watches an input file and recompiles on changes

Usage: typst [OPTIONS] watch [OPTIONS] <INPUT> [OUTPUT]

Arguments:
  <INPUT>   Path to input Typst file. Use `-` to read input from stdin
  [OUTPUT]  Path to output file (PDF, PNG, SVG, or HTML). Use `-` to write output to stdout

Options:
  -f, --format <FORMAT>      The format of the output file, inferred from the extension by default
                             [possible values: pdf, png, svg, html]
      --ignore-system-fonts  Ensures system fonts won't be searched, unless explicitly included via
                             `--font-path`
  -j, --jobs <JOBS>          Number of parallel jobs spawned during compilation. Defaults to number
                             of CPUs
  -h, --help                 Print help
adityasz@github:clap-py$ python examples/typst.py watch --help
Watches an input file and recompiles on changes

Usage: typst [OPTIONS] watch [OPTIONS] <INPUT> [OUTPUT]

Arguments:
  <INPUT>
          Path to input Typst file. Use `-` to read input from stdin

  [OUTPUT]
          Path to output file (PDF, PNG, SVG, or HTML). Use `-` to write output to stdout.

          For output formats emitting one file per page (PNG & SVG), a page number template must be
          present if the source document renders to multiple pages. Use `{p}` for page numbers,
          `{0p}` for zero padded page numbers and `{t}` for page count. For example,
          `page-{0p}-of-{t}.png` creates `page-01-of-10.png`, `page-02-of-10.png`, and so on.

Options:
  -f, --format <FORMAT>      The format of the output file, inferred from the extension by default
                             [possible values: pdf, png, svg, html]
      --ignore-system-fonts  Ensures system fonts won't be searched, unless explicitly included via
                             `--font-path`
  -j, --jobs <JOBS>          Number of parallel jobs spawned during compilation. Defaults to number
                             of CPUs
  -h, --help                 Print help

Sharp edges

The decorators @clap.command and @clap.subcommand are decorated with @dataclass_transform to tell type checkers that they provide dataclass-like functionality (for example, pattern matching with positionals in match-case).

This also brings some dataclass limitations: If fields without default values are present after fields with default values, the type checker complains. There are no runtime implications, but to satisfy the type checkers, the following (reasonable) workarounds can be used:

  • For positionals where you don't need to modify the default behavior, you can simply assign arg() if there are fields with default values above.
  • For a field that contains the subcommand, nothing can be assigned. So, keep this as the first field. (The order only matters for positionals; the subcommand is always parsed after all the positionals and options.)

See also: