Configuration formats¶
Click Extra reads a configuration file in any of the dialects below. Each one lists the extensions it matches, what it brings, and whether it is available without an extra dependency.
Format |
Extensions |
Description |
Enabled by default |
|---|---|---|---|
|
- |
✅ |
|
|
- |
❌ |
|
|
- |
✅ |
|
|
❌ |
||
|
Like JSON, but with comments and trailing commas |
❌ |
|
|
Another flavor of a user-friendly JSON |
❌ |
|
|
With extended interpolation, multi-level sections and non-native types ( |
✅ |
|
|
- |
❌ |
|
|
Apple’s property list, in its XML or binary variant |
✅ |
|
|
Reads a |
✅ |
|
|
Plain-text list of command-line options, in the style of |
✅ |
|
|
Reads |
✅ |
Formats depending on third-party packages are not enabled by default. You need to install Click Extra with the corresponding extra dependency group to enable them.
Every supported format expresses the same configuration. Here is the my-cli section from the standalone option example, written in each one: they all set the same defaults and produce the same result. The one exception is ARGFILE, which cannot reach a subcommand’s options and is shown on its own below.
[my-cli]
extra_value = "is ignored too"
dummy_flag = true
my_list = ["item 1", "item #2", "Very Last Item!"]
[my-cli.subcommand]
int_param = 3
random_stuff = "will be ignored"
my-cli:
extra_value: is ignored too
dummy_flag: true
my_list:
- item 1
- "item #2"
- Very Last Item!
subcommand:
int_param: 3
random_stuff: will be ignored
{
"my-cli": {
"extra_value": "is ignored too",
"dummy_flag": true,
"my_list": ["item 1", "item #2", "Very Last Item!"],
"subcommand": {
"int_param": 3,
"random_stuff": "will be ignored"
}
}
}
{
// Unquoted keys, comments, trailing commas, single quotes.
'my-cli': {
extra_value: 'is ignored too',
dummy_flag: true,
my_list: ['item 1', 'item #2', 'Very Last Item!'],
subcommand: {
int_param: 3,
random_stuff: 'will be ignored',
},
},
}
{
// JSON, plus comments and trailing commas.
"my-cli": {
"extra_value": "is ignored too",
"dummy_flag": true,
"my_list": ["item 1", "item #2", "Very Last Item!"],
"subcommand": {
"int_param": 3,
"random_stuff": "will be ignored",
},
},
}
{
# No quotes, no commas.
my-cli:
{
extra_value: is ignored too
dummy_flag: true
my_list:
[
item 1
item #2
Very Last Item!
]
subcommand:
{
int_param: 3
random_stuff: will be ignored
}
}
}
[my-cli]
extra_value = is ignored too
dummy_flag = true
my_list = ["item 1", "item #2", "Very Last Item!"]
[my-cli.subcommand]
int_param = 3
random_stuff = will be ignored
<?xml version="1.0"?>
<my-cli>
<extra_value>is ignored too</extra_value>
<dummy_flag>true</dummy_flag>
<my_list>item 1</my_list>
<my_list>item #2</my_list>
<my_list>Very Last Item!</my_list>
<subcommand>
<int_param>3</int_param>
<random_stuff>will be ignored</random_stuff>
</subcommand>
</my-cli>
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>my-cli</key>
<dict>
<key>extra_value</key>
<string>is ignored too</string>
<key>dummy_flag</key>
<true/>
<key>my_list</key>
<array>
<string>item 1</string>
<string>item #2</string>
<string>Very Last Item!</string>
</array>
<key>subcommand</key>
<dict>
<key>int_param</key>
<integer>3</integer>
<key>random_stuff</key>
<string>will be ignored</string>
</dict>
</dict>
</dict>
</plist>
CREATE TABLE config (key TEXT PRIMARY KEY, value TEXT);
INSERT INTO config VALUES
('my-cli.extra_value', '"is ignored too"'),
('my-cli.dummy_flag', 'true'),
('my-cli.my_list', '["item 1", "item #2", "Very Last Item!"]'),
('my-cli.subcommand.int_param', '3'),
('my-cli.subcommand.random_stuff', '"will be ignored"');
TOML¶
TOML is enabled by default, and is the reference format used in the examples throughout this page.
YAML¶
Important
YAML support requires the yaml extra: install click-extra[yaml].
JSON¶
JSON is enabled by default.
JSON5¶
Important
JSON5 support requires the json5 extra: install click-extra[json5].
The JSON5 parser also reads JWCC files, matched on the *.jwcc extension. JWCC is JSON plus comments and trailing commas, which JSON5 already accepts, so it needs no parser and no extra of its own. A file written for hujson, the Go implementation of the same format, parses too once you name it *.jwcc.
JSONC¶
Important
JSONC support requires the jsonc extra: install click-extra[jsonc].
HJSON¶
Important
HJSON support requires the hjson extra: install click-extra[hjson].
INI¶
INI files use sections, and a dot (.) in a section name marks a sub-level: [my-cli.subcommand] nests under my-cli. ExtendedInterpolation is enabled by default. Each value is typed after its matching CLI parameter; types INI has no native syntax for (lists, sets, …) are read as JSON-serialized strings, like my_list above.
XML¶
Important
XML support requires the xml extra: install click-extra[xml].
The root element is the CLI’s name. A repeated element (like my_list above) is collected into a list, and every value is read as a string, then coerced to its matching parameter’s type.
plist¶
plist is enabled by default, and read through Python’s built-in plistlib module, so no extra dependency is needed. Both the XML and the binary variants of the format are supported, but a plist fetched over http:// or https:// is only parsed in its XML variant, as remote content is downloaded as text. The root of the property list is a dictionary, with the same top-level sections as every other format.
--export-config writes the XML variant, and drops parameters without a value from the export, as plist has no null type.
SQLite¶
SQLITE is enabled by default, and read through Python’s built-in sqlite3 module, so no extra dependency is needed. The database holds a single config table of key/value rows: keys are parameter paths, with a dot (.) separating each level, and values are JSON-encoded, which carries every type the other formats do. Other tables in the database are ignored, so a configuration table can live alongside an application’s own data.
SQLITE is read-only: it cannot be produced by --export-config, and a database fetched over http:// or https:// is skipped.
Argfile¶
ARGFILE is enabled by default, and needs no extra dependency. The file is a plain-text list of command-line options, one per line, in the style of mpv and yt-dlp configuration files. Each line is written exactly as it would be typed on the command line, and comments start with a hash sign (#):
~/.config/my-cli/my-cli.conf¶# Print more details.
--verbose
# Repeat the operation three times, in French.
--count 3
--language fr
# A list is fed one item per occurrence.
--my-list pip
--my-list npm
Both the --option value and --option=value spellings are supported, and shell quoting rules apply, so a value containing spaces or a # is wrapped in quotes. A boolean flag takes no value: --flag sets it, and its --no-flag counterpart unsets it.
Note
An argfile can only address the options of the CLI’s top-level command: the format has no section syntax with which to reach a subcommand’s own options, and positional arguments are skipped.
ARGFILE is read-only: it cannot be produced by --export-config.
pyproject.toml¶
The PYPROJECT_TOML format reads [tool.<cli-name>] sections from a pyproject.toml file, following PEP 518. This stores the CLI’s configuration alongside project metadata. Non-Python tools like ruff and typos use the same convention.
Tip
pyproject.toml is becoming the standard place to centralize tool configuration for Python projects. Instead of scattering dedicated config files at the root of your repository (ruff.toml, typos.toml, mypy.ini, …), you can consolidate them all under [tool.*] sections in a single pyproject.toml. This keeps the repository root clean, makes it easy to review and coordinate tool configurations in one place, and reduces the number of files contributors need to discover.
PYPROJECT_TOML is included in the default format patterns, so it is automatically discovered alongside other formats. The [tool] wrapper is automatically unwrapped: merge_default_map sees {"cli": {"int_param": 3}}, exactly the same structure as a regular TOML config file.
See also
For a production example of a CLI built on Click Extra’s pyproject.toml configuration with a typed dataclass schema, nested sub-tables, and 48 config options, see repomatic’s configuration reference. Repomatic also uses Click Extra’s config system to bridge [tool.X] sections for third-party tools that don’t read pyproject.toml natively.
CWD-first discovery¶
When auto-discovering configuration (no explicit --config flag), Click Extra searches for pyproject.toml starting from the current working directory and walking up to the VCS root before checking the standard app config directory. This matches the discovery behavior of uv, ruff, and mypy, so users get the configuration they expect without passing --config explicitly.
The CWD search only applies to pyproject.toml: other config formats (TOML, YAML, JSON, etc.) are still discovered from the app config directory. If a pyproject.toml is found via CWD search, the app-dir search is skipped entirely. If --config is passed explicitly, CWD search is bypassed.
Given a pyproject.toml in the search path:
pyproject.toml¶[build-system]
requires = ["setuptools"]
[tool.cli]
int_param = 3
This is especially powerful combined with search_parents to walk up from a project directory:
from click import command, option, echo
from click_extra import config_option
@command
@option("--int-param", type=int, default=10)
@config_option(search_parents=True)
def cli(int_param):
echo(f"int_parameter is {int_param!r}")
Running cli from anywhere inside the project tree will find pyproject.toml at the repository root and apply [tool.cli] values. The walk automatically stops at the VCS root.
Dedicated file wins, no merging¶
When both a dedicated configuration file (like my-cli.toml) and a pyproject.toml with a [tool.my-cli] section exist, Click Extra uses the first parseable file it finds and ignores all others. There is no merging across files, unless cascade=True opts into layering every discovered file.
This is the de facto standard across the ecosystem. Every major tool that supports both a dedicated config file and pyproject.toml follows the same strict precedence (dedicated file wins, pyproject.toml is ignored entirely):
Tool |
Precedence rule |
|---|---|
|
|
|
|
|
The rationale:
No merging surprises. Merging two config sources creates ambiguity: which key wins when both files define it? Are arrays concatenated or replaced? Every tool above chose “first match wins, full stop” to avoid this class of problems entirely.
Explicit intent. A dedicated file at the repository root, named after the tool, is the most visible and explicit signal. If someone creates one alongside a
[tool.*]section, the dedicated file represents a deliberate override.Clean migration path. Users moving from a dedicated file to
pyproject.tomlsimply delete the dedicated file. Users who need the dedicated file (for example, sharing it across non-Python repos) keep it andpyproject.tomlis silently ignored.
See also
Other non-Python tools that support [tool.*] in pyproject.toml:
basedpyright,
lychee,
maturin,
pixi,
Pyrefly,
Pyright,
rumdl,
Tombi,
ty,
typos,
uv,
and Zuban.
Click Extra’s own [tool.*] bridge in repomatic’s tool runner translates [tool.yamllint], [tool.actionlint], [tool.biome], and others into native config files at invocation time, giving tools that lack native pyproject.toml support the same single-file experience.
Other tools are following suit: actionlint#623, biome#9239, gitleaks#2066, Nuitka#3909, taplo#603, zizmor#322. sh#1268 was declined.
click_extra.config.formats API¶
classDiagram
Enum <|-- ConfigFormat
Configuration file formats and their stateless content parsers.
Holds the ConfigFormat enum, the optional third-party parser probes
that decide which formats are enabled, and parse_content(), the stateless
dispatch used by ConfigOption for every format that
does not need the CLI parameter structure.
Caution
This module is imported early in the package’s import graph
(table reaches it before the
parameters / context chain has
settled), so it takes no top-level import from click_extra itself. A format
whose parsing needs the CLI structure or a binary file, like INI, ARGFILE
and SQLITE, lives as a ConfigOption method in
option instead of here.
- click_extra.config.formats.PARSER_SUPPORT: dict[str, bool] = {'hjson': True, 'json5': True, 'jsonc': True, 'xml': True, 'yaml': True}
Availability of each optional parser, keyed by
click-extra[extra]name.Populated once at import time by probing each module in
_OPTIONAL_PARSERSwithimportlib.util.find_spec(). Read byConfigFormatto mark the matching format as enabled or disabled. The probe does not import the module, so the actual parser is loaded lazily byparse_content()only when used.
- class click_extra.config.formats.ConfigFormat(*values)[source]
Bases:
EnumAll configuration formats, associated to their support status.
The first element of the tuple is a sequence of file extensions associated to the format. Patterns are fed to
wcmatch.globfor matching, and are influenced by the flags set on theConfigOptioninstance.The second element indicates whether the format is supported or not, depending on the availability of the required third-party packages. This evaluation is performed at runtime when this module is imported.
The third element is the human-readable label of the format, and the fourth the media types a server may serve it as, as matched by
format_from_mime().Caution
The order is important for both format members and file patterns. It defines the priority order in which formats are tried when multiple candidate files are found.
- TOML = (('*.toml',), True, 'TOML', ('application/toml', 'text/x-toml'))
- YAML = (('*.yaml', '*.yml'), True, 'YAML', ('application/yaml', 'text/yaml', 'application/x-yaml', 'text/x-yaml'))
- JSON = (('*.json',), True, 'JSON', ('application/json', 'text/json'))
- JSON5 = (('*.json5', '*.jwcc'), True, 'JSON5', ('application/json5',))
- JSONC = (('*.jsonc',), True, 'JSONC', ('application/jsonc',))
- HJSON = (('*.hjson',), True, 'Hjson', ('application/hjson',))
- INI = (('*.ini',), True, 'INI', ())
- XML = (('*.xml',), True, 'XML', ('application/xml', 'text/xml'))
- PLIST = (('*.plist',), True, 'plist', ('application/x-plist',))
- SQLITE = (('*.sqlite', '*.sqlite3'), True, 'SQLite', ('application/vnd.sqlite3', 'application/x-sqlite3'))
- ARGFILE = (('*.conf',), True, 'Argfile', ())
- PYPROJECT_TOML = (('pyproject.toml',), True, 'pyproject.toml', ())
- property label: str
Human-friendly name of the format for display in messages.
- property enabled: bool
Returns
Trueif the format is supported,Falseotherwise.
- property mime_types: tuple[str, ...]
Media types a server may advertise the format as.
Feeds
format_from_mime(). Empty for a format noContent-Typeheader designates:INIandARGFILEare both served astext/plain, which names no format, andPYPROJECT_TOMLis keyed on a file name, so no media type tells it apart from plainTOML.
- click_extra.config.formats.SQLITE_CONFIG_TABLE = 'config'
Name of the table
click_extra.config.option.ConfigOption.load_sqlite_config()reads aSQLITEconfiguration from.The table holds
key/valuecolumns: dotted parameter paths and their JSON-encoded values.
- click_extra.config.formats.parse_content(fmt, content)[source]
Parse content with a single stateless format.
INI is excluded: it needs the CLI parameter structure for type coercion and is handled by ConfigOption.load_ini_config. ARGFILE is excluded for the same reason: it maps command-line tokens to the CLI’s parameters and is handled by ConfigOption.load_argfile_config. SQLITE is excluded too: it is a binary format, read from its file path by ConfigOption.load_sqlite_config instead of a text payload.
PLISTparses here from its XML variant, the only one expressible as a text payload; the binary variant is read from its file path by ConfigOption.load_plist_config.Note
Optional third-party parsers are imported lazily, at the point of use, rather than at module load. Only enabled formats reach this function (disabled ones are filtered out of
ConfigOption.file_format_patterns), so the import always resolves for the formats actually parsed here.- Return type:
- click_extra.config.formats.SERIALIZABLE_FORMATS: tuple[ConfigFormat, ...] = (ConfigFormat.TOML, ConfigFormat.YAML, ConfigFormat.JSON, ConfigFormat.JSON5, ConfigFormat.JSONC, ConfigFormat.HJSON, ConfigFormat.XML, ConfigFormat.PLIST)
Configuration formats
serialize_content()can write, in priority order.Every
ConfigFormatexceptINI,SQLITE,ARGFILEandPYPROJECT_TOML, which have no serializer.JSON,JSON5andJSONCare emitted as plain JSON andPLISTthroughplistlib, all from the standard library, so they need no optional dependency; the others require their format’s extra.Caution
Keep this in sync with the
matchstatement inserialize_content().
- click_extra.config.formats.serialize_content(fmt, data, **kwargs)[source]
Serialize a Python object to a string in the given format.
The dumping counterpart to
parse_content(). Per-format defaults can be overridden throughkwargs(forwarded to the underlying serializer). JSON5 and JSONC are emitted as plain JSON, a valid subset of both.Caution
Not every format round-trips:
TOML,XMLandPLISThave no null type (plistlibeven raises onNonevalues), andXMLexpects a single root mapping, so the caller is responsible for shapingdataaccordingly.INI,SQLITEandpyproject.tomlhave no serializer here.Note
Optional third-party serializers are imported lazily, at the point of use. Writing
TOMLusestomlkit(the[toml]extra), unlike reading which relies on the built-intomllib.- Raises:
ValueError – the format has no serializer.
- Return type:
- click_extra.config.formats.format_from_path(path, formats=None)[source]
Return the configuration format whose patterns match a file name.
The name is matched against each format’s
patterns, soapp.tomlresolves toTOMLandapp.ymltoYAML.formatsrestricts and orders the candidates (the first match wins); it defaults to everyConfigFormat.- Return type:
- click_extra.config.formats.format_from_mime(mime_type, formats=None)[source]
Return the configuration format a media type designates.
The counterpart of
format_from_path()for a configuration fetched over HTTP, whose URL often carries no usable file extension: theContent-Typeheader is then the only thing typing the payload. The media type is matched against each format’smime_types, soapplication/tomlresolves toTOMLandtext/yamltoYAML.formatsrestricts and orders the candidates (the first match wins); it defaults to everyConfigFormat.Parameters are stripped, so a raw
application/yaml; charset=utf-8header value can be passed as-is, and matching is case-insensitive.Note
A RFC 6839 structured syntax suffix is honored, so the
application/vnd.acme.settings+jsona private API answers with resolves toJSON. An exact match wins over a suffix.Returns
Nonefor a media type no format claims, which covers the generictext/plainandapplication/octet-streama server falls back to for an extension it does not recognize.- Return type:
- click_extra.config.formats.disabled_format_message(fmt)[source]
Build the “format support disabled, install the extra” message for a format.
The single source for the
ImportErrortext raised when a format whose optional parser is not installed is requested, shared byread_file()andclick_extra.test_suite.parse_test_suite(). A format’slabel, lower-cased, is itsclick-extra[<extra>]install target.- Return type:
- click_extra.config.formats.read_file(path, formats=None)[source]
Read a file and parse it, picking the format from its name.
The format is resolved with
format_from_path()overformats(everyConfigFormatby default), then the content is parsed withparse_content().- Raises:
ValueError – the file name matches none of the candidate
formats.ImportError – the matched format’s optional parser is not installed.
- Return type: