When you call add_name() and add_surname() directly from another function, you actually call the decorated versions of them so the arguments expected may not be as you defined them (see the answers to How to strip decorators from a function in python for some details on why).

I would suggest modifying your implementation so that you keep the original functions undecorated and create thin click-specific wrappers for them, for example:

def add_name(content, to_stdout=False):
    if not content:
        content = ''.join(sys.stdin.readlines())
    result = content + "\n\tadded name"
    if to_stdout is True:
        sys.stdout.writelines(result)
    return result

@click.command()
@click.argument('content', required=False)
@click.option('--to_stdout', default=True)
def add_name_command(content, to_stdout=False):
    return add_name(content, to_stdout)

You can then either call these functions directly or invoke them via a CLI wrapper script created by setup.py.

This might seem redundant but in fact is probably the right way to do it: one function represents your business logic, the other (the click command) is a "controller" exposing this logic via command line (there could be, for the sake of example, also a function exposing the same logic via a Web service for example).

In fact, I would even advise to put them in separate Python modules - Your "core" logic and a click-specific implementation which could be replaced for any other interface if needed.

Answer from shevron on Stack Overflow
Top answer
1 of 4
72

When you call add_name() and add_surname() directly from another function, you actually call the decorated versions of them so the arguments expected may not be as you defined them (see the answers to How to strip decorators from a function in python for some details on why).

I would suggest modifying your implementation so that you keep the original functions undecorated and create thin click-specific wrappers for them, for example:

def add_name(content, to_stdout=False):
    if not content:
        content = ''.join(sys.stdin.readlines())
    result = content + "\n\tadded name"
    if to_stdout is True:
        sys.stdout.writelines(result)
    return result

@click.command()
@click.argument('content', required=False)
@click.option('--to_stdout', default=True)
def add_name_command(content, to_stdout=False):
    return add_name(content, to_stdout)

You can then either call these functions directly or invoke them via a CLI wrapper script created by setup.py.

This might seem redundant but in fact is probably the right way to do it: one function represents your business logic, the other (the click command) is a "controller" exposing this logic via command line (there could be, for the sake of example, also a function exposing the same logic via a Web service for example).

In fact, I would even advise to put them in separate Python modules - Your "core" logic and a click-specific implementation which could be replaced for any other interface if needed.

2 of 4
67

Due to the click decorators the functions can no longer be called just by specifying the arguments. The Context class is your friend here, specifically:

  1. Context.invoke() - invokes another command with the arguments you supply
  2. Context.forward() - fills in the arguments from the current command

So your code for add_name_and_surname should look like:

@click.command()
@click.argument('content', required=False)
@click.option('--to_stdout', default=False)
@click.pass_context
def add_name_and_surname(ctx, content, to_stdout=False):
    result = ctx.invoke(add_surname, content=ctx.forward(add_name))
    if to_stdout is True:
        sys.stdout.writelines(result)
    return result

Reference: http://click.pocoo.org/6/advanced/#invoking-other-commands

🌐
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 - Two things happen here: the chain=True tells the group that multiple commands can be executed after each other, while invoke_without_command=True tells click that the group can be invoked without any subcommand.
Discussions

python - Call a click command from code - Stack Overflow
The question specifically says ... solution invoked from the command line will not answer the question. 2025-03-11T17:03:40.763Z+00:00 ... Save this answer. ... Show activity on this post. If you just want to call the underlying function, you can directly access it as click.Command.callback. Click stores the underlying wrapped Python function as ... More on stackoverflow.com
🌐 stackoverflow.com
Invoke python click command with custom class from another command - Stack Overflow
So here's my issue: Assume that I have built a CLI using Python Click for which I have created custom classes of groups and commands that wrap invoking to catch exceptions: logger = logging.getLo... More on stackoverflow.com
🌐 stackoverflow.com
Question: How to invoke subcommands directly from the main Click Group?
Dear Click Developers, I'm trying to make a command line interface with Click, with a series of subcommands and a callable main command which would call some of the subcommands in a specific or... More on github.com
🌐 github.com
1
April 9, 2019
How do I manually invoke click CLIs from within a Python calling context with parameters? - Stack Overflow
In order to set up automatic function aliases in setuptools, it's necessary to wrap all args in some kind of function. If I am using a click interface, I'll need some way to parameterize specific ... More on stackoverflow.com
🌐 stackoverflow.com
🌐
Click Documentation
click.palletsprojects.com › en › stable › advanced
Advanced Patterns — Click Documentation (8.5.x)
$ cli -n 100 'a = 1; b = 2; a * b' python -mtimeit -n 100 a = 1; b = 2; a * b $ cli -v 'a = 1; b = 2; a * b' Invoking: echo python -mtimeit a = 1; b = 2; a * b python -mtimeit a = 1; b = 2; a * b · As you can see, Click handles the verbosity flag and everything else ends up in the timeit_args variable for further processing, which then for instance, allows invoking a subprocess.
Top answer
1 of 1
3

The trouble you are running into, is that you are calling click.Context.invoke, which does not use the click.Command.invoke. With a little DRY we can factor out your invoke wrapper and use it like:

Code:

def invoke_with_catch(self, ctx, original_invoke):

    fmt = dict(command=getattr(ctx, 'command', ctx).name)
    try:
        click.echo("Running {command} command".format(**fmt))
        result = original_invoke(self, ctx)
        click.echo("Completed {command} command".format(**fmt))
        return result

    except Exception as exc:
        click.echo(
            'Command {command} failed with exception: {exc}'.format(
                exc=exc, **fmt)
        )

        """ In case command invoked from another command """
        raise click.ClickException(
            "Failed to invoke {command} command".format(**fmt))

Calling the Code:

The wrapper can be called directly like:

invoke_with_catch(ctx, c1, click.Context.invoke)

or can be used in the inherited class like:

class CLICommandInvoker(click.Command):
    def invoke(self, ctx):
        return invoke_with_catch(self, ctx, click.Command.invoke)

Test Code:

import click

class CLICommandInvoker(click.Command):
    def invoke(self, ctx):
        return invoke_with_catch(self, ctx, click.Command.invoke)


class CLIGroupInvoker(click.Group):
    def invoke(self, ctx):
        return invoke_with_catch(self, ctx, click.Group.invoke)


@click.group(cls=CLIGroupInvoker)
def g():
    pass

@g.command(cls=CLICommandInvoker)
@click.option("--throw", is_flag=True)
def c1(throw):
    click.echo("C1")
    if throw:
        raise Exception('Throwing in C1')

@g.command(cls=CLICommandInvoker)
@click.option("--throw", is_flag=True)
@click.pass_context
def c2(ctx, throw):
    invoke_with_catch(ctx, c1, click.Context.invoke)
    click.echo("C2")
    if throw:
        raise Exception('Throwing in C2')


if __name__ == "__main__":
    commands = (
        'c1',
        'c1 --throw',
        'c2',
        'c2 --throw',
        '--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)
            g(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)]
-----------
> c1
Running g command
Running c1 command
C1
Completed c1 command
Completed g command
-----------
> c1 --throw
Running g command
Running c1 command
C1
Command c1 failed with exception: Throwing in C1
Command g failed with exception: Failed to invoke c1 command
Error: Failed to invoke g command
-----------
> c2
Running g command
Running c2 command
Running c1 command
C1
Completed c1 command
C2
Completed c2 command
Completed g command
-----------
> c2 --throw
Running g command
Running c2 command
Running c1 command
C1
Completed c1 command
C2
Command c2 failed with exception: Throwing in C2
Command g failed with exception: Failed to invoke c2 command
Error: Failed to invoke g command
-----------
> --help
Usage: test.py [OPTIONS] COMMAND [ARGS]...

Options:
  --help  Show this message and exit.

Commands:
  c1
  c2
-----------
> 
Usage: test.py [OPTIONS] COMMAND [ARGS]...

Options:
  --help  Show this message and exit.

Commands:
  c1
  c2
🌐
GitHub
github.com › pallets › click › issues › 1274
Question: How to invoke subcommands directly from the main Click Group? · Issue #1274 · pallets/click
April 9, 2019 - import sys import click @click.group(invoke_without_command=True) @click.option("--a", default="a string") @click.option("--b", default=2) @click.option("--c", default=False) @click.pass_context def cli(ctx, a, b, c): if ctx.invoked_subcommand is None: click.echo('I was invoked without subcommand') ctx.ensure_object(dict) ctx.obj['a'] = a ctx.obj['b'] = b ctx.obj['c'] = c click.echo("Running A from cli") A(ctx, **ctx.obj) click.echo("Running B from cli") B(ctx, **ctx.obj) else: click.echo('I am about to invoke %s' % ctx.invoked_subcommand) @cli.command() @click.pass_context def A(ctx, a, b, c)
Author: pallets
🌐
Click Documentation
click.palletsprojects.com › en › stable › quickstart
Quickstart — Click Documentation (8.5.x)
import click @click.command() def hello(): click.echo('Hello World!') What’s happening is that the decorator converts the function into a Command which then can be invoked: if __name__ == '__main__': hello() And what it looks like: $ python hello.py Hello World!
🌐
Stack Overflow
stackoverflow.com › questions › 77106224 › how-do-i-manually-invoke-click-clis-from-within-a-python-calling-context-with-pa
How do I manually invoke click CLIs from within a Python calling context with parameters? - Stack Overflow
import click @click.group() def cli(): """ A click group """ pass @cli.command() @click.argument("name") def hello(name): click.echo(f"hello, {name}") Sourcing this in a python interpreter, hello can be invoked with an arg list: >>> cli(["hello", "chris"]) hello, chris Process finished with exit code 0 ·
Find elsewhere
🌐
Readthedocs
pocoo-click.readthedocs.io › en › latest › api
API - the Click Documentation
Given a context, this invokes the attached callback (if it exists) in the right way. ... Creates the underlying option parser for this command. ... the list of parameters for this command in the order they should show up in the help page and execute. Eager parameters will automatically be handled before non eager ones. class click.MultiCommand(name=None, invoke_without_command=False, no_args_is_help=None, subcommand_metavar=None, chain=False, result_callback=None, **attrs)¶
🌐
OneUptime
oneuptime.com › home › blog › how to build cli applications with click in python
How to Build CLI Applications with Click in Python
January 30, 2026 - # test_cli.py from click.testing import CliRunner from greet import greet def test_greet(): runner = CliRunner() # Invoke the command with arguments result = runner.invoke(greet, ['Alice']) # Check the output and exit code assert result.exit_code == 0 assert 'Hello, Alice!' in result.output def test_greet_help(): runner = CliRunner() result = runner.invoke(greet, ['--help']) assert result.exit_code == 0 assert 'Simple program that greets NAME' in result.output
🌐
PyPI
pypi.org › project › clickutil
Client Challenge
JavaScript is disabled in your browser · Please enable JavaScript to proceed · A required part of this site couldn’t load. This may be due to a browser extension, network issues, or browser settings. Please check your connection, disable any ad blockers, or try using a different browser
🌐
Prout
prout.tech › blog › writing a python click cli without decorators
Writing a Python Click CLI without decorators | Eugene Prout
February 24, 2025 - This works by creating an instance of the click.Command class and providing it with a function to invoke, options for the command, help text, and an empty name as we don’t need to give top-level commands names.
Top answer
1 of 2
5

I think the Multi Command Chaining and Multi Command Pipelines features are designed to cover this situation. Pipelining gives the exact behaviour requested (both step1 and step2 invoked when nothing is given on the command line), but it's more verbose, and with chain=True none of the arguments can be optional; you must either (a) always give both arg1 and arg2, even when invoking only step2; or (b) convert these arguments to options (--arg1 foo instead of foo).

Chaining

import click

@click.group(chain=True)
def cli():
    pass

@cli.command()
@click.argument('arg1')
def step1(arg1):
    click.echo('Step 1: ' + arg1)

@cli.command()
@click.argument('arg2')
def step2(arg2):
    click.echo('Step 2: ' + arg2)

cli()

Then:

$ python3 chain.py step1 foo step2 bar
Step 1: foo
Step 2: bar
$ python3 chain.py step2 bar
Step 2: bar

Pipelining

import click

@click.group(chain=True, invoke_without_command=True)
@click.argument('arg1')
@click.argument('arg2')
def cli(arg1, arg2):
    pass

@cli.resultcallback()
def process_pipeline(processors, **kwargs):
    # If no commands given, invoke step1 then step2
    processors = processors if len(processors) else [step1, step2]
    for processor in processors:
        processor(**kwargs)

def step1(**kwargs):
    click.echo('Step 1: ' + kwargs['arg1'])

def step2(**kwargs):
    click.echo('Step 2: ' + kwargs['arg2'])

@cli.command('step1')
def make_step1():
    return step1

@cli.command('step2')
def make_step2():
    return step2

cli()

Then

$ python3 pipeline.py foo bar
Step 1: foo
Step 2: bar
$ python3 pipeline.py foo bar step2
Step 2: bar
2 of 2
2

I didn't phrase the question to fully explain what I was trying to do, since each step also requires the output from the previous one. Huge thanks to Paul for starting me on the right path.

My solution was something like:

@click.group(invoke_without_command=True)
@click.option('--arg1')
@click.option('--arg2')
@click.pass_context
def cli(ctx, arg1, arg2):
    '''Description
    '''
    if ctx.invoked_subcommand is None:
        do_everything(ctx, arg1, arg2)

@cli.command()
@click.option('--arg1')
def step_1(arg1):
    return do_something(arg1)

@cli.command()
@click.argument('step_one_result')
@click.option('--arg2')
def step_2(step_one_result, arg2):
    do_something_else(step_one_result, arg2)

def do_everything(ctx, arg1, arg2):
    step_one_result = ctx.invoke(step_1, arg1=arg1)
    ctx.invoke(do_something_else, step_one_result=step_one_result, arg2=arg2)

#and because of weirdness with pass_context and using setuptools

def main():
    cli(obj={})

if __name__ == '__main__':
    main()

EDIT: you'll notice the use of ctx.invoke() which was necessary to call the functions without getting the following error

line 619, in make_context
    ctx = Context(self, info_name=info_name, parent=parent, **extra)
TypeError: __init__() got an unexpected keyword argument 'arg1'
🌐
Blue Book
lyz-code.github.io › blue-book › coding › python › click
Click - The Blue Book
Click is a Python package for creating beautiful command line interfaces in a composable way with as little code as necessary. It’s the “Command Line Interface Creation Kit”. It’s highly configurable but comes with sensible defaults out of the box · Click has the following features:
🌐
AssemblyAI
assemblyai.com › blog › the-definitive-guide-to-python-click
The Definitive Guide to Python Click
July 28, 2021 - Click, or “Command Line Interface Creation Kit” is a Python library for building command line interfaces. The three main points of Python Click are arbitrary nesting of commands, automatic help page generation, and supporting lazy loading ...
🌐
Click Documentation
click.palletsprojects.com › en › stable › commands
Advanced Groups and Context — Click Documentation (8.5.x)
When working with command return values in Click, this is what you need to know: The return value of a command callback is generally returned from the Command.invoke() method.
🌐
Readthedocs
pocoo-click.readthedocs.io › en › latest › commands
Commands and Groups - the Click Documentation
When working with command return values in Click, this is what you need to know: The return value of a command callback is generally returned from the BaseCommand.invoke() method.
🌐
GitHub
github.com › pallets › click › issues › 1124
allow for prompting in invoked command · Issue #1124 · pallets/click
September 26, 2018 - When invoking one click command from another command, options that are not specified are replaced by default values (or None), even if the option has prompt=True. Is there currently a way to have an invoked command prompt for its options?
Author: pallets