Is it possible to determine which level/key of a nested dict that contained None, causing 'NoneType' object is not subscriptable?

The users of my framework (who may or may not be well versed in Python) write code that navigates a dict (that originally came from a json response from some API).

Sometimes they make a mistake, or sometimes the API returns data with some value missing, and they get the dreaded 'NoneType' object is not subscriptable

How can I make it clear at what level the error occured? (what key returned None)

def user_code(some_dict):
    # I can't modify this code, it is written by the user
    something = some_dict["a"]["b"]["c"]

# I don't control the contents of this.
data_from_api = '{"a": {"b": None}}'

# framework code, I control this
try:
    user_code(json.loads(data_from_api))
except TypeError as e:
    # I'd like to print an error message containing "a","b" here

I can overload/alter the dict implementation if necessary, but I don't want to do source code inspection.

There may already be answers to this question (or maybe it is impossible), but it is terribly hard to find among all the basic Why am I getting 'NoneType' object is not subscriptable? questions. My apologies if this is a duplicate.

Edit: @2e0byo's answer is the most correct to my original question, but I did find autoviv to provice a nice solution to my "real" underlying issue (allowing users to easily navigate a dict that sometimes doesnt have all the expected data), so I chose that approach instead. The only real down side with it is if someone relies on some_dict["a"]["b"]["c"] to throw an exception. My solution is something like this:

def user_code(some_dict):
    # this doesnt crash anymore, and instead sets something to None
    something = some_dict["a"]["b"]["c"]

# I don't control the contents of this.
data_from_api = '{"a": {"b": None}}'

# framework code, I control this
user_code(autoviv.loads(data_from_api))

Solution 1:

Here is one approach to this problem: make your code return a custom Result() object wrapping each object. (This approach could be generalised to a monad approach with .left() and .right(), but I didn't go there as I don't see that pattern very often (in my admittedly small experience!).)

Example Code

Firstly the custom Result() object:

class Result:
    def __init__(self, val):
        self._val = val

    def __getitem__(self, k):
        try:
            return self._val[k]
        except KeyError:
            raise Exception("No such key")
        except TypeError:
            raise Exception(
                "Result is None.  This probably indicates an error in your code."
            )

    def __getattr__(self, a):
        try:
            return self._val.a
        except AttributeError:
            if self._val is None:
                raise Exception(
                    "Result is None.  This probably indicates an error in your code."
                )
            else:
                raise Exception(
                    f"No such attribute for value of type {type(self._val)}, valid attributes are {dir(self._val)}"
                )

    @property
    def val(self):
        return self._val

Of course, there's a lot of room for improvement here (e.g. __repr__() and you might want to modify the error messages).

In action:

def to_result(thing):
    if isinstance(thing, dict):
        return Result({k: to_result(v) for k, v in thing.items()})
    else:
        return Result(thing)

d = {"a": {"b": None}}
r_dd = to_result(d)
r_dd["a"] # Returns a Result object
r_dd["a"]["b"] # Returns a Result object
r_dd["a"]["c"] # Raises a helpful error
r_dd["a"]["b"]["c"] # Raises a helpful error
r_dd["a"]["b"].val # None
r_dd["a"]["b"].nosuchattr # Raises a helpful error

Reasoning

If I'm going to serve up a custom object I want my users to know it's a custom object. So we have a wrapper class, and we tell users that the paradim is 'get at the object, and then use .val to get the result'. Handling the wrong .val is their code's problem (so if .val is None, they have to handle that). But handling a problem in the data structure is sort of our problem, so we hand them a custom class with helpful messages rather than anything else.

Getting the level of the nested error

As currently implemented it's easy to get one above in the error msg (for dict lookups). If you want to get more than that you need to keep a reference up the hierarchy in the Result---which might be better written with Result as something other than just a wrapper.

I'm not sure if this is the kind of solution you were looking for, but it might be a step in the right direction.