Skip to main content
CI PyPI Python versions

Dotted access over dicts, for objectifying JSON and YAML you do not own.

No schema to declare and no model to keep in sync with someone else’s API. Zero dependencies, one file. Conversion happens on write, so reads are plain attribute lookups.

>>> import json
>>> from dotted_dict import DottedDict

>>> document = '{"database": {"replicas": [{"host": "db-1.internal"}]}}'
>>> config = DottedDict(json.loads(document))
>>> config.database.replicas[0].host
'db-1.internal'

In practice that is DottedDict(json.load(handle)) or DottedDict(yaml.safe_load(handle)).

Declaring a model for a document you did not author means maintaining a mirror of a contract that changes without telling you, and breaking on fields you never asked about. This does not do that. It takes what arrived.

Install

pip install dotted_dict

Requires Python 3.9 or later. No runtime dependencies.

Usage

Keys are reachable as attributes, and attributes are keys. They are the same operation.

>>> from dotted_dict import DottedDict

>>> example = DottedDict()
>>> example["foo"] = 1
>>> example.foo
1

>>> example.bar = 2
>>> example
DottedDict({'foo': 1, 'bar': 2})

>>> del example["foo"]
>>> del example.bar
>>> example
DottedDict({})

Nested dicts, and dicts inside lists, are converted as they are stored – whether they arrive through the constructor, an assignment, an update, or setdefault.

>>> d = DottedDict()
>>> d.service = {"ports": [{"name": "http", "number": 80}]}
>>> d.service.ports[0].number
80

A value that is already the right type is stored as-is rather than copied, so shared references survive.

Convert back to plain dicts with to_dict(), which is cycle-safe at any depth.

>>> d.to_dict()
{'service': {'ports': [{'name': 'http', 'number': 80}]}}

It remains a real dict, so anything expecting one keeps working.

>>> isinstance(d, dict)
True
>>> json.dumps(d)
'{"service": {"ports": [{"name": "http", "number": 80}]}}'

Two classes, not a flag

DottedDict sanitises keys into valid Python identifiers so attribute access works. A valid key matches [a-zA-Z_][a-zA-Z0-9_]*$. Spaces and invalid characters become _, and a leading digit gets a _ prefix.

>>> DottedDict({"My fun key": 1, "John's": 1, "Mr. Man": 1})
DottedDict({'My_fun_key': 1, 'John_s': 1, 'Mr__Man': 1})

>>> DottedDict({1: 2})
DottedDict({'_1': 2})

Reserved words are refused rather than renamed, because any substitute would be unpredictable to you.

>>> DottedDict({"class": 1})
Traceback (most recent call last):
ValueError: Key "class" is a reserved keyword.

Sanitising can collide: "a b" and "a-b" both become "a_b", and the later write wins. When the exact keys matter more than attribute access, use the other class.

PreserveKeysDottedDict stores keys exactly as given. Keys that are not valid identifiers are still there, reachable via d["key"].

>>> from dotted_dict import PreserveKeysDottedDict

>>> d = PreserveKeysDottedDict({"content-type": "application/json"})
>>> d["content-type"]
'application/json'

Two named classes rather than one class with a boolean, so the choice is visible at the call site and cannot be passed wrongly.

Missing keys raise

A key that is not there raises, rather than creating an empty node. A typo should fail, not quietly succeed.

>>> d = DottedDict({"user_name": "josh"})
>>> d.usre_name
Traceback (most recent call last):
AttributeError: usre_name

Subclassing

BaseDottedDict provides the behaviour with no key policy. Subclasses customise three hooks and nothing else:

_transform_key(key)

Applied to every key on write. Default preserves it. Raising rejects the write.

_child_type()

The class nested dicts become. Defaults to the runtime type, so a subclass recurses into itself.

_convert(value)

How a value is converted on write.

Every mutating method routes through __setitem__, so a hook applies uniformly no matter how the data arrived.

Upgrading from 1.x

Fixed: pop, popitem, setdefault and clear previously mutated only one of the two internal stores, so a popped key could still be readable and len() could disagree with what was reachable.

Changed: assignment now converts. d.foo = {"bar": 1} yields a DottedDict, where 1.x stored a plain dict. If you relied on getting a plain dict back from an assignment, call to_dict() on it.

Added: BaseDottedDict is public, to_dict() is cycle-safe at any depth rather than only for direct self-reference, and | / |= / fromkeys return the correct class.

Dropped: Python 2 and Python 3.8 and earlier.

License

Apache-2.0.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

dotted_dict-2.0.0.tar.gz (17.2 kB view details)

Uploaded Source

Built Distribution

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

dotted_dict-2.0.0-py3-none-any.whl (12.3 kB view details)

Uploaded Python 3

File details

Details for the file dotted_dict-2.0.0.tar.gz.

File metadata

  • Download URL: dotted_dict-2.0.0.tar.gz
  • Upload date:
  • Size: 17.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for dotted_dict-2.0.0.tar.gz
Algorithm Hash digest
SHA256 ee5fb2745e2edc3303abcb149bd1d500d9a0eac350584105babf3f06a2459364
MD5 f57a4bf14c580a3d94de9f0183599cfa
BLAKE2b-256 a92c7619500bc99662f1a7d80070047088d852d6fd77414ed6eed17e4db4b02d

See more details on using hashes here.

Provenance

The following attestation bundles were made for dotted_dict-2.0.0.tar.gz:

Publisher: ci.yml on josh-paul/dotted_dict

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file dotted_dict-2.0.0-py3-none-any.whl.

File metadata

  • Download URL: dotted_dict-2.0.0-py3-none-any.whl
  • Upload date:
  • Size: 12.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for dotted_dict-2.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 a136c70e6718ee891a5df104ffca73703fa5e16adf0cdacd43e8a08283e92c4d
MD5 0c5bd2030623f3d7da9ed1cf2cef9457
BLAKE2b-256 3c3883f434598ed784dcaed7a1ba8d3fe3e9dd818f0a158054d1d2b67a38018f

See more details on using hashes here.

Provenance

The following attestation bundles were made for dotted_dict-2.0.0-py3-none-any.whl:

Publisher: ci.yml on josh-paul/dotted_dict

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

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