Skip to main content

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}
  • Full-value replacement with ${path.to.value}$
  • Nested reference strings such as ${${keys.${env}}}
  • Default values such as ${missing:42} or ${missing:fallback}
  • 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

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. Inline references

Use ${...} when the reference is part of a larger string.

import smartdict

parsed = smartdict.parse({
    "name": "smartdict",
    "message": "hello-${name}",
})

print(parsed["message"])
# hello-smartdict

2. Full-match references

Use ${...}$ when the whole value should become the referenced object instead of a string.

import smartdict

parsed = smartdict.parse({
    "config": {
        "debug": True,
        "retries": 3,
    },
    "selected": "${config}$",
})

print(parsed["selected"])
# {'debug': True, 'retries': 3}

This is useful when the target is a dict, list, tuple, number, boolean, or any other non-string value.

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:

  • true / false -> bool
  • null -> None
  • integers -> int
  • floats -> float
  • anything else -> str

5. 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

6. 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}

7. Referencing custom objects

SmartDict resolves path components in this order:

  1. obj[key]
  2. getattr(obj, key)
  3. 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:

  • SmartDict
  • Path
  • CircularReferenceError
  • ReferenceNotFoundError
  • UnresolvedReference
  • RefStringStatus
  • RefStringStatusWithValue
  • ComponentWithValue

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, and str values.
  • Intermediate path components can be aliases, including full-match references such as ${config}$.
  • In strict mode, unresolved references raise ReferenceNotFoundError.
  • ReferenceNotFoundError.unresolved contains structured unresolved entries with path and reference.
  • iterations must be greater than 0.
  • 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

smartdict-0.4.0.tar.gz (13.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

smartdict-0.4.0-py3-none-any.whl (10.0 kB view details)

Uploaded Python 3

File details

Details for the file smartdict-0.4.0.tar.gz.

File metadata

  • Download URL: smartdict-0.4.0.tar.gz
  • Upload date:
  • Size: 13.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.9

File hashes

Hashes for smartdict-0.4.0.tar.gz
Algorithm Hash digest
SHA256 99b622a63e7eb0f0e7d2e87182b9e6211ef3deacc1bfa9b367bd9ca1ed810e30
MD5 9f1b96f6ad63a477bafd38dc9733a67d
BLAKE2b-256 d3f96eb92afd92733e3298b40c2824abed95aef670a68e2ff2b5d5497ed4dbfb

See more details on using hashes here.

File details

Details for the file smartdict-0.4.0-py3-none-any.whl.

File metadata

  • Download URL: smartdict-0.4.0-py3-none-any.whl
  • Upload date:
  • Size: 10.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.9

File hashes

Hashes for smartdict-0.4.0-py3-none-any.whl
Algorithm Hash digest
SHA256 13515eb8cbb853347a1b233859154facb64852af05ebb8c8da9e8a7a85f47f04
MD5 8f1cb6f98cb7a401136e8032dfd84856
BLAKE2b-256 63a0ea508877e4fa47cd3a0316d7fa4e67546ff6fdae4378859b157801ea2efd

See more details on using hashes here.

Release history Release notifications | RSS feed

0.5.1

2 files

0.5.0

2 files

This release

0.4.0 This release

2 files

0.3.2

1 file

0.3.1

1 file

0.3.0

1 file

0.2.1

1 file

0.2.0

1 file

0.1.2

1 file

0.1.1

1 file

0.1.0

1 file

0.0.7

1 file

0.0.6

1 file

0.0.5

1 file

0.0.4

1 file

0.0.3

1 file

0.0.2

1 file

0.0.1

1 file

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page