ArgSense is a small command-line toolkit for Python. Point it at a function, and it turns the function signature plus its docstring into a complete CLI: argument parsing, type conversion, and a styled help panel — with no parser object to build and no decorator to remember. And when you would rather fill in a command than type it, the very same function is also a form.
If you have ever written this:
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)
...you know the pattern: the parser, the type=, the help= strings and the real function all describe the same thing — and they slowly drift apart.
ArgSense removes that duplication.
Install
pip install argsense
Requires Python 3.9 and above. The optional form (the TUI) pulls in Textual as well:
pip install argsense[tui]
From a function to a CLI in 30 seconds
# main.py
from argsense import cli
def hello(name: str = 'world'):
print(f'hello {name}')
if __name__ == '__main__':
cli.run(hello)
python main.py # hello world
python main.py Alice # hello Alice
python main.py -h # a styled help panel
No parser object, no add_argument, no help=. The function is the interface.
The same thing, in four libraries
-
argparse builds a parser imperatively and then unpacks
args. -
Click adds
@click.command()and@click.option(...)decorators. -
Typer infers from type hints, but help text still lives in
Annotated[...]. - ArgSense infers from type hints — and the docstring is the help text.
What makes it click
The docstring is the help text
Write each parameter's description once, in the docstring:
def install(appid: str, version: str = '', use_exist: bool = True):
"""
install an app from the cloud.
params:
appid: the app id (a normalized app name).
version: the app version; the last installed one is used if omitted.
use_exist: reuse the installed version when possible.
"""
With Click or Typer you would pass those same strings into help= / Annotated. Here there is one source of truth, so the code and the help can never disagree.
Types — including the values you actually want
Annotations drive the conversion, so count: int and rate: float just work. On top of that, ArgSense understands a few sentinels that argparse and Typer don't have:
| Input | Becomes |
|---|---|
:true / :false
|
True / False
|
:none |
None |
:empty |
'' |
That matters more than it sounds: --tag :none gives you a real None, instead of the string ":none" that you would have to post-process yourself.
Several commands in one file
Define more functions and call cli.run() with no argument — every public function in the module becomes a command:
def foo(a: str):
"""download a file by name."""
def bar(b: int):
"""update the local cache."""
if __name__ == '__main__':
cli.run()
Click needs a @click.group() and Typer needs a Typer() app. Here every public function is simply a command.
Group them when they multiply
Past a handful of commands a flat list gets hard to scan. cli.run_group(...) puts them in named groups — the same functions, the same docstrings, just organized:
if __name__ == '__main__':
cli.run_group({
'app': (install, upgrade, run),
'debug': (doctor, echo),
})
The group name is a label in the help interface, not a word you have to type: it stays python main.py install my-app. (With Click or Typer, nested groups are real sub-applications, so the invocation becomes python main.py app install my-app.)
A form, not just a command line
Sometimes you do not want to remember the flags — you want to see them. Pass mode='tui', or type :tui on the command line, and the same commands are rendered as a form: a sidebar of groups, a tab per command, a widget per parameter, and a log panel for whatever the command prints.
if __name__ == '__main__':
cli.run_group({'app': (install, upgrade, run)}, mode='tui')
Nothing extra is declared for it: the form reads the same signature and docstring, so the parameters, the types, the help text and the defaults are the ones you already wrote. A text parameter becomes an input, a number gets a narrow box of its own, and a flag becomes a switch; consecutive small widgets are packed into a single row so the form stays compact. It does need the optional dependency:
pip install argsense[tui]
Document a shared parameter once
When a few commands share a parameter, describe it once and reference it elsewhere:
def foo(name: str):
"""
params:
name: the leader's name.
"""
def bar(name: str):
"""
params:
name: see `@foo:name`
"""
The reference is resolved lazily, so registration order doesn't matter (and references can even chain). The libraries compared here have no docstring-level equivalent — the usual workaround is extracting a shared decorator by hand.
Help that stays readable
Long descriptions are the classic failure mode of CLI help: the description column gets squeezed into a narrow strip. ArgSense notices when a description doesn't get enough room and reflows it onto a full-width line.
Colors that belong on a terminal
The styling adapts to what the terminal can actually show. On a truecolor terminal it draws the Catppuccin mocha palette with a subtle gradient; on a 256-color terminal the gradient drops but the palette stays; and on an old 16-color terminal it falls back to plain ANSI without ever looking broken.
You pick nothing and configure nothing — the palette is chosen from the terminal's own color system.
How it compares
| Library | Interface declared with | Help text from | Type conversion | Multiple commands | Help UI | Form UI |
|---|---|---|---|---|---|---|
| argparse |
add_argument calls |
help= |
manual | subparsers | plain | no |
| Click | decorators |
help= + docstring |
manual | @click.group |
pretty | no |
| Typer | type hints |
Annotated + docstring |
yes |
Typer app |
rich | no |
| Python Fire | function signature | docstring | yes | dict / class | plain | no |
| ArgSense | type hints + docstring | docstring (params:) |
yes |
cli.run() / cli.run_group()
|
rich | TUI |
Built on Rich, so the help panel is styled out of the box, and on Textual the very same commands can be filled in as a form — with a deliberately small surface area: annotate a function and you're done.
Try it
pip install argsense # the CLI
pip install argsense[tui] # the CLI, plus the optional TUI form
- Repo: https://github.com/likianta/argsense-cli
- PyPI: https://pypi.org/project/argsense/
- In-depth comparison (argparse, Click, Typer, Python Fire, docopt, clize, cyclopts, defopt): see the "Comparison with Other Libraries" section of the README.
If you build command-line tools in Python, give it a look. And if it saves you some boilerplate, a star on GitHub goes a long way.











Top comments (0)