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, PEP 561 type hints, explicit error handling

Installation

pip install argdec

Or install from source:

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

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.0

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.0
File Size Uploaded
argdec-0.3.0.tar.gz 12.0 kB Details

Built distribution (wheel)

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

Total release size: 24.3 kB

Release files / argdec-0.3.0.tar.gz

Download URL argdec-0.3.0.tar.gz
Size 12.0 kB
Tags Source
SHA-256 checksum
How to use checksums
888e28967afef5a926dd70fca7b88b9adbea9be12b36038b91d70325fad6a0d6
BLAKE2b-256 checksum
How to use checksums
94f58bb8454d2b714725e1055c9ba2377fb9f194797902b15f2208584b32860a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.2

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

Download URL argdec-0.3.0-py3-none-any.whl
Size 12.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
34ce123bf98522c60108c4846e2e94f523070f49e7c17c29f59eac20e1b63487
BLAKE2b-256 checksum
How to use checksums
d5ac6095feae6ef35d576488f2983f8033c4fa913be8ea71fcce4c5f1b1faed2
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.2

Release history Release notifications | RSS feed

0.4.0

2 release files

0.3.1

2 release files

This release

0.3.0 This release

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