6

I have a dictionary in python with a pretty standard structure. I want to retrieve one value if present, and if not retrieve another value. If for some reason both values are missing I need to throw an error.

Example of dicts:

# Data that has been modified
data_a = {
    "date_created": "2020-01-23T16:12:35+02:00",
    "date_modified": "2020-01-27T07:15:00+02:00"
}

# Data that has NOT been modified
data_b = {
    "date_created": "2020-01-23T16:12:35+02:00",
}

What is the best practice for this? What could be an intuitive and easily readable way to do this?

The way I do this at the moment:

mod_date_a = data_a.get('date_modified', data_a.get('date_created', False))
# mod_data_a = "2020-01-27T07:15:00+02:00"

mod_date_b = data_b.get('date_modified', data_b.get('date_created', False))
# mod_data_b = "2020-01-23T16:12:35+02:00"

if not mod_date_a or not mod_date_b:
    log.error('Some ERROR')

This nested get() just seems a little clumsy so I was wondering if anyone had a better solution for this.

4 Answers4

6

If you need this often, it would be entirely reasonable to write a function for it:

def get_or_error(d, *keys):
    for k in keys:
        try:
            return d[k]
        except KeyError:
            pass
    else:
        raise KeyError(', '.join(keys))

print(get_or_error(data_a, 'date_modified', 'date_created'))
deceze
  • 510,633
  • 85
  • 743
  • 889
  • This looks good too. In my case, I prefer a one-liner since It is only needed once. – Kasper Keinänen Jan 27 '20 at 07:22
  • 2
    …at least for now. As soon as you find yourself typing the same one-liner again, consider a function instead. And it really depends on the "error" you want. There's no reasonable one-liner that would also log or raise an exception… – deceze Jan 27 '20 at 07:23
  • Why not just use [in](https://stackoverflow.com/a/1602964/3700414) instead of try-except? – Rusi Jan 27 '20 at 07:29
  • @Rusi Depends on your error handling philosophy. EAFP is typically considered more pythonic, but you may of course disagree. – deceze Jan 27 '20 at 07:31
5
value = data_a['date_modified' if 'date_modified' in data_a else 'date_created']
Boris Verkhovskiy
  • 14,854
  • 11
  • 100
  • 103
  • This does exactly whan you want (try the second if the first is missing and raise an error if both missing) and will also work correctly if you have a falsy value in your dictionary. – Boris Verkhovskiy Jan 27 '20 at 07:33
  • 2
    I find it a bit repetitive and clumsy to read, but it surely is a one-liner which will also raise a `KeyError`. Though that `KeyError` may be slightly misleading since it'll only mention the second key, but that's a minor nitpick. – deceze Jan 27 '20 at 07:37
4

Assuming there are no false values and you don't really need False but just anything false:

mod_date_a = data_a.get('date_modified') or data_a.get('date_created')
Kelly Bundy
  • 23,480
  • 7
  • 29
  • 65
  • This is actually much more readable, thank you! And no I don specifically need the False, I just put it to be more clear when it should fail. Without the False, it will work in my cod just as well. – Kasper Keinänen Jan 27 '20 at 07:15
  • @HeapOverflow you can pass the second get as default value if you *want* to retrieve a falsy `date_modified`: `data_a.get('date_modified', data_a.get('date_created'))` will only return the creation date if the modification date is missing entirely, but not if it's present and e.g. `None`. And if the fallback is required you may want to use regular indexing ([]) as well. – Masklinn Jan 27 '20 at 07:17
  • 1
    @Masklinn *"And if the fallback is required [..]"* — You mean `d.get('m', d['c'])`? That would error if `c` didn't exist even if `m` did… – deceze Jan 27 '20 at 07:21
  • You should probably add a comment saying that you're assuming the dict will never contain a falsy value anytime you use this snippet. I think @deceze's answer is better. – Boris Verkhovskiy Jan 27 '20 at 07:21
  • 1
    @Boris Why would I say that in a comment when I have already said it in the answer? – Kelly Bundy Jan 27 '20 at 07:24
  • Not you, the people using this snippet – Boris Verkhovskiy Jan 27 '20 at 07:25
  • @Masklinn Nesting the `get`s is the original. Which the OP found clumsy. And it'll always evaluate the second access, which mine doesn't (and that even has the issue that deceze pointed out). – Kelly Bundy Jan 27 '20 at 07:28
  • @Boris I think it's likely that if there actually can be a false modification date value (presumably the empty string), they *want* to treat it as missing. – Kelly Bundy Jan 27 '20 at 07:46
  • This is not a general "best practice for conditionally getting values from Python dictionary". This might be a "best practice for conditionally getting values from Python dictionary which cannot contain a falsy value". – Boris Verkhovskiy Jan 27 '20 at 08:39
  • In this specific question, if he has an empty string where he expects a date, then that could be some malformed data, and the right behavior would be to error when you're trying to parse an empty string as a date, not silently fallback to the created date. – Boris Verkhovskiy Jan 27 '20 at 08:41
  • ""And if the fallback is required [..]" — You mean d.get('m', d['c'])? That would error if c didn't exist even if m did…" yes, the assumption here is that *fallbacks* are normally always present e.g. thinking for a few seconds here a record may or may not have been updated, but it's pretty much guaranteed to have been created, and thus it may or may not have a date_modified but it should always have a date_created. – Masklinn Jan 27 '20 at 09:10
0

You could use next()

def get_first(d, *keys):
    return next(d[key] for key in keys if key in d)

This raises StopIteration if no key is found. Also, sounds funny when read out loud, which usually means it's pythonic ;)

JollyJoker
  • 1,256
  • 8
  • 12
  • 1
    Best answer Afaic. You could convert the spurious StopIteration to a proper KeyError with a try... re-raise @couple extra lines – Rusi Jan 28 '20 at 06:58
  • @Rusi KeyError would say none of the keys were in the dictionary while StopIteration says all the keys were looped through without finding anything ¯\_(ツ)_/¯ – JollyJoker Jan 28 '20 at 08:11