lz: Too lazy to type
lazy
Import and go. No init, no loader calls, no boilerplate.
import lzconfig as lz
# Attribute-style access
print(lz.database.host) # → "localhost"
print(lz.database.credentials.user) # → "admin"
# Dict-style access
print(lz['database']['host']) # → "localhost"
# Mixed access
print(lz.database['port']) # → 5432
# Safe access with defaults
print(lz.get('debug', False)) # → TrueAt import time, lzconfig discovers your config file, parses it, interpolates
environment variables, and replaces itself in sys.modules with the loaded
configuration object. From then on, lz is your config.
Command-line --key value arguments are merged over the file, so the command
line always wins. And if command-line arguments are present, no config file is
needed at all.
-
If
LZCONFIG_FILEis set (and non-empty), that file is used. Relative paths are resolved against the current working directory. -
Otherwise, the current directory is scanned for the first match:
Priority Filename 1 lzconfig.yaml2 lzconfig.yml3 lzconfig.json4 lzconfig.toml5 lzconfig.ini6 lzconfig.cfg7 lzconfig.env8 lzconfig.xml -
If nothing is found and there are no
--key valuecommand-line arguments,ConfigNotFoundErroris raised at import time.
| Format | Extensions |
|---|---|
| YAML | .yaml .yml |
| JSON | .json |
| TOML | .toml |
| INI | .ini .cfg |
| .env | .env |
| XML | .xml |
All formats work out of the box — no extras needed.
$VAR and ${VAR} references in config values are expanded at load time
using os.path.expandvars:
# lzconfig.yaml
database:
host: $DB_HOST
password: ${DB_PASSWORD}Undefined variables are left as literal text — no error is raised.
--key value and --key=value arguments are merged over the file-based
configuration — the command line is the highest-priority source:
python app.py --database.host example.com --port 8080 --debug trueimport lzconfig as lz
lz.database.host # "example.com" — overrides the config file
lz.port # 8080 — an int, not a string
lz.debug # True — a boolRules:
- Dotted keys build nested structure:
--database.host xsetslz.database.host. - Values are parsed with JSON when possible (
--port 5432→ int,--debug true→ bool,--tags '["a","b"]'→ list); otherwise they stay strings (--zip 01234→"01234"). - A bare
--key(no value) is treated asTrue. - Only
--options are consumed; everything else is left insys.argvuntouched, and a bare--ends option parsing. - If command-line arguments are present, no config file is necessary, the config will be built from the command line alone.
- If neither a config file nor command-line arguments exist,
ConfigNotFoundErroris raised.
| Pattern | Example |
|---|---|
| Attribute chain | lz.a.b.c |
| Dict chain | lz['a']['b']['c'] |
| Mixed | lz.a['b'].c |
| List index | lz['items'][0].name |
| Membership | 'key' in lz |
| Safe get | lz.get('key', default) |
| Keys | lz.keys() |
| Length | len(lz) |
| Iteration | for k in lz: ... |
| Attribute write | lz.debug = True |
| Dict write | lz['database']['port'] = 8080 |
| Delete key | del lz.debug / del lz['debug'] |
The config object is mutable — attribute and dict-style writes update the
underlying data in place, and nested values share the same storage
(lz.database.host = '...' is visible through lz['database']['host']).
If a config key happens to have the same name as a ConfigObject method
(e.g., get, items, keys):
- Attribute access (
lz.items) returns the method. - Dict access (
lz['items']) always returns the config value.
This is a deliberate trade-off. When in doubt, use [] — it always works.
The same rule applies to writes: lz.items = [...] still stores the config
value, but reading lz.items returns the method.
| Exception | When |
|---|---|
ConfigNotFoundError |
No config file found and no --key value arguments present |
ConfigParseError |
File found but cannot be parsed |
ConfigKeyError |
Accessing a non-existent key |
All exceptions inherit from ConfigError and can be imported directly:
from lzconfig import ConfigError, ConfigNotFoundError, ConfigKeyErrorMIT