Search code examples
pythondictionarymappingtry-except

How to Pythonically map content from one dict to another in a fail safe manner?


I've got one dict from an api:

initial_dict = {
    "content": {
        "text":
    },
    "meta": {
        "title": "something",
        "created": "2016-03-04 15:30",
        "author": "Pete",
        "extra": {
            "a": 123,
            "b": 456
        }
    }
}

and I need to map this to another dict:

new_dict = {
    "content_text": initial_dict['content']['text'],
    "meta_title": initial_dict['meta']['title'],
    "meta_extras": {
        "time_related": {
            initial_dict['meta']['created']
        },
        "by": initial_dict['meta']['author']
    }
}

The problem is that not all fields are always in the initial_dict. I can of course wrap the whole creation of new_dict into a try/except, but then it would fail if one of the initial fields doesn't exist.

Is there no other way than creating a try/except for each and every field I add to the new_dict? In reality the dict is way bigger than this (about 400 key/value pairs), so this will become a mess quite fast.

Isn't there a better and more pythonic way of doing this?


Solution

  • How about using dict.get? Instead of throwing an error, this returns None if the key isn't in the dictionary.

    new_dict = {
        "content_text": initial_dict['content'].get('text'),
        "meta_title": initial_dict['meta'].get('title'),
        "meta_extras": {
            "time_related": {
                initial_dict['meta'].get('created')
            },
            "by": initial_dict['meta'].get('author')
        }
    }
    

    If this goes deeper than one level, you can do some_dict.get('key1', {}).get('key2') as was suggested in the comments.

    Converting the original dict to a defaultdict is also an option, which allows you to keep using the [] notation (more practical than having to chain get methods):

    from collections import defaultdict
    def to_defaultdict(d):
        return defaultdict(lambda: None, ((k, to_defaultdict(v) if isinstance(v, dict) else v)
                                          for k, v in d.items()))
    initial_dict = to_defaultdict(initial_dict)
    

    You can then filter out the None values:

    def filter_dict(d):
        return dict((k, filter_dict(v) if isinstance(v, dict) else v)
                    for k, v in d.items() if v is not None)
    new_dict = filter_dict(new_dict)