how to use .get() in a nested dict? - python

I use .get() to query for keys which may or may not be present in a dictionary.
In [1]: a = {'hello': True}
In [3]: print(a.get('world'))
None
I have, however, dictionaries where the key I want to check for is deeper in the structure and I do not know if the ancestors are present or not. If the dict is b = {'x': {'y': {'z': True}}} do I have to resort to
In [5]: b.get('x') and b['x'].get('y') and b['x']['y'].get('z')
Out[5]: True
to check for 'z' when I do not know whether 'x' and 'y' exist?

You can return an empty dictionary object from dict.get() to ease chaining calls:
b.get('x', {}).get('y', {}).get('z')
but perhaps you'd be better off catching the KeyError exception:
try:
value = b['x']['y']['z']
except KeyError:
value = None

Related

TypeError: argument of type 'NoneType' is not iterable while extracting from the ditionary

I am trying to extract the one value from dictionary
If value not present in parames key I am passing a default value
I am getting TypeError: argument of type 'NoneType'
Below is the dictionary
event = {
'resource': '/test',
'path': '/test',
'httpMethod': 'GET',
'headers': None,
'multiValueHeaders': None,
'query': None,
'parames': None,
'path': None,
'Variables': None}
My code is below
if 'value' in event['parames']:
if isinstance('value', int)
val = int(event['parames']['value'][0])
else:
val = 10
else:
val = 10
print(val)
My idea,
check value is present in event
if present then value is extracted
In exception condition i will get value=10
You probably want
try:
val = int(event['parames']['value'][0])
except (TypeError, LookupError, ValueError):
val = 10
This will set val to 10 if any of the following are true:
event['parames'] raises a KeyError
event['parames']['value'] raises a KeyError or TypeError
event['parames']['value'][0]) raises either a TypeError, KeyError or IndexError
int(event['parames']['value'][0]) raises a ValueError.
When you do in event['parames'], Python tries to iterate through it. And in your example event['parames'] is None.
In your code 'value' is a string because there are quotes around it, so it won't ever enter the if because isinstance will fail. I understand you wanted a key 'value', looking at if contents? But: you access event['parames']['value'][0], so isinstance will still fail.
"If value not present in parames key I am passing a default value" - this sounds like a place for dict.get.
Python likes the "better ask for forgiveness than for permission", so chepner's code is shorter and clearer, but in case you want to do things manually:
tmp = (event['parames'] or {}).get('value')
Using a trick with or, we will get parames or -if parames is None- empty dict. Thus we fix the (1).
Then we use .get as I suggested in (3). It basically looks up given key in the dict (that's why we needed failsafe empty dict) and if it doesn't find it, it returns a default value (second argument, if not given returns None).
So now our tmp is either contents of value or None.
If value is always a list/iterable when it exists, we can do just if check on it:
if tmp:
...
This checks bool(tmp) underneath - which is False only for intuitively empty stuff - None, 0, empty iterables/strings (len(something) == 0)...
Since we assumed tmp is always an iterable and now we know it's not None and its length is not 0, then we can get our [0].
But it leaves the int stuff. But! We can check strings for numeric values using str.isnumeric. But this also assumes we get a string inside and not something else that can be
So full code:
tmp = (event['parames'] or {}).get('value')
if tmp and tmp[0].isnumeric():
val = int(tmp[0])
else:
val = 10
(If you use Python3.8+, you can use walrus operator to declare tmp in if.)
So... that's it. But as you can see, we got multiple assumptions here about what parames and value are. That's why chepner's code is better - it doesn't make any assumptions. But since you seemed like you're learning, I decided to post longer code to explain each problem.
I got one more solution. if it helps
if event.get('parames', False) != False:
if isinstance('value',int):
val = 10
else:
val = 5
print(val)

Is it okay to nest a dict.get() inside another or is this bad design?

So, I am working on a code base where a dictionary contains some key information. At some point in the development process the name of one of the keys was changed, but the older key still exists in a lot of places. Lets call the keys new and old for reference.
In order to make it compatible with the older version, I am doing something like:
dict_name.get(new_key,dict_name.get(old_key,None))
Is this bad design or is it okay? Why/Why not?
Example for clarification: (Based on input by #Alexander)
There are two dictionaries d1 and d2.
d1={k1:v1,old_key:some_value}
d2={k1:v1,new_key:some_value}
The function which I am designing right now could get either d1 or d2 like dictionary as an argument. My function should be able to pick up some_value, regardless of whether old_key or new_key is present.
That is a reasonable approach. The only downside is that it will perform the get for both keys, which will not affect performance in most situations.
My only notes are nitpicks:
dict is a reserved word, so don't use it as a variable
None is the default, so it can be dropped for old_key, e.g.:
info.get('a', info.get('b'))
In response to "Is there a way to prevent the double call?": Yup, several reasonable ways exist =).
The one-liner would probably look like:
info['a'] if 'a' in info else info.get('b')
which starts to get difficult to read if your keys are longer.
A more verbose way would be to expand it out into full statements:
val = None
if 'a' in info:
val = info['a']
elif 'b' in info:
val = info['b']
And finally a generic option (default after *keys) will only work with python 3):
def multiget(info, *keys, default=None):
''' Try multiple keys in order, or default if not present '''
for k in keys:
if k in info:
return info[k]
return default
which would let you resolve multiple invocations cleanly, e.g.:
option_1 = multiget(info, 'a', 'b')
option_2 = multiget(info, 'x', 'y', 'z', default=10)
If this is somehow a pandemic of multiple api versions or something (?) you could even go so far as wrapping dict, though it is likely to be overkill:
>>> class MultiGetDict(dict):
... def multiget(self, *keys, default=None):
... for k in keys:
... if k in self:
... return self[k]
... return default
...
>>> d = MultiGetDict({1: 2})
>>> d.multiget(1)
2
>>> d.multiget(0, 1)
2
>>> d.multiget(0, 2)
>>> d.multiget(0, 2, default=3)
3
dict.get is there for exactly this reason, so you can fall back on default values if the keys are not in there.
Having a double fallback is very much OK. For example:
d = {}
result = d.get('new_key',d.get('old_key', None))
This would mean that result is None in the worse case, but there is no error (which is the goal of get in the first place.
In other words, it will get the value of new_key as a first priority, old_key as the second priority, and None as a third.
Also worth noting that get(key, None) is the same as get(key) so you might want to shorten that line:
result = d.get('new_key', d.get('old_key'))
If you want to avoid calling get multiple times (for example, if you have to do more than 2 of those, it will be unreadable) you can do something like this:
priority = ('new_key', 'old_key', 'older_key', 'oldest_key')
for key in priority:
result = d.get(key)
if result is not None:
break
And result becomes whatever is encountered first in that loop, or None otherwise
Based on the sample dictionary provided, I would argue that this is bad design...
Lets say your original dictionary is:
d1 = {'k1': 1, 'k2': 2}
If I understand you correctly, you then 'update' one of the keys, e.g.:
d1 = {'k3': 1, 'k2': 2}
If you try to access via:
d1.get('k3', d1.get('k1')) # 'k3' is new key, 'k1' is old key.
then the first lookup will always be present and the second lookup will never be used.
If you meant that the new dictionary would looks like:
d2 = {'k1': 1, 'k2': 2, 'k3': 1}
then you are storing the 'same' data in two different locations in your dictionary, which will surely lead to trouble (similar to normalized data in a database). For example, if the value of 'k3' was updated to 3, then the value of k1 would need to be updated as well.
Given the dictionaries provided in your example:
d1={k1: v1, old_key: some_value}
d2={k1: v1, new_key: some_value}
I assume that some_value are intended to be equal in both, i.e. d1[old_key] == d2[new_key]. If so, then you could use d2.get(new_key, d1.get(old_key). However, it just seems like a mess.
If some_value needs to be updated, for example, it must be updated in both dictionaries.
You are wasting memory by storing the some_value twice.
Your new_key in d2 may accidentally clobber an existing key in d1.
I would recommend not changing the key names in the first place.

jmespath: Differentiate between a key not found and a key having a null value

I am using the jmespath module in python to search through a nested dictionary.
The problem I am running into is that I want to raise an exception if the key is not found in the dictionary. However, some of the keys have None values, which is completely valid. In both cases where jmespath finds a key with a None value, or does not find a key, it returns None.
Is there any way to differentiate between the two? As far as I can tell, jmespath has no equivalent to a "function." Thanks!
There was a detailed discussion about this issue here - https://github.com/jmespath/jmespath.py/issues/113
The outcome is to use contains to check if the key exists
So to check if foo.bar exists in a nested dict, you can use search with arg "contains(keys(foo), 'bar')"
>>> print (jmespath.search('foo.bar', {'foo': {'bar': None}}))
None
>>> jmespath.search("contains(keys(foo), 'bar')", {'foo': {'bar': None}})
True
>>> jmespath.search("contains(keys(foo), 'bar2')", {'foo': {'bar': None}})
False

Initializing a dictionary in python with a key value and no corresponding values

I was wondering if there was a way to initialize a dictionary in python with keys but no corresponding values until I set them. Such as:
Definition = {'apple': , 'ball': }
and then later i can set them:
Definition[key] = something
I only want to initialize keys but I don't know the corresponding values until I have to set them later. Basically I know what keys I want to add the values as they are found. Thanks.
Use the fromkeys function to initialize a dictionary with any default value. In your case, you will initialize with None since you don't have a default value in mind.
empty_dict = dict.fromkeys(['apple','ball'])
this will initialize empty_dict as:
empty_dict = {'apple': None, 'ball': None}
As an alternative, if you wanted to initialize the dictionary with some default value other than None, you can do:
default_value = 'xyz'
nonempty_dict = dict.fromkeys(['apple','ball'],default_value)
You could initialize them to None.
you could use a defaultdict. It will let you set dictionary values without worrying if the key already exists. If you access a key that has not been initialized yet it will return a value you specify (in the below example it will return None)
from collections import defaultdict
your_dict = defaultdict(lambda : None)
It would be good to know what your purpose is, why you want to initialize the keys in the first place. I am not sure you need to do that at all.
1) If you want to count the number of occurrences of keys, you can just do:
Definition = {}
# ...
Definition[key] = Definition.get(key, 0) + 1
2) If you want to get None (or some other value) later for keys that you did not encounter, again you can just use the get() method:
Definition.get(key) # returns None if key not stored
Definition.get(key, default_other_than_none)
3) For all other purposes, you can just use a list of the expected keys, and check if the keys found later match those.
For example, if you only want to store values for those keys:
expected_keys = ['apple', 'banana']
# ...
if key_found in expected_keys:
Definition[key_found] = value
Or if you want to make sure all expected keys were found:
assert(all(key in Definition for key in expected_keys))
You can initialize the values as empty strings and fill them in later as they are found.
dictionary = {'one':'','two':''}
dictionary['one']=1
dictionary['two']=2
Comprehension could be also convenient in this case:
# from a list
keys = ["k1", "k2"]
d = {k:None for k in keys}
# or from another dict
d1 = {"k1" : 1, "k2" : 2}
d2 = {k:None for k in d1.keys()}
d2
# {'k1': None, 'k2': None}
q = input("Apple")
w = input("Ball")
Definition = {'apple': q, 'ball': w}
Based on the clarifying comment by #user2989027, I think a good solution is the following:
definition = ['apple', 'ball']
data = {'orange':1, 'pear':2, 'apple':3, 'ball':4}
my_data = {}
for k in definition:
try:
my_data[k]=data[k]
except KeyError:
pass
print my_data
I tried not to do anything fancy here. I setup my data and an empty dictionary. I then loop through a list of strings that represent potential keys in my data dictionary. I copy each value from data to my_data, but consider the case where data may not have the key that I want.

What does "str indices must be integers" mean?

I'm working with dicts in jython which are created from importing/parsing JSON. Working with certain sections I see the following message:
TypeError: str indices must be integers
This occurs when I do something like:
if jsondata['foo']['bar'].lower() == 'baz':
...
Where jsondata looks like:
{'foo': {'bar':'baz'} }
What does this mean, and how do I fix it?
As Marcelo and Ivo say, it sounds like you're trying to access the raw JSON string, without first parsing it into Python via json.loads(my_json_string).
You need to check the type for dict and existance of 'z' in the dict before getting data from dict.
>>> jsondata = {'a': '', 'b': {'z': True} }
>>> for key in jsondata:
... if type(jsondata[key]) is dict and 'z' in jsondata[key].keys() and jsondata[key]['z'] is True:
... print 'yes'
...
yes
>>>
or shorter one with dict.get
>>> jsondata = {'a': '', 'b': {'z': True}, 'c' :{'zz':True}}
>>> for key in jsondata:
... if type(jsondata[key]) is dict and jsondata[key].get('z',False):
... print 'yes'
...
yes
>>>
Actually your statement should raise SyntaxError: can't assign to function call due to the fact that you're missing a = and thus making an assignment instead of a check for equality.
Since I don't get the TypeError when running the code you've shown, I suppose that you first fix the missing = and after that check back on what the Stacktrace says.
But it might also be possible that your jsondata hasn't been decoded and therefore is still plain text, which would of course then raise the indexing error.

Categories