smartdict
smartdict is a small Python library for resolving references inside nested data structures.
It is especially useful for configuration dictionaries where one field needs to reuse another.
smartdict walks through built-in dict, list, and tuple containers, finds reference
expressions inside strings, and replaces them with resolved values.
Features
- Inline string interpolation with
${path.to.value} - Native-value resolution for single references such as
${path.to.value} - Optional explicit full-match syntax with
${path.to.value}$ - Nested reference strings such as
${${keys.${env}}} - Default values such as
${missing:42},${missing:fallback}, or${missing:[1, 2, 3]} - Optional pipelines such as
${env.PORT:8000|int}or${dataset:"unknown"|slug} - Dictionary key generation from references
- List and tuple index lookup through dotted paths
- Circular reference detection
- Strict mode, partial mode, and iterative parsing mode
Installation
pip install smartdict
Ecosystem
smartdict-python: the Python package in this repositorysmartdict-js: the JavaScript/browser port at Jyonn/smartdict-jssmartdict-handbook: the multi-page docs site and live playground at Jyonn/smartdict-handbook
Quick Start
import smartdict
data = {
"dataset": "spotify",
"load": {
"base_path": "~/data/${dataset}",
"train_path": "${load.base_path}/train",
"dev_path": "${load.base_path}/dev",
"test_path": "${load.base_path}/test",
},
"network": {
"num_hidden_layers": 3,
"num_attention_heads": 8,
},
"store": "checkpoints/${dataset}/${network.num_hidden_layers}L${network.num_attention_heads}H/",
}
parsed = smartdict.parse(data)
print(parsed["load"]["base_path"])
# ~/data/spotify
print(parsed["load"]["dev_path"])
# ~/data/spotify/dev
print(parsed["store"])
# checkpoints/spotify/3L8H/
Reference Syntax
1. References
Use ${...} as the default reference syntax.
import smartdict
parsed = smartdict.parse({
"name": "smartdict",
"message": "hello-${name}",
})
print(parsed["message"])
# hello-smartdict
When ${...} is part of a larger string, smartdict performs string interpolation.
When the whole value is a single reference, smartdict preserves the referenced value type:
import smartdict
parsed = smartdict.parse({
"config": {"debug": True},
"selected": "${config}",
})
print(parsed["selected"])
# {'debug': True}
This means ${config}, ${missing:null}, and ${missing:[1, 2, 3]} can all resolve to
native Python values instead of strings.
2. Explicit full-match references
Use ${...}$ when you want to make that intent explicit in config files or examples.
import smartdict
parsed = smartdict.parse({
"config": {
"debug": True,
"retries": 3,
},
"selected": "${config}$",
})
print(parsed["selected"])
# {'debug': True, 'retries': 3}
${...}$ is kept for readability and backward compatibility, but it is no longer the only way
to get non-string values.
3. Nested reference strings
Reference expressions can themselves contain reference expressions.
import smartdict
parsed = smartdict.parse({
"env": "prod",
"keys": {"prod": "url"},
"url": "https://example.com",
"result": "${${keys.${env}}}",
})
print(parsed["result"])
# https://example.com
4. Default values
If a path cannot be found, you can provide a default value with :.
import smartdict
parsed = smartdict.parse({
"int_value": "${missing:42}",
"bool_value": "${missing:true}",
"null_value": "${missing:null}",
"text_value": "${missing:fallback}",
})
print(parsed)
# {
# 'int_value': 42,
# 'bool_value': True,
# 'null_value': None,
# 'text_value': 'fallback'
# }
Default values are automatically interpreted as JSON when possible:
true/false->boolnull->None- integers ->
int - floats ->
float - arrays ->
list - objects ->
dict - anything else ->
str
Bare fallback strings such as ${missing:fallback} remain supported for convenience.
Nested default expressions are also supported:
import smartdict
parsed = smartdict.parse({
"repr_source_model": "text-embedding-3-small",
"embedding_model": "${sid_embedding_model:${repr_source_model:null}}",
})
print(parsed["embedding_model"])
# text-embedding-3-small
If both references are missing, the same expression resolves to None.
JSON arrays and objects are also valid defaults:
import smartdict
parsed = smartdict.parse({
"sinkhorn_epsilon": "${sid_sinkhorn_epsilon:[0.0, 0.0, 0.003]}",
"metadata": '${config:{"hello": "world"}}',
})
print(parsed["sinkhorn_epsilon"])
# [0.0, 0.0, 0.003]
print(parsed["metadata"])
# {'hello': 'world'}
If the primary value already exists, smartdict keeps that value and ignores the default:
import smartdict
parsed = smartdict.parse({
"sid_sinkhorn_epsilon": [1.0, 2.0, 3.0],
"sinkhorn_epsilon": "${sid_sinkhorn_epsilon:[0.0, 0.0, 0.003]}",
})
print(parsed["sinkhorn_epsilon"])
# [1.0, 2.0, 3.0]
5. Pipelines
smartdict also supports simple pipelines:
import smartdict
parsed = smartdict.parse({
"dataset": " My Dataset ",
"save_dir": "${dataset|strip|lower|slug}",
"port": "${env.PORT:8000|int}",
})
print(parsed["save_dir"])
# my-dataset
print(parsed["port"])
# 8000
Pipeline execution order is:
- Resolve the reference
- If missing, apply the default value
- Run pipeline stages from left to right
Current built-in stages:
intfloatbooljsonlowerupperstripslug
Examples:
import smartdict
parsed = smartdict.parse({
"raw": '{"hello": "world"}',
"value": "${raw|json}",
"embedding_model": "${sid_embedding_model:${repr_source_model:null}|lower}",
})
If a pipeline stage fails, smartdict raises PipelineStageError.
6. List and tuple indices
Dotted paths can also index built-in sequences.
import smartdict
parsed = smartdict.parse({
"items": ["a", "b"],
"pair": ("x", "y"),
"pick_list": "${items.1}",
"pick_tuple": "${pair.0}",
})
print(parsed["pick_list"])
# b
print(parsed["pick_tuple"])
# x
7. Dictionary keys can be generated
References are resolved in both keys and values.
import smartdict
parsed = smartdict.parse({
"name": "k",
"${name}": 1,
})
print(parsed)
# {'name': 'k', 'k': 1}
8. Referencing custom objects
smartdict resolves path components in this order:
obj[key]getattr(obj, key)obj[int(key)]
That means you can expose custom lookup behavior through objects used inside your data.
import random
import string
import smartdict
class Rand(dict):
chars = string.ascii_letters + string.digits
def __getitem__(self, item):
return "".join(random.choice(self.chars) for _ in range(int(item)))
parsed = smartdict.parse({
"utils": {
"rand": Rand(),
},
"filename": "${utils.rand.4}",
})
print(parsed["filename"])
# for example: aZ19
Parse Modes
smartdict.parse(obj)
Strict mode.
- Resolves all references
- Raises an error if any reference cannot be resolved
- Detects circular references
import smartdict
parsed = smartdict.parse({
"a": "x",
"b": "${a}/y",
})
print(parsed)
# {'a': 'x', 'b': 'x/y'}
smartdict.partial_parse(obj)
Best-effort mode.
- Resolves what it can
- Does not raise for missing references
- Leaves unresolved results in their current best-effort form
import smartdict
parsed = smartdict.partial_parse({
"a": "${missing}",
"b": "pre-${missing}-post",
"c": "${missing}$",
})
print(parsed)
# {'a': '${missing}', 'b': 'pre-${missing}-post', 'c': '${missing}$'}
smartdict.iterative_parse(obj, iterations=1)
Repeated best-effort parsing.
This is useful when one pass unlocks another pass.
import smartdict
parsed = smartdict.iterative_parse({
"a": "${b}",
"b": "${c}",
"c": "ok",
}, iterations=2)
print(parsed)
# {'a': 'ok', 'b': 'ok', 'c': 'ok'}
Errors
ReferenceNotFoundError
Raised by smartdict.parse() when a reference cannot be resolved.
import smartdict
from smartdict.smartdict import ReferenceNotFoundError
try:
smartdict.parse({
"a": "${missing}",
})
except ReferenceNotFoundError as exc:
print(type(exc).__name__, exc)
Nested missing references are also detected:
import smartdict
smartdict.parse({
"app": {
"profile": "prod",
},
"services": {
"prod": {
"url": "${config.endpoints.api}",
},
},
"result": "${services.${app.profile}.url}",
})
CircularReferenceError
Raised when references depend on each other in a cycle.
import smartdict
from smartdict.smartdict import CircularReferenceError
try:
smartdict.parse({
"a": "${b}$",
"b": "${a}$",
})
except CircularReferenceError as exc:
print(type(exc).__name__, exc)
Cycles can also appear across nested dictionaries:
import smartdict
smartdict.parse({
"app": {
"profile": "${services.primary.profile}$",
},
"services": {
"primary": {
"profile": "${app.profile}$",
},
},
})
KeyError
Raised when two dictionary keys resolve to the same final key.
import smartdict
smartdict.parse({
"aliases": {
"primary": "stable",
},
"${aliases.primary}": 1,
"stable": 2,
})
Public API
The main public entry points are:
import smartdict
smartdict.parse(obj)
smartdict.partial_parse(obj)
smartdict.iterative_parse(obj, iterations=2)
The package also exports:
SmartDictPathCircularReferenceErrorPipelineStageErrorPipelineStageReferenceNotFoundErrorUnresolvedReferenceRefStringStatusRefStringStatusWithValueComponentWithValue
Development
Run the test suite with:
python -m unittest discover -s tests -v
Build distributions with:
python -m build
Notes and Current Behavior
- smartdict recursively parses built-in
dict,list,tuple, andstrvalues. - Intermediate path components can be aliases, including full-match references such as
${config}$. - In strict mode, unresolved references raise
ReferenceNotFoundError. ReferenceNotFoundError.unresolvedcontains structured unresolved entries withpathandreference.iterationsmust be greater than0.- If resolved dictionary keys collide, smartdict raises
KeyError.
License
MIT
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file smartdict-0.5.1.tar.gz.
File metadata
- Download URL: smartdict-0.5.1.tar.gz
- Upload date:
- Size: 17.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.11.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
74c9622417999c29f87d5e169f7300de22926cfbc44ec90f75d6a1c315bc65d6
|
|
| MD5 |
0bf4acb6f7df62a4bfebeac765537bf7
|
|
| BLAKE2b-256 |
86d0e53aa47c8dbbcf257435399d393f349e2534207280424cb4c5c48bfcb679
|
File details
Details for the file smartdict-0.5.1-py3-none-any.whl.
File metadata
- Download URL: smartdict-0.5.1-py3-none-any.whl
- Upload date:
- Size: 12.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.11.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0c414b26390241b56f90da012f6ccf177a26de015908126e09bc555d75c15830
|
|
| MD5 |
fcaa0e86a221053bddd28d5c6d2d491c
|
|
| BLAKE2b-256 |
1f5148641328a4299231c5db89c72c824d912d329625ff39fe13cadb10a78c6b
|