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:
confeaseis 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.
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 anargparse.Namespacepassed toload_sources()orreload_cli().ENV: known environment variables loaded byreload_env().SYS: config files outside the current user home directory.USR: config files inside the current user home directory and values assigned withset().DEF: defaults supplied throughitemsor 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
- Documentation: https://octanima-labs.github.io/confease
- Repository: https://github.com/octanima-labs/confease
- Issues: https://github.com/octanima-labs/confease/issues
License
MIT
Metadata
Release files for confease 5.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| confease-5.0.0.tar.gz | 83.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| confease-5.0.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 126.6 kB
Release files / confease-5.0.0.tar.gz
| Download URL | confease-5.0.0.tar.gz |
|---|---|
| Size | 83.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
486c89617407e4098a1f62a487dbe2fde92a605d56b6b165d3799288f6e0401d
|
|
BLAKE2b-256 checksum How to use checksums |
23750bbf51d59fd06b2c4130fbe87716e11bc6889a1bb7d549e7f8e3333aed6e
|
| 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 10, 2026.
Transparency logRelease files / confease-5.0.0-py3-none-any.whl
| Download URL | confease-5.0.0-py3-none-any.whl |
|---|---|
| Size | 43.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
2c1f310b8b9cb79b592cfd23a46ebedd4830325a17d83eb33951598b4b217bc7
|
|
BLAKE2b-256 checksum How to use checksums |
10d8d1e4dd297253b47c9050a88dc44760925731886270d3d3c6c66c3db1bf00
|
| 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 10, 2026.
Transparency log