Skip to main content

confease

confease is a small Python configuration helper for applications that need predictable precedence across defaults, configuration files, environment variables, and CLI arguments.

Use it when you want one shared configuration object that can load user and system config files, accept argparse overrides, read typed environment values, and persist user preferences without building a full settings framework.

Status: confease is an alpha prototype. The public API is intentionally small, but behavior may still change before a stable release.

Installation

When published, install from PyPI:

pip install confease

For local development from this repository:

git clone https://github.com/octanima-labs/confease.git
cd confease
hatch run pytest

The package requires Python 3.11 or newer. The edital dependency provides the Textual editor. Interactive editing needs terminal input and output; no separately installed editor application is required. Scripted edits do not start the TUI.

Edital 0.2.0 is available on PyPI and is installed automatically as a dependency (edital>=0.2.0,<0.3). Hatch uses this published dependency for local development. For pip-based development, install Confease in editable mode:

python -m pip install -e /path/to/confease/public

Quickstart

Command line

The installed confease command creates, edits, and restores configuration files:

# Open a new, preloaded buffer in the embedded terminal editor
confease init settings.yaml -i debug=true -i database.port=5432

# Preload an exact template; leave the target absent if you abandon the edit
confease init settings.yaml --template defaults.yaml

# Edit or repair an existing file interactively
confease edit settings.yaml
confease settings.yaml                 # shorthand for edit

# Apply a validated batch without opening an editor
confease settings.yaml -u database.host=localhost -u retries=3
confease edit settings.yaml -d obsolete -d database.port

# Keep the original before an interactive or scripted edit
confease edit settings.yaml -b
confease settings.yaml --backup -u retries=5

# Restore the latest matching backup, or choose a source explicitly
confease restore settings.yaml
confease restore settings.yaml --from settings-20261007-143052.yaml.bkp

# Back up the current version too, so another restore can undo this restore
confease restore settings.yaml -b

-i/--item, -u/--update, and -d/--delete are repeatable. Updates add or replace values; the last assignment to the same leaf wins. A deletion names a leaf or an entire section. Missing keys produce warnings and the batch continues. Supplying any update or deletion selects noninteractive editing. A batch cannot update a deleted leaf or a child of a deleted section, but deleting a scalar and adding a child can convert it into a section.

init --template PATH is mutually exclusive with --item. It preloads exact template text, using the format selected from the destination or --format regardless of the template's suffix. F2/F3 (Accept & close) explicitly accepts the buffer, including an unchanged template. Cancellation leaves the target absent. Invalid templates fail before launch; invalid edits stay available for correction.

init refuses existing destinations, and edit requires an existing file. Format selection uses -f/--format first, then a recognized suffix. Extensionless init defaults to YAML; extensionless edit/restore and unknown suffixes require an override:

confease init settings -f toml -i debug=true
confease edit settings -f toml -u debug=false

Supported format names are yaml, yml, json, toml, ini, cfg, conf, config, xml, and csv. A format override keeps the path unchanged; INI-family suffixes use INI syntax. Use ./init, ./edit, or ./restore to disambiguate files named after commands, and -- before paths starting with a dash.

Values use YAML syntax regardless of the file format:

confease settings.yaml -u enabled=true -u 'servers=[alpha, beta]'
confease settings.yaml -u 'label="true"' -u 'token=a=b=c'

Shell quoting differs from value quoting: label="true" reaches the command as label=true and becomes a boolean, whereas 'label="true"' retains the inner quotes and becomes a string. YAML also interprets tokens such as yes as booleans. Assignments split at the first =. Invalid keys and values that the destination cannot represent (such as TOML null) are rejected before replacement.

Interactive init and edit use one embedded Textual session:

Key Action
F1 Show contextual keyboard help.
F2 / F3 Validate and accept the current text, then close.
Ctrl-Q Cancel; changed text requires discard confirmation.
Esc Dismiss help, a dialog, or search; otherwise leave the session open.
F4 Find text; Enter locates the next occurrence, wrapping to the start.
F5 Find/Replace, with replace-one and replace-all actions.
Ctrl-Z / Ctrl-Y Undo / redo.
Shift + arrows Select text.
F7 Select all.

Find and Find/Replace support literal text or Python regular expressions. Replacements are undoable and must pass configuration validation on acceptance. Editing uses an in-memory buffer without cached or resumable drafts: accept to install the changes, or cancel to discard them.

Validation errors appear in the same session, preserving text, cursor, and undo history. Only accepted valid text is installed, preserving comments, formatting, uniform LF/CRLF line endings, and final-newline presence. Mixed line endings and bare-CR separators are rejected rather than silently normalized. Existing invalid files can be opened for repair. Cancellation returns CLI status 130; other failures return nonzero. The original destination stays untouched until installation.

If installation or backup fails after acceptance, the CLI reports a recovery file containing the accepted text. Interactive invocation with redirected input or output reports a terminal error; use --update/--delete for scripted editing.

Scripted edits require a valid source document and preserve comments through round-trip document adapters. Unaffected order and formatting are retained where supported, but structured edits are not byte-for-byte formatting guarantees. Deleting a key removes its attached comments; document headers and footers remain. Validation checks file syntax, supported key structure, and representability; application-specific rules still belong to your application.

Python library

Create one shared configuration object near your application entry point:

from argparse import ArgumentParser

from confease import Confease

parser = ArgumentParser()
parser.add_argument("--debug", dest="DEBUG", action="store_true")
parser.add_argument("--database-host", dest="database", action="store_const", const={"host": "db.internal"})
args = parser.parse_args()

conf = Confease(
    "~/.config/my-app/conf.yaml",
    items={
        "APP_DIR": "~/Apps",
        "DEBUG": False,
        "database": {"host": "localhost", "port": 5432},
    },
)

conf.load_sources(args, "/etc/my-app/conf.yaml")

Pass in-code defaults through items, a dictionary supporting arbitrary-depth nested sections. Omitted items, items=None, and items={} provide no defaults; other input types raise TypeError. Configuration keys can use any constructor option name, including path, backup, or items, without changing those options. Values retain their Python types and participate as default-origin entries.

Empty dictionaries declare sections that can be populated later, including by a loaded user configuration:

conf = Confease(items={"plugins": {}})
conf["plugins.example"] = {"enabled": True}
assert conf["plugins"] == {"example": {"enabled": True}}

conf.set("plugins", {})  # Empty merge: existing descendants are retained.
conf.reset()            # Restore the original empty default in memory.
assert conf["plugins"] == {}

These rules apply at every nesting depth. Empty sections retain their presence and source origin while empty; after population, individual descendants carry their own origins and participate in normal leaf precedence and user-only persistence. An empty section does not block additions from a lower-priority source. Environment overlays still look up only known keys: a mapping value at a known empty section can populate it, but unknown dotted environment names are not discovered.

Mapping/non-mapping changes raise ValueError: neither a = 2 followed by a.b = "hello" nor replacing a = {} with a = 2 is allowed. Ordinary leaf type changes, such as 2 to "hello", remain valid. To deliberately redefine a section as a value (or the reverse), call conf.delete("a") before setting it again. Empty mapping assignments merge rather than clear sections; use deletion to remove their contents. A rejected mapping assignment or individual source overlay preserves the prior effective values and origins.

Constructor Migration

The constructor now separates defaults from controls. Replace old calls as follows:

# Before
conf = Confease("settings.yaml", __backup__=True, DEBUG=False)

# Now
conf = Confease("settings.yaml", backup=True, items={"DEBUG": False})

Arbitrary configuration keywords and the legacy __backup__ option are no longer accepted; they raise TypeError. The existing positional option order is retained, with backup in the former __backup__ slot and items appended after it.

Deferred Loading And Recovery

By default, construction immediately loads an existing destination and reports invalid content. Use the keyword-only autoload=False option to bind a path without parsing it, writing files, creating directories, or taking backups:

conf = Confease("settings.yaml", autoload=False, items={"retries": 3})

Parser, preference, defaults, and any configured template are still validated. The first ordinary read, assignment, deletion, save, or source overlay loads the existing destination; if it is missing, defaults initialize in memory. Source overlays include reload_cli(), reload_env(), reload_files(), and load_sources(). First-use loading preserves unrelated file values before a mutation or save. Invalid content blocks that operation without changing the file or initializing entries; retry after repairing the destination.

Recovery operations bypass ordinary first-use loading, making these public patterns suitable for malformed destinations:

# Repair exact destination text in the embedded editor
conf = Confease("settings.yaml", autoload=False, backup=True)
conf.edit_file()

# Replace the destination with exact template bytes
conf = Confease("settings.yaml", template="defaults.yaml", autoload=False)
conf.reset(backup=True)

# Recover the latest backup, or an explicit source
conf = Confease("settings.yaml", autoload=False)
conf.restore(backup=True)
# conf.restore("historical.snapshot", backup=True)

Cancelled transactional editing and failed recovery preserve deferred state; a later ordinary read still tries to load the destination. Successful recovery or explicit load(alternate_path) initializes entries, so later access with reload=False keeps that state. Reset without a template deliberately initializes only defaults in memory, even if the destination is malformed.

autoload=False also works with reload=True: first ordinary use initializes once, then established automatic-read and automatic-save behavior applies. First use of a missing destination can read defaults without creating it; later automatic reads require the configured file to exist.

A configuration key named autoload remains independent of the loading option:

conf = Confease(autoload=False, items={"autoload": "application-value"})
conf["autoload"]  # "application-value"

Migrate former keyword defaults named autoload to items={"autoload": value}.

Read values with get() or indexed access:

conf.get("APP_DIR")          # "~/Apps"
conf["APP_DIR"]             # "~/Apps"
conf.get("MISSING")          # None
conf["MISSING"]             # None
conf.get("DEBUG", cast=bool) # False

Update values with set() or indexed assignment:

conf.set("DEBUG", True)
conf["APP_DIR"] = "/srv/app"
conf.save()

By default, save() writes only user-origin values. Use save(user_only=False) when you want to persist the full effective configuration, including defaults and overrides.

save() preserves comments in existing YAML, TOML, INI-family, and XML files. It reads the destination's latest document to retain externally edited comments, then applies the selected in-memory values. External values are not implicitly merged. Invalid destinations or unrepresentable output cause an error without replacing the file. JSON has no comment syntax; CSV retains its key,value format. Concurrent-writer merging and locking are not provided.

Backups And Restore

Enable backups for an instance or for a single operation:

conf = Confease("settings.yaml", backup=True)
conf.set("retries", 5)
conf.save()                       # inherits the instance policy
conf.save(backup=False)           # skips the snapshot for this save only
conf.edit_file(backup=True)       # snapshots after acceptance, before installation
conf.reset(backup=True)           # snapshots when restoring a template to disk

conf.restore()                   # latest matching sibling; inherits backup=True
conf.restore("older.snapshot", backup=False)  # explicit source, any filename

The constructor's backup option controls the instance policy; a configuration default named backup belongs in items. Backups are disabled by default. On save(), edit_file(), reset(), and restore(), backup=None inherits the instance policy; True or False overrides that operation only. With reload=True, backup=True, every automatic save triggered by set(), indexed assignment, or a successful delete() snapshots the previous file. Memory-only operations and first writes to missing files have no previous document to back up.

The policy and a config key named backup can coexist:

conf = Confease("settings.yaml", backup=True, items={"backup": "daily"})
conf.get("backup")               # "daily" as a default unless the file overrides it
conf.save(backup=False)           # skips a snapshot; does not change the config key

Snapshots copy exact bytes, preserving original comments, formatting, and even malformed content being repaired. They live beside the destination:

settings-20261007-143052.yaml.bkp
settings-20261007-143052-001.yaml.bkp   # another snapshot in the same second
settings-20261007-143052-002.yaml.bkp

The timestamp uses local time in YYYYMMDD-HHMMSS form. Increasing collision suffixes are padded to at least three digits; existing backups are never overwritten. For settings, the name is settings-20261007-143052.bkp; for app.settings.toml, it is app.settings-20261007-143052.toml.bkp.

Draft-based writes validate first, then back up immediately before replacement. Cancelled or rejected embedded editing creates no snapshot, for both CLI and API sessions. Explicit external API editing backs up before launching its editor, so its snapshot remains even if no changes are saved. A backup failure aborts replacement or direct editor launch. Complete snapshots remain available if subsequent installation fails. Backups are not pruned automatically; manage their retention as needed.

Without a source path, restore() selects a backup matching the destination's exact name and extension, ordered by embedded timestamp and numeric suffix, not modification time. With an explicit source it accepts any filename or directory. It validates the exact candidate with the active parser, installs the bytes without reserialization, retains the source, and reloads restored values as user-origin entries with existing default fallbacks. Defaults and the configured template do not change. Missing or invalid sources raise an error; an invalid latest backup does not silently fall back to an older one.

Restore can create a missing destination or replace malformed content. The CLI supports recovery without loading a broken destination first and chooses the parser from the destination or --format, not from the backup's .bkp suffix:

confease restore settings --from history.json.bkp -f json -b

With backups enabled, restore selects its source before snapshotting the current destination. Consecutive latest-backup restores therefore toggle between versions, including within the same second. This assumes chronological backup timestamps; a clock rollback or future-dated imported backup can change which file is latest. Use an explicit source for deterministic recovery in that case. Backups do not add locking or concurrent-writer merging.

Nested Keys

Confease supports arbitrary nesting depth, independently of the file format. Internally, terminal values are stored as dotted paths with individual origins:

conf.set("database", {"host": "localhost", "port": 5432})

conf.get("database.host")    # "localhost"
conf["database.host"]        # "localhost"
conf.get("database")         # {"host": "localhost", "port": 5432}
conf["database"]["host"]     # "localhost"

conf.set("database.primary", {"connection": {"host": "db.internal"}})
conf["database.primary.connection.host"]  # "db.internal"
conf["database"]["primary"]["connection"]["host"]  # "db.internal"
conf["database.primary"]    # {"connection": {"host": "db.internal"}}

Section access at any depth returns a plain nested snapshot dictionary. Changes to its structure do not assign configuration values. Missing subkeys raise KeyError, so write nested values through dotted keys or set():

conf["database.port"] = 5433

Mapping assignments merge compatible leaves and retain unspecified siblings. Dots in keys always separate paths: {"a.b": {"c": 1}} means a → b → c. Empty path segments, duplicate logical paths, and scalar/section conflicts such as a.b = 1 alongside a.b.c = 2 are rejected. Literal-dot escaping is not supported.

Empty mappings are retained: conf.set("units", {}) makes conf["units"] return {}, distinct from an absent key, and gives that mapping a user origin. Lists remain whole values, including lists containing mappings; there are no indexed configuration paths such as units.0.name.

Delete a leaf or a section explicitly:

conf.delete("database.port")  # True if present, including a null-valued leaf
conf.delete("database")       # removes all leaves in the section
conf.delete("database.primary.connection")  # removes only this deep subtree
conf.delete("absent")         # False
conf.save()

With reload=True, deletion reads the current file and saves immediately. A failed automatic save restores the entries before deletion. Deleting an effective default does not create a tombstone: a later load() or reset() can provide that default again. Origin-filtered values omitted by a user-only save follow the same comment-removal policy as deleted values.

Source Precedence

By default, sources are resolved in this order:

CLI > ENV > SYS > USR > DEF

Origins mean:

  • CLI: values from an argparse.Namespace passed to load_sources() or reload_cli().
  • ENV: known environment variables loaded by reload_env().
  • SYS: config files outside the current user home directory.
  • USR: config files inside the current user home directory and values assigned with set().
  • DEF: defaults supplied through items or values read from a configured template.

Customize precedence with preference:

conf = Confease("conf.yaml", preference=["env", "cli", "user", "default"])
conf.load_sources(args, "conf.yaml", preference=["cli", "env", "user", "default"])

Omitted origins are appended after the origins you provide.

Environment Variables

reload_env() only imports environment variables whose keys are already known from defaults or loaded files. Values are parsed with yaml.safe_load, so common scalar text recovers Python types:

export DEBUG=true
export PORT=5432
conf = Confease(items={"DEBUG": False, "PORT": 8000})
conf.reload_env()

conf["DEBUG"] # True
conf["PORT"]  # 5432

File Formats

The parser is inferred from the path suffix when parser=None is used, or you can pass a parser class explicitly.

Supported suffixes:

  • .yaml, .yml
  • .json
  • .toml
  • .ini, .cfg, .conf, .config
  • .xml
  • .csv
from confease import Confease, Json

yaml_conf = Confease("conf.yaml", parser=None)
json_conf = Confease("conf.json", parser=Json)

Every built-in parser loads a canonical nested mapping. YAML, JSON, and TOML use native hierarchy; XML uses recursive sections. INI-family formats encode hierarchy in dotted section names, and CSV uses full dotted row keys with the existing key,value header. Loading reverses those encodings.

For example, this YAML configuration:

key:
  one: 1
  subkey:
    two: 2
    subsubkey:
      three: 3
      subsubsubkey: hello

can be represented natively in TOML:

[key]
one = 1

[key.subkey]
two = 2

[key.subkey.subsubkey]
three = 3
subsubsubkey = "hello"

INI uses the same paths as flat section names, reconstructed by Confease on load:

[key]
one = 1

[key.subkey]
two = 2

[key.subkey.subsubkey]
three = 3
subsubsubkey = hello

XML uses the existing vocabulary recursively:

<config>
  <section name="key">
    <entry key="one">1</entry>
    <section name="subkey">
      <entry key="two">2</entry>
      <section name="subsubkey">
        <entry key="three">3</entry>
        <entry key="subsubsubkey">hello</entry>
      </section>
    </section>
  </section>
</config>

CSV stores terminal paths:

key,value
key.one,1
key.subkey.two,2
key.subkey.subsubkey.three,3
key.subkey.subsubkey.subsubsubkey,hello

INI-family, XML entry text, and CSV values use YAML typing to preserve numbers, booleans, nulls, and supported lists. New empty mappings use native empty nodes in YAML/JSON/TOML/XML and explicit {} values in INI options or CSV rows; existing empty INI section headers also load as empty mappings.

Switching formats preserves structure and types within the target format's supported value domain. TOML has no null value, and JSON does not support native Python dates, for example. Unsupported values fail saving before replacement; values are not silently stringified and no TOML null markers are introduced.

CSV Loader Migration

Direct Csv.load() now returns nested dictionaries rather than flat dotted keys:

from confease import Csv, Ini

data = Csv.load("conf.csv")
data["key"]["subkey"]["two"]  # 2; formerly data["key.subkey.two"]
Ini.save("conf.ini", data)    # the same shared mapping works across parsers

Confease["key.subkey.two"] continues to support dotted lookup. Existing shallow files remain readable, but older Confease versions cannot read the newly supported deep representations. See file-format guidance for details.

Edit Config Files

edit_file() preloads exact file text in the embedded editor without serializing in-memory values. It returns the same configuration instance after acceptance or cancellation:

conf = Confease("settings.yaml", items={"retries": 3})
conf.edit_file()

F2 or F3 accepts validated text and installs it before updating live entries. Ctrl+Q cancels, preserving the file and live entries, including unsaved in-memory values. A missing file stays absent on cancellation. Parent directories are created when needed for installation. Installation failure retains accepted text in a recovery file and adds its path to the exception notes.

Preload a missing file

Pass an edit-only template to show its contents automatically when the target does not exist:

conf = Confease("settings.yaml")
conf.edit_file(template="defaults.yaml")

The template is validated with the active parser before launch. Existing targets open normally and ignore the template argument. This argument does not change the instance's configured defaults or constructor template; to use the same file for both, pass it explicitly to both the constructor and edit_file(). F2 or F3 creates the target even if the preloaded text is unchanged. Installation refuses to overwrite a target created during editing. First creation has no previous file to back up.

Edital integration

The reusable editor now lives in the independent edital package. Import edit_text, TuiEditor, EditResult, and Validator directly from edital; Confease no longer exports editor APIs or contains a confease.tui_editor module. See Edital's documentation for standalone text editing. Confease supplies its parser/key validator and owns installation, backups, recovery files, and live configuration synchronization. UI imports remain lazy.

Explicit external editors

The public external launcher remains available for library consumers:

from confease import TextEditor

conf.editor = TextEditor("code")
conf.edit_file()

This explicitly selected path edits the real file and validates after editor exit. Invalid saves remain on disk while previous live entries are retained; enabled backups happen before launch. External command discovery uses EDITOR, then VISUAL, then available fallbacks, adding wait flags for known graphical editors. It does not configure the default TUI or the CLI.

For missing-target template preloading, Vim/Neovim use native unsaved buffers; other external commands use seeded drafts with best-effort filesystem save detection. A detected save is validated before create-only installation. Saving unchanged text is reliable in the TUI but best-effort for external draft editors. Written fallback drafts are retained on failure with recovery paths in exception notes. The external launcher still requires its executable to be installed.

Upgrading: Embedded transactional editing is now the default; explicitly assign TextEditor(...) to keep direct-file behavior. Default backups now happen after acceptance, not before launch. Replace edit_file(user_only=...) with edit_file(); save(user_only=...) is unchanged.

Templates And Reset

Use a template instead of items defaults when defaults belong in a file:

conf = Confease("conf.yaml", template="defaults.yaml")  # defaults.yaml must exist
conf.get("DEBUG")  # falls back to the template if absent from conf.yaml
conf.reset()       # explicitly overwrites conf.yaml with exact template content

Template values are DEF defaults in memory; initialization and reads do not copy them to the target. Explicit template-backed reset() validates the template, restores its exact bytes (including comments), and reloads the target as USR values. Missing or invalid templates leave the existing file and entries unchanged. Without a template, reset() resets items defaults only in memory. A template cannot be combined with nonempty items; omitted, None, or empty items are allowed.

See configuration guidance for details.

Documentation

License

MIT

Metadata

Release files for confease 6.0.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for confease 6.0.0
File Size Uploaded
confease-6.0.0.tar.gz 86.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for confease 6.0.0
File Interpreter ABI Platform
confease-6.0.0-py3-none-any.whl Python 3 none any Details

Total release size: 130.4 kB

Release files / confease-6.0.0.tar.gz

Download URL confease-6.0.0.tar.gz
Size 86.3 kB
Tags Source
SHA-256 checksum
How to use checksums
69114f7378bc4d0a583b8ded066457fc49928a7dacd6f65edd38fc3eb82de61c
BLAKE2b-256 checksum
How to use checksums
866ba761e4ed4ef75af9595ed2ce4ad3764e78a89a4c9e26d485c261241997f1
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 11, 2026.

Transparency log

Release files / confease-6.0.0-py3-none-any.whl

Download URL confease-6.0.0-py3-none-any.whl
Size 44.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
a77fcd676ce6c05640c1b1b918a5394b8b98f6127c82aa88cc1494257b903a01
BLAKE2b-256 checksum
How to use checksums
37b5bbb3c6f97178e1b3567cec624bed16b6b5820f766def7c0ae1c02b61b57b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 11, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

6.0.0 This release

2 release files

5.0.0

2 release files

4.0.0

2 release files

3.0.0

2 release files

2.0.0

2 release files

1.0.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page