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

TOML

*.toml

-

YAML

*.yaml, *.yml

-

JSON

*.json

-

JSON5

*.json5, *.jwcc

A superset of JSON made for configuration file

JSONC

*.jsonc

Like JSON, but with comments and trailing commas

HJSON

*.hjson

Another flavor of a user-friendly JSON

INI

*.ini

With extended interpolation, multi-level sections and non-native types (list, set, …)

XML

*.xml

-

plist

*.plist

Apple’s property list, in its XML or binary variant

SQLITE

*.sqlite, *.sqlite3

Reads a config table of dotted keys and JSON-encoded values

ARGFILE

*.conf

Plain-text list of command-line options, in the style of mpv and yt-dlp

PYPROJECT_TOML

pyproject.toml

Reads [tool.*] sections from pyproject.toml

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

ruff

.ruff.toml > ruff.toml > pyproject.toml

uv

uv.toml > pyproject.toml

typos

typos.toml / _typos.toml / .typos.toml > Cargo.toml > pyproject.toml

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.toml simply delete the dedicated file. Users who need the dedicated file (for example, sharing it across non-Python repos) keep it and pyproject.toml is 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_PARSERS with importlib.util.find_spec(). Read by ConfigFormat to mark the matching format as enabled or disabled. The probe does not import the module, so the actual parser is loaded lazily by parse_content() only when used.

class click_extra.config.formats.ConfigFormat(*values)[source]

Bases: Enum

All 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.glob for matching, and are influenced by the flags set on the ConfigOption instance.

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 True if the format is supported, False otherwise.

property patterns: tuple[str, ...]

Returns the default file patterns associated to the format.

property mime_types: tuple[str, ...]

Media types a server may advertise the format as.

Feeds format_from_mime(). Empty for a format no Content-Type header designates: INI and ARGFILE are both served as text/plain, which names no format, and PYPROJECT_TOML is keyed on a file name, so no media type tells it apart from plain TOML.

click_extra.config.formats.SQLITE_CONFIG_TABLE = 'config'

Name of the table click_extra.config.option.ConfigOption.load_sqlite_config() reads a SQLITE configuration from.

The table holds key/value columns: 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.

PLIST parses 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:

Any

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 ConfigFormat except INI, SQLITE, ARGFILE and PYPROJECT_TOML, which have no serializer. JSON, JSON5 and JSONC are emitted as plain JSON and PLIST through plistlib, 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 match statement in serialize_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 through kwargs (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, XML and PLIST have no null type (plistlib even raises on None values), and XML expects a single root mapping, so the caller is responsible for shaping data accordingly. INI, SQLITE and pyproject.toml have no serializer here.

Note

Optional third-party serializers are imported lazily, at the point of use. Writing TOML uses tomlkit (the [toml] extra), unlike reading which relies on the built-in tomllib.

Raises:

ValueError – the format has no serializer.

Return type:

str

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, so app.toml resolves to TOML and app.yml to YAML. formats restricts and orders the candidates (the first match wins); it defaults to every ConfigFormat.

Return type:

ConfigFormat | None

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: the Content-Type header is then the only thing typing the payload. The media type is matched against each format’s mime_types, so application/toml resolves to TOML and text/yaml to YAML. formats restricts and orders the candidates (the first match wins); it defaults to every ConfigFormat.

Parameters are stripped, so a raw application/yaml; charset=utf-8 header 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+json a private API answers with resolves to JSON. An exact match wins over a suffix.

Returns None for a media type no format claims, which covers the generic text/plain and application/octet-stream a server falls back to for an extension it does not recognize.

Return type:

ConfigFormat | None

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 ImportError text raised when a format whose optional parser is not installed is requested, shared by read_file() and click_extra.test_suite.parse_test_suite(). A format’s label, lower-cased, is its click-extra[<extra>] install target.

Return type:

str

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() over formats (every ConfigFormat by default), then the content is parsed with parse_content().

Raises:
  • ValueError – the file name matches none of the candidate formats.

  • ImportError – the matched format’s optional parser is not installed.

Return type:

Any