Types

A collection of custom Click parameter types for common use-cases.

EnumChoice

click.Choice is supporting Enums, but naively: the Enum.name property of each members is used for choices. It was designed that way to simplify the implementation, because it is the part of Enum that is guaranteed to be unique strings.

But this is not always what we want, especially when the Enum’s names are not user-friendly: they may contain underscores, uppercase letters, etc. This custom EnumChoice type solve this issue by allowing you to select which part of the Enum members to use as choice strings.

Limits of Click.Choice

Let’s start with a simple example to demonstrate the limitations of click.Choice. Starting with this Format definition:

from enum import Enum

class Format(Enum):
    TEXT = "text"
    HTML = "html"
    OTHER_FORMAT = "other-format"

This Format gets standard names for each member:

>>> Format.TEXT.name
'TEXT'

>>> Format.HTML.name
'HTML'

>>> Format.OTHER_FORMAT.name
'OTHER_FORMAT'

But we made its values more user-friendly:

>>> Format.TEXT.value
'text'

>>> Format.HTML.value
'html'

>>> Format.OTHER_FORMAT.value
'other-format'

Now let’s combine this Enum with click.Choice into a simple CLI:

from enum import Enum

from click import command, option, echo, Choice


class Format(Enum):
    TEXT = "text"
    HTML = "html"
    OTHER_FORMAT = "other-format"


@command
@option(
    "--format",
    type=Choice(Format),
    show_choices=True,
    default=Format.HTML,
    show_default=True,
    help="Select format.",
)
def cli(format):
    echo(f"Selected format: {format!r}")

All Enum’s members are properly registered and recognized when using their name:

$ cli --format TEXT
Selected format: <Format.TEXT: 'text'>
$ cli --format HTML
Selected format: <Format.HTML: 'html'>
$ cli --format OTHER_FORMAT
Selected format: <Format.OTHER_FORMAT: 'other-format'>

However, using the value fails:

$ cli --format text
Usage: cli [OPTIONS]
Try 'cli --help' for help.

Error: Invalid value for '--format': 'text' is not one of 'TEXT', 'HTML', 'OTHER_FORMAT'.
$ cli --format html
Usage: cli [OPTIONS]
Try 'cli --help' for help.

Error: Invalid value for '--format': 'html' is not one of 'TEXT', 'HTML', 'OTHER_FORMAT'.
$ cli --format other-format
Usage: cli [OPTIONS]
Try 'cli --help' for help.

Error: Invalid value for '--format': 'other-format' is not one of 'TEXT', 'HTML', 'OTHER_FORMAT'.

This preference for Enum.name is also reflected in the help message, both for choices and default value:

$ cli --help
Usage: cli [OPTIONS]

Options:
  --format [TEXT|HTML|OTHER_FORMAT]
                                  Select format.  [default: HTML]
  --help                          Show this message and exit.

To change this behavior, we need EnumChoice.

Usage

Let’s use click_extra.EnumChoice instead of click.Choice, and then override the __str__ method of our Enum:

from enum import Enum

from click import command, option, echo
from click_extra import EnumChoice


class Format(Enum):
    TEXT = "text"
    HTML = "html"
    OTHER_FORMAT = "other-format"

    def __str__(self):
        return self.value


@command
@option(
    "--format",
    type=EnumChoice(Format),
    show_choices=True,
    help="Select format.",
)
def cli(format):
    echo(f"Selected format: {format!r}")

This renders into much better help messages:

$ cli --help
Usage: cli [OPTIONS]

Options:
  --format [text|html|other-format]
                                  Select format.
  --help                          Show this message and exit.

User inputs are now matched against the str() representation:

$ cli --format other-format
Selected format: <Format.OTHER_FORMAT: 'other-format'>

And not the Enum.name:

$ cli --format OTHER_FORMAT
Usage: cli [OPTIONS]
Try 'cli --help' for help.

Error: Invalid value for '--format': 'OTHER_FORMAT' is not one of 'text', 'html', 'other-format'.

By customizing the __str__ method of the Enum, you have full control over how choices are displayed and matched.

Case-sensitivity

EnumChoice is case-insensitive by default, unlike click.Choice, so random casing are recognized:

$ cli --format oThER-forMAt
Selected format: <Format.OTHER_FORMAT: 'other-format'>

If you want to restore case-sensitive matching, you can enable it by setting the case_sensitive parameter to True:

from enum import Enum

from click import command, option, echo
from click_extra import EnumChoice


class Format(Enum):
    TEXT = "text"
    HTML = "html"
    OTHER_FORMAT = "other-format"

    def __str__(self):
        return self.value


@command
@option(
    "--format",
    type=EnumChoice(Format, case_sensitive=True),
    show_choices=True,
    help="Select format.",
)
def cli(format):
    echo(f"Selected format: {format!r}")
$ cli --format oThER-forMAt
Usage: cli [OPTIONS]
Try 'cli --help' for help.

Error: Invalid value for '--format': 'oThER-forMAt' is not one of 'text', 'html', 'other-format'.

Choice source

EnumChoice use the str() representation of each member by default. But you can configure it to select which part of the members to use as choice strings.

That’s done by setting the choice_source parameter to one of:

Here is an example using ChoiceSource.KEY, which is equivalent to click.Choice behavior:

from enum import Enum

from click import command, option, echo
from click_extra import EnumChoice, ChoiceSource


class Format(Enum):
    TEXT = "text"
    HTML = "html"
    OTHER_FORMAT = "other-format"

    def __str__(self):
        return self.value


@command
@option(
    "--format",
    type=EnumChoice(Format, choice_source=ChoiceSource.KEY),
    show_choices=True,
    help="Select format.",
)
def cli(format):
    echo(f"Selected format: {format!r}")

So even though we still override the __str__ method, user inputs are now matched against the name:

$ cli --format OTHER_FORMAT
Selected format: <Format.OTHER_FORMAT: 'other-format'>

And not the str() representation:

$ cli --format other-format
Usage: cli [OPTIONS]
Try 'cli --help' for help.

Error: Invalid value for '--format': 'other-format' is not one of 'text', 'html', 'other_format'.

Still, as you can see above, the choice strings are lower-cased, as per EnumChoice default. And this is also reflected in the help message:

$ cli --help
Usage: cli [OPTIONS]

Options:
  --format [text|html|other_format]
                                  Select format.
  --help                          Show this message and exit.

Tip

If you don’t want to import ChoiceSource, you can also pass the string values "key", "name", "value", or "str" to the choice_source parameter:

>>> choice_type = EnumChoice(Format, choice_source="key")

>>> choice_type
EnumChoice('TEXT', 'HTML', 'OTHER_FORMAT')

Custom choice source

In addition to the built-in choice sources detailed above, you can also provide a custom callable to the choice_source parameter. This callable should accept an Enum member and return the corresponding string to use as choice.

This is practical when you want to use a specific attribute or method of the Enum members as choice strings. Here’s an example:

from enum import Enum

from click import command, option, echo
from click_extra import EnumChoice


class Format(Enum):
    TEXT = "text"
    HTML = "html"
    OTHER_FORMAT = "other-format"

    def display_name(self):
        return f"custom-{self.value}"


@command
@option(
    "--format",
    type=EnumChoice(Format, choice_source=getattr(Format, "display_name")),
    show_choices=True,
    help="Select format.",
)
def cli(format):
    echo(f"Selected format: {format!r}")
$ cli --help
Usage: cli [OPTIONS]

Options:
  --format [custom-text|custom-html|custom-other-format]
                                  Select format.
  --help                          Show this message and exit.

Default value

Another limit of click.Choice is how the default value is displayed in help messages. Click is hard-coded to use the Enum.name in help messages for the default value.

To fix this limitation, you have to use EnumChoice with @click_extra.option or @click_extra.argument decorators, which override Click’s default help formatter to properly display the default value according to the choice strings.

For example, using @click_extra.option:

from enum import Enum

import click
import click_extra


class Format(Enum):
    TEXT = "text"
    HTML = "html"
    OTHER_FORMAT = "other-format"

    def __str__(self):
        return self.value


@click.command
@click_extra.option(
    "--format",
    type=click_extra.EnumChoice(Format),
    show_choices=True,
    default=Format.HTML,
    show_default=True,
    help="Select format.",
)
def cli(format):
    click.echo(f"Selected format: {format!r}")

This renders into much better help messages, where the default value is displayed using the choice strings:

$ cli --help
Usage: cli [OPTIONS]

Options:
  --format [text|html|other-format]
                                  Select format.  [default: html]
  --help                          Show this message and exit.

Warning

Without Click Extra’s @option or @argument, Click’s default help formatter is used, which always displays the default value using the Enum.name, even when using EnumChoice:

from enum import Enum

import click
import click_extra


class Format(Enum):
    TEXT = "text"
    HTML = "html"
    OTHER_FORMAT = "other-format"

    def __str__(self):
        return self.value


@click.command
@click.option(
    "--format",
    type=click_extra.EnumChoice(Format),
    show_choices=True,
    default=Format.HTML,
    show_default=True,
    help="Select format.",
)
def cli(format):
    click.echo(f"Selected format: {format!r}")

See the unmatched default value in the help message:

$ cli --help
Usage: cli [OPTIONS]

Options:
  --format [text|html|other-format]
                                  Select format.  [default: HTML]
  --help                          Show this message and exit.

You can still work around this limitation by forcing the default value:

from enum import Enum

import click
import click_extra


class Format(Enum):
    TEXT = "text"
    HTML = "html"
    OTHER_FORMAT = "other-format"

    def __str__(self):
        return self.value


@click.command
@click.option(
    "--format",
    type=click_extra.EnumChoice(Format),
    show_choices=True,
    default=str(Format.HTML),
    show_default=True,
    help="Select format.",
)
def cli(format):
    click.echo(f"Selected format: {format!r}")
$ cli --help
Usage: cli [OPTIONS]

Options:
  --format [text|html|other-format]
                                  Select format.  [default: html]
  --help                          Show this message and exit.

Aliases

EnumChoice also supports aliases on both names and values.

Here’s an example using aliases:

from enum import Enum

from click import command, option, echo
from click_extra import EnumChoice


class State(Enum):
    NEW = "new"
    IN_PROGRESS = "in_progress"
    ONGOING = "in_progress"  # Alias for IN_PROGRESS
    COMPLETED = "completed"

# Dynamiccally add names and values aliases.
State.NEW._add_alias_("fresh")  # Alias for NEW
State.COMPLETED._add_value_alias_("done")  # Alias for COMPLETED

@command
@option(
    "--state",
    type=EnumChoice(State, choice_source="name"),
    show_choices=True,
)
@option(
    "--state-name",
    type=EnumChoice(State, choice_source="name", show_aliases=True),
    show_choices=True,
)
@option(
    "--state-value",
    type=EnumChoice(State, choice_source="value", show_aliases=True),
    show_choices=True,
)
def cli(state, state_name, state_value):
    echo(f"Selected state:       {state!r}")
    echo(f"Selected state-name:  {state_name!r}")
    echo(f"Selected state-value: {state_value!r}")

You can now see the name aliases ongoing and fresh are now featured in the help message if show_aliases=True, as well as the value alias done:

$ cli --help
Usage: cli [OPTIONS]

Options:
  --state [new|in_progress|completed]
  --state-name [new|in_progress|ongoing|completed|fresh]
  --state-value [new|in_progress|completed|done]
  --help                          Show this message and exit.

And both names and values aliases are properly recognized, and normalized to their corresponding canonocal Enum members:

$ cli --state in_progress --state-name ongoing --state-value done
Selected state:       <State.IN_PROGRESS: 'in_progress'>
Selected state-name:  <State.IN_PROGRESS: 'in_progress'>
Selected state-value: <State.COMPLETED: 'completed'>

MultiChoice

click.Choice is the canonical Click type for “pick one of these values”. For the pick-many case, the recommended Click idiom is multiple=True paired with click.Choice, requiring the flag to be repeated:

$ my-cli --tag alpha --tag beta --tag gamma

MultiChoice ships the SQL SELECT a, b, c-style alternative: a single, comma-separated token validated against a fixed set of values. The rendered metavar is [a,b,c], parallel to click.Choice’s [a|b|c], and click-extra’s help colorizer highlights each value individually in both shapes.

from click import command, echo, option
from click_extra import MultiChoice

@command
@option(
    "--tags",
    type=MultiChoice(("alpha", "beta", "gamma"), case_sensitive=False),
    default=(),
    help="Comma-separated tags to apply.",
)
def tag_cli(tags):
    echo(f"Selected tags: {tags!r}")
$ tag-cli --tags alpha,gamma
Selected tags: ('alpha', 'gamma')

The metavar enumerates the accepted values inline, parallel to click.Choice:

$ tag-cli --help
Usage: tag-cli [OPTIONS]

Options:
  --tags [alpha,beta,gamma]  Comma-separated tags to apply.
  --help                     Show this message and exit.

Unknown values fail at parse time with a BadParameter:

$ tag-cli --tags alpha,delta
Usage: tag-cli [OPTIONS]
Try 'tag-cli --help' for help.

Error: Invalid value for '--tags': Unknown value(s): 'delta'. Accepted: alpha, beta, gamma.

With case_sensitive=False, tokens match the choices regardless of case, and surrounding whitespace is trimmed. The returned tuple holds the canonical, original-case values from choices:

$ tag-cli --tags 'AlPhA, BETA'
Selected tags: ('alpha', 'beta')

Skip the choices argument entirely to use MultiChoice as a pure comma-separated parser, with no validation. The consumer is then responsible for checking the values, and the metavar falls back to Click’s default rendering.

Upstream Click status

Click does not ship an equivalent type. The closest idiom is click.Choice plus multiple=True, and Pallets has historically pushed back on adding a separator-based variant. Two open/closed discussions on the topic:

  • pallets/click#2771 (open): proposes nargs=-1 with a non-whitespace separator, the exact feature MultiChoice provides.

  • pallets/click#2537 (closed as not planned): earlier request for space-separated multi-values on a single flag.

The maintainers’ position has been roughly: multiple=True already does the job, separator conventions vary across communities (, vs : vs ;), and escaping breaks when a value contains the separator. MultiChoice opts into the convention anyway, because SQL-style SELECT a, b, c is the dominant mental model for the table-rendering use cases click-extra supports. click_extra.table.ColumnsOption is the headline consumer (see the --columns documentation).

click_extra.types API

        classDiagram
  Choice <|-- EnumChoice
  Enum <|-- ChoiceSource
  ParamType <|-- MultiChoice
    

Custom click.ParamType subclasses for multi-pick and Enum choices.

class click_extra.types.MultiChoice(choices=(), separator=',', case_sensitive=True)[source]

Bases: ParamType

Comma-separated multi-pick from a fixed set of values.

The pick-many counterpart to click.Choice. Accepts a single token containing several values joined by a configurable separator (defaults to ,), parses it into a tuple[str, ...] and validates each value against choices when that set is non-empty.

The rendered metavar is [a,b,c] (separator-joined, parallel to Choice’s [a|b|c]): click_extra.highlight._HelpColorsMixin auto-detects the separator and highlights each individual value the same way it does for Choice.

Note

Click does not ship a built-in equivalent. The closest idiomatic approach is click.Choice([...]) + multiple=True, which requires the flag to be repeated (--tag a --tag b --tag c) rather than comma-separated. The lack of a single-token, separator-based variant upstream has been raised in:

  • pallets/click#2771 (open): request for nargs=-1 with a non-whitespace separator, covering exactly this use case.

  • pallets/click#2537 (closed as not planned): earlier request for space-separated multi values via nargs=-1 on options.

Maintainers have leaned on the orthogonality argument: multiple=True already exists, separator conventions vary across communities (, vs. : vs. ;), and escaping breaks down when a value contains the chosen separator. MultiChoice ships the convention anyway because SQL-style SELECT a, b, c syntax reads more naturally for the tabular use cases click-extra supports (click_extra.table.ColumnsOption is the headline consumer).

Initialize the type.

Parameters:
  • choices (Sequence[str]) – the accepted values. When non-empty, convert() rejects unknown tokens with fail. When empty, the type behaves as a pure separator-aware parser and leaves validation to the consumer.

  • separator (str) – the token boundary. Use any single character; this also drives the metavar rendering ([a<sep>b<sep>c]).

  • case_sensitive (bool) – when False, tokens match choices case-insensitively and the returned tuple holds the canonical (original-case) values from choices.

name: str = 'multi'

the descriptive name of this type

choices: tuple[str, ...]
separator: str
case_sensitive: bool
get_metavar(param, ctx=None)[source]

Render [a<sep>b<sep>c] when choices is set, None otherwise.

None falls back to Click’s default rendering (the uppercased name, like MULTI).

convert(value, param, ctx)[source]

Split value on separator and validate each token.

Already-parsed tuples and lists are returned unchanged so defaults declared as tuples flow through untouched. Empty tokens (consecutive separators, trailing separator) are dropped silently.

Return type:

tuple[str, ...]

class click_extra.types.ChoiceSource(*values)[source]

Bases: Enum

Source of choices for EnumChoice.

KEY = 'key'
NAME = 'name'
VALUE = 'value'
STR = 'str'
class click_extra.types.EnumChoice(choices, case_sensitive=False, choice_source=ChoiceSource.STR, show_aliases=False)[source]

Bases: Choice

Choice type for Enum.

Allows to select which part of the members to use as choice strings, by setting the choice_source parameter to one of:

  • ChoiceSource.KEY or ChoiceSource.NAME to use the key (the name property),

  • ChoiceSource.VALUE to use the value,

  • ChoiceSource.STR to use the str() string representation, or

  • A custom callable that takes an Enum member and returns a string.

Defaults to ChoiceSource.STR, which only requires you to define the __str__() method on your Enum to produce beautiful choice strings.

Same as click.Choice, but takes an Enum as choices.

Also defaults to case-insensitive matching.

choices: tuple[str, ...]

The strings available as choice.

Hint

Contrary to the parent Choice class, we store choices directly as strings, not the Enum members themselves. That way there is no surprises when displaying them to the user.

This trick bypass Enum-specific code path in the Click library. Because, after all, a terminal environment only deals with strings: arguments, parameters, parsing, help messages, environment variables, etc.

get_choice_string(member)[source]

Derive the choice string from the given Enum’s member.

Return type:

str

normalize_choice(choice, ctx)[source]

Expand the parent’s normalize_choice() to accept Enum members as input.

An Enum member is mapped to its choice string first; any other value is passed to the parent untouched.

Return type:

str

shell_complete(ctx, param, incomplete)[source]

Return completion items with choices normalized via normalize_choice().

Overrides the parent to ensure normalize_choice() is always called on each candidate, fixing Click 8.4.0 where shell_complete() returned raw (unnormalized) choice strings for ChoiceSource.KEY.

Note

On Click 8.4.1+ this override is a no-op: the parent already calls normalize_choice(), and re-normalizing is idempotent (casefold(casefold(s)) == casefold(s)).

Return type:

list[CompletionItem]

convert(value, param, ctx)[source]

Convert the input value to the corresponding Enum member.

The parent’s convert() is going to return the choice string, which we then map back to the corresponding Enum member.

Return type:

Enum