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 versionBy default, the program does nothing:
adityasz@github:clap-py$ python docs/docs/quickstart/01_quick.py
Debug mode is offBut 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:
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 versionadityasz@github:clap-py$ python docs/docs/quickstart/02_apps.py --version
MyApp 1.0Adding Arguments
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 versionadityasz@github:clap-py$ python docs/docs/quickstart/03_03_positional.py bob
name: bobNote 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 versionadityasz@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,
shortandlongcan 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 versionadityasz@github:clap-py$ python docs/docs/quickstart/03_02_option.py --name bob
name: bobadityasz@github:clap-py$ python docs/docs/quickstart/03_02_option.py --name=bob
name: bobadityasz@github:clap-py$ python docs/docs/quickstart/03_02_option.py -n bob
name: bobadityasz@github:clap-py$ python docs/docs/quickstart/03_02_option.py -n=bob
name: bobadityasz@github:clap-py$ python docs/docs/quickstart/03_02_option.py -nbob
name: bobNote 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 versionadityasz@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 versionadityasz@github:clap-py$ python docs/docs/quickstart/03_01_flag_bool.py
verbose: Falseadityasz@github:clap-py$ python docs/docs/quickstart/03_01_flag_bool.py --verbose
verbose: TrueNote 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 versionadityasz@github:clap-py$ python docs/docs/quickstart/03_01_flag_count.py
verbose: 0adityasz@github:clap-py$ python docs/docs/quickstart/03_01_flag_count.py --verbose
verbose: 1adityasz@github:clap-py$ python docs/docs/quickstart/03_01_flag_count.py --verbose --verbose
verbose: 2Optional
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 versionadityasz@github:clap-py$ python docs/docs/quickstart/03_06_optional.py
name: Noneadityasz@github:clap-py$ python docs/docs/quickstart/03_06_optional.py bob
name: bobDefaults
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 versionadityasz@github:clap-py$ python docs/docs/quickstart/03_05_default_values.py
port: 2020adityasz@github:clap-py$ python docs/docs/quickstart/03_05_default_values.py 22
port: 22Subcommands
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 versionadityasz@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 versionadityasz@github:clap-py$ python docs/docs/quickstart/03_04_subcommands.py add bob
'myapp add' was used, name is: bobTo 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.0adityasz@github:clap-py$ python docs/docs/quickstart/03_04_subcommands.py add --version
add 1.0Validation
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 versionadityasz@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 versionadityasz@github:clap-py$ python docs/docs/quickstart/04_01_enum.py fast
Hareadityasz@github:clap-py$ python docs/docs/quickstart/04_01_enum.py slow
TortoiseArgument 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 versionadityasz@github:clap-py$ python docs/docs/quickstart/04_04_custom.py --major
Version: 2.2.3adityasz@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.tomlDocstrings
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 helpadityasz@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 helpadityasz@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 helpSharp 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:
- API Reference for complete documentation.
- Examples for application-focused examples.