0

How to check if a dict has a key in an encapsulated dict and have the same else statement.

Basically how to optimize the following code, am assuming there must be a better way?

if "key_1" in my_dict:
    if "key_2" in my_dict["key_1"]:
        func_1()
    else:
        func_2()
else:
    func_2()

Of-course you cannot do the following cause if key_1 is not in my_dict then it will throw an error

if "key_2" in my_dict["key_1"]:
    func_1()
else:
    func_2()
AnarKi
  • 857
  • 1
  • 7
  • 27
  • 1
    In your first example you are checking twice against `my_dict`, in the second one `"key_2"` is supposed to be in the nested dict `my_dict["key_1"]`. Which one is correct? – FlyingTeller Apr 12 '22 at 14:35
  • 1
    Did you mean for your second `if` in the first block of code to be `if "key_2" in my_dict["key_1"]:`? Because your "cannot do the following" has nothing to do with what you're doing in the first block. – ShadowRanger Apr 12 '22 at 14:35
  • fixed the first example. it was `if "key_2" in my_dict` and now is `if "key_2" in my_dict["key_1"]`. hope now it makes more sense – AnarKi Apr 12 '22 at 14:39

3 Answers3

2

The simplest check is:

 if "key_2" in my_dict.get("key_1", ()):

where get never raises a KeyError; when the key is missing, it returns the default value provided, an empty tuple, which "key_2" will never be in (the choice of the empty tuple over the empty dict is a minor efficiency boost; Python would have to reconstruct the empty dict for each call, but it can reuse the singleton tuple over and over without building new ones).

While it's more verbose, the other approach to this is the EAFP approach; lookup the values assuming they exist, if you end up receiving a KeyError, it means one was missing:

try:
    my_dict["key_1"]["key_2"]  # If the value will be needed, assign to name here, use name in else:
except KeyError:
    func_2()  # One of the lookups failed, call func_2
else:
    func_1()  # Lookup succeeded, both keys exist, call func_1

Obviously:

if "key_1" in my_dict and "key_2" in my_dict["key_1"]:
    func_1()
else:
    func_2()

is also an option, but it requires looking up "key_1" twice, and is relatively verbose (that said, it is straightforward; it's not an awful solution, I just enjoy being needlessly clever).

ShadowRanger
  • 143,180
  • 12
  • 188
  • 271
1

You could use the get method with a default:

if "key_2" in my_dict.get("key_1", {}):
trincot
  • 317,000
  • 35
  • 244
  • 286
  • 1
    Minor note: We came up with *almost* the same solution, but there is an advantage to using `()` for the default: the default value is always "constructed" for every call, and since `dict` is mutable, Python builds a new empty `dict` for every call. The empty `tuple` is a singleton that is cached from the start (`tuple`s of any constant literals are also cached at compile time) and since the membership testing is intended to fail, any empty thing works equally well, so you may as well avoid paying the cost of building new empty things each time. – ShadowRanger Apr 12 '22 at 14:46
  • 2
    I expected this comment. I chose for a dict to have type consistency. A constant can be defined to hold the reference to the dict. – trincot Apr 12 '22 at 14:46
  • :-) Yeah, I'm not saying it's wrong, it's just unnecessary in this case (the behavior will be the same). In other scenarios (where the key #2 being checked might be entirely unhashable, as opposed to guaranteed hashable `str`), you'll get a `TypeError`, where using a `tuple` will silently just say "nope, not in here". Odds are the `TypeError` is the correct way to go, though it would depend on the scenario. – ShadowRanger Apr 12 '22 at 14:48
0

If your actual example involves accessing the value inside a nested dict and passing it to func1, then try/except lets you do the entire "happy path" operation in a single line:

try:
    func1(my_dict["key1"]["key2"])
except KeyError:
    func2()

If you don't need to do anything with the value and just want to make sure it's there before calling func1, it's still fairly neat:

try:
    my_dict["key1"]["key2"]
    func1()
except KeyError:
    func2()

The other option would be an and, which IMO is not as nice because it needs to be more verbose:

if "key1" in my_dict and "key2" in my_dict["key1"]:
    func1()
else:
    func2()
Samwise
  • 68,105
  • 3
  • 30
  • 44
  • I wouldn't call `func1()` in the `try` statement. It could raise a `KeyError` of its own that has nothing to do with deciding to call `func2`. – chepner Apr 12 '22 at 14:43
  • True, it really depends a lot on what `func1`'s expected behavior is and whether `func2` might make sense as a fallback regardless. Only OP knows for sure. – Samwise Apr 12 '22 at 14:51