Logging¶
The Python’s standard library logging module is a bit tricky to use. Click Extra provides pre-configured helpers with sane defaults to simplify the logging configuration.
Colored verbosity¶
--verbosity is a pre-configured option which allow users of your CLI to set the log level of a logging.Logger instance.
Default behavior is to:
print to
<stderr>,send messages to the
rootlogger,show
WARNING-level messages and above,use
{-style format strings,render logs with the
{levelname}: {message}format,color the log’s
{levelname}variable.
Integrated option¶
This option is part of @command and @group by default:
from click_extra import command, echo
@command
def my_cli():
echo("It works!")
See that --verbosity is featured in the help screen:
$ my-cli --help
Usage: my-cli [OPTIONS]
Options:
--time / --no-time Measure and print elapsed execution time. [default:
no-time]
--color, --ansi / --no-color, --no-ansi
Strip out all colors and all ANSI codes from output.
[default: color]
--config CONFIG_PATH Location of the configuration file. Supports local
path with glob patterns or remote URL. [default:
~/.config/my-cli/{*.toml,*.yaml,*.yml,*.json,*.json5,*
.jsonc,*.hjson,*.ini,*.xml,pyproject.toml}]
--no-config Ignore all configuration files and only use command
line parameters and environment variables.
--validate-config FILE Validate the configuration file and exit.
--show-params Show all CLI parameters, their provenance, defaults
and value, then exit.
--table-format [aligned|asciidoc|colon-grid|csv|csv-excel|csv-excel-tab|csv-unix|double-grid|double-outline|fancy-grid|fancy-outline|github|grid|heavy-grid|heavy-outline|hjson|html|jira|json|json5|jsonc|latex|latex-booktabs|latex-longtable|latex-raw|mediawiki|mixed-grid|mixed-outline|moinmoin|orgtbl|outline|pipe|plain|presto|pretty|psql|rounded-grid|rounded-outline|rst|simple|simple-grid|simple-outline|textile|toml|tsv|unsafehtml|vertical|xml|yaml|youtrack]
Rendering style of tables. [default: rounded-outline]
--verbosity LEVEL Either CRITICAL, ERROR, WARNING, INFO, DEBUG.
[default: WARNING]
-v, --verbose Increase the default WARNING verbosity by one level
for each additional repetition of the option.
[default: 0]
--version Show the version and exit.
-h, --help Show this message and exit.
This option can be invoked to display all the gory details of your CLI with the DEBUG level:
$ my-cli --verbosity DEBUG
debug: Set <Logger click_extra (DEBUG)> to DEBUG.
debug: Set <RootLogger root (DEBUG)> to DEBUG.
debug: Load configuration matching /home/runner/.config/my-cli/{*.toml,*.yaml,*.yml,*.json,*.json5,*.jsonc,*.hjson,*.ini,*.xml,pyproject.toml}
debug: Found /home/runner/work/click-extra/click-extra/pyproject.toml, parsing as pyproject.toml.
debug: pyproject.toml parsing successful, got {'uv': {'dependency-groups': {'docs': {'requires-python': '>= 3.14'}}, 'exclude-newer': '1 week', 'exclude-newer-package': {'requests': '0 day'}, 'build-backend': {'module-root': ''}}, 'ruff': {'preview': True, 'fix': True, 'unsafe-fixes': True, 'show-fixes': True, 'format': {'docstring-code-format': True}, 'lint': {'ignore': ['B008', 'D400', 'ERA001', 'LOG015'], 'isort': {'combine-as-imports': True}, 'future-annotations': True}}, 'typos': {'default': {'extend-identifiers': {'Github': 'GitHub', 'IOS': 'iOS', 'Javascript': 'JavaScript', 'MacOS': 'macOS', 'PyPi': 'PyPI', 'Typescript': 'TypeScript'}, 'extend-ignore-re': ['(?s)<!-- typos:off -->.*?<!-- typos:on -->'], 'extend-ignore-identifiers-re': ['valuE', 'SeCoNd', 'sUper', 'fAlsE', 'nd', 'mey', 'certifi']}}, 'pytest': {'markers': ['once: Tests that only need to run once, not across the full CI matrix.'], 'addopts': ['--durations=10', '--cov', '--cov-report=term'], 'xfail_strict': True}, 'coverage': {'run': {'branch': True, 'source': ['click_extra']}, 'report': {'precision': 2}}, 'bumpversion': {'current_version': '7.11.0.dev0', 'allow_dirty': True, 'ignore_missing_files': True, 'parse': '(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)(\\.dev(?P<dev>\\d+))?', 'serialize': ['{major}.{minor}.{patch}.dev{dev}', '{major}.{minor}.{patch}'], 'parts': {'dev': {'values': ['0', 'release'], 'optional_value': 'release'}}, 'files': [{'glob': './**/__init__.py', 'ignore_missing_version': True}, {'filename': './pyproject.toml', 'search': 'version = "{current_version}"', 'replace': 'version = "{new_version}"'}, {'filename': './pyproject.toml', 'search': 'releases/tag/v{current_version}', 'replace': 'releases/tag/v{new_version}'}, {'filename': './changelog.md', 'search': '## [`{current_version}` (unreleased)](', 'replace': '## [`{new_version}` (unreleased)]('}, {'filename': './citation.cff', 'search': 'version: {current_version}', 'replace': 'version: {new_version}'}, {'filename': './citation.cff', 'regex': True, 'search': 'date-released: \\d{{4}}-\\d{{2}}-\\d{{2}}', 'replace': 'date-released: {utcnow:%Y-%m-%d}'}, {'filename': './readme.md', 'ignore_missing_version': True, 'search': 'raw.githubusercontent.com/kdeldycke/click-extra/main/', 'replace': 'raw.githubusercontent.com/kdeldycke/click-extra/v{new_version}/'}, {'filename': './docs/tutorial.md', 'ignore_missing_version': True, 'search': 'raw.githubusercontent.com/kdeldycke/click-extra/main/', 'replace': 'raw.githubusercontent.com/kdeldycke/click-extra/v{new_version}/'}, {'filename': './readme.md', 'ignore_missing_version': True, 'search': 'raw.githubusercontent.com/kdeldycke/click-extra/v{current_version}/', 'replace': 'raw.githubusercontent.com/kdeldycke/click-extra/main/'}, {'filename': './docs/tutorial.md', 'ignore_missing_version': True, 'search': 'raw.githubusercontent.com/kdeldycke/click-extra/v{current_version}/', 'replace': 'raw.githubusercontent.com/kdeldycke/click-extra/main/'}]}, 'mypy': {'check_untyped_defs': True, 'warn_unused_configs': True, 'warn_redundant_casts': True, 'warn_unused_ignores': True, 'warn_return_any': True, 'warn_unreachable': True, 'pretty': True, 'overrides': [{'ignore_missing_imports': True, 'module': ['hjson', 'jsonc']}, {'ignore_missing_imports': True, 'module': ['sphinx.*'], 'follow_imports': 'skip'}]}, 'lychee': {'exclude': ['^https://github.com/.+/issues/[0-9]+#issuecomment-.*$', '^https://github.com/.+/releases/.+/download/.*$', '^https://doi.org/.+/zenodo.*$']}, 'repomatic': {'dependency-graph': {'all-extras': True, 'all-groups': False}, 'exclude': ['workflows/debug.yaml'], 'include': ['skills'], 'test-matrix': {'variations': {'click-version': ['released', 'stable', 'main'], 'cloup-version': ['released', 'master']}, 'include': [{'click-version': 'released'}, {'cloup-version': 'released'}], 'exclude': [{'os': 'ubuntu-slim', 'click-version': 'stable'}, {'os': 'ubuntu-slim', 'click-version': 'main'}, {'os': 'ubuntu-slim', 'cloup-version': 'master'}, {'os': 'macos-15-intel', 'click-version': 'stable'}, {'os': 'macos-15-intel', 'click-version': 'main'}, {'os': 'macos-15-intel', 'cloup-version': 'master'}, {'os': 'windows-2025', 'click-version': 'stable'}, {'os': 'windows-2025', 'click-version': 'main'}, {'os': 'windows-2025', 'cloup-version': 'master'}, {'python-version': '3.10', 'click-version': 'stable'}, {'python-version': '3.10', 'click-version': 'main'}, {'python-version': '3.10', 'cloup-version': 'master'}, {'python-version': '3.15', 'click-version': 'stable'}, {'python-version': '3.15', 'click-version': 'main'}, {'python-version': '3.15', 'cloup-version': 'master'}]}}}.
debug: Using /home/runner/work/click-extra/click-extra/pyproject.toml from CWD search.
debug: Parsed user configuration: {'uv': {'dependency-groups': {'docs': {'requires-python': '>= 3.14'}}, 'exclude-newer': '1 week', 'exclude-newer-package': {'requests': '0 day'}, 'build-backend': {'module-root': ''}}, 'ruff': {'preview': True, 'fix': True, 'unsafe-fixes': True, 'show-fixes': True, 'format': {'docstring-code-format': True}, 'lint': {'ignore': ['B008', 'D400', 'ERA001', 'LOG015'], 'isort': {'combine-as-imports': True}, 'future-annotations': True}}, 'typos': {'default': {'extend-identifiers': {'Github': 'GitHub', 'IOS': 'iOS', 'Javascript': 'JavaScript', 'MacOS': 'macOS', 'PyPi': 'PyPI', 'Typescript': 'TypeScript'}, 'extend-ignore-re': ['(?s)<!-- typos:off -->.*?<!-- typos:on -->'], 'extend-ignore-identifiers-re': ['valuE', 'SeCoNd', 'sUper', 'fAlsE', 'nd', 'mey', 'certifi']}}, 'pytest': {'markers': ['once: Tests that only need to run once, not across the full CI matrix.'], 'addopts': ['--durations=10', '--cov', '--cov-report=term'], 'xfail_strict': True}, 'coverage': {'run': {'branch': True, 'source': ['click_extra']}, 'report': {'precision': 2}}, 'bumpversion': {'current_version': '7.11.0.dev0', 'allow_dirty': True, 'ignore_missing_files': True, 'parse': '(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)(\\.dev(?P<dev>\\d+))?', 'serialize': ['{major}.{minor}.{patch}.dev{dev}', '{major}.{minor}.{patch}'], 'parts': {'dev': {'values': ['0', 'release'], 'optional_value': 'release'}}, 'files': [{'glob': './**/__init__.py', 'ignore_missing_version': True}, {'filename': './pyproject.toml', 'search': 'version = "{current_version}"', 'replace': 'version = "{new_version}"'}, {'filename': './pyproject.toml', 'search': 'releases/tag/v{current_version}', 'replace': 'releases/tag/v{new_version}'}, {'filename': './changelog.md', 'search': '## [`{current_version}` (unreleased)](', 'replace': '## [`{new_version}` (unreleased)]('}, {'filename': './citation.cff', 'search': 'version: {current_version}', 'replace': 'version: {new_version}'}, {'filename': './citation.cff', 'regex': True, 'search': 'date-released: \\d{{4}}-\\d{{2}}-\\d{{2}}', 'replace': 'date-released: {utcnow:%Y-%m-%d}'}, {'filename': './readme.md', 'ignore_missing_version': True, 'search': 'raw.githubusercontent.com/kdeldycke/click-extra/main/', 'replace': 'raw.githubusercontent.com/kdeldycke/click-extra/v{new_version}/'}, {'filename': './docs/tutorial.md', 'ignore_missing_version': True, 'search': 'raw.githubusercontent.com/kdeldycke/click-extra/main/', 'replace': 'raw.githubusercontent.com/kdeldycke/click-extra/v{new_version}/'}, {'filename': './readme.md', 'ignore_missing_version': True, 'search': 'raw.githubusercontent.com/kdeldycke/click-extra/v{current_version}/', 'replace': 'raw.githubusercontent.com/kdeldycke/click-extra/main/'}, {'filename': './docs/tutorial.md', 'ignore_missing_version': True, 'search': 'raw.githubusercontent.com/kdeldycke/click-extra/v{current_version}/', 'replace': 'raw.githubusercontent.com/kdeldycke/click-extra/main/'}]}, 'mypy': {'check_untyped_defs': True, 'warn_unused_configs': True, 'warn_redundant_casts': True, 'warn_unused_ignores': True, 'warn_return_any': True, 'warn_unreachable': True, 'pretty': True, 'overrides': [{'ignore_missing_imports': True, 'module': ['hjson', 'jsonc']}, {'ignore_missing_imports': True, 'module': ['sphinx.*'], 'follow_imports': 'skip'}]}, 'lychee': {'exclude': ['^https://github.com/.+/issues/[0-9]+#issuecomment-.*$', '^https://github.com/.+/releases/.+/download/.*$', '^https://doi.org/.+/zenodo.*$']}, 'repomatic': {'dependency-graph': {'all-extras': True, 'all-groups': False}, 'exclude': ['workflows/debug.yaml'], 'include': ['skills'], 'test-matrix': {'variations': {'click-version': ['released', 'stable', 'main'], 'cloup-version': ['released', 'master']}, 'include': [{'click-version': 'released'}, {'cloup-version': 'released'}], 'exclude': [{'os': 'ubuntu-slim', 'click-version': 'stable'}, {'os': 'ubuntu-slim', 'click-version': 'main'}, {'os': 'ubuntu-slim', 'cloup-version': 'master'}, {'os': 'macos-15-intel', 'click-version': 'stable'}, {'os': 'macos-15-intel', 'click-version': 'main'}, {'os': 'macos-15-intel', 'cloup-version': 'master'}, {'os': 'windows-2025', 'click-version': 'stable'}, {'os': 'windows-2025', 'click-version': 'main'}, {'os': 'windows-2025', 'cloup-version': 'master'}, {'python-version': '3.10', 'click-version': 'stable'}, {'python-version': '3.10', 'click-version': 'main'}, {'python-version': '3.10', 'cloup-version': 'master'}, {'python-version': '3.15', 'click-version': 'stable'}, {'python-version': '3.15', 'click-version': 'main'}, {'python-version': '3.15', 'cloup-version': 'master'}]}}}
debug: Initial defaults: None
debug: New defaults: ChainMap({}, {})
debug: Cannot get version: 'click_extra.sphinx' package not found or not installed.
debug: Version string template variables:
debug: {module} : <module 'click_extra.sphinx.click' from '/home/runner/work/click-extra/click-extra/click_extra/sphinx/click.py'>
debug: {module_name} : click_extra.sphinx.click
debug: {module_file} : /home/runner/work/click-extra/click-extra/click_extra/sphinx/click.py
debug: {module_version} : None
debug: {package_name} : click_extra.sphinx
debug: {package_version}: None
debug: {exec_name} : click_extra.sphinx.click
debug: {version} : None
debug: {git_repo_path} : /home/runner/work/click-extra/click-extra
debug: {git_branch} : main
debug: {git_long_hash} : e3d04dc70388b481f404efcacbfc39b870be6c22
debug: {git_short_hash} : e3d04dc
debug: {git_date} : 2026-04-12 09:30:32 +0200
debug: {git_tag} : None
debug: {git_tag_sha} : None
debug: {prog_name} : my-cli
debug: {env_info} : {'username': '-', 'guid': '9bd06c9151cab0c899d4a2f24b3df76', 'hostname': '-', 'hostfqdn': '-', 'uname': {'system': 'Linux', 'node': '-', 'release': '6.1.146.1-microsoft-standard', 'version': '#1 SMP Mon Jul 21 20:38:16 UTC 2025', 'machine': 'x86_64', 'processor': 'x86_64'}, 'linux_dist_name': '', 'linux_dist_version': '', 'cpu_count': 1, 'fs_encoding': 'utf-8', 'ulimit_soft': 1048576, 'ulimit_hard': 1048576, 'cwd': '-', 'umask': '0o2', 'python': {'argv': '-', 'bin': '-', 'version': '3.14.4 (main, Apr 7 2026, 20:47:53) [Clang 22.1.1 ]', 'compiler': 'Clang 22.1.1 ', 'build_date': 'Apr 7 2026 20:47:53', 'version_info': [3, 14, 4, 'final', 0], 'features': {'openssl': 'OpenSSL 3.5.5 27 Jan 2026', 'expat': 'expat_2.6.3', 'sqlite': '3.50.4', 'tkinter': '9.0', 'zlib': '1.3.1', 'unicode_wide': True, 'readline': True, '64bit': True, 'ipv6': True, 'threading': True, 'urandom': True}}, 'time_utc': '2026-04-12 07:32:42.384428+00:00', 'time_utc_offset': 0.0, '_eco_version': '1.1.0'}
It works!
debug: Reset <RootLogger root (DEBUG)> to WARNING.
debug: Reset <Logger click_extra (DEBUG)> to WARNING.
Hint
Notice how, in the output above, each logger’s own level is printed as debug messages.
And how they’re all first set to DEBUG at the beginning of the command, then reset back to their default WARNING at the end.
Important
Besides --verbosity, there is another -v/--verbose option. The later works relative to --verbosity, and can be used to increase the level in steps, simply by repeating it.
In the rest of this documentation, we will mainly focus on the canonical --verbosity option to keep things simple (logging is already complicated enough…).
Standalone option¶
The verbosity option can be used independently of @command, and you can attach it to a vanilla Click command:
import logging
import click
import click_extra
@click.command
@click_extra.verbosity_option
def vanilla_command():
click.echo("It works!")
logging.info("We're printing stuff.")
$ vanilla --help
Usage: vanilla [OPTIONS]
Options:
--verbosity LEVEL Either CRITICAL, ERROR, WARNING, INFO, DEBUG.
--help Show this message and exit.
$ vanilla
It works!
$ vanilla --verbosity INFO
It works!
info: We're printing stuff.
The -v/--verbose option can also be used as a standalone option:
import logging
import click
import click_extra
@click.command
@click_extra.verbose_option
def verbose_command():
click.echo("It works!")
logging.info("We're printing stuff.")
$ verbose --help
Usage: verbose [OPTIONS]
Options:
-v, --verbose Increase the default WARNING verbosity by one level for each
additional repetition of the option.
--help Show this message and exit.
$ verbose
It works!
$ verbose -v
It works!
info: We're printing stuff.
Default logger¶
The --verbosity option is by default attached to the global root logger.
This allows you to use module-level helpers like logging.debug. That way you don’t have to worry about setting up your own logger. And logging messages can be easily produced with minimal code:
import logging
import click
import click_extra
@click.command
@click_extra.verbosity_option
def my_cli():
# Print a message for each level.
logging.critical("Complete meltdown!")
logging.error("Does not compute.")
logging.warning("Mad scientist at work!")
logging.info("This is a message.")
logging.debug("We're printing stuff.")
The --verbosity option print by default all messages at the WARNING level and above:
$ my-cli
critical: Complete meltdown!
error: Does not compute.
warning: Mad scientist at work!
But each level can be selected with the option:
$ my-cli --verbosity CRITICAL
critical: Complete meltdown!
$ my-cli --verbosity ERROR
critical: Complete meltdown!
error: Does not compute.
$ my-cli --verbosity WARNING
critical: Complete meltdown!
error: Does not compute.
warning: Mad scientist at work!
$ my-cli --verbosity INFO
critical: Complete meltdown!
error: Does not compute.
warning: Mad scientist at work!
info: This is a message.
$ my-cli --verbosity DEBUG
debug: Set <Logger click_extra (DEBUG)> to DEBUG.
debug: Set <RootLogger root (DEBUG)> to DEBUG.
critical: Complete meltdown!
error: Does not compute.
warning: Mad scientist at work!
info: This is a message.
debug: We're printing stuff.
debug: Reset <RootLogger root (DEBUG)> to WARNING.
debug: Reset <Logger click_extra (DEBUG)> to WARNING.
Caution
root is the default logger associated with --verbosity. Which means that, if not configured, all loggers will be printed at the verbosity level set by the option:
import logging
import click
import click_extra
@click.command
@click_extra.verbosity_option
def multiple_loggers_cli():
# Use the default root logger.
root_logger = logging.getLogger()
root_logger.warning("Root warning message")
root_logger.info("Root info message")
# Create a custom logger and use it.
my_logger = logging.getLogger("my_logger")
my_logger.warning("My warning message")
my_logger.info("My info message")
In that case, a normal invocation will only print the default WARNING messages:
$ multiple-loggers-cli
warning: Root warning message
warning: My warning message
And calling --verbosity INFO will print both root and my_logger messages of that level:
$ multiple-loggers-cli --verbosity INFO
warning: Root warning message
info: Root info message
warning: My warning message
info: My info message
To prevent this behavior, you can associate the --verbosity option with your own custom logger. This is explained in the next section.
Custom logger¶
The preferred way to customize log messages is to create your own logger and attach it to the --verbosity option.
This can be done with new_extra_logger. Here is how we can for example change the format of the log messages:
import logging
from click import command
from click_extra import new_extra_logger, verbosity_option
new_extra_logger(
name="app_logger",
format="{levelname} | {name} | {message}"
)
@command
@verbosity_option(default_logger="app_logger")
def custom_logger_cli():
# Default root logger.
logging.warning("Root logger warning")
logging.info("Root logger info")
# Use our custom logger.
my_logger = logging.getLogger("app_logger")
my_logger.warning("Custom warning")
my_logger.info("Custom info")
That way the root logger keeps its default format, while the custom logger uses the new one:
$ custom-logger-cli
warning: Root logger warning
warning | app_logger | Custom warning
And changing the verbosity level will only affect the custom logger:
$ custom-logger-cli --verbosity INFO
warning: Root logger warning
warning | app_logger | Custom warning
info | app_logger | Custom info
Now if we don’t explicitly pass the custom logger to the --verbosity option, the default root logger will be tied to it instead:
import logging
from click import command
from click_extra import new_extra_logger, verbosity_option
new_extra_logger(
name="app_logger",
format="{levelname} | {name} | {message}"
)
@command
@verbosity_option
def root_verbosity_cli():
# Default root logger.
logging.warning("Root logger warning")
logging.info("Root logger info")
# Use our custom logger.
my_logger = logging.getLogger("app_logger")
my_logger.warning("Custom warning")
my_logger.info("Custom info")
In that case the default behavior doesn’t change and messages are rendered in their own logger’s format, at the default WARNING level:
$ root-verbosity-cli
warning: Root logger warning
warning | app_logger | Custom warning
But changing the verbosity level only affects root, in the opposite of the previous example:
$ root-verbosity-cli --verbosity INFO
warning: Root logger warning
info: Root logger info
warning | app_logger | Custom warning
Important
By design, new loggers are always created as sub-loggers of root. And as such, their messages are propagated back to it.
But new_extra_logger always creates new loggers by setting their propagate attribute to False. This means that messages of new loggers won’t be propagated to their parents.
This is the reason why, in the example above, the root and app_logger loggers are independent.
Let’s experiment with that property and set the propagate attribute to True:
import logging
from click import command
from click_extra import new_extra_logger, verbosity_option
new_extra_logger(
name="app_logger",
propagate=True,
format="{levelname} | {name} | {message}"
)
@command
@verbosity_option
def logger_propagation_cli():
# Default root logger.
logging.warning("Root logger warning")
logging.info("Root logger info")
# Use our custom logger.
my_logger = logging.getLogger("app_logger")
my_logger.warning("Custom warning")
my_logger.info("Custom info")
$ logger-propagation-cli
warning: Root logger warning
warning | app_logger | Custom warning
warning: Custom warning
Here you can immediately spot the issue with propagation: app_logger’s messages are displayed twice. Once in their custom format, and once in the format of the root logger.
See also
The reason for that hierarchycal design is to allow for dot-separated logger names, like foo.bar.baz. Which allows for even more granular control of loggers by filtering.
Tip
You can creatively configure loggers to produce any kind of messages, like this JSON-like format:
import logging
from click import command
from click_extra import new_extra_logger, verbosity_option
new_extra_logger(
name="json_logger",
format='{{"time": "{asctime}", "name": "{name}", "level": "{levelname}", "msg": "{message}"}}',
)
@command
@verbosity_option(default_logger="json_logger")
def json_logs_cli():
my_logger = logging.getLogger("json_logger")
my_logger.info("This is an info message.")
$ json-logs-cli --verbosity INFO
{"time": "2026-04-12 07:32:50,465", "name": "json_logger", "level": "info", "msg": "This is an info message."}
Hint
Because loggers are registered in a global registry, you can set them up in one place and use them in another. That is the idiomatic approach, which consist in always referring to them by name, as in all examples above.
But for convenience, you can pass the logger object directly to the option:
import logging
from click import command
from click_extra import new_extra_logger, verbosity_option
my_logger = new_extra_logger(name="app_logger")
@command
@verbosity_option(default_logger=my_logger)
def logger_object_cli():
# Default root logger.
logging.warning("Root warning message")
logging.info("Root info message")
# My custom logger.
my_logger.warning("My warning message")
my_logger.info("My info message")
$ logger-object-cli --verbosity INFO
warning: Root warning message
warning: My warning message
info: My info message
Global configuration¶
If you want to change the global configuration of all loggers, you can rely on new_extra_logger. Because the latter defaults to the root logger, any default logger propagating their messages to it will be affected:
import logging
from click import command
from click_extra import new_extra_logger, verbosity_option
root_logger = new_extra_logger(format="{levelname} | {name} | {message}")
@command
@verbosity_option(default_logger=root_logger)
def root_format_cli():
# Default root logger.
logging.warning("Root logger warning")
logging.info("Root logger info")
# Use our custom logger.
my_logger = logging.getLogger("my_logger")
my_logger.warning("Custom warning")
my_logger.info("Custom info")
$ root-format-cli
warning | root | Root logger warning
warning | my_logger | Custom warning
$ root-format-cli --verbosity INFO
warning | root | Root logger warning
info | root | Root logger info
warning | my_logger | Custom warning
info | my_logger | Custom info
Verbose base level¶
When both --verbosity and -v/--verbose are present on the same command, -v increments from the default level of --verbosity. If you change that default, the base level of -v shifts accordingly:
import logging
import click
from click_extra import LogLevel, verbose_option, verbosity_option
@click.command
@verbosity_option(default=LogLevel.ERROR)
@verbose_option
def shifted_cli():
logging.critical("critical message")
logging.error("error message")
logging.warning("warning message")
logging.info("info message")
Without -v, only CRITICAL and ERROR messages are shown:
$ shifted-cli
critical: critical message
error: error message
With -v, the level increases by one step from ERROR to WARNING:
$ shifted-cli -v
critical: critical message
error: error message
warning: warning message
The help message reflects the custom base level:
$ shifted-cli --help
Usage: shifted-cli [OPTIONS]
Options:
--verbosity LEVEL Either CRITICAL, ERROR, WARNING, INFO, DEBUG.
-v, --verbose Increase the default ERROR verbosity by one level for each
additional repetition of the option.
--help Show this message and exit.
Caution
You can attach several @verbose_option decorators to the same command, each targeting a different logger via default_logger. Individual options work in isolation, but combining them on the same invocation (e.g. -d -H) does not work as expected: an internal reconciliation mechanism shares a single verbosity level across all options, so the second option may be silently skipped if its target level is equal to or less verbose than the first’s.
Until this limitation is resolved, prefer a single @verbosity_option with a shared logger if you need to control multiple loggers from the CLI.
Get verbosity level¶
You can get the name of the current verbosity level from the context or the logger itself:
import logging
from click import command, echo, pass_context
from click_extra import verbosity_option
@command
@verbosity_option
@pass_context
def vanilla_command(ctx):
level_from_context = ctx.meta["click_extra.verbosity_level"]
level_from_logger = logging._levelToName[logging.getLogger().getEffectiveLevel()]
echo(f"Level from context: {level_from_context!r}")
echo(f"Level from logger: {level_from_logger!r}")
$ vanilla --verbosity INFO
Level from context: <LogLevel.INFO: 20>
Level from logger: 'INFO'
Internal click_extra logger¶
Click Extra has its own logger, named click_extra, which is used to print debug messages to inspect its own internal behavior.
click_extra.logging API¶
classDiagram
ExtraOption <|-- ExtraVerbosity
ExtraVerbosity <|-- VerboseOption
ExtraVerbosity <|-- VerbosityOption
Formatter <|-- ExtraFormatter
IntEnum <|-- LogLevel
StreamHandler <|-- ExtraStreamHandler
… py:module:: click_extra.logging
Logging utilities.
… py:class:: LogLevel(*values)
- module:
click_extra.logging
Bases: :py:class:
~enum.IntEnumMapping of :ref:
canonical log level names <levels>to their integer level.That’s our own version of
logging._nameToLevel <https://github.com/python/cpython/blob/3.14/Lib/logging/__init__.py#L115-L124>_, but:sorted from lowest to highest verbosity,
excludes the following levels:
- data:
NOTSET <logging.NOTSET>, which is considered internal -WARN, which :meth:is obsolete <logging.Logger.warning>-FATAL, whichshouldn't be used <https://github.com/python/cpython/issues/85013>_ and has beenreplaced by CRITICAL <https://github.com/python/cpython/blob/3.14/Lib/logging/__init__.py#L2150-L2154>_… py:attribute:: LogLevel.CRITICAL
- module:
click_extra.logging
- value:
50
… py:attribute:: LogLevel.ERROR
- module:
click_extra.logging
- value:
40
… py:attribute:: LogLevel.WARNING
- module:
click_extra.logging
- value: