click-option-group review
Our click-option-group 0.5.9 sandbox install took 0.2 seconds, occupied 1 MB across 2 packages, and imported successfully in 0.18 seconds. The library is a focused Click extension: `optgroup.group` prints a named block in command help, while five supplied group classes enforce rules such as exactly one input or a complete set of credentials. Release 0.5.9 fixes mypy use without dropping pyright compatibility. It remains pure Python, supports Python 3.7 and newer, and ships the `py.typed` marker.
click-option-group 0.5.9 installed in 0.2 seconds and used 1 MB in our sandbox, with a working import, typed packaging, and 0 audit findings. Install it for constrained option blocks in an existing Click CLI; choose a broader framework when you also need command-level structure or annotation-led declarations.
We installed it
| Install | ✓ · 0.2s | 2 packages on disk · 1 MB |
| Import | ✓ | import click_option_group in 0.18s · pure Python · py.typed · requires Python >=3.7 |
| Known vulns | 0 | (pip-audit) |
Answers from our run
Does click-option-group install cleanly?
Yes. In a fresh container with an empty cache, pip install click-option-group finished in 0.2s, leaving 2 packages and 1 MB on disk. pip-audit reported no known vulnerabilities.
What does click-option-group need to run?
Python >=3.7, and nothing compiled: it is pure Python. In our run import click_option_group succeeded in 0.18s, and the package ships py.typed for type checkers.
click-option-group or click: which should you use?
click: Keep plain Click when one callback check is clearer than a new help section and constraint class. click-option-group 0.5.9 installed in 0.2 seconds and used 1 MB in our sandbox, with a working import, typed packaging, and 0 audit findings.
When should you not use click-option-group?
Your flags need nested groups. The tutorial calls subgroups unsupported by design and recommends nested commands instead.
Use it if
- An established Click command needs option headings without moving to another command framework.
- Two input flags must be mutually exclusive, with the parser rejecting an empty choice as well.
- A credential or connection group must be supplied completely instead of reaching application code half filled.
- A project needs a custom `OptionGroup` subclass for a command-local rule that Click does not express itself.
- Your flags need nested groups. The tutorial calls subgroups unsupported by design and recommends nested commands instead.
- Options from separate commands or positional arguments participate in one rule. A group only collects option decorators attached to the same command callback.
- You want to interleave grouped and ordinary decorators. The declaration checker raises an exception when `optgroup.option` and `click.option` are mixed inside a group.
- A new CLI should be generated from type annotations. Typer and Cyclopts make that the main authoring model, while this package keeps Click's decorator syntax.
- You also need aliases, command sections, themed help, and richer constraints. Cloup covers that wider Click extension surface in one dependency.
Setup reality
Our fresh Python 3.12 install of click-option-group 0.5.9 succeeded in 0.2 seconds. It left 2 packages and 1 MB on disk, with 9 direct dependencies recorded by our lab run. The package is pure Python, needs Python 3.7 or newer, and carries a BSD license. import click_option_group worked in 0.18 seconds, and pip-audit found 0 known vulnerabilities. The wheel includes py.typed for type checkers.
No service, account, compiler, or project config is involved. Declaration order is the part that causes first-run failures. Every optgroup.option must sit next to the other options collected by its optgroup.group. Putting a normal click.option between them raises an exception. Calling optgroup.option without a matching group also raises, while declaring an empty group emits a runtime warning.
The five constraint classes disagree about the empty case. MutuallyExclusiveOptionGroup accepts 0 or 1 selected option; RequiredMutuallyExclusiveOptionGroup accepts exactly 1. AllOptionGroup permits none or all, RequiredAllOptionGroup demands the full set, and RequiredAnyOptionGroup needs at least 1. Click still supplies conversions, prompts, defaults, environment values, and callbacks, so a value provided outside argv can satisfy a group rule.
Grouping only changes help layout and option validation. It adds no nested-group model and does not coordinate values across subcommands. Exercise the empty, partial, valid, and conflicting paths with CliRunner, especially when defaults or environment variables are present. Version 0.5.9 changed typing behavior, so teams that run both mypy and pyright should keep both checks in CI rather than treating the bundled marker as proof that every custom subclass is sound.
Patterns
Print a connection section in help label-option-section
import click
from click_option_group import optgroup
@click.command()
@optgroup.group('Connection', help='Remote endpoint')
@optgroup.option('--host', default='127.0.0.1', show_default=True)
@optgroup.option('--port', type=click.IntRange(1, 65535), default=8080)
def main(host, port):
click.echo(f'{host}:{port}')The group decorator collects the adjacent `optgroup.option` declarations directly below it and prints one heading above them.
Demand exactly one input source require-one-source
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
click.echo(source.name)`RequiredMutuallyExclusiveOptionGroup` rejects 0 selections and also rejects 2 selections before the callback runs.
Allow zero or one format switch allow-optional-format
import click
from click_option_group import MutuallyExclusiveOptionGroup, optgroup
@click.command()
@optgroup.group('Format', cls=MutuallyExclusiveOptionGroup)
@optgroup.option('--json', 'output_format', flag_value='json')
@optgroup.option('--yaml', 'output_format', flag_value='yaml')
def render(output_format):
click.echo(output_format or 'text')`MutuallyExclusiveOptionGroup` allows 0 or 1 option, so the callback can retain a default format when neither flag appears.
Require at least one output require-any-destination
import click
from click_option_group import RequiredAnyOptionGroup, optgroup
@click.command()
@optgroup.group('Destination', cls=RequiredAnyOptionGroup)
@optgroup.option('--path', type=click.Path(dir_okay=False))
@optgroup.option('--stdout', is_flag=True)
def export(path, stdout):
pass`RequiredAnyOptionGroup` accepts 1 or 2 supplied destinations; it only rejects the empty invocation.
Accept both proxy fields or neither pair-optional-credentials
import click
from click_option_group import AllOptionGroup, optgroup
@click.command()
@optgroup.group('Proxy', cls=AllOptionGroup)
@optgroup.option('--proxy-user')
@optgroup.option('--proxy-password', hide_input=True)
def fetch(proxy_user, proxy_password):
pass`AllOptionGroup` permits 0 or 2 values in this pair and reports which option is missing from a partial invocation.
Demand every database field require-complete-connection
import click
from click_option_group import RequiredAllOptionGroup, optgroup
@click.command()
@optgroup.group('Database', cls=RequiredAllOptionGroup)
@optgroup.option('--host')
@optgroup.option('--database')
def migrate(host, database):
pass`RequiredAllOptionGroup` makes the 2-option set mandatory, so separate `required=True` attributes are unnecessary.
Keep a Click flag outside the group separate-plain-option
import click
from click_option_group import optgroup
@click.command()
@click.option('--verbose', is_flag=True)
@optgroup.group('Server')
@optgroup.option('--host', default='localhost')
@optgroup.option('--port', type=int, default=8000)
def main(verbose, host, port):
passA normal `click.option` may sit above or below the complete group, but placing it between grouped options raises a declaration error.
Read a grouped token from the environment supply-value-from-env
import click
from click_option_group import RequiredAnyOptionGroup, optgroup
@click.command()
@optgroup.group('Authentication', cls=RequiredAnyOptionGroup)
@optgroup.option('--token', envvar='APP_TOKEN', hide_input=True)
@optgroup.option('--profile')
def main(token, profile):
pass`APP_TOKEN` can satisfy the required group without a token flag because Click resolves environment values during parsing.
Place help inside a named block group-help-option
import click
from click_option_group import optgroup
@click.command(add_help_option=False)
@optgroup.group('General')
@optgroup.help_option('--help', '-h')
@optgroup.option('--version', is_flag=True)
def main(version):
pass`add_help_option=False` prevents Click from registering a second help flag when `optgroup.help_option` supplies one.
Declare a reusable group instance reuse-group-object
import click
from click_option_group import OptionGroup
network = OptionGroup('Network', help='Connection controls')
@click.command()
@network.option('--host', default='localhost')
@network.option('--timeout', type=float, default=5.0)
def main(host, timeout):
pass`OptionGroup.option` is the documented second API and avoids the global helper when a named group object is easier to share.
Test empty and conflicting choices test-constraint-errors
from click.testing import CliRunner
def test_sources():
runner = CliRunner()
assert runner.invoke(load, []).exit_code != 0
ok = runner.invoke(load, ['--json-file', 'one.json'])
assert ok.exit_code == 0
conflict = runner.invoke(load, ['--json-file', 'one.json', '--csv-file', 'two.csv'])
assert conflict.exit_code != 0A required exclusive group has 3 distinct paths to test: empty, exactly one value, and conflicting values.
Reject more than two selected options define-custom-rule
from click_option_group import OptionGroup
class AtMostTwo(OptionGroup):
@property
def name_extra(self):
return ['at_most_two']
def handle_parse_result(self, option, ctx, opts):
selected = [name for name in self.get_options(ctx) if name in opts]
if len(selected) > 2:
self.fail('choose no more than two options', ctx)
super().handle_parse_result(option, ctx, opts)Custom constraints override `handle_parse_result`, which ties them to click-option-group's parser lifecycle more closely than the five supplied classes.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| click | PyPI | Keep plain Click when one callback check is clearer than a new help section and constraint class. |
| typer | PyPI | Use Typer for a new Click-backed CLI whose commands and parameters should come from Python annotations. |
| cyclopts | PyPI | Use Cyclopts when annotation parsing, structured parameters, and grouping should share one framework. |
| cloup | PyPI | Use Cloup when grouped options are one requirement among aliases, command sections, styled help, and constraints. |
More cli & tooling guides
chalk · commander · typescript · esbuild · yargs · click · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

