This can be done by building a custom class derived from click.Option, and in that class over riding the click.Option.handle_parse_result() method like:

Custom Class:

import click

class NotRequiredIf(click.Option):
    def __init__(self, *args, **kwargs):
        self.not_required_if = kwargs.pop('not_required_if')
        assert self.not_required_if, "'not_required_if' parameter required"
        kwargs['help'] = (kwargs.get('help', '') +
            ' NOTE: This argument is mutually exclusive with %s' %
            self.not_required_if
        ).strip()
        super(NotRequiredIf, self).__init__(*args, **kwargs)

    def handle_parse_result(self, ctx, opts, args):
        we_are_present = self.name in opts
        other_present = self.not_required_if in opts

        if other_present:
            if we_are_present:
                raise click.UsageError(
                    "Illegal usage: `%s` is mutually exclusive with `%s`" % (
                        self.name, self.not_required_if))
            else:
                self.prompt = None

        return super(NotRequiredIf, self).handle_parse_result(
            ctx, opts, args)

Using Custom Class:

To use the custom class, pass the cls parameter to click.option decorator like:

@click.option('--username', prompt=True, cls=NotRequiredIf,
              not_required_if='authentication_token')

How does this work?

This works because click is a well designed OO framework. The @click.option() decorator usually instantiates a click.Option object but allows this behavior to be overridden with the cls parameter. So it is a relatively easy matter to inherit from click.Option in our own class and over ride the desired methods.

In this case we over ride click.Option.handle_parse_result() and disable the need to user/password if authentication-token token is present, and complain if both user/password are authentication-token are present.

Note: This answer was inspired by this answer

Test Code:

@click.command()
@click.option('--authentication-token')
@click.option('--username', prompt=True, cls=NotRequiredIf,
              not_required_if='authentication_token')
@click.option('--password', prompt=True, hide_input=True, cls=NotRequiredIf,
              not_required_if='authentication_token')
def login(authentication_token, username, password):
    click.echo('t:%s  u:%s  p:%s' % (
        authentication_token, username, password))

if __name__ == '__main__':
    login('--username name --password pword'.split())
    login('--help'.split())
    login(''.split())
    login('--username name'.split())
    login('--authentication-token token'.split())

Results:

from login('--username name --password pword'.split()):

t:None  u:name  p:pword

from login('--help'.split()):

Usage: test.py [OPTIONS]

Options:
  --authentication-token TEXT
  --username TEXT              NOTE: This argument is mutually exclusive with
                               authentication_token
  --password TEXT              NOTE: This argument is mutually exclusive with
                               authentication_token
  --help                       Show this message and exit.
Answer from Stephen Rauch on Stack Overflow
Top answer
1 of 3
53

This can be done by building a custom class derived from click.Option, and in that class over riding the click.Option.handle_parse_result() method like:

Custom Class:

import click

class NotRequiredIf(click.Option):
    def __init__(self, *args, **kwargs):
        self.not_required_if = kwargs.pop('not_required_if')
        assert self.not_required_if, "'not_required_if' parameter required"
        kwargs['help'] = (kwargs.get('help', '') +
            ' NOTE: This argument is mutually exclusive with %s' %
            self.not_required_if
        ).strip()
        super(NotRequiredIf, self).__init__(*args, **kwargs)

    def handle_parse_result(self, ctx, opts, args):
        we_are_present = self.name in opts
        other_present = self.not_required_if in opts

        if other_present:
            if we_are_present:
                raise click.UsageError(
                    "Illegal usage: `%s` is mutually exclusive with `%s`" % (
                        self.name, self.not_required_if))
            else:
                self.prompt = None

        return super(NotRequiredIf, self).handle_parse_result(
            ctx, opts, args)

Using Custom Class:

To use the custom class, pass the cls parameter to click.option decorator like:

@click.option('--username', prompt=True, cls=NotRequiredIf,
              not_required_if='authentication_token')

How does this work?

This works because click is a well designed OO framework. The @click.option() decorator usually instantiates a click.Option object but allows this behavior to be overridden with the cls parameter. So it is a relatively easy matter to inherit from click.Option in our own class and over ride the desired methods.

In this case we over ride click.Option.handle_parse_result() and disable the need to user/password if authentication-token token is present, and complain if both user/password are authentication-token are present.

Note: This answer was inspired by this answer

Test Code:

@click.command()
@click.option('--authentication-token')
@click.option('--username', prompt=True, cls=NotRequiredIf,
              not_required_if='authentication_token')
@click.option('--password', prompt=True, hide_input=True, cls=NotRequiredIf,
              not_required_if='authentication_token')
def login(authentication_token, username, password):
    click.echo('t:%s  u:%s  p:%s' % (
        authentication_token, username, password))

if __name__ == '__main__':
    login('--username name --password pword'.split())
    login('--help'.split())
    login(''.split())
    login('--username name'.split())
    login('--authentication-token token'.split())

Results:

from login('--username name --password pword'.split()):

t:None  u:name  p:pword

from login('--help'.split()):

Usage: test.py [OPTIONS]

Options:
  --authentication-token TEXT
  --username TEXT              NOTE: This argument is mutually exclusive with
                               authentication_token
  --password TEXT              NOTE: This argument is mutually exclusive with
                               authentication_token
  --help                       Show this message and exit.
2 of 3
18

Slightly improved Stephen Rauch's answer to have multiple mutex parameters.

import click

class Mutex(click.Option):
    def __init__(self, *args, **kwargs):
        self.not_required_if:list = kwargs.pop("not_required_if")

        assert self.not_required_if, "'not_required_if' parameter required"
        kwargs["help"] = (kwargs.get("help", "") + "Option is mutually exclusive with " + ", ".join(self.not_required_if) + ".").strip()
        super(Mutex, self).__init__(*args, **kwargs)

    def handle_parse_result(self, ctx, opts, args):
        current_opt:bool = self.name in opts
        for mutex_opt in self.not_required_if:
            if mutex_opt in opts:
                if current_opt:
                    raise click.UsageError("Illegal usage: '" + str(self.name) + "' is mutually exclusive with " + str(mutex_opt) + ".")
                else:
                    self.prompt = None
        return super(Mutex, self).handle_parse_result(ctx, opts, args)

use like this:

@click.group()
@click.option("--username", prompt=True, cls=Mutex, not_required_if=["token"])
@click.option("--password", prompt=True, hide_input=True, cls=Mutex, not_required_if=["token"])
@click.option("--token", cls=Mutex, not_required_if=["username","password"])
def login(ctx=None, username:str=None, password:str=None, token:str=None) -> None:
    print("...do what you like with the params you got...")
🌐
Click Documentation
click.palletsprojects.com › en › stable › options
Options — Click Documentation (8.5.x)
A simple click.Option takes one option name. By default, it’s assumed that the decorated function argument is not required and the expected type is str.
Discussions

Documentation: mention required=True for options
This is never mentioned as a possibility for click.option anywhere in the docs as far as I can tell (or at least it should be more prominent, maybe a section). (it talks about it in click.argument, but its unclear that it exists for both... More on github.com
🌐 github.com
12
February 4, 2016
python - Require an option only if a choice is made when using click - Stack Overflow
When using click I know how to define a multiple choice option. I also know how to set an option as a required one. But, how can I indicate that an option B is required only if the value of option ... More on stackoverflow.com
🌐 stackoverflow.com
command line interface - how to specify a required option out of 3 in python `click` - Stack Overflow
I am trying to use https://click.palletsprojects.com/en/8.0.x/options/ I have a use case where one out of these 3 parameters has to be a required field. This is how I am doing it. 10:45 $ python te... More on stackoverflow.com
🌐 stackoverflow.com
python - Dependent options on another option - Stack Overflow
8 How to define common python-click options for multiple commands? 2 Accept arbitrary arguments and options with Click · What to do if my institution is not listed in the journal submission ecosystem? Could a person be very evil & still go to heaven after death due to past lives good karma (?) reflectedness of PolyGamma depends on sum direction irrespective of Assumptions, requiring ... More on stackoverflow.com
🌐 stackoverflow.com
🌐
Digitalk
digitalk.tech › blog › python-click-or-how-to-write-professional-cli-applications
Mndwrk Blog - Python click or how to write professional CLI applications
June 17, 2022 - For further information, read the official Python documentation · At this point, we should be able to create the required program with what we have learned so far: import click, pwd @click.command(help="get user shell") @click.option("-u", "--user", required=True, help="Username") def main(user): print(pwd.getpwnam(user).pw_shell) if __name__ == "__main__": exit(main())
🌐
GitHub
github.com › pallets › click › issues › 514
Documentation: mention required=True for options · Issue #514 · pallets/click
February 4, 2016 - This is never mentioned as a possibility for click.option anywhere in the docs as far as I can tell (or at least it should be more prominent, maybe a section). (it talks about it in click.argument, but its unclear that it exists for both) Someone just came to me saying, "we have to stop using click, i checked everywhere in their documentation, they don't support required options, which is important!"
Author: pallets
🌐
Devnet-academy
docs.devnet-academy.com › docs › click_v7.1.2 › options.html
Options — Click Documentation (7.x)
# How to make an option required @click.command() @click.option('--n', required=True, type=int) def dots(n): click.echo('.' * n) # How to use a Python reserved word such as `from` as a parameter @click.command() @click.option('--from', '-f', 'from_') @click.option('--to', '-t') def reserved_param_name(from_, to): click.echo('from %s to %s' % (from_, to))
🌐
Readthedocs
pocoo-click.readthedocs.io › en › latest › options
Options - the Click Documentation
Note: an actual implementation ... Click as click.version_option(). The code here is merely an example of how to implement such a flag. In such cases, you need two concepts: eager parameters and a callback. An eager parameter is a parameter that is handled before others, and a callback is what executes after the parameter is handled. The eagerness is necessary so that an earlier required parameter ...
🌐
ZetCode
zetcode.com › python › click
Python click - creating command line interfaces
January 29, 2024 - #!/usr/bin/python import click @click.command() @click.option('--data', required=True, type=(str, int)) def output(data): click.echo(f'name={data[0]} age={data[1]}') if __name__ == '__main__': output() In the example, the --data option takes two values that become a Python tuple.
Top answer
1 of 3
7

In the particular case of this example, I think an easier method would be to get rid of --output, and simply assume stdout if --filename is not specified and if --filename is specified, then use it instead of stdout.

But assuming this is a contrived example, you can inherit from click.Option to allow hooking into the click processing:

Custom Class:

class OptionRequiredIf(click.Option):

    def full_process_value(self, ctx, value):
        value = super(OptionRequiredIf, self).full_process_value(ctx, value)

        if value is None and ctx.params['output'] == 'file':
            msg = 'Required if --output=file'
            raise click.MissingParameter(ctx=ctx, param=self, message=msg)
        return value

Using Custom Class:

To use the custom class, pass it as the cls argument to the option decorator like:

@click.option('--filename', type=click.STRING, cls=OptionRequiredIf)

Test Code:

import click

@click.command()
@click.option('--output',
              type=click.Choice(['stdout', 'file']), default='stdout')
@click.option('--filename', type=click.STRING, cls=OptionRequiredIf)
def main(output, filename):
    print("output: " + output)
    if output == 'file':
        if filename is None:
            print("filename must be provided!")
        else:
            print("filename: " + str(filename))


main('--output=file'.split())

Results:

Usage: test.py [OPTIONS]

Error: Missing option "--filename".  Required if --output=file
2 of 3
3

You can do the same thing with a custom validation callback:

import click


def required_with_output(ctx, param, value):
    if ctx.params.get("output") != "stdout" and value is None:
        raise click.BadParameter("--output requires --filename")
    return value


@click.command()
@click.option(
    "--output",
    type=click.Choice(["stdout", "file"]),
    default="stdout",
)
@click.option("--filename", callback=required_with_output)
def main(output, filename):
    print("output: " + output)
    if output == "file":
        if filename is None:
            print("filename must be provided!")
        else:
            print("filename: " + str(filename))


if __name__ == "__main__":
    main()

I think this is a little simpler.

Find elsewhere
🌐
Click Documentation
click.palletsprojects.com › en › stable › prompts
User Input Prompts — Click Documentation (8.5.x)
value = click.prompt('Please enter a number', default=42.0) If the option has prompt enabled, then setting prompt_required=False tells Click to only show the prompt if the option’s flag is given, instead of if the option is not provided at all.
🌐
Click Documentation
click.palletsprojects.com › en › stable › arguments
Arguments — Click Documentation (8.5.x)
If no type is provided, the type ... to be STRING. See How Click Infers a Type for the types that are recognized. ... It is possible to make an argument required by setting required=True....
🌐
Stack Overflow
stackoverflow.com › questions › 71355981 › how-to-specify-a-required-option-out-of-3-in-python-click
command line interface - how to specify a required option out of 3 in python `click` - Stack Overflow
import click @click.command() @click.option('--foo', flag_value='foo', required=True, help='foo is bar.') @click.option('--count', flag_value='count', required=True, help='Number of greetings.') @click.option('--name', flag_value='name', required=True, help='The person to greet.') def hello(foo, count, name): if not (count or name or foo): raise click.UsageError( 'Must specify --foo or bar or car') if foo: click.echo(f"Hello {foo}!") if count: click.echo(f"Hello {count}!") if name: click.echo(f"Hello {name}!") if __name__ == '__main__': hello() ... tried feature switches but it needs all 3 params. Usage: test.py [OPTIONS] Options: --foo foo is bar. [required] --count Number of greetings. [required] --name The person to greet. [required] --help Show this message and exit. 12:41 $ python test.py --foo Usage: test.py [OPTIONS] Try 'test.py --help' for help.
🌐
Readthedocs
pymbook.readthedocs.io › en › latest › click.html
Building command line applications with Click — Python for you and me 0.5.beta1 documentation
$ myhello Hello World $ myhello --help Usage: myhello [OPTIONS] Options: --help Show this message and exit. The click module suggests using echo function to print, rather than the standard print function. So, we will make the required change in our code.
🌐
CSDN
devpress.csdn.net › python › 6304555bc67703293080b52e.html
Click Command Line Interfaces: Make options required if other optional option is unset_python_Mangs-Python
August 23, 2022 - import click class NotRequiredIf(click.Option): def __init__(self, *args, **kwargs): self.not_required_if = kwargs.pop('not_required_if') assert self.not_required_if, "'not_required_if' parameter required" kwargs['help'] = (kwargs.get('help', '') + ' NOTE: This argument is mutually exclusive with %s' % self.not_required_if ).strip() super(NotRequiredIf, self).__init__(*args, **kwargs) def handle_parse_result(self, ctx, opts, args): we_are_present = self.name in opts other_present = self.not_required_if in opts if other_present: if we_are_present: raise click.UsageError( "Illegal usage: `%s` is mutually exclusive with `%s`" % ( self.name, self.not_required_if)) else: self.prompt = None return super(NotRequiredIf, self).handle_parse_result( ctx, opts, args)
Top answer
1 of 2
18

I would suggest doing that with a custom click.Command class like:

Custom Class:

def command_required_option_from_option(require_name, require_map):

    class CommandOptionRequiredClass(click.Command):

        def invoke(self, ctx):
            require = ctx.params[require_name]
            if require not in require_map:
                raise click.ClickException(
                    "Unexpected value for --'{}': {}".format(
                        require_name, require))
            if ctx.params[require_map[require].lower()] is None:
                raise click.ClickException(
                    "With {}={} must specify option --{}".format(
                        require_name, require, require_map[require]))
            super(CommandOptionRequiredClass, self).invoke(ctx)

    return CommandOptionRequiredClass

Using the Custom Class

required_options = {
    1: 'generator_string',
    2: 'number_of_sample_points',
    3: 'number_of_center_points',
}

@click.command(cls=command_required_option_from_option('doe', required_options))
...

How does this work?

This works because click is a well designed OO framework. The @click.command() decorator usually instantiates a click.Command object but allows this behavior to be over ridden with the cls parameter. So it is a relatively easy matter to inherit from click.Command in our own class and over ride desired methods.

In this case, we override click.Command.invoke() and then validate that the required option has been set before running the command

Test Code:

import click

required_options = {
    1: 'generator_string',
    2: 'number_of_sample_points',
    3: 'number_of_center_points',
}

@click.command(context_settings=dict(max_content_width=800),
               cls=command_required_option_from_option('doe', required_options))
@click.option('--input', required=True,
              type=click.Path(exists=True))
@click.option('--doe', required=True, type=int)
@click.option('--generator_string', required=False, type=str, is_eager=True)
@click.option('--number_of_sample_points', required=False, type=int,
              is_eager=True)
@click.option('--number_of_center_points', required=False, type=int,
              is_eager=True)
def main(input, doe, generator_string, number_of_sample_points,
         number_of_center_points):
    click.echo('input: {}'.format(input))
    click.echo('doe: {}'.format(doe))
    click.echo('generator_string: {}'.format(generator_string))
    click.echo('Num of sample_points: {}'.format(number_of_sample_points))
    click.echo('Num of center_points: {}'.format(number_of_center_points))


if __name__ == "__main__":
    commands = (
        '--input ./input.txt --doe 0',
        '--input ./input.txt --doe 1',
        '--input ./input.txt --doe 2',
        '--input ./input.txt --doe 3',
        '--input ./input.txt --doe 1 --generator_string 1234',
        '--input ./input.txt --doe 2 --number_of_sample_points 3',
        '--input ./input.txt --doe 3 --number_of_center_points 2',
        '',
        '--help',
    )

    import sys, time

    time.sleep(1)
    print('Click Version: {}'.format(click.__version__))
    print('Python Version: {}'.format(sys.version))
    for cmd in commands:
        try:
            time.sleep(0.1)
            print('-----------')
            print('> ' + cmd)
            time.sleep(0.1)
            main(cmd.split())

        except BaseException as exc:
            if str(exc) != '0' and \
                    not isinstance(exc, (click.ClickException, SystemExit)):
                raise

Results:

Click Version: 6.7
Python Version: 3.6.3 (v3.6.3:2c5fed8, Oct  3 2017, 18:11:49) [MSC v.1900 64 bit (AMD64)]
-----------
> --input ./input.txt --doe 0
Error: Unexpected value for --'doe': 0
-----------
> --input ./input.txt --doe 1
Error: With doe=1 must specify option --generator_string
-----------
> --input ./input.txt --doe 2
Error: With doe=2 must specify option --number_of_sample_points
-----------
> --input ./input.txt --doe 3
Error: With doe=3 must specify option --number_of_center_points
-----------
> --input ./input.txt --doe 1 --generator_string 1234
input: ./input.txt
doe: 1
generator_string: 1234
Num of sample_points: None
Num of center_points: None
-----------
> --input ./input.txt --doe 2 --number_of_sample_points 3
input: ./input.txt
doe: 2
generator_string: None
Num of sample_points: 3
Num of center_points: None
-----------
> --input ./input.txt --doe 3 --number_of_center_points 2
input: ./input.txt
doe: 3
generator_string: None
Num of sample_points: None
Num of center_points: 2
-----------
> 
Usage: test.py [OPTIONS]

Error: Missing option "--input".
-----------
> --help
Usage: test.py [OPTIONS]

Options:
  --input PATH                    [required]
  --doe INTEGER                   [required]
  --generator_string TEXT
  --number_of_sample_points INTEGER
  --number_of_center_points INTEGER
  --help                          Show this message and exit.
2 of 2
1

If you're OK with an additional dependency there's click-constrained-option. Seems very comprehensive. I've used it as follows:

@click.option("--location", "-l", type=click.STRING, help="Get location (Town/City)", required=False)
@click.option("--dms", is_flag=True, default=False, cls=ConstrainedOption,allowed_if="location", help="Display Degrees, Minutes, Seconds")
🌐
Click Documentation
click.palletsprojects.com › en › stable › parameters
Parameters — Click Documentation (8.5.x)
The name must match the python arg name. To provide a different name for use in help text, see Placeholder / Meta Variable. The option’s names are -t and --times. More names are available for options and are covered in Options. @click.command() @click.argument('filename') @click.option('-t', '--times', type=int) def multi_echo(filename, times): """Print value filename multiple times.""" for x in range(times): click.echo(filename)
Top answer
1 of 2
5

One way that I have managed to achieve this behaviour was by actually using arguments as below. I'll post this as a workaround, while I try to see if it could be done with an option, and I'll update my post accordingly

@click.command(context_settings={"ignore_unknown_options": True})
@click.argument("options", nargs=-1)
def myscript(options):
    option = False
    if options is ():
        option = False
    if '-o' in options or '--option' in options:
        option = True
    if len(options) > 1:
        option = options[1]
    print(option)

Later Edit Using an option, I have managed to achieve this by adding an argument to the command definition.

@click.command()
@click.option('-o', '--option', is_flag=True, default=False, is_eager=True)
@click.argument('value', nargs=-1)
def myscript(option, value):
    if option and value != ():
        option = value[0]
    print(option)

The nargs can be removed if you only expect at most one argument to follow, and can be treated as not required.

@click.command()
@click.option('-o', '--option', is_flag=True, default=False, is_eager=True)
@click.argument('value', required=False)
def myscript(option, value=None):
    if option and value is not None:
        option = value
    print(option)

It might also be possible by putting together a context generator and storing some state, but that seems the least desirable solution, since you would be relying on the context storing your state.

2 of 2
5

FYI, if anybody bumps into this problem in 2024, it seems that click has made something specific for this use case, see: https://click.palletsprojects.com/en/8.1.x/options/#optional-value

Setting is_flag=False, flag_value=value tells Click that the option can still be passed a value, but if only the flag is given the flag_value is used.

@click.command()
@click.option("--name", is_flag=False, flag_value="Flag", default="Default")
def hello(name):
    click.echo(f"Hello, {name}!")

>> hello
Hello, Default!
>> hello --name Value
Hello, Value!
>> hello --name
Hello, Flag!
🌐
Maurice Brg
mauricebrg.com › article › 202008_intro_to_click.html
Building CLIs with Python and Click | Maurice Borgmeier
August 12, 2020 - Setting the default allows us to always pass in a value to our function even if the parameter isn’t set explicitly. We could also make the option required, by passing the required=True argument to the option decorator.
Top answer
1 of 1
3

You can use a custom click.Group class to ignore the required args when --help is requested like:

Custom Class:

class IgnoreRequiredWithHelp(click.Group):
    def parse_args(self, ctx, args):
        try:
            return super(IgnoreRequiredWithHelp, self).parse_args(ctx, args)
        except click.MissingParameter as exc:
            if '--help' not in args:
                raise

            # remove the required params so that help can display
            for param in self.params:
                param.required = False
            return super(IgnoreRequiredWithHelp, self).parse_args(ctx, args)

Using the Custom Class:

To use the custom class, pass it as the cls argument to the group decorator like:

@click.group(cls=IgnoreRequiredWithHelp)
....
def my_group():
    ....

How does this work?

This works because click is a well designed OO framework. The @click.group() decorator usually instantiates a click.Group object but allows this behavior to be over ridden with the cls parameter. So it is a relatively easy matter to inherit from click.Group in our own class and over ride desired methods.

In this case we over ride click.Group.parse_args() and catch the click.MissingParameter exception. We then negate the required attribute from all of the params, and retry the parse.

Test Code:

import click

@click.group(cls=IgnoreRequiredWithHelp)
@click.option('--directory', required=True)
def cli(directory):
    """
    this is a tool that has an add and remove command
    """
    click.echo(directory)

@cli.command()
@click.option('--overwrite', is_flag=True)
def add(overwrite):
    """
    this is the add command
    """
    click.echo("add overwrite={}".format(overwrite))


@cli.command()
def remove():
    """
    this is the remove command
    """
    click.echo('remove')


if __name__ == "__main__":
    commands = (
        'add --help',
        '--help',
        '--directory a_dir add'
        '',
    )

    import sys, time

    time.sleep(1)
    print('Click Version: {}'.format(click.__version__))
    print('Python Version: {}'.format(sys.version))
    for cmd in commands:
        try:
            time.sleep(0.1)
            print('-----------')
            print('> ' + cmd)
            time.sleep(0.1)
            cli(cmd.split())

        except BaseException as exc:
            if str(exc) != '0' and \
                    not isinstance(exc, (click.ClickException, SystemExit)):
                raise

Results:

Click Version: 6.7
Python Version: 3.6.3 (v3.6.3:2c5fed8, Oct  3 2017, 18:11:49) [MSC v.1900 64 bit (AMD64)]
-----------
> add --help

Usage: test.py add [OPTIONS]

  this is the add command

Options:
  --overwrite
  --help       Show this message and exit.
-----------
> --help
Usage: test.py [OPTIONS] COMMAND [ARGS]...

  this is a tool that has an add and remove command

Options:
  --directory TEXT
  --help            Show this message and exit.

Commands:
  add     this is the add command
  remove  this is the remove command
-----------
> --directory a_dir add
a_dir
add overwrite=False