Skip to main content

argdec

A decorator-based, declarative interface to Python's argparse for building hierarchical CLI applications.

Python 3.10+ License: MIT

Overview

argdec (formerly argdeclare) provides two complementary approaches to configuring argparse:

  1. Decorator-based configuration - Use @option and @option_group decorators to attach argparse arguments directly to command methods, keeping argument definitions co-located with the code that uses them.

  2. Declarative class structure - Define CLI applications as classes where methods become commands, docstrings become help text, and class attributes configure parser behavior.

This combination eliminates boilerplate while preserving full access to argparse's capabilities.

Features

  • Decorator-driven options - Configure argparse arguments with @option and @option_group decorators directly on methods

  • Declarative command structure - Methods prefixed with do_ automatically become subcommands

  • Hierarchical commands - Build nested command structures (e.g., git remote add) using underscore-separated method names, to a depth you control

  • Reusable option groups - Define common options once, apply to multiple commands with @option_group

  • Full argparse compatibility - All argparse features available through decorator parameters

  • Customizable - Configure command prefix, hierarchy levels, and more

  • Inheritance-friendly - Share commands across applications with a common base class

  • Typed - Comprehensive test suite, inline type hints, explicit error handling

  • Single module - One dependency-free file, so it can be vendored by copying argdec.py

Installation

pip install argdec

Or install from source:

git clone https://github.com/shakfu/argdec.git
cd argdec
pip install .

argdec is a single module with no dependencies. To vendor it, copy argdec.py into your project instead of installing.

Type checking

argdec.py is annotated. pyright and Pylance read those annotations from the installed source, so decorated commands keep their signatures:

App.do_build  ->  (self: App, args: Unknown) -> None

mypy honours inline annotations only from a package shipping a py.typed marker. PEP 561 has no equivalent for a top-level module, so mypy reports import-untyped and treats the module as Any. Silence it with an override:

[[tool.mypy.overrides]]
module = ["argdec"]
ignore_missing_imports = true

Quick Start

from argdec import Commander, option

class MyApp(Commander):
    """My awesome CLI application."""
    name = 'myapp'
    version = '1.0'

    @option("-v", "--verbose", action="store_true", help="verbose output")
    def do_build(self, args):
        """Build the project."""
        print(f"Building... (verbose={args.verbose})")

if __name__ == '__main__':
    app = MyApp()
    app.cmdline()
$ python myapp.py build --verbose
Building... (verbose=True)

Declarative Format Example

#!/usr/bin/env python3

from argdec import Commander, option, option_group

# ----------------------------------------------------------------------------
# Commandline interface

common_options = option_group(
    option("--dump", action="store_true", help="dump project and product vars"),
    option("-d","--download",
           action="store_true",
           help="download python build/downloads"),
    option("-r", "--reset", action="store_true", help="reset python build"),
    option("-i","--install",
           action="store_true",
           help="install python to build/lib"),
    option("-b","--build",
           action="store_true",
           help="build python in build/src"),
    option("-c","--clean",
           action="store_true",
           help="clean python in build/src"),
    option("-z", "--ziplib", action="store_true", help="zip python library"),
    option("-p", "--py-version", type=str,
           help="set required python version to download and build"),
)

class Application(Commander):
    """builder: builds the py-js max external and python from source."""
    name = 'builder'
    epilog = ''
    version = '0.1'
    default_args = ['--help']
    _argparse_levels = 1


# ----------------------------------------------------------------------------
# python builder methods

    # def do_python(self, args):
    #     "download and build python from src"

    @common_options
    def do_python_static(self, args):
        """build static python"""
        print(args)

    @common_options
    def do_python_shared(self, args):
        """build shared python"""
        print(args)

    @common_options
    def do_python_shared_pkg(self, args):
        """build shared python to embed in package"""
        print(args)

    @common_options
    def do_python_framework(self, args):
        """build framework python"""
        print(args)

    @common_options
    def do_python_framework_pkg(self, args):
        """build framework python to embed in a package"""
        print(args)


# ----------------------------------------------------------------------------
# utility methods

    # def do_check(self, args):
    #     """check reference utilities"""
    #     print(args)

    @common_options    
    def do_check_log_day(self, args):
        """analyze log day"""
        print(args)

    @common_options    
    def do_check_log_week(self, args):
        """analyze log week"""
        print(args)

    @common_options
    def do_check_sys_month(self, args):
        """analyze sys month"""
        print(args)

    @common_options
    def do_check_sys_def(self, args):
        """analyze sys def"""
        print(args)

    @common_options
    def do_check_sys_xyz(self, args):
        """analyze sys xyz"""
        print(args)

    @common_options
    def do_test(self, args):
        """test suite"""
        print(args)


    @common_options
    def do_test_app(self, args):
        """test app"""
        print(args)

    @common_options
    def do_test_functions(self, args):
        """test functions"""
        print(args)

if __name__ == '__main__':
    app = Application()
    app.cmdline()

The _argparse_levels attribute controls how deep the command hierarchy goes. A method name is split on at most _argparse_levels underscores, so the leaf command keeps whatever underscores remain:

_argparse_levels do_python_shared_pkg is invoked as
0 (default) app python_shared_pkg
1 app python shared_pkg
2 app python shared pkg

with levels=0 gives:

$ python3 demo.py
usage: demo.py [-h] [-v]  ...

builder: builds the py-js max external and python from source.

optional arguments:
  -h, --help            show this help message and exit
  -v, --version         show program's version number and exit

subcommands:
  valid subcommands

                        additional help
    check_log_day       analyze log day
    check_log_week      analyze log week
    check_sys_def       analyze sys def
    check_sys_month     analyze sys month
    check_sys_xyz       analyze sys xyz
    python_framework    build framework python
    python_framework_pkg
                        build framework python to embed in a package
    python_shared       build shared python
    python_shared_pkg   build shared python to embed in package
    python_static       build static python
    test                test suite
    test_app            test app
    test_functions      test functions

with levels=1 gives:

$ python3 demo.py
usage: demo.py [-h] [-v]  ...

builder: builds the py-js max external and python from source.

optional arguments:
  -h, --help     show this help message and exit
  -v, --version  show program's version number and exit

subcommands:
  valid subcommands

                 additional help
    check        check commands
    python       python commands
    test         test suite

Advanced Features

Sharing Commands Between Applications

Commands are inherited, so a common base class can supply commands to several applications. A subclass may override an inherited command by redefining the method under the same name.

class CommonCommands(Commander):
    def do_version_info(self, args):
        """show build information"""
        print("...")

class MyApp(CommonCommands):
    """My application."""
    def do_build(self, args):
        """Build the project."""
        print("building")

# MyApp now has both `build` and `version_info`

Driving the CLI Programmatically

cmdline() reads sys.argv[1:] by default, but accepts an explicit argument list — useful in tests, in a REPL, or when embedding the CLI in a larger program. A Commander instance can be invoked repeatedly.

app = MyApp()
app.cmdline(argv=["build", "--verbose"])
app.cmdline(argv=["test"])

build_parser() is also public, if you want the configured argparse.ArgumentParser without executing anything.

Custom Command Prefix

By default, methods starting with do_ become commands. You can customize this:

class MyApp(Commander):
    _command_prefix = "cmd_"  # Use 'cmd_' instead of 'do_'

    def cmd_build(self, args):
        """Build the project."""
        pass

    def cmd_deploy(self, args):
        """Deploy the project."""
        pass

The prefix is inherited by subclasses.

See examples/custom_prefix.py for more examples.

Error Handling

Version 0.2.0+ includes comprehensive error handling:

from argdec import ArgDecError, CommandExecutionError

try:
    app.cmdline()
except CommandExecutionError as e:
    print(f"Command failed: {e}")
except ArgDecError as e:
    print(f"Configuration error: {e}")

Argparse conventions are preserved: --help, --version, argparse errors and a missing subcommand all raise SystemExit rather than an ArgDecError. Invoking an application (or an intermediate command) with no subcommand prints help to stderr and exits with status 2.

Examples

Can be found in the examples directory:

  • basic.py - Basic example application

  • hierarchical.py - Full-featured example application

  • custom_prefix.py - Custom prefix demonstrations

Development

Running Tests

make test           # Run test suite
make coverage       # Run with coverage report
make lint           # Run ruff linter (read-only)
make fix            # Run ruff linter and apply fixes
make typecheck      # Run mypy type checker
make all            # Run all checks

Requirements

  • Python 3.10+

  • No external dependencies (uses stdlib only)

  • Development: pytest, ruff, mypy (optional)

Version History

See CHANGELOG.md for detailed version history.

License

MIT License - See LICENSE file for details.

Credits

Based on the original argdeclare recipe from ActiveState.

Contributing

Contributions welcome! Please:

  1. Run tests: make test

  2. Check types: make typecheck

  3. Lint code: make lint

  4. Add tests for new features

The suite is kept at 100% statement and branch coverage (make coverage). That is a floor, not a goal: coverage sat at 94% while several real defects hid in covered lines, so please add tests that exercise behaviour, not just lines.

Release files for argdec 0.3.1

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

Source distribution (sdist)

Source distribution for argdec 0.3.1
File Size Uploaded
argdec-0.3.1.tar.gz 94.1 kB Details

Built distribution (wheel)

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

Total release size: 106.2 kB

Release files / argdec-0.3.1.tar.gz

Download URL argdec-0.3.1.tar.gz
Size 94.1 kB
Tags Source
SHA-256 checksum
How to use checksums
6854f67e1422fff9d6903eb1721d228c3cb6132bc4dfe6ae7f5666d62e3b888d
BLAKE2b-256 checksum
How to use checksums
5e7d1a3aad5c6cec4200658e105b780e8709b516010d8729405a6ca21da019ef
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.7

Release files / argdec-0.3.1-py3-none-any.whl

Download URL argdec-0.3.1-py3-none-any.whl
Size 12.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
76526e3c484da220223b63c82d835ef21897cf373c1797b5829e33bb3c17a2fd
BLAKE2b-256 checksum
How to use checksums
a6d6077f6f1bee94d7e14131df15a87764f320671d20cb47d8d1b76e2662f89c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.7

Release history Release notifications | RSS feed

0.4.0

2 release files

This release

0.3.1 This release

2 release files

0.3.0

2 release files

0.2.2

2 release files

0.2.1

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