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:Paweł Zadrożny @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

Installation

pipenv install dotty-dict  # or pip install dotty-dict

TODO

  • key=value caching to speed up lookups and low down memory consumption

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

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 list(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']

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['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['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)[source]

Factory function for Dotty class.

Create Dotty wrapper around existing or new dictionary.

Parameters:dictionary (dict) – Any dictionary or dict-like object
Returns:Dotty instance
class dotty_dict.dotty_dict.Dotty(dictionary, separator, esc_char)[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.
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

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

Credits

Development

Contributors

None yet. Why not be the first?

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. Assuming you have PipEnv installed, this is how you set up your fork for local development:

$ cd dotty_dict/
$ make install-dev
  1. Create a branch for local development:

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

    Now you can make 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
    

    To get flake8 and tox, just pip install them into your virtualenv.

  3. Commit your changes and push your branch to GitHub:

    $ git add .
    $ git commit -m "Your detailed description of your changes."
    $ git push origin name-of-your-bugfix-or-feature
    
  4. 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, Python 3.6, and for PyPy3.5-5.10.

    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, Paweł Zadrożny

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.