CLI tool
python-poetry/cleo avatar
python-poetry/cleo

Cleo: a Python framework for command line interfaces that need testing

Cleo allows you to create beautiful and testable command-line interfaces.

1,359 stars106 forksPythonMIT

At a glance

What is it?
Cleo, from the Poetry organisation, gives you commands as classes with declared arguments and options, a styled output layer with tags and verbosity levels, and a testable structure. Its README opens with a caution: the library is mid-rewrite for version 3.0, so contributions are welcome only after checking the writeup issue.
Who is it for?
Adopt Cleo if you are building a Python CLI with several commands and want the argument parsing, the help output and the styling to be part of a testable structure rather than assembled by hand. Do not adopt it in a project that cannot tolerate a library mid-rewrite, because the README states plainly that Cleo is currently being rewritten for version 3.0 and asks contributors to consult the writeup issue first.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 11 days ago.
What is it written in?
Mainly Python, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 25, 2026, and from our analysis. They are not legal advice.

Editorial analysis

A command is a class, not a function with argparse

Cleo exists to create beautiful and testable command-line interfaces, and the design is visible in the smallest example in the README. A command is a class that subclasses Command and declares its interface as class attributes: a name, a description, a list of arguments built with the argument helper, and a list of options built with the option helper. The behaviour lives in a handle method, and the arguments and options are read back from the instance inside it:

python
from cleo.commands.command import Command
from cleo.helpers import argument, option


class GreetCommand(Command):
    name = "greet"
    description = "Greets someone"
    arguments = [
        argument("name", description="Who do you want to greet?", optional=True)
    ]
    options = [
        option(
            "yell",
            "y",
            description="If set, the task will yell in uppercase letters",
            flag=True,
        )
    ]

    def handle(self):
        name = self.argument("name")
        text = f"Hello {name}" if name else "Hello"
        if self.option("yell"):
            text = text.upper()
        self.line(text)

The commands are then collected by an Application object and run:

python
from greet_command import GreetCommand
from cleo.application import Application

application = Application()
application.add(GreetCommand())

if __name__ == "__main__":
    application.run()

The point of the structure is testability. The command is a class, so a test can construct it, set the input and the output, call handle, and assert on what was written, which is a very different proposition from testing a script that calls sys.argv directly.

The 3.0 rewrite is announced in the first box of the README

The first thing in the README is a caution, and any evaluation should start there. Cleo is currently being rewritten for version 3.0, and while contributions are appreciated, the request is to consult the writeup issue and the Discord channel before submitting a pull request. The project metadata backs that up. The manifest version is 3.0.0.dev0, a development version for the rewrite, while the published releases on PyPI are 2.1.0 from 2023-10-30, with 2.0.1 and 2.0.0 both from November 2022. The last push to the repository was on 2026-10-15, so the work is continuing even though nothing has been published since 2023. For an adopter, the practical reading is that the 2.x line is the stable, released API and the 3.0 rewrite is where new features are landing, which means a decision to depend on Cleo is a decision about which line to pin. The repository is under the python-poetry organisation, and the project is the toolkit behind Poetry's own command line, which is a reasonable signal about the shape of the API but not about its release cadence.

Styling is done with tags, and you can define your own

The output layer is where Cleo earns the word beautiful. Text written through the line method can be wrapped in tags that map to styles, and the built-in ones are the four that a command line tool actually needs: info for blue, comment for green, question for cyan and error for bold red. The closing tag can be written as </> , which revokes all formatting options established by the last opened tag, which is the detail that makes the scheme usable in a loop without leaking style state:

python
# blue text
self.line("<info>foo</info>")

# green text
self.line("<comment>foo</comment>")

# cyan text
self.line("<question>foo</question>")

# bold red text
self.line("<error>foo</error>")

Custom styles are added with the add_style method, taking a foreground colour, a background colour and a list of options:

python
self.add_style("fire", fg="red", bg="yellow", options=["bold", "blink"])
self.line("<fire>foo</fire>")

The available foreground and background colours are black, red, green, yellow, blue, magenta, cyan and white, and the available options are bold, underscore, blink, reverse and conceal. The same information can also be written inside the tag name, so <fg=green> sets the foreground, <fg=black;bg=cyan> sets both and <bg=yellow;options=bold> sets a background with an option, which is convenient when a single call needs a specific combination.

Verbosity levels, and a small inconsistency in the documentation

Cleo has a verbosity model with modes defined in the Output class, and a table maps each mode to its console option. Verbosity.QUIET means do not output any messages and is selected with -q or --quiet, NORMAL is the default with no option, VERBOSE is increased verbosity of messages with -v, VERY_VERBOSE is informative non essential messages with -vv, and DEBUG is debug messages with -vvv. The interesting part is that the prose says Cleo has four verbosity levels while the table lists five modes, which is a small documentation slip worth knowing about if you are reading the levels closely. A command can test the level directly, either by comparing against self.io.verbosity, or with the semantic helpers self.output.is_quiet() and self.output.is_verbose(), and a single call can be scoped to a level by passing the verbosity argument to line:

python
if Verbosity.VERBOSE <= self.io.verbosity:
    self.line(...)

self.line("", verbosity=Verbosity.VERBOSE)

When the quiet level is used, all output is suppressed, which is what a command needs when it is being called from a script and its chatter would corrupt the result.

Arguments are ordered, options are not, and one rule catches people

The argument and option model follows the conventions people expect from other command line tools, with one implementation detail that is worth reading carefully. Arguments are the space-separated values that come after the command name, they are ordered, and each can be optional or required. Options are not ordered, they are specified with two dashes such as --yell, you can declare a one-letter shortcut that is called with a single dash such as -y, and they are always optional. The rule that catches people out is the one about lists. To let an argument take several values, it must be specified at the end of the argument list, and the value is then read as a list:

python
arguments = [
    argument("names", description="Who do you want to greet?", multiple=True)
]

with the result read as a Python list, so a command greeting many names joins them with commas. Arguments can also be made required by simply dropping the optional flag, and the example adds an optional last_name argument alongside a required name, with the value retrieved through self.argument("last_name") and appended to the text. The library generates the parsing, the usage message and the validation from those declarations, which is the whole point of the exercise.

The project mechanics: poetry, ruff, mypy and towncrier

The repository is a modern Python project and its configuration says what the maintainers care about. The build is handled by poetry-core 2.0 or later, the package requires Python 3.9 or newer, and the sources live under src/ with the package included from there. The linting is ruff with fix set to true, a target version of py39 and a line length of 88, which is the default the ecosystem has settled on. The development dependencies are grouped by purpose rather than lumped together, with a types group holding mypy 1.18 or later, a test group with pytest 8.3.5 or later, pytest-cov and pytest-mock, and a docs group with Sphinx 7.1 and the furo theme, plus towncrier for changelog fragments and pre-commit for the hook configuration. The news/ directory in the tree is where towncrier fragments live, which means changelog entries are pull requests rather than edits to a file, and the presence of a .pre-commit-config.yaml confirms the hooks are run locally as well as in continuous integration. The classifiers describe the package as production stable, for the console environment, and for developers, information technology and system administrators, which is an accurate summary of who a Cleo command line is for.

Editorial conclusion

Adopt Cleo if you are building a Python CLI with several commands and want the argument parsing, the help output and the styling to be part of a testable structure rather than assembled by hand. Do not adopt it in a project that cannot tolerate a library mid-rewrite, because the README states plainly that Cleo is currently being rewritten for version 3.0 and asks contributors to consult the writeup issue first. Before you pin a version, note that the manifest already reads 3.0.0.dev0 while the newest published release is 2.1.0 from 2023-10-30, and that the last push was on 2026-10-15, so the 2.x line is the safe one to depend on today.

Frequently asked questions

What is Cleo?

It is a Python library, maintained under the python-poetry organisation, for creating command line interfaces that are described by classes and are testable. A command subclasses Command, declares its arguments and options, and implements handle, and an Application collects the commands and runs them.

Is Cleo being rewritten?

Yes. The README opens with a caution that Cleo is currently being rewritten for version 3.0, and asks contributors to consult the writeup issue and the Discord channel before submitting a pull request. The manifest version is 3.0.0.dev0 while the newest published release is 2.1.0 from 2023-10-30.

How do I add colour to command output in Cleo?

You wrap the text in tags, with info for blue, comment for green, question for cyan and error for bold red, and the closing tag can be written as </> to revoke the formatting. You can also define your own style with the add_style method using foreground, background and options, and set the same attributes inside the tag name, for example <fg=green> or <bg=yellow;options=bold>.

What verbosity levels does Cleo support?

The Output class defines QUIET, selected with -q or --quiet, NORMAL as the default, VERBOSE with -v, VERY_VERBOSE with -vv and DEBUG with -vvv. A command can test the level with the verbosity helpers such as is_quiet() and is_verbose(), or pass a verbosity argument to line.

How do I take several values for one argument in Cleo?

Declare the argument with multiple=True, and it must be the last entry in the argument list. The value is then read as a list through self.argument, while options are never ordered and are always optional, and can have a single-letter shortcut.

Official sources

  1. License: MIT
  2. Project website
  3. python-poetry/cleo on GitHub
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/python-poetry-cleo.svg)](https://hysenlabs.com/projects/python-poetry-cleo)

Was this useful?