Skip to main content

Argsense CLI

PyPI version Downloads Downloads

中文版

ArgSense is a command line interface (CLI) toolkit for Python programmers.

It is much lighter than argparse, Click and Typer, and closest in spirit to yapx. If you are already used to another CLI library, jump to Comparison with Other Libraries to see how ArgSense differs.

Screenshots:

Highlights

  • Python first. ArgSense takes :true :false :none from CLI and convert them to direct Python types.
  • Intuitive. It gets all information from the function signature and docstrings.
  • Less peripheral code.
  • Rich panel of help interface.

Comparison with Other Libraries

The Python ecosystem already has many mature CLI libraries. This chapter walks through a few common scenarios and implements each of them with ArgSense and its most popular alternatives, so that you can see the differences side by side. Except for the "Hello World" case, each scenario only shows one to three representative competitors — nobody enjoys reading the same comparison nine times.

The "Hello World" Case

ArgSense

from argsense import cli

@cli
def hello(name: str = 'world'):
    print(f'hello {name}')

if __name__ == '__main__':
    cli.run(hello)
# get help
python main.py -h
python main.py hello -h

# run with default name
python main.py hello

# run with given name
python main.py hello Alice
python main.py hello --name Alice

argparse (standard library)

argparse is the standard-library solution. The interface is declared imperatively, and help text is passed explicitly.

import argparse

def hello(name: str = 'world'):
    print(f'hello {name}')

if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument('name', nargs='?', default='world', help='Who to greet.')
    args = parser.parse_args()
    hello(name=args.name)

Click

Click uses decorators to declare commands and parameters, and generates a nicely formatted help page.

import click

@click.command()
@click.option('--name', default='world', help='Who to greet.')
def hello(name: str) -> None:
    click.echo(f'hello {name}')

if __name__ == '__main__':
    hello()

Typer

Typer is built on top of Click and infers the interface from type hints.

import typer

def hello(name: str = 'world'):
    print(f'hello {name}')

if __name__ == '__main__':
    typer.run(hello)

Python Fire

Python Fire (originally released by Google) turns almost any Python object into a CLI with a single call.

import fire

def hello(name: str = 'world'):
    print(f'hello {name}')

if __name__ == '__main__':
    fire.Fire(hello)

docopt

docopt generates the parser from a usage message written in the docstring. All values are received as plain strings.

"""Greet someone.

Usage:
    hello.py <name>
"""
from docopt import docopt

if __name__ == '__main__':
    arguments = docopt(__doc__)
    print(f"hello {arguments['<name>']}")

clize

clize derives the interface from type hints and the docstring, in a spirit close to ArgSense.

from clize import run

def hello(name: str = 'world'):
    """Greet someone.

    :param name: Who to greet.
    """
    print(f'hello {name}')

if __name__ == '__main__':
    run(hello)

cyclopts

cyclopts is a modern, type-hint-driven library with rich help output.

from cyclopts import App

app = App()

@app.default
def hello(name: str = 'world'):
    print(f'hello {name}')

if __name__ == '__main__':
    app()

defopt

defopt builds the CLI from type hints and a reStructuredText-style docstring.

import defopt

def hello(name: str = 'world'):
    """Greet someone.

    :param name: Who to greet.
    """
    print(f'hello {name}')

if __name__ == '__main__':
    defopt.run(hello)

Rich, Docstring-driven Help

ArgSense

ArgSense reads the whole help page from the function's docstring: the command description, each parameter's help, the short option and even the default value. There is nothing to repeat in code.

from argsense import cli

@cli
def hello(name: str = 'world', *, verbose: bool = False):
    """
    greet someone.

    params:
        name: who to greet.
        verbose (-v): print more information.
    """
    ...

if __name__ == '__main__':
    cli.run(hello)

Screenshot (python temp.py -h):

Click

Click takes the docstring as the command description, but each parameter's help still has to be written in its decorator.

import click

@click.command()
@click.option('--name', default='world', help='Who to greet.')
@click.option('--verbose', '-v', is_flag=True, help='Print more information.')
def hello(name: str, verbose: bool) -> None:
    """Greet someone."""
    click.echo(f'hello {name}')

if __name__ == '__main__':
    hello()

Typer

Typer does the same: the docstring is the description, while per-parameter help is given in typer.Option / typer.Argument.

import typer

def hello(
    name: str = 'world',
    verbose: bool = typer.Option(
        False, '--verbose', '-v', help='Print more information.'
    ),
):
    """Greet someone."""
    print(f'hello {name}')

if __name__ == '__main__':
    typer.run(hello)

Multiple Commands in One File

ArgSense

Decorate every function with @cli, then call cli.run() without an argument. python main.py -h lists all commands, and python main.py <cmd> -h shows one.

from argsense import cli

@cli
def foo(a: str):
    ...

@cli
def bar(b: int):
    ...

if __name__ == '__main__':
    cli.run()
python main.py -h
python main.py foo -h
python main.py foo 123
python main.py bar -h
python main.py bar 456

Click

import click

@click.group()
def cli():
    pass

@cli.command()
def foo(a: str):
    ...

@cli.command()
def bar(b: int):
    ...

if __name__ == '__main__':
    cli()

Typer

import typer

app = typer.Typer()

@app.command()
def foo(a: str):
    ...

@app.command()
def bar(b: int):
    ...

if __name__ == '__main__':
    app()

Parameter Types and Special Values

ArgSense

ArgSense infers a parameter's type from its annotation, converts the input accordingly, and additionally understands a set of special values.

from argsense import cli

@cli
def train(
    count: int, 
    addr: int, 
    rate: float = 0.1, 
    tag: str = '', 
    verbose: bool = False
):
    ...

if __name__ == '__main__':
    cli.run(train)
python main.py -h
python main.py 5 0xFFFF 0.25 abc :true  # positional args
python main.py --count 5 --addr 0xFFFF --rate 0.25 --tag abc --verbose  # keyword args
python main.py --count 5 --addr 0xFFFF --rate 0.25 --tag :empty --verbose  # empty string
python main.py --count 5 --addr 0xFFFF --rate 0.25 --tag :none --verbose  # None type

User input Recognized as
:true True
:false False
:none None
:empty '' (empty string)

See Passing Special Values for the full rules.

argparse

argparse requires you to spell out every type= and action=, and it has no notion of "special values": the literal text :none would simply stay the string ":none" unless you convert it yourself.

parser.add_argument('count', type=int)
parser.add_argument('--rate', type=float, default=0.1)
parser.add_argument('--tag', default='')
parser.add_argument('--verbose', action='store_true')

Typer

Typer, like ArgSense, infers types from the annotations — but it does not provide :none / :empty sentinels out of the box.

def train(count: int, rate: float = 0.1, tag: str = '', verbose: bool = False):
    ...

*args and **kwargs

ArgSense

from argsense import cli
from neoprint import print

@cli
def foo(aaa: int, *bbb, ccc: int = 999, **ddd):
    print(locals(), ':l')

cli.run(foo)
python main.py -h
python main.py 111 222 333 --ccc 444 --ddd 555 --eee :true

Click

Click supports variadic positional arguments via nargs=-1, but it cannot accept arbitrary extra --keyword arguments — every option has to be declared upfront.

import click

@click.command()
@click.argument('aaa', type=int)
@click.argument('bbb', nargs=-1, type=int)
@click.option('--ccc', type=int, default=999)
def foo(aaa: int, bbb: tuple, ccc: int) -> None:
    click.echo((aaa, bbb, ccc))

if __name__ == '__main__':
    foo()

Typer

In Typer, a parameter annotated as List[...] becomes a variadic positional argument.

import typer
from typing import List

def foo(aaa: int, bbb: List[int], ccc: int = 999):
    print(aaa, bbb, ccc)

if __name__ == '__main__':
    typer.run(foo)

Neither Click nor Typer accepts arbitrary extra --keyword options the way ArgSense's **kwargs does.

Reusable Parameter Annotations

ArgSense

When several commands share the same parameter, you only need to document it once. Write the full description in the first function, and let the others reference it with see `@<command>:<parameter>`:

from argsense import cli

@cli
def foo(name: str, age: int):
    """
    params:
        name: the leader's name.
        age: the leader's age.
    """

@cli
def bar(name: str, age: int):
    """
    params:
        name: see `@foo:name`
        age: see `@foo:age`
    """

The reference is resolved lazily, so the registration order does not matter, and references can even be chained (bar -> foo -> ...). If a referenced command or parameter does not exist, the text is left untouched.

Click

Other libraries have no docstring-level reference. The usual workaround is to extract a shared decorator (or a type alias, in Typer) and reuse it in code:

import click

def name_option(func):
    return click.option('--name', help="the leader's name.")(func)

@click.command()
@name_option
def foo(name): ...

@click.command()
@name_option
def bar(name): ...

At a Glance

Library How the interface is declared Help text source Automatic type conversion Multiple commands Help UI
argparse imperative (add_argument) help= arguments manual (type=) subparsers plain
click decorators (@click.option) help= + docstring manual (type=) @click.group pretty
typer type hints Annotated + docstring yes Typer app rich (via Click)
python-fire function signature docstring yes dict / class plain
docopt docstring DSL the docstring no (all strings) usage patterns plain
clize type hints + docstring docstring yes run decent
cyclopts type hints Annotated + docstring yes App rich
defopt type hints + reST docstring reST docstring yes function list plain
argsense type hints + docstring docstring (params:) yes @cli commands rich

Install

ArgSense supports Python 3.9 and above:

pip install argsense

The latest version is 1.2+.

Quick Start

from argsense import cli
from typing import Any

@cli
def main(foo: Any, bar: Any):
    print(f'{foo=}, {bar=}')

if __name__ == '__main__':
    cli.run(main)
python main.py -h
python main.py --help
python main.py Foo Bar
python main.py :true :false
python main.py :empty :none
python main.py 123 456
python main.py 0xFFFF 0b1111

That is all you need: decorate the function with @cli, then call cli.run(). The parameter list, the help text and the type conversion are all derived from the signature and the docstring. See Comparison with Other Libraries for the big picture, and In-Depth Reading for the details.

In-Depth Reading

The Format of a Function Docstring

  • Blank lines between text lines are optional. If present, they are preserved as-is, and consecutive blank lines are rendered as consecutive blank lines.

  • A line that is too long can be continued by ending it with -:

    # test.py
    from argsense import cli
    
    @cli
    def main(foo, bar):
        """
        training speak stay try me. nothing generation side trial. seven rate -
        worry not word family.
        lay prepare anything piece method now work interesting. where mission -
        move impact future. daughter remember leave. red read message talk hand.
        similar bed capital public. former east later form people lawyer. -
        operation true similar lose receive compare how film.
    
        service cold choice model finish. send ask star without huge evening.
        next black there production space treat must often. purpose course -
        actually whatever grow. take perform our billion receive food law.
        result focus property yard let both. baby mind work style.
        """
        ...
    
    if __name__ == '__main__':
        cli.run(main)
    

  • Parameter annotations must start with the params: field.

  • The format of a parameter annotation is:

    <param_name> (<short_name>): <first_line_of_description>
        <more_description_lines>
    
    • param_name must be a parameter that appears in the function. If a non-existent name is used and the function has no **kwargs, an error is raised.

    • short_name must start with a hyphen, followed by a lowercase letter and zero or more digits, for example:

      # correct
      -a
      -a1
      -a123
      
      # incorrect
      -A
      -A0
      -Abc
      -ABC
      
    • first_line_of_description is optional; if the description is short, write it here.

      If you leave this field blank, you can write the description in more_description_lines instead:

      def foo(...):
          """
          params:
              aaa: degree serve across recently.
              bbb:
                  least support seem success word.
          """
      

      If the text is long and there is not enough space on the first_line_of_description, write it in more_description_lines instead:

      def foo(...):
          """
          params:
              aaa: degree serve across recently.
              bbb:
                  least support seem success word.
              ccc:
                  scientist do war statement pay or reflect. fill -
                  get skill scene medical. side generation energy -
                  but popular. ago war act nice down.
          """
      

Supported Parameter Types

ArgSense supports a limited set of types. Any unsupported type is treated as "any".

The supported types are as follows:

  • bool
  • float
  • int
  • str

They can also be wrapped with typing.Optional[...], as shown in the following example:

import typing as t
from argsense import cli

@cli
def main(
    aaa: bool,
    bbb: float,
    ccc: int,
    ddd: str,
    eee: bool = None,
    fff: float = None,
    ggg: int = None,
    hhh: str = None,
    iii: t.Optional[bool] = None,
    jjj: t.Optional[float] = None,
    kkk: t.Optional[int] = 123,
    lll: t.Optional[str] = None,
):
    ...

if __name__ == '__main__':
    cli.run(main)

int, float

Both int and float are treated as NUMBER.

ArgSense handles NUMBER types flexibly. Besides plain numbers, you can pass any numeric literal that Python accepts, such as binary 0b1010 / 0b1100_0011 or hexadecimal 0xAF2C / 0xFF36_A2B2.

All of them are parsed into the corresponding int/float values.

str

...

Optional[...]

...

What Happens When a Wrong Type Is Passed?

...

Passing Special Values

ArgSense supports converting the following inputs into specific Python values:

User input Recognized as
:true True
:false False
:none None
:empty '' (empty string)

Passing Boolean Values

The usage of boolean values (:true :false) is rather special. Please refer to the following example to understand it:

# test.py
from argsense import cli

@cli
def main(foo: bool, bar: bool):
    """
    params:
        foo (-f):
        bar (-b):
    """
    ...

if __name__ == '__main__':
    cli.run(main)

To pass True, the following are equivalent:

python test.py :true :true
python test.py --foo --bar
python test.py -f -b

They are parsed as main(foo=True, bar=True).

Note that you cannot write python test.py --foo :true --bar :true, because --foo on its own already means foo=True. This is ArgSense's special treatment of boolean values.

To represent False, the following are equivalent:

python test.py :false :false
python test.py --no-foo --no-bar
python test.py --not-foo --not-bar

This is parsed as main(foo=False, bar=False).

Note the --no-foo / --not-foo syntax.

The abbreviation in foo (-f) must be lowercase (otherwise ArgSense reports a parsing error), so the docstring never mixes uppercase and lowercase. Uppercase is reserved to mean "not".

Downside: because of this design, ArgSense cannot handle certain parameter names. For example, def main(no_alpha: bool) makes --no-alpha parse incorrectly and raise an error.

*args and **kwargs

ArgSense supports passing *args and **kwargs, and its usage is intuitive. See the following example:

# test.py
from argsense import cli

@cli
def main(aaa, *bbb, ccc=123, **ddd) -> None:
    print(f'{aaa=}, {bbb=}, {ccc=}, {ddd=}')

if __name__ == '__main__':
    cli.run(main)

Frequently Asked Questions

Q: What are the advantages of ArgSense, compared to argparse, Click, Typer, Python Fire and other libraries?

See Comparison with Other Libraries. In short: ArgSense keeps the simplicity of python-fire (write a normal function, get a CLI), while adopting the docstring-driven, type-hint-based interface and the rich help UI of typer/click. It also adds a few features the others do not have, such as reusable parameter annotations and special value conversion.

Q: How to pass a complex type parameter?

...

Q: When there are too many functions, I want to group them. How can I do it?

...

Q: Why is ArgSense not designed to follow traditional command line usage?

...

Metadata

Release files for argsense 1.3.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Built distribution (wheel)

Table of built distributions (wheels) for argsense 1.3.0
File Interpreter ABI Platform
argsense-1.3.0-py3-none-any.whl Python 3 none any Details

Release files / argsense-1.3.0-py3-none-any.whl

Download URL argsense-1.3.0-py3-none-any.whl
Size 51.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
074f97adb0654378eb36a969a128eddb564a4be3da92e962cbb5c0085b677d7f
BLAKE2b-256 checksum
How to use checksums
bee503fbdaad7db73c3012b887fcc6938694b77ba6cad9eb7f21ba179f330236
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.11.12 {"installer":{"name":"uv","version":"0.11.12","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release history Release notifications | RSS feed

2.1.0

1 release file

2.0.0

1 release file

1.4.0

1 release file

1.3.2

1 release file

1.3.1

1 release file

This release

1.3.0 This release

1 release file

1.2.0

1 release file

1.1.2

1 release file

1.1.1

1 release file

1.1.0

1 release file

1.0.1

1 release file

1.0.0

1 release file

0.7.2

1 release file

0.7.1

1 release file

0.7.0

1 release file

0.6.4

1 release file

0.6.3

1 release file

0.6.2

1 release file

0.6.1

1 release file

0.6.0

1 release file

0.5.8

1 release file

0.5.7

1 release file

0.5.6

1 release file

0.5.5

1 release file

0.5.4

1 release file

0.5.3

1 release file

0.5.2

1 release file

0.5.1

1 release file

0.5.0

1 release file

0.4.3

1 release file

0.4.2

2 release files

0.4.1

2 release files

0.4.0

2 release files

0.3.1

2 release files

0.3.0

2 release files

0.2.2

2 release files

0.2.1

2 release files

0.2.0

2 release files

0.1.2

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page