Packaging
Packaging is bundling up your python library so that it can be easily pip install by others.
Typically this involves:
Bundling the code into a Built Distribution (wheel) and/or Source Distribution (sdist).
Uploading (publishing) the distribution(s) to python package repository, like PyPI.
This section is a brief bootcamp on package configuration for a CLI application.
This is not intended to be a complete tutorial on python packaging and publishing.
In this tutorial, replace all instances of mypackage with your own project name.
__main__.py
In python, if you have a module mypackage/__main__.py, it will be executed with the bash command python -m mypackage.
A pretty bare-bones Cyclopts mypackage/__main__.py will look like:
# mypackage/__main__.py
import cyclopts
app = cyclopts.App()
@app.command
def foo(name: str):
print(f"Hello {name}!")
if __name__ == "__main__":
app()
$ python -m mypackage World
Hello World!
Entrypoints
If you want your application to be callable like a standard bash executable (i.e. my-package instead of python -m mypackage), we'll need to add an entrypoint.
Modern Python projects typically use pyproject.toml for configuration. The standard way to define console scripts is:
# pyproject.toml
[project.scripts]
my-package = "mypackage.__main__:app"
This creates an executable named my-package that executes the callable app object (from the right of the colon) from the python module mypackage.__main__.
Note that this configuration is independent of any special naming, like __main__ or app.
Legacy Configurations
For older projects, you may encounter these alternative formats:
setup.py:
# setup.py
from setuptools import setup
setup(
# There should be a lot more fields populated here.
entry_points={
"console_scripts": [
"my-package = mypackage.__main__:app",
]
},
)
setup.cfg:
# setup.cfg
[options.entry_points]
console_scripts =
my-package = mypackage.__main__:app
Poetry:
# pyproject.toml
[tool.poetry.scripts]
my-package = "mypackage.__main__:app"
The setuptools entrypoint documentation goes into further detail.
Result Action
When using Cyclopts as a CLI application, command return values are automatically handled appropriately. By default, App uses "print_non_int_sys_exit" mode, which calls sys.exit() with the appropriate exit code:
String returns are printed to stdout, then
sys.exit(0)is calledInteger returns are passed to
sys.exit(int)as the exit codeBoolean returns are converted:
True→sys.exit(0),False→sys.exit(1)Nonereturns callsys.exit(0)
This default behavior makes Cyclopts applications work consistently whether run directly as scripts or installed via console_scripts entry points. The result_action can be customized if different behavior is needed:
Custom Return Code Protocol
Whenever a built-in result_action would otherwise use 0 as the exit code, Cyclopts checks the return value for a __cyclopts_returncode__ method. If present, its return value is used as the exit code instead. This lets returned objects describe their own success/failure without forcing the CLI layer to translate them.
A common use case is a CLI that wraps a library and returns rich result objects directly, deferring presentation to the object's __rich__ method:
from cyclopts import App
class HealthCheck:
def __init__(self, service: str, healthy: bool):
self.service = service
self.healthy = healthy
def __rich__(self) -> str:
status = "[green]OK[/green]" if self.healthy else "[red]FAIL[/red]"
return f"{self.service}: {status}"
def __cyclopts_returncode__(self) -> int:
return 0 if self.healthy else 1
app = App()
@app.command
def check(service: str) -> HealthCheck:
"""Check the health of a service."""
return HealthCheck(service, healthy=ping(service))
app()
$ my-script check database; echo "exit=$?"
database: OK
exit=0
$ my-script check cache; echo "exit=$?"
cache: FAIL
exit=1
The protocol is opt-in: objects that don't define __cyclopts_returncode__ continue to use the previous 0 default. The method must be a zero-argument callable that returns an int; non-callable attributes are ignored. Branches that derive their exit code from an int or bool return value (e.g. sys.exit(result)) ignore the protocol — return an integer/bool directly to set those exit codes.
Custom result_action callables can opt into the same protocol via cyclopts.resolve_returncode():
import sys
from typing import Any
from cyclopts import App, resolve_returncode
def result_action(result: Any) -> None:
if isinstance(result, bool):
sys.exit(0 if result else 1)
if isinstance(result, int):
sys.exit(result)
if result is not None:
print(result)
sys.exit(resolve_returncode(result))
app = App(result_action=result_action)