This release is a pre-release and may not be stable for production use.
hyera
A small, dependency-light Python implementation of Puppet
Hiera hierarchical data
lookup. It reads a Hiera base config, walks the hierarchy for a given context,
and fully resolves values — including %{...} interpolation and the
hiera/lookup/scope/literal/alias functions — with optional array,
hash, and deep-hash merging.
Install
pip install hyera # library only
pip install hyera[cli] # + the `hyera` command-line tool (via duho)
The PyPI distribution, the import package and the command are all named
hyera.
Library
from hyera import Hiera, Scope
h = Hiera("hiera.yaml", scope=Scope(facts={"os": {"family": "Debian"}}, environment="production"))
# First match wins:
h.get("ntp::servers")
# Merge across the whole hierarchy:
h.get("classes", merge=list) # array merge
h.get("users", merge=dict, merge_deep=True) # deep hash merge
# Missing keys return the default (with throw=True they raise KeyNotFoundError, a KeyError):
h.get("missing", default="fallback")
h.has("some::key")
# Bind a derived scope once and reuse:
prod = h.scoped(environment="production")
prod.get("ntp::servers")
Scope and facts
Scope is Puppet's top scope: it holds variables (node parameters),
facts, trusted data, server_facts, environment and strict, and is
what every Hiera lookup runs against. Precedence for a top-scope
variable name: an explicit variables entry wins over a fact of the same
name, which wins over a server_facts entry; $environment defaults to
"production"; $trusted defaults to Puppet's local hash (certname taken
from a clientcert variable/fact, else empty). Facts are also reachable as
a whole through $facts, and server_facts through $server_facts.
from hyera import Hiera, Scope, load_facts
scope = Scope(facts=load_facts("facts.yaml"), environment="production", strict="error")
h = Hiera("hiera.yaml", scope=scope)
h.get("ntp::servers")
load_facts(path) reads a puppet lookup --facts-style file (JSON for
.json, YAML for .yaml/.yml, otherwise JSON then YAML); the result
must be a mapping, and hostname/domain/fqdn/clientcert are
all-or-nothing. facts_from_facter() runs a bare facter -j instead:
from hyera import facts_from_facter
scope = Scope(facts=facts_from_facter())
h.scoped(**derive_args) returns a ScopedHiera bound to
h.scope.derive(**derive_args): variables/facts/server_facts
shallow-update the parent scope's own (new values win, nothing goes
stale); environment/strict/trusted/node_name replace the parent's
when given.
Base config
A standard Hiera 5 hiera.yaml works. Each level names a data_hash backend
and a source (path, paths, glob, globs, or mapped_paths):
---
version: 5
defaults:
data_hash: yaml_data
datadir: data
hierarchy:
- name: "Per-node"
path: "nodes/%{trusted.certname}.yaml"
- name: "Per-environment"
path: "environments/%{environment}.yaml"
- name: "Per-role"
mapped_paths: [roles, role, "roles/%{role}.yaml"]
- name: "Modules"
globs:
- "modules/*.yaml"
- name: "Common"
path: "common.yaml"
default_hierarchy: # consulted only when the hierarchy above misses
- name: "Module defaults"
path: "module_defaults.yaml"
Backends (by data_hash name — Puppet function names only; see "Differences
from Puppet" below for the one exception):
| Backend | data_hash name |
Notes |
|---|---|---|
YAMLBackend |
yaml_data |
parses YAML the way Puppet's Psych does (types, symbols, BOM), on libyaml when available |
JSONBackend |
json_data |
|
HOCONBackend |
hocon_data |
requires pip install hyera[hocon] |
SopsBackend |
sops_data (also sops, sops_<yaml|json|ini|dotenv>) |
decrypts via the sops CLI on the fly |
A third-party backend registers itself the same way, by subclassing
hyera.Backend and declaring NAMES; Backend.find/.get/.new/.names
look a backend up by name, and Hiera(backends=[...]) restricts a lookup to
an explicit allow-list of classes.
HOCONBackend resolves include directives exactly as Puppet's own
hocon_data does by default: a plain include "file" contributes
nothing; include file(…) really reads the file (relative to the process
working directory, or absolute); a directive in value position (including
inside a [...] array) is kept as literal text; url(…), classpath(…),
required(…), package(…) and a case-mismatched keyword all raise
BackendError, matching Puppet's own parse/method errors for those forms.
Pass hocon_includes=False to HOCONBackend (or set hocon_includes: false on the hierarchy entry/defaults — hyera's own extension, not
Puppet vocabulary) to restore the stricter, pre-fidelity behaviour instead:
every form but a plain quoted include raises, include file(…) included.
In either mode, pyhocon's own include-resolving methods stay wrapped as a
fail-closed backstop, so an undiscovered gap in the text scanner still
cannot read a file or reach the network for a form the active mode does
not intend to resolve.
Merging and lookup_options
Pass merge= to get() — a strategy name, a legacy type, or a hash of deep
options:
h.get("classes", merge="unique") # flatten + dedupe arrays
h.get("classes", merge=list) # legacy alias for unique
h.get("conf", merge="deep") # recursive hash merge
h.get("conf", merge={"strategy": "deep", # deep-merge options
"knockout_prefix": "--",
"sort_merged_arrays": True,
"merge_hash_arrays": True})
More idiomatically, declare the strategy (and optional convert_to) in the
data under the reserved lookup_options key — then callers need not pass
merge= at all:
# common.yaml
classes:
- base
lookup_options:
classes: { merge: unique }
"^app::.*": { merge: { strategy: deep } } # regex: must start with ^
port: { convert_to: Integer }
db::password: { convert_to: Sensitive }
A lookup_options key is treated as a regular expression only when it
starts with ^ (Hiera 5's rule); every other key is matched literally, so
a key containing . or other metacharacters cannot shadow unrelated keys.
An exact key match always wins over a pattern match.
An explicit merge= argument overrides lookup_options. convert_to takes
a Puppet type string (Integer, Optional[Integer]) or [Type, *args]
([Integer, 16], [String, '%x']) and converts with Puppet's new():
Integer, Float, Numeric, String, Boolean, Array, Hash, Tuple, Struct,
Optional, NotUndef and Sensitive (a redacting hyera.Sensitive wrapper). An
invalid type or a failed conversion raises hyera.HieraLookupError.
Command line
hyera KEY [options]
hyera ntp::servers --config hiera.yaml --scope environment=production
hyera classes --merge unique --output json
hyera missing::key --default '(none)'
Options: --config/-c, --scope key=value (repeatable), --merge first|unique|hash|deep (array/set alias unique), --deep,
--knockout-prefix, --output/-o raw|json|yaml, --default, plus duho's
-v/-q/--loglevel. Without --merge, the data's lookup_options decides;
an explicit --merge, first included, overrides it.
The CLI needs the cli extra (pip install "hyera[cli]"); without it the
command prints that hint and exits 2.
The CLI is built for unattended use: no interactive prompts, deterministic
output, and meaningful exit codes: 0 found (or --default printed), 1
key not found, 2 any other error — reported as one stderr line (-v or
DUHO_TRACEBACK=1 adds the traceback). puppet lookup exits 1 for both a
miss and an error, printing nothing for the error case; hyera's CLI tells
the two apart.
HYERA_MCP=stdio hyera runs the same command as an MCP server over
stdin/stdout, so an MCP client can drive lookups: it exposes one tool,
hyera, whose arguments are the command-line fields (key, config,
scope, merge, ...) and whose result is what the command would print.
sops and unattended runs
SopsBackend (data_hash: sops_data) shells out to sops to decrypt a
level on the fly. The format (YAML, JSON, INI or dotenv) is inferred from
the file's extension the same way the sops CLI itself picks it
(.yaml/.yml/.json/.env/.ini, case-sensitive); any other
extension is a clear error, since sops would read that file as binary.
It is hardened so an automated lookup never hangs, dies opaquely, or
leaks a decrypted secret:
- a finite subprocess timeout (
hyera.backends.SOPS_TIMEOUT, default 30 s), - captured stderr surfaced in a
BackendError, - a clear error when the
sopsbinary is not onPATH, - the data file is passed to
sopsas an absolute path after a literal--, so a level or scope value that starts with-can never be read as asopsoption, - the
sopsfound onPATHis the one executed, by its full resolved path; asops.bat/sops.cmdshim is refused (cmd.exere-parses a batch file's argument line, which a data-derived path could abuse), - a decrypted file that fails to parse reports only the problem and its
line/column — never the decrypted plaintext; a YAML value shaped to
quote itself into the error message (
!!float,!ruby/object:...) is redacted instead, - an INI file is always decrypted through sops's own JSON view, never ini text — sops's INI writer can otherwise emit a value that a text parser reads as a different key or an injected section.
Hiera 5 spec coverage
Supported: version: 5 validation · Puppet's version 5 schema validation
(closed key sets, a required unique name, at most one function/location
key, non-empty strings, the options name pattern, and the rest) ·
defaults · hierarchy · name ·
path/paths/glob/globs/mapped_paths · datadir (default data,
next to hiera.yaml) ·
default_hierarchy · data_hash backends (yaml/json/hocon, plus the
non-Puppet sops_data) · all five
interpolation methods (hiera/lookup/alias/scope/literal) with dotted
subkeys and alias native-type preservation · merges first/unique/hash/
deep with knockout_prefix/sort_merged_arrays/merge_hash_arrays ·
lookup_options (per-key/regex merge strategy + convert_to).
Not implemented: hiera.yaml version 3/4 (a file without version is version
3) · lookup_key/data_dig provider backends (such entries raise
ConfigError) · uri/uris
sources · eyaml_lookup_key (use the sops backend instead) ·
hiera3_backend legacy shim · encrypted-value convert_to beyond Sensitive.
Differences from Puppet
hyera aims to resolve exactly like puppet lookup. Every data_hash/
lookup_key/data_dig name it accepts is a real Puppet function name —
with one deliberate exception:
sops_data(alsosops, andsops_yaml/sops_json/sops_ini/sops_dotenvto force the format) — adata_hashbackend with no Puppet equivalent, for decrypting a sops-encrypted data file on the fly. A hierarchy that uses it does not load under real Puppet.convert_to(Puppet'snew()) does not support every type Puppet does. SemVer, SemVerRange, Timespan, Timestamp, Regexp, Binary, URI, Type and Object all raisehyera.HieraLookupError("hiera does not support new() for the Puppet type '...'") instead of converting — these are types whose values are not plain data. A type alias other thanData/RichDatais also unsupported (parse_typeresolves only the five Puppet static-loader aliases; any other capitalized name becomes an unresolved type reference).hocon_data'sinclude file("*.conf")globs. Puppet's ownhocon_datanever expands a glob in afile(...)argument (it contributes nothing); hyera's default lets pyhocon's own resolution run for real, which does glob and includes every match. Every otherincludeform matches Puppet exactly (see "Backends" above).Hiera(path)raisesConfigErrorwhen the file does not exist. Puppet then falls back to its built-in default configuration; ask for that explicitly withHiera(None, base_path=...)here.
Notes
Everything raised derives from HieraError (.path names the file
concerned, where there is one):
ConfigError—hiera.yamlis missing, unreadable, unparsable, or violates Puppet's version 5 schema..linenames the 1-based line in.paththe problem was found at, when known.BackendError— a data file could not be read or parsed;.pathnames it.HieraLookupError— a failure while resolving a key, with subclassesInterpolationError(a%{...}call or reference could not be resolved),MergeError(an unknown or invalid merge strategy), andKeyNotFoundError(also aKeyError) —.get(..., throw=True)'s miss.
License
MIT, for this project's own code. It is derived from
phiera, which is Apache-2.0; the files
taken from it keep that license. Several modules also port code translated
from Puppet (Apache-2.0) and from
Psych (MIT), Ruby's YAML library; those
files carry their own notice. See NOTICE and LICENSES/.
Metadata
Release files for hyera 0.0.0a0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| hyera-0.0.0a0.tar.gz | 311.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| hyera-0.0.0a0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 446.3 kB
Release files / hyera-0.0.0a0.tar.gz
| Download URL | hyera-0.0.0a0.tar.gz |
|---|---|
| Size | 311.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
3eb3c48f46a31ae6c9748d9a139421b6a403823d31d12f784dfac92fd62f4354
|
|
BLAKE2b-256 checksum How to use checksums |
292e18df874c22731b7127f8ca5dd85f0d6ee671a7c37f69de7001293df619df
|
| 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 Sep 29, 2026.
Transparency logRelease files / hyera-0.0.0a0-py3-none-any.whl
| Download URL | hyera-0.0.0a0-py3-none-any.whl |
|---|---|
| Size | 135.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
4accc656c2796fc09d224095c994369c2b9a0bc7b5b438c5e5a7c3af718c90dc
|
|
BLAKE2b-256 checksum How to use checksums |
f4b71e055b9038681a92c861e591050ab40bc1b1a749fc94dfb4e196d5428ef1
|
| 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 Sep 29, 2026.
Transparency log