Dotty Dict

Dotty Dict is a wrapper around builtin dictionary. Provides quick access to deeply nested keys and values with dot notation. Dotty Dict expose dictionary public API as proxy to dict implemented underneath and should work with all dict-like objects which are instances of Mapping.

Dotty-Dict

Info:Dictionary wrapper for quick access to deeply nested keys.
Author:Pawel Zadrozny @pawelzny <pawel.zny@gmail.com>
CI Status Documentation Status PyPI Repository Status Release Status Project Status Supported python versions Supported interpreters License

Features

  • Simple wrapper around python dictionary and dict like objects
  • Two wrappers with the same dict are considered equal
  • Access to deeply nested keys with dot notation: dot['deeply.nested.key']
  • Create, read, update and delete nested keys of any length
  • Expose all dictionary methods like .get, .pop, .keys and other
  • Access dicts in lists by index dot['parents.0.first_name']
  • key=value caching to speed up lookups and low down memory consumption
  • support for setting value in multidimensional lists
  • support for accessing lists with slices

Installation

pip install dotty-dict

TODO

Waiting for your feature requests ;)

Quick Example

Create new dotty using factory function.

>>> from dotty_dict import dotty
>>> dot = dotty({'plain': {'old': {'python': 'dictionary'}}})
>>> dot['plain.old']
{'python': 'dictionary'}

You can start with empty dotty

>>> from dotty_dict import dotty
>>> dot = dotty()
>>> dot['very.deeply.nested.thing'] = 'spam'
>>> dot
Dotty(dictionary={'very': {'deeply': {'nested': {'thing': 'spam'}}}}, separator='.', esc_char='\\')

>>> dot['very.deeply.spam'] = 'indeed'
>>> dot
Dotty(dictionary={'very': {'deeply': {'nested': {'thing': 'spam'}, 'spam': 'indeed'}}}, separator='.', esc_char='\\')

>>> del dot['very.deeply.nested']
>>> dot
Dotty(dictionary={'very': {'deeply': {'spam': 'indeed'}}}, separator='.', esc_char='\\')

>>> dot.get('very.not_existing.key')
None

NOTE: Using integer in dictionary keys will be treated as embedded list index.

Install for development

Install dev dependencies

$ make install

Testing

$ make test

Or full tests with TOX:

$ make test-all

Limitations

In some very rare cases dotty may not work properly.

  • When nested dictionary has two keys of different type, but with the same value. In that case dotty will return dict or list under random key with passed value.
  • Keys in dictionary may not contain dots. If you need to use dots, please specify dotty with custom separator.
  • Nested keys may not be bool type. Bool type keys are only supported when calling keys with type defined value (e.g. dot[True], dot[False]).

Examples

Yes, I know it’s dangerous to follow code examples. Usually examples aren’t in sync with real source code.

But I found a solution … I hope!

Note

All examples are derived from real code hooked to Pytest.
Every change in source code enforce change in examples.
Outdated examples == failed build.

See also

Look at Public API for more details.

Basics

The easiest way to use Dotty dict is with function factory. Factory takes only one, optional dictionary as argument.

If leaved empty, factory function will create new, empty dictionary.

Wrap existing dict

from dotty_dict import dotty

data = {'status': 'ok', 'code': 200, 'data': {'timestamp': 1525018224,
                                              'payload': []}}
data = dotty(data)
assert data['data.timestamp'] == 1525018224

Create new dotty

from dotty_dict import dotty

data = dotty()
data['status'] = 'ok'
data['data.timestamp'] = 1525018224
data['data.fancy.deeply.nested.key.for'] = 'fun'

assert data == {'status': 'ok',
                'data': {
                    'timestamp': 1525018224,
                    'fancy': {
                        'deeply': {
                            'nested': {
                                'key': {
                                    'for': 'fun',
                                },
                            },
                        },
                    },
                }}

Builtin methods

Dotty exposes all native to dict, builtin methods. Only change is made to method which uses key as input to accept dot notation.

from dotty_dict import dotty

dot = dotty({'status': 'ok',
             'data': {
                 'timestamp': 1525018224,
                 'fancy': {
                     'deeply': {
                         'nested': {
                             'key': {
                                 'for': 'fun',
                             },
                         },
                     },
                 },
             }})

# get value, return None if not exist
assert dot.get('data.payload') is None

# pop key
assert dot.pop('data.fancy.deeply.nested.key') == {'for': 'fun'}

# get value and set new value if not exist
assert dot.setdefault('data.payload', []) == []
assert 'payload' in dot['data']

# check what changed
assert dot == {'status': 'ok',
               'data': {
                   'timestamp': 1525018224,
                   'fancy': {
                       'deeply': {
                           'nested': {},
                       },
                   },
                   'payload': [],
               }}

# get keys
assert sorted(dot.keys()) == ['data', 'status']

Advanced

Lets simulate more real scenario. API requests and responses are often very complex with many deeply nested keys. And when you need to check one of them it may looks like: res.get('data', {}).get('service', {}).get('status', {}).get('current', False).

It’s awful! All this empty dictionary fallback to dig in for current status!

Make API request

In this scenario we will send post request to create new user with superuser privileges. Below there is example response as dictionary, and then the way to check granted privileges.

def make_request(payload):
    """Fake request for example purpose.

    :param dict payload: Example payload
    :return dict: Example response
    """
    return {
        'status': {
            'code': 200,
            'msg': 'User created',
        },
        'data': {
            'user': {
                'id': 123,
                'personal': {
                    'name': 'Arnold',
                    'email': 'arnold@dotty.dict',
                },
                'privileges': {
                    'granted': ['login', 'guest', 'superuser'],
                    'denied': ['admin'],
                    'history': {
                        'actions': [
                            ['superuser granted', '2018-04-29T17:08:48'],
                            ['login granted', '2018-04-29T17:08:48'],
                            ['guest granted', '2018-04-29T17:08:48'],
                            ['created', '2018-04-29T17:08:48'],
                            ['signup_submit', '2018-04-29T17:08:47'],
                        ],
                    },
                },
            },
        },
    }

from dotty_dict import dotty

request = dotty()
request['request.data.payload'] = {'name': 'Arnold',
                                   'email': 'arnold@dotty.dict',
                                   'type': 'superuser'}
request['request.data.headers'] = {'content_type': 'application/json'}
request['request.url'] = 'http://127.0.0.1/api/user/create'

response = dotty(make_request(request.to_dict()))

assert response['status.code'] == 200
assert 'superuser' in response['data.user.privileges.granted']

Access dict with embedded lists

This scenario shows how to access subfield in a list.

from dotty_dict import dotty

# dotty supports embedded lists
# WARNING!
# Dotty used to support lists only with dotty_l.
# This feature is depreciated and was removed - now lists have native support.
# If you need old functionality pass additional flag 'no_list' to dotty

dot = dotty({
    'annotations': [
        {'label': 'app', 'value': 'webapi'},
        {'label': 'role', 'value': 'admin'},
    ],
    'spec': {
        'containers': [
            ['gpu', 'tensorflow', 'ML'],
            ['cpu', 'webserver', 'sql'],
        ]
    }
})

assert dot['annotations.0.label'] == 'app'
assert dot['annotations.0.value'] == 'webapi'
assert dot['annotations.1.label'] == 'role'
assert dot['annotations.1.value'] == 'admin'
assert dot['spec.containers.0.0'] == 'gpu'
assert dot['spec.containers.0.1'] == 'tensorflow'
assert dot['spec.containers.0.2'] == 'ML'
assert dot['spec.containers.1.0'] == 'cpu'
assert dot['spec.containers.1.1'] == 'webserver'
assert dot['spec.containers.1.2'] == 'sql'

Access multiple fields with list slices

This scenario shows how to access multiple subfields in a list of dicts.

from dotty_dict import dotty

# dotty supports standard Python slices for lists

dot = dotty({
    'annotations': [
        {'label': 'app', 'value': 'webapi'},
        {'label': 'role', 'value': 'admin'},
        {'label': 'service', 'value': 'mail'},
        {'label': 'database', 'value': 'postgres'}
    ],
})

assert dot['annotations.:.label'] == ['app', 'role', 'service', 'database']
assert dot['annotations.:2.label'] == ['app', 'role']
assert dot['annotations.2:.label'] == ['service', 'database']
assert dot['annotations.::2.label'] == ['app', 'service']

Access numeric fields as dict keys

This scenario shows how to access numeric keys which should not be treated as list indices.

from dotty_dict import dotty

# For special use cases dotty supports dictionary key only access
# With additional flag no_list passed to dotty
# all digits and slices will be treated as string keys

dot = dotty({
    'special': {
        '1': 'one',
        ':': 'colon',
        '2:': 'two colons'
    }
})

assert dot['special.1'] == 'one'
assert dot['special.:'] == 'colon'
assert dot['special.2:'] == 'two colons'

Escape character

In some cases we want to preserve dot in key name and do not treat it as keys separator. It can by done with escape character.

from dotty_dict import dotty

dot = dotty({
    'deep': {
        'key': 'value',
    },
    'key.with.dot': {
        'deeper': 'other value',
    },
})

# how to access deeper value?
assert dot[r'key\.with\.dot.deeper'] == 'other value'

Escape the escape character

What if escape character should be preserved as integral key name, but it happens to be placed right before separator character?

The answer is: Escape the escape character.

Warning

Be careful because backslashes in Python require special treatment.

from dotty_dict import dotty

dot = dotty({
    'deep': {
        'key': 'value',
    },
    'key.with_backslash\\': {  # backslash at the end of key
        'deeper': 'other value',
    },
})

# escape first dot and escape the escape character before second dot
assert dot[r'key\.with_backslash\\.deeper'] == 'other value'

Customization

By default Dotty uses dot as keys separator and backslash as escape character. In special occasions you may want to use different set of chars.

Customization require using Dotty class directly instead of factory function.

Custom separator

In fact any valid string can be used as separator.

from dotty_dict import Dotty

dot = Dotty({'deep': {'deeper': {'harder': 'faster'}}}, separator='$', esc_char='\\')

assert dot['deep$deeper$harder'] == 'faster'

Custom escape char

As separator, escape character can be any valid string not only single character.

from dotty_dict import Dotty

dot = Dotty({'deep.deeper': {'harder': 'faster'}}, separator='.', esc_char='#')

assert dot['deep#.deeper.harder'] == 'faster'

Public API

See also

Check out Examples derived from real and fully tested source code.

dotty_dict.dotty_dict.dotty(dictionary=None, no_list=False)[source]

Factory function for Dotty class.

Create Dotty wrapper around existing or new dictionary.

Parameters:
  • dictionary (dict) – Any dictionary or dict-like object
  • no_list (bool) – If set to True then numeric keys will NOT be converted to list indices
Returns:

Dotty instance

class dotty_dict.dotty_dict.Dotty(dictionary, separator='.', esc_char='\', no_list=False)[source]

Dictionary and dict-like objects wrapper.

Dotty wraps dictionary and provides proxy for quick accessing to deeply nested keys and values using dot notation.

Dot notation can be customize in special cases. Let’s say dot character has special meaning, and you want to use other character for accessing deep keys.

Dotty does not copy original dictionary but it operates on it. All changes made in original dictionary are reflected in dotty wrapped dict and vice versa.

Parameters:
  • dictionary (dict) – Any dictionary or dict-like object
  • separator (str) – Character used to chain deep access.
  • esc_char (str) – Escape character for separator.
  • no_list (bool) – If set to True then numeric keys will NOT be converted to list indices
clear()

Removes all elements from dotty dict.

items()

Returns generator of dotty dict’s (key, value) tuple pairs.

keys()

Returns generator of dotty dict’s keys.

values()

Returns generator of dotty dict’s values.

update(dict2)

Adds dictionary dict2’s key-values pairs to dotty dict.

copy()[source]

Returns a shallow copy of dictionary wrapped in Dotty.

Returns:Dotty instance
static fromkeys(seq, value=None)[source]

Create a new dictionary with keys from seq and values set to value.

New created dictionary is wrapped in Dotty.

Parameters:
  • seq – Sequence of elements which is to be used as keys for the new dictionary
  • value – Value which is set to each element of the dictionary
Returns:

Dotty instance

get(key, default=None)[source]

Get value from deep key or default if key does not exist.

This method match 1:1 with dict .get method except that it accepts deeply nested key with dot notation.

Parameters:
  • key (str) – Single key or chain of keys
  • default (Any) – Default value if deep key does not exist
Returns:

Any or default value

pop(key, default=None)[source]

Pop key from Dotty.

This method match 1:1 with dict .pop method except that it accepts deeply nested key with dot notation.

Parameters:
  • key (str) – Single key or chain of keys
  • default (Any) – If default is provided will be returned
Raises:

KeyError – If key does not exist and default has not been provided

Returns:

Any or default value

static set_list_index(data, index, value)[source]

Set value in list at specified index. All the values before target index should stay unchanged or be filled with None. :param data: List where value should be set :param index: String or Int of target index :param value: Target value to put under index

setdefault(key, default=None)[source]

Get key value if exist otherwise set default value under given key and return its value.

This method match 1:1 with dict .setdefault method except that it accepts deeply nested key with dot notation.

Parameters:
  • key (str) – Single key or chain of keys
  • default (Any) – Default value for not existing key
Returns:

Value under given key or default

to_dict()[source]

Return wrapped dictionary.

This method does not copy wrapped dictionary.

Return dict:Wrapped dictionary
to_json()[source]

Return wrapped dictionary as json string.

This method does not copy wrapped dictionary.

Return str:Wrapped dictionary as json string

Credits

Development

Contributors

  • Linus Groh @linusg
  • Andreas Motl @amotl
  • Aneesh Devasthale @aneeshd16
  • Szymon Piotr Krasuski @Dysproz

Read more how to contribute on Contributing.

Contributing

Contributions are welcome, and they are greatly appreciated! Every little bit helps, and credit will always be given.

You can contribute in many ways:

Types of Contributions

Report Bugs

Report bugs at https://github.com/pawelzny/dotty_dict/issues

If you are reporting a bug, please include:

  • Your operating system name and version.
  • Any details about your local setup that might be helpful in troubleshooting.
  • Detailed steps to reproduce the bug.

Fix Bugs

Look through the GitHub issues for bugs. Anything tagged with “bug” is open to whoever wants to implement it.

Implement Features

Look through the GitHub issues for features. Anything tagged with “feature” is open to whoever wants to implement it.

Write Documentation

authentication could always use more documentation, whether as part of the official authentication docs, in docstrings, or even on the web in blog posts, articles, and such.

Submit Feedback

The best way to send feedback is to file an issue at https://github.com/pawelzny/dotty_dict/issues

If you are proposing a feature:

  • Explain in detail how it would work.
  • Keep the scope as narrow as possible, to make it easier to implement.
  • Remember that this is a volunteer-driven project, and that contributions are welcome :)

Get Started!

Ready to contribute? Here’s how to set up dotty_dict for local development.

  1. Fork the dotty_dict repo on GitHub.

  2. Clone your fork locally:

    $ git clone git@github.com:your_name_here/dotty_dict.git
    
  3. Install your local copy into a virtualenv. This is how you set up your fork for local development:

    $ cd dotty_dict/
    $ make install
    

or if you don’t have ‘make’, do it manually:

$ cd dotty_dict/
$ pip install poetry==1.1.14
$ poetry install --no-root
  1. Create a branch for local development:

    $ git checkout -b name-of-your-bugfix-or-feature
    

    Now you can introduce your changes locally.

  2. When you’re done making changes, check that your changes pass flake8 and the tests, including testing other Python versions with tox:

    $ make test-all
    

or if you don’t have ‘make’, run tox directly:

$ poetry run tox --skip-missing-interpreters
  1. Commit your changes and push your branch to GitHub:

    $ git add .
    $ git commit -m "Your detailed description of your changes."
    $ git push origin HEAD
    
  2. Submit a pull request through the GitHub website.

Pull Request Guidelines

Before you submit a pull request, check that it meets these guidelines:

  1. The pull request should include tests.

  2. If the pull request adds functionality, the docs should be updated. Put your new functionality into a function with a docstring, and add the feature to the list in README.rst.

  3. The pull request should work for: Python >=3.5,<4.0 and for >=PyPy3.8-7.3.9.

    Check https://circleci.com/gh/pawelzny/dotty_dict and make sure that the tests pass for all supported Python versions.

LICENSE

MIT License

Copyright (c) 2017, Pawel Zadrozny

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.