App Calling & Return Values

In this section, we'll take a closer look at the App.__call__() method.

Input Command

Typically, a Cyclopts app looks something like:

from cyclopts import App

app = App()

@app.command
def foo(a: int, b: int, c: int):
    print(a + b + c)

app()
$ my-script 1 2 3
6

App.__call__() takes in an optional input that it parses into an action. If not specified, Cyclopts defaults to sys.argv[1:], i.e. the list of command line arguments. An explicit string or list of strings can instead be passed in.

app("foo 1 2 3")
# 6
app(["foo", "1", "2", "3"])
# 6

If a string is passed in, it will be internally converted into a list using shlex.split.

Return Value

The app invocation processes the command's return value based on App.result_action. By default, Cyclopts calls sys.exit() with an appropriate exit code:

from cyclopts import App

app = App()  # Default result_action="print_non_int_sys_exit"

@app.command
def success():
    return 0  # Exit code for success

@app.command
def greet(name: str) -> str:
    return f"Hello {name}!"  # Prints and exits with 0

if __name__ == "__main__":
    app()  # Will call sys.exit with the returned 0 error code (success).

Installed scripts call sys.exit() with the returned value of the entry point. So the default Cyclopts App.result_action will have consistent behavior for standalone scripts and installed apps.

For embedding Cyclopts in other Python code or testing, use result_action="return_value" to get the raw command return value without calling sys.exit():

from cyclopts import App

app = App(result_action="return_value")

@app.command
def foo(a: int, b: int, c: int):
    return a + b + c

return_value = app("foo 1 2 3")  # no longer exits!
print(f"The return value was: {return_value}.")
# The return value was: 6.

See Result Action for all available modes and detailed behavior.

Exception Handling and Exiting

For the most part, Cyclopts is hands-off when it comes to handling exceptions and exiting the application. However, by default, if there is a Cyclopts runtime error, like CoercionError or a ValidationError, then Cyclopts will perform a sys.exit(1). This is to avoid displaying the unformatted, uncaught exception to the CLI user.

These behaviors can be controlled via App attributes or method parameters:

These attributes are inherited by child apps and can be overridden by providing parameters to method calls.

Note

Cyclopts separates normal output from error messages using two different consoles:

  • App.console - Used for normal output like help messages and version information (defaults to stdout)

  • App.error_console - Used for error messages like parsing errors and exceptions (defaults to stderr)

Setting at App Level:

# Configure error handling at the app level
app = App(
    exit_on_error=False,  # Don't exit on errors
    print_error=False,    # Don't print formatted errors
)

# Child apps inherit these settings
child_app = App(name="child")
app.command(child_app)

Method-Level Override:

app("this-is-not-a-registered-command")
print("this will not be printed since cyclopts exited above.")
# ╭─ Error ─────────────────────────────────────────────────────────────╮
# │ Unknown command "this-is-not-a-registered-command".                 │
# ╰─────────────────────────────────────────────────────────────────────╯

app("this-is-not-a-registered-command", exit_on_error=False, print_error=False)
# Traceback (most recent call last):
#   File "/cyclopts/scratch.py", line 9, in <module>
#     app("this-is-not-a-registered-command", exit_on_error=False, print_error=False)
#   File "/cyclopts/cyclopts/core.py", line 1102, in __call__
#     command, bound, _ = self.parse_args(
#   File "/cyclopts/cyclopts/core.py", line 1037, in parse_args
#     command, bound, unused_tokens, ignored, argument_collection = self._parse_known_args(
#   File "/cyclopts/cyclopts/core.py", line 966, in _parse_known_args
#     raise UnknownCommandError(unused_tokens=unused_tokens)
# cyclopts.exceptions.UnknownCommandError: Unknown command "this-is-not-a-registered-command".

try:
    app("this-is-not-a-registered-command", exit_on_error=False, print_error=False)
except CycloptsError:
    pass
print("Execution continues since we caught the exception.")

With exit_on_error=False, the UnknownCommandError is raised the same as a normal python exception.

Custom Error Formatting

By default, Cyclopts displays errors using CycloptsPanel(), which renders a Rich panel:

╭─ Error ───────────────────────────────────────────────╮
│ Invalid value "foo" for "VALUE": unable to convert    │
│ "foo" into int.                                       │
╰───────────────────────────────────────────────────────╯

To customize this, set App.error_formatter to a callable that receives a CycloptsError and returns any Rich-printable object.

from cyclopts import App, CycloptsError

def my_error_formatter(e: CycloptsError):
    return f"[bold red]error[/bold red]: {e}"

app = App(error_formatter=my_error_formatter)

@app.default
def main(value: int):
    pass
$ my-app foo
error: Invalid value "foo" for "VALUE": unable to convert "foo" into int.

The formatter receives the full CycloptsError exception, which contains context like the command_chain, argument, and target. Use str(e) for just the message text.

Like other error-handling attributes, error_formatter can also be passed as a runtime override:

app.parse_args("foo", error_formatter=my_error_formatter)