1

I'm using Python to convert a json file to more human readable output, and a specific entry in that json can take one of the following formats. I'm trying create a method that handles creating appropriate output without checking for type.

"responseType": null

or

"responseType": "FEATURE MODEL"

or

"responseType": {
    "type": "array",
    "of": "Feature"
}

or

"responseType": {
    "type": "array",
     "of": {
             "type": "number",
             "format": "int32"
           }
}

My desired output in each case is something like:

The responseType is null
The responseType is a Feature Model
The responseType is an array of Feature
The responseType is an array of int32 numbers

I'm new to Python and my first inclination is to do a lot of type checking and string concatenation. i.e.:

str = "The response type is "
if type(obj["responseType"]) is str:
    str += obj["responseType"]
elif type(obj["responseType"]) is dict:
    str += obj["responseType"]["type"] + " "
    if type(obj["responseType"]["of"] is str
        str += obj["responseType"]["of"]
    else:
        #dict output
        #etc...
elif type(obj["responseType"] is None:
     print("The response type is null")

Doing this feels very naive and incorrect, given that I repeatedly read that you should never check for type.

So, what is the pythonic way to handle this case without doing all that type checking?

martineau
  • 119,623
  • 25
  • 170
  • 301
Del
  • 11
  • 1
  • I don't think there's much you can do to improve this. It's a poorly designed data structure. – Barmar Mar 15 '17 at 23:47
  • 1
    @Barmar: that doesn't mean the code itself can't be structured better. – Martijn Pieters Mar 15 '17 at 23:53
  • Both @martineau and @martijn-pieters answers provide runnable solutions. But I'm not sure I can actually judge which is the "better" answer. IAs a novice in python, I'll say that @martineau 's solution is more readable and I don't know enough yet that I can recognize what @martijn-pieters does. Helpful to me that both leverage the technique of nested `TypeErrors` – Del Mar 21 '17 at 20:59
  • The essence of both answers is the same: "use exceptions instead of type-checking" which will make the code more "Pythonic". I'm biased, of course, but beyond that, what you say about readability and being able to understand the code seems like it would break the tie—since those things are also important facets of the notion. – martineau Mar 21 '17 at 21:31

2 Answers2

1

You could just use key access and catch the exception, a technique called Easy to Ask for Forgiveness than Permission, or EAFP:

rtype = obj["responseType"]
output = []
try:
    container_type = rtype['type']
except TypeError:
    # not a dictionary, so plain type
    output.extend(['null'] if rtype is None else ['a', rtype])
else:
    # Container type
    output.extend(['a', container_type, 'of'])
    # Check for subtype
    try:
        sub_type = rtype['of']['type']
    except TypeError:
        output.append(rtype['of'])
    else:
        output.extend([rtype['of']['format'], sub_type + 's'])
print('The response type is', *output)

I've ignored using the correct article (a vs. an) for now, but you get the idea.

It's not necessarily better than what you were doing, Looking Before You Leap (LBYL); it'll depend on the distribution of the mix which one is faster (as handling an exception is relatively slower than using if tests, but faster if exceptions are less common), or which one you find to be more readable for your use-cases.

Demo:

>>> def output(obj):
...     rtype = obj["responseType"]
...     output = []
...     try:
...         container_type = rtype['type']
...     except TypeError:
...         # not a dictionary, so plain type
...         output.extend(['null'] if rtype is None else ['a', rtype])
...     else:
...         # Container type
...         output.extend(['a', container_type, 'of'])
...         # Check for subtype
...         try:
...             sub_type = rtype['of']['type']
...         except TypeError:
...             output.append(rtype['of'])
...         else:
...             output.extend([rtype['of']['format'], sub_type + 's'])
...     print('The response type is', *output)
...
>>> responses = [
...     {"responseType": None},
...     {"responseType": "FEATURE MODEL"},
...     {"responseType": {"type": "array", "of": "Feature"}},
...     {"responseType": {"type": "array", "of": {"type": "number", "format": "int32"}}},
... ]
>>> for r in responses:
...     output(r)
...
The response type is null
The response type is a FEATURE MODEL
The response type is a array of Feature
The response type is a array of int32 numbers

Note that if you do use type checks, I'd use isinstance(ob, class) over type(ob) is class. With data coming from JSON, there is no need to do strict checks (where you can't accept a subclass, only the exact type), and isinstance() is less work for Python and more readable for other maintainers.

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
1

It would be more "Pythonic" to use exceptions. See my answer to another question for more details.

At any rate, here's a runnable example of applying them to your problem:

import json

json_resp = '''[{"responseType": null},
                {"responseType": "FEATURE MODEL"},
                {"responseType": {"of": "Feature", "type": "array"}},
                {"responseType": {"type": "array",
                                  "of": {"format": "int32", "type": "number"}}}]'''
objs = json.loads(json_resp)

for obj in objs:
    try:
        kind = ('an ' + obj['responseType']['type']
                    + ' of ' + obj['responseType']['of']['format']
                        + ' ' + obj['responseType']['of']['type']
                            + 's')
    except TypeError:
        try:
            kind = obj['responseType']['of']
        except TypeError:
            kind = obj['responseType']

    print('The responseType is {}'.format(kind))

Output:

The responseType is None
The responseType is FEATURE MODEL
The responseType is Feature
The responseType is an array of int32 numbers
Community
  • 1
  • 1
martineau
  • 119,623
  • 25
  • 170
  • 301