Why tranche?

tranche builds on Python’s configparser but adds a few capabilities that are hard to get elsewhere in one place: multi-file layering with precedence, provenance, safe expression evaluation (optional NumPy), interpolation, and comment-preserving writes.

Comparison

Legend: ✓ supported · ✗ not supported · — not applicable

Feature

configparser

ConfigUpdater

ConfigObj

tranche

Multi-file layering with explicit user precedence

Provenance (where did this value come from?)

Comment round-trip when writing

Extended interpolation

Expressions as values

Optional NumPy in expressions

Write out with provenance

Notes:

  • ConfigUpdater is excellent for editing INI files while preserving formatting, but it is not a layered reader/combiner.

  • ConfigObj offers comment round‑tripping and a different API, but not multi-layer precedence or provenance.

Typical layering flow

Example: merge defaults, site, and user config files with user taking precedence.

from tranche import Tranche

cfg = Tranche()
cfg.add_from_file('defaults.cfg')   # shipped with package
cfg.add_from_file('/etc/myapp.cfg') # site‑wide
cfg.add_user_config('~/.config/myapp.cfg')  # highest precedence

# Access values (with ExtendedInterpolation enabled by default)
val = cfg.get('general', 'output_dir')

You can also combine two Tranche objects explicitly:

higher = Tranche(); higher.add_from_file('override.cfg')
lower = Tranche(); lower.add_from_file('base.cfg')
lower.append(higher)  # values from 'higher' win

Interpolation and expressions

Extended interpolation works out of the box:

[paths]
root = /data/run
output = ${paths:root}/out

Expressions are read via getexpression:

[calc]
# List literal
levels = [0, 10, 20]
# With numpy (opt‑in, safe backend)
lev_np = np.linspace(0, 1, 5)
# Literal backend (safest):
levels = cfg.getexpression('calc', 'levels', backend='literal')

# Safe backend with a constrained whitelist; enable numpy explicitly:
levels_np = cfg.getexpression('calc', 'lev_np', backend='safe', allow_numpy=True)

You can expose additional safe helpers:

import statistics
cfg.register_symbol('mean', statistics.mean)
# Now INI can use: avg = mean([1,2,3])

Writing out with comments and provenance

Preserve original comments and include # source: lines showing where each value came from:

with open('combined.cfg', 'w') as f:
    cfg.write(f, include_sources=True, include_comments=True)

This yields, for example:

[general]
# original comment about threads
# source: /etc/myapp.cfg
threads = 8

You can also get the source programmatically:

info = cfg.explain('general', 'threads')
# {'value': '8', 'source': '/etc/myapp.cfg', 'layer': 'base'}

Security notes

  • Expression evaluation is opt-in and safe by default. The default backend='literal' uses ast.literal_eval.

  • The backend='safe' evaluator allows a small, whitelisted set of operations. NumPy is disabled unless you pass allow_numpy=True, and only a minimal subset is exposed (np.arange, np.linspace, np.array).

  • You can register additional helpers with register_symbol; names with dunders or dots are rejected.