6

I have a deep nested dict (decoded from json, from the instagram api). My initial code was like this:

caption = post['caption']['text']

But that would throw a NoneType or KeyError error if the 'caption' key or the 'text' key doesn't exist.

So I came up with this:

caption = post.get('caption', {}).get("text")

Which works, but I'm not sure about the style of it. For instance, if I apply this technique to one of the deeper nested attributes I'm trying to retrieve, it looks pretty ugly:

image_url = post.get('images',{}).get('standard_resolution',{}).get('url')

Is there a better, more pythonic, way to write this? My goal is to retrieve the data, if it's there, but not to hold up execution if it's not there.

Thanks!

Kenny Winker
  • 11,919
  • 7
  • 56
  • 78
  • Why can't you just catch the exception? – Cairnarvon Feb 23 '13 at 03:42
  • I can. I guess because I'm pulling ~7 keys, I didn't want to have to try/except 7 times. – Kenny Winker Feb 23 '13 at 03:48
  • 1
    related: [Python: Change values in dict of nested dicts using items in a list](http://stackoverflow.com/questions/11918852/python-change-values-in-dict-of-nested-dicts-using-items-in-a-list) – jfs Feb 23 '13 at 03:55

4 Answers4

12

The most Pythonic way would be simply to catch the KeyError:

try:
    caption = post['caption']['text']
except KeyError:
    caption = None

This is simple, obvious, and immediately understandable to a Python programmer.

nneonneo
  • 171,345
  • 36
  • 312
  • 383
  • 1
    Please don't put the bodies of `try:` and `except:` on the same line as their respective introductions. It's one of [PEP 8](http://www.python.org/dev/peps/pep-0008/)'s Definitely Nots, and unpythonic in its own right. – Cairnarvon Feb 23 '13 at 03:55
  • 4
    Fixed, with apologies to PEP 8. – nneonneo Feb 23 '13 at 03:56
  • 1
    Is there a way to generalize this for multiple keys? e.g. if I want to retrieve `caption.text` but also `images.standard_resolution.url` and `user.username` and a few others, do I have to do n try/except blocks? – Kenny Winker Feb 23 '13 at 04:05
  • 2
    You could define a function to retrieve a list of keys with the try/except in there. – nneonneo Feb 23 '13 at 04:10
12

Python 3.4 and newer versions contains a contextlib context manager suppress, which is for exactly this kind of thing. Suppressing specific errors when you know in advance they may happen and your code can handle it.

from contextlib import suppress

sample = {'foo': 'bar'}

with suppress(KeyError):
    print(sample['baz'])

Will prevent the KeyError from being raised.

So for accessing getting a deeply nested dictionary value, you can use suppress like this.

value = None
with suppress(KeyError):
    value = data['deeply']['nested']['dictionary']['key']
Techdragon
  • 502
  • 8
  • 15
  • Most clean and Pythonic solution for fetching multiple lines of nested keys at once using a single `with suppress(KeyError):` – IODEV Apr 14 '21 at 16:54
2

How do you feel about something like this

if 'caption' in post:
    caption = post['caption']['text']

But it also starts to break down

if 'images' in post and 'standard_resolution' in post['images']:
    image_url = post['images']['standard_resolution']['url']

So I think the most Pythonic way is to just ask for forgiveness and not permission

try:
    image_url = post['images']['standard_resolution']['url']
except KeyError:
    image_url = None
Ric
  • 8,615
  • 3
  • 17
  • 21
  • 1
    Don't use bare except, though, or funny bad things will happen. (E.g. `KeyboardInterrupt` getting swallowed) – nneonneo Feb 23 '13 at 03:45
-1

I'd create a custom dict subclass, and then just address that:

class SafeDict(dict):
    def __getitem__(self,k):
        if k in self:
            return dict.__getitem__(self,k)
        return None


a = SafeDict({'a':'a'})
print a['a']
>> a
print a['b']
>> None

You could either do a custom init to handle nested dicts as another instance of SafeDict ( which would allow you to pass them around ) or you could use testing (or a try/except block) to prevent KeyErrors

also, you could just make it an object class, overload __getattr__ , and then handle things with dot notation. i tend to prefer that approach ( I first saw this in the Pylons framework )

class AttributeSafeObject(object):

    def __init__(self,**kwargs):
        for key in kwargs:
            setattr(self,key,kwargs[key])

    def __getattr__(self, name):
        try:
            return object.__getattribute__(self,name)
        except AttributeError:
            return None

post = AttributeSafeObject({'a':'a'})
print post.a
>> a
print post.title
>> None
Jonathan Vanasco
  • 15,111
  • 10
  • 48
  • 72
  • The `post` dict is coming from simplejson, I'm not sure how I would get simplejson to return a SafeDict, or to convert a standard dict to a SafeDict. – Kenny Winker Feb 23 '13 at 04:03
  • Your code for `__getitem__()` would be simpler as `return self.get(k)` (you are mostly rewriting the `get()` method). Anyway, this does answer the question, because even `a = SafeDict({'a': SafeDict({'b': 'b'})})` fails on `a['c']['d']`, which is the problem the question asks to solve. – Eric O. Lebigot Feb 23 '13 at 04:04
  • If you step back for a second... you'll notice that calling AnyClass(yourdict) really calls `AnyClass.__init__` with the dict as kwargs. If you inherit a class from `dict`, these kwargs become the dict. if you inherit from object, you can have fun with __init__. personally, i would probably go with the object notation. it makes api programming much easier. – Jonathan Vanasco Feb 23 '13 at 04:06
  • 1
    @EOL nice catch. i noted in the response that I explicitly didn't include recursion. This is just an idea to potentially pursue. – Jonathan Vanasco Feb 23 '13 at 04:07