mrkeyoor.com_
Sat 08 Aug 17:42 UTC
PyPICLI & Toolingupdated 08 Aug 2026

click-option-group

click-option-group is a small Click extension that puts related options under labeled sections in `--help` and can validate relationships among those options. Its built-in group classes cover at-least-one, all-or-none, all-required, mutually-exclusive, and exactly-one-required rules. It keeps Click's decorator style and passes ordinary option declarations through to Click, so it is best viewed as missing presentation and constraint glue for an existing Click command.

Verdict

Install it when a mature Click command needs readable help sections and a few local option constraints. For a new typed CLI or one small command, it is extra decorator machinery you can avoid.

API stability4/5The public API is compact and follows Click's established decorator conventions: `optgroup.group`, `optgroup.option`, `OptionGroup`, and five named constraint classes. Version 0.5.9 still supports Click 7 and Python 3.7, suggesting careful compatibility. The pre-1.0 version number and dependence on Click's decorator internals leave some upgrade risk, but the narrow scope limits how much can move.
Docs3/5The README gives a complete runnable example and the Read the Docs site lists every group class, method, property, and type signature. It clearly distinguishes the built-in constraint families in API descriptions. What is missing is a deeper task guide for decorator-order failures, defaults, callbacks, reusable command composition, and custom group classes, so users often need tests or source reading for edge cases.
Maintenance3/5Version 0.5.9 was released and the repository pushed in October 2025, and GitHub currently reports 6 open issues and pull requests. That is healthy enough for a focused compatibility extension, but the repository has only 121 stars and release activity is naturally much smaller than Click itself. Teams should pin and test Click upgrades rather than assume the extension receives same-day compatibility work.
Ecosystem4/5The package recorded 7,727,421 downloads in the latest measured week and composes directly with Click option types, callbacks, defaults, CliRunner tests, commands, and groups. That unusually high install footprint makes it far less obscure than its star count suggests. Its ecosystem is still bounded by Click; projects on argparse, Typer-style annotations, or another parser gain nothing from it.

Use it if

  • Your Click command has enough options that flat help output is hard to scan
  • You need declarative exactly-one, at-least-one, all-or-none, or all-required option rules
  • You want constraints checked during Click parsing with standard usage errors
  • You already use Click decorators and do not want to adopt a new CLI framework
Skip it if

Setup reality

`pip install -U click-option-group` installs a pure Python package requiring Python 3.7 or later and Click 7 or later. There are no services, generated files, or native builds. The learning cost is decorator placement. `@optgroup.group(...)` collects the contiguous `@optgroup.option(...)` decorators beneath it because Python applies decorators from the function outward; place another group marker at the wrong point and options appear under the wrong heading or outside the intended constraint. These are grouped options, not ordinary `@click.option` declarations, so use `optgroup.option` for every member a rule must inspect. Pick the semantic class carefully: `MutuallyExclusiveOptionGroup` permits none, while `RequiredMutuallyExclusiveOptionGroup` requires exactly one; `AllOptionGroup` allows all or none, while `RequiredAllOptionGroup` requires all. The extension still inherits Click's parsing, type conversion, callbacks, environment values, and testing behavior, so Click remains a required skill and dependency. Constraints fire at parse time and produce `UsageError` output, which means automated tests should cover empty, partial, conflicting, defaulted, and valid combinations. Defaults can make an option count as present in ways that are easy to miss, so avoid clever defaults inside constrained groups unless tests prove the behavior you intend.

Patterns

Put related options under one help headinggroup-help-options

import click
from click_option_group import optgroup

@click.command()
@optgroup.group('Server', help='Connection settings')
@optgroup.option('--host', default='localhost', show_default=True)
@optgroup.option('--port', type=click.IntRange(1, 65535), default=8000, show_default=True)
def cli(host, port):
    click.echo(f'{host}:{port}')

Keep each group's `optgroup.option` decorators contiguous directly below its group decorator.

Require at least one optionrequire-any-option

import click
from click_option_group import RequiredAnyOptionGroup, optgroup

@click.command()
@optgroup.group('Output', cls=RequiredAnyOptionGroup)
@optgroup.option('--json-file', type=click.Path())
@optgroup.option('--stdout', is_flag=True)
def export(json_file, stdout):
    pass

This group accepts one or both options; use a required mutually-exclusive group when exactly one is allowed.

Require exactly one input sourcerequire-exactly-one

import click
from click_option_group import RequiredMutuallyExclusiveOptionGroup, optgroup

@click.command()
@optgroup.group('Input', cls=RequiredMutuallyExclusiveOptionGroup)
@optgroup.option('--json-file', type=click.File('r'))
@optgroup.option('--csv-file', type=click.File('r'))
def load(json_file, csv_file):
    source = json_file or csv_file

The group rejects both zero selections and multiple selections during command-line parsing.

Allow at most one output formatallow-zero-or-one

import click
from click_option_group import MutuallyExclusiveOptionGroup, optgroup

@click.command()
@optgroup.group('Format', cls=MutuallyExclusiveOptionGroup)
@optgroup.option('--json', 'format_', flag_value='json')
@optgroup.option('--yaml', 'format_', flag_value='yaml')
def report(format_):
    click.echo(format_ or 'text')

This group permits no choice; do not use it when one format must always be explicit.

Require credentials together or not at allrequire-all-or-none

import click
from click_option_group import AllOptionGroup, optgroup

@click.command()
@optgroup.group('Proxy credentials', cls=AllOptionGroup)
@optgroup.option('--proxy-user')
@optgroup.option('--proxy-password', hide_input=True)
def fetch(proxy_user, proxy_password):
    pass

AllOptionGroup accepts neither option or every option, preventing half-configured credentials.

Require every option in a grouprequire-all-options

import click
from click_option_group import RequiredAllOptionGroup, optgroup

@click.command()
@optgroup.group('Database', cls=RequiredAllOptionGroup)
@optgroup.option('--host', required=False)
@optgroup.option('--database', required=False)
def migrate(host, database):
    pass

The group supplies the collective required rule; adding individual `required=True` flags would obscure why the group class is present.

Use multiple groups on one commandcombine-option-groups

import click
from click_option_group import RequiredMutuallyExclusiveOptionGroup, optgroup

@click.command()
@optgroup.group('Server')
@optgroup.option('--host', default='localhost')
@optgroup.option('--port', type=int, default=8000)
@optgroup.group('Input', cls=RequiredMutuallyExclusiveOptionGroup)
@optgroup.option('--file', type=click.Path(exists=True))
@optgroup.option('--stdin', is_flag=True)
@click.option('--verbose', is_flag=True)
def cli(host, port, file, stdin, verbose):
    pass

Ordinary Click options can coexist, but only options declared with `optgroup.option` participate in a group's validation.

Use Click types, prompts, and environment valuesreuse-click-option-features

import click
from click_option_group import optgroup

@click.command()
@optgroup.group('Authentication')
@optgroup.option('--token', envvar='APP_TOKEN', hide_input=True)
@optgroup.option('--timeout', type=click.FloatRange(min=0.1), default=5.0)
def cli(token, timeout):
    pass

Grouped options are still Click options, so environment values and defaults may affect whether a constraint sees an option as supplied.

Hide an internal option group from helphide-option-group

import click
from click_option_group import optgroup

@click.command()
@optgroup.group('Internal', hidden=True)
@optgroup.option('--trace-parser', is_flag=True, hidden=True)
def cli(trace_parser):
    pass

Hidden options remain parseable; hiding is not access control and should not protect unsafe behavior.

Test valid and conflicting combinationstest-group-constraints

from click.testing import CliRunner

def test_input_group():
    runner = CliRunner()
    assert runner.invoke(load, []).exit_code != 0
    assert runner.invoke(load, ['--json-file', 'a.json']).exit_code == 0
    result = runner.invoke(load, ['--json-file', 'a.json', '--csv-file', 'a.csv'])
    assert result.exit_code != 0
    assert 'mutually exclusive' in result.output.lower()

Test empty, partial, conflicting, and valid cases because defaults and callbacks can change what the parser considers present.

Alternatives

PackageRegistryPick it when
clickPyPISimple commands only need core options, commands, callbacks, and custom validation
typerPyPIYou want Click underneath but prefer type-annotation-driven command definitions
cycloptsPyPIYou want modern annotation-driven parsing with parameter groups and validation
cloupPyPIYou want a broader Click extension covering option groups, constraints, sections, and styled help