390

Why doesn't list have a safe "get" method like dictionary?

>>> d = {'a':'b'}
>>> d['a']
'b'
>>> d['c']
KeyError: 'c'
>>> d.get('c', 'fail')
'fail'

>>> l = [1]
>>> l[10]
IndexError: list index out of range
dreftymac
  • 31,404
  • 26
  • 119
  • 182
Mikhail M.
  • 5,588
  • 3
  • 23
  • 31
  • 1
    Lists are used for different purposes than dictionaries. The get() is not needed for typical use cases of list. However, for dictionary the get() is quite often useful. – mgronber Feb 26 '11 at 07:35
  • 63
    You can always get an empty sublist out of a list without raising IndexError if you ask for a slice instead: `l[10:11]` instead of `l[10]`, for example. ()Th sublist will have the desired element if it exists) – jsbueno Feb 26 '11 at 14:01
  • 89
    Contrary to some here, I support the idea of a safe `.get`. It would be the equivalent of `l[i] if i < len(l) else default`, but more readable, more concise, and allowing for `i` to be an expression without having to recalculate it – Paul Draper Oct 29 '13 at 09:22
  • 8
    Today I wished this existed. I use a expensive function that returns a list, but I only wanted the first item, or ``None`` if one didn't exist. It would have been nice to say ``x = expensive().get(0, None)`` so I wouldn't have to put the useless return of expensive into a temporary variable. – Ryan Hiebert Jan 02 '14 at 01:32
  • 2
    @Ryan my answer may help you http://stackoverflow.com/a/23003811/246265 – Jake Apr 11 '14 at 04:59
  • 7
    @PaulDraper There is a related issue on the Python issue tracker. Believe it or not but `mylist.get(myindex, mydefault)` was rejected in favor of `(mylist[myindex:myindex+1] or mydefault)[0]` :-/ – Rotareti Mar 01 '18 at 19:40
  • 2
    related: [How to get the nth element of a python list or a default if not available](https://stackoverflow.com/q/2492087/4279) – jfs Mar 04 '18 at 09:25
  • 1
    @mgronber I beg to differ. This deficiency hinders generic programming. Dealing with graphs, you might want to represent adjacencies with a list or a dict (for sparse graphes). Not being able to interchangeably use them is annoying. – YvesgereY Oct 02 '19 at 23:15
  • "Why" questions on language design are generally not on topic, because their answers don't help users of that language fix the practical problem they encountered; as such, they're curiosity at best, disguised rants at worst. See [What is the rationale for closing "why" questions on language design?](https://meta.stackexchange.com/questions/170394/what-is-the-rationale-for-closing-why-questions-on-a-language-design) – Charles Duffy Jul 22 '21 at 02:27
  • 1
    That's good comment @PaulDraper but I think the correct expression is `(mylist[myindex:myindex+1] or [mydefault])[0]`. Otherwise if `mydefault` is a string the expression will return the first char, and if it isn't subscriptable it will raise. – NeilG Aug 24 '21 at 05:20
  • [I don't think `.get()` is _safe_, exactly](https://stackoverflow.com/a/70400361/6243352). – ggorlen Apr 25 '23 at 05:51

13 Answers13

159

Ultimately it probably doesn't have a safe .get method because a dict is an associative collection (values are associated with names) where it is inefficient to check if a key is present (and return its value) without throwing an exception, while it is super trivial to avoid exceptions accessing list elements (as the len method is very fast). The .get method allows you to query the value associated with a name, not directly access the 37th item in the dictionary (which would be more like what you're asking of your list).

Of course, you can easily implement this yourself:

def safe_list_get (l, idx, default):
  try:
    return l[idx]
  except IndexError:
    return default

You could even monkeypatch it onto the __builtins__.list constructor in __main__, but that would be a less pervasive change since most code doesn't use it. If you just wanted to use this with lists created by your own code you could simply subclass list and add the get method.

Nick Bastin
  • 30,415
  • 7
  • 59
  • 78
  • 37
    Python doesn't allow monkeypatching builtin types like `list` – Imran Feb 26 '11 at 07:32
  • 9
    @CSZ: `.get` solves a problem that lists don't have - an efficient way to avoid exceptions when getting data that may not exist. It is super trivial and very efficient to know what a valid list index is, but there's no particularly good way to do this for key values in a dictionary. – Nick Bastin Feb 26 '11 at 07:35
  • Needlessly-inside-baseball comment - PEP 384 really ought to allow types conforming to it to be monkeypatched, but it seems not to at this point. – Nick Bastin Feb 26 '11 at 07:51
  • 15
    I don't think this is about efficiency at at all - checking if a key is present in a dictionary and / or returning an item are `O(1)`. It won't be quite as fast in raw terms as checking `len`, but from a complexity point of view they're all `O(1)`. The right answer is the typical usage / semantics one... – Mark Longair Feb 26 '11 at 08:15
  • 5
    @Mark: Not all O(1) are created equal. Also, `dict` is only best-case O(1), not all cases. – Nick Bastin Feb 26 '11 at 18:56
  • @wvxvw: `len()` of a python list is O(1) (it's an array, it's a trivial constant-time operation to determine how many elements there are) – Nick Bastin Oct 12 '12 at 06:35
  • @wvxvw: Since you know it's different for different kinds of objects, it seems hasty to downvote an answer based on an assumption and not a fact. In fact, Py_SIZE is not different for different types of objects - it's a simple macro that redirects to the ob_size member (not a function call). (And indeed, `len()` does not in general map to Py_SIZE - it maps to PyObject_Size(), which calls the type-appropriate method, which in the case of list happens to instantly redirect to the Py_SIZE macro) – Nick Bastin Oct 12 '12 at 22:05
  • @wvxvw: the fact that `len()` is constant time actually matters. Obviously then a builtin `get()` method has at least the opportunity to be considerably more efficient since the thing you have to do to be safe - testing the length - would not be something the user could optimize. As it is now the use case is not any more efficient if you had a `.get()` method (as opposed to a dictionary, where indeed it is). Also, you can't gain direct access to list cells in Python (as in fact they're arrays, so not built as a linked list as you might imagine), so you can't build a circular list. – Nick Bastin Oct 13 '12 at 04:56
  • @wvxvw: `len()` for lists is constant time. Always. We're not talking about `len()` for a random data type here - it's always been `list`. I'd love to see some python code which you believe builds a circular `list` (not a generic list - the builtin type `list`, which is what the question is about). – Nick Bastin Oct 13 '12 at 19:32
  • 35
    I think that people are missing the point here. The discussion shouldn't be about efficiency. Please stop with the premature optimization. If your program is too slow, you are either abusing `.get()` or you have problems elsewhere in your code (or environment). The point of using such a method is code readability. The "vanilla" technique requires four lines of code in every place that this needs to be done. The `.get()` technique only requires one and can easily be chained with subsequent method calls (e.g. `my_list.get(2, '').uppercase()`). – Tyler Crompton Jul 03 '17 at 18:10
  • @TylerCrompton: Method chaining is not particularly pythonic and the language libraries and types are not implemented to support it (explicitly in the case of mutation, but also generally as it breaks some of the dynamism of the language). If you want to have your own `list_default_get` function you are free to write one and use it. The only reason you'd want the built-in type to support it is for efficiency. – Nick Bastin Nov 23 '17 at 11:47
  • "premature optimization is the root of all evil" – cryanbhu Apr 11 '23 at 05:25
124

This works if you want the first element, like my_list.get(0)

>>> my_list = [1,2,3]
>>> next(iter(my_list), 'fail')
1
>>> my_list = []
>>> next(iter(my_list), 'fail')
'fail'

I know it's not exactly what you asked for but it might help others.

Jake
  • 12,713
  • 18
  • 66
  • 96
70

Probably because it just didn't make much sense for list semantics. However, you can easily create your own by subclassing.

class safelist(list):
    def get(self, index, default=None):
        try:
            return self.__getitem__(index)
        except IndexError:
            return default

def _test():
    l = safelist(range(10))
    print l.get(20, "oops")

if __name__ == "__main__":
    _test()
Craig McQueen
  • 41,871
  • 30
  • 130
  • 181
Keith
  • 42,110
  • 11
  • 57
  • 76
  • 7
    This is, by far, the most pythonic answering the OP. Note that you could also extract a sublist, which is a safe operation in Python. Given mylist = [1, 2, 3], you can try to extract the 9th element with mylist[8:9] without triggering an exception. You can then test if the list is empty and, in case it is not empty, extract the single element from the returned list. – jose.angel.jimenez Oct 17 '15 at 09:14
  • 1
    This should be the accepted answer, not the other non-pythonic one-liner hacks, especially because it conserves the symmetry with dictionaries. – Eric Jul 23 '17 at 10:56
  • 4
    There is nothing pythonic about subclassing your own lists just because you a need a nice `get` method. Readability counts. And readability suffers with every additional unnecessary class. Just use the `try / except` approach without creating subclasses. – Jeyekomon Oct 02 '19 at 08:31
  • 3
    @Jeyekomon It's perfectly Pythonic to reduce boilerplate by subclassing. – Keith Oct 06 '19 at 16:14
  • 1
    Why not `return self[index]`? – timgeb Feb 27 '21 at 16:09
  • @Keith totally true. OOP design principles are fully matched here. Good job! – Alex Vergara Aug 20 '21 at 20:16
49

Instead of using .get, using like this should be ok for lists. Just a usage difference.

>>> l = [1]
>>> l[10] if 10 < len(l) else 'fail'
'fail'
Tim
  • 41,901
  • 18
  • 127
  • 145
YOU
  • 120,166
  • 34
  • 186
  • 219
  • 22
    This fails if we try to get the latest element with -1. – ludo Apr 03 '15 at 22:21
  • Note that this doesn't work for circularly linked list objects. Additionally, the syntax causes what I like to call a "scanning block". When scanning through code to see what it does, this is a line that would slow me down for a moment. – Tyler Crompton Jul 03 '17 at 18:15
  • inline if/else does not work with older python like 2.6 (or is it 2.5?) – Eric Jul 23 '17 at 10:50
  • 4
    @TylerCrompton: There is no circularly linked list in python. If you wrote one on your own you are free to have implemented a `.get` method (except I'm not sure how you would explain what the index meant in this case, or why it would ever fail). – Nick Bastin Nov 23 '17 at 11:48
  • 2
    An alternative that handles out-of-bounds negative indices would be `lst[i] if -len(lst) <= i < len(l) else 'fail'` – mic Jun 14 '20 at 02:39
28

Credits to jose.angel.jimenez and Gus Bus.


For the "oneliner" fans…


If you want the first element of a list or if you want a default value if the list is empty try:

liste = ['a', 'b', 'c']
value = (liste[0:1] or ('default',))[0]
print(value)

returns a

and

liste = []
value = (liste[0:1] or ('default',))[0]
print(value)

returns default


Examples for other elements…

liste = ['a', 'b', 'c']
print(liste[0:1])  # returns ['a']
print(liste[1:2])  # returns ['b']
print(liste[2:3])  # returns ['c']
print(liste[3:4])  # returns []

With default fallback…

liste = ['a', 'b', 'c']
print((liste[0:1] or ('default',))[0])  # returns a
print((liste[1:2] or ('default',))[0])  # returns b
print((liste[2:3] or ('default',))[0])  # returns c
print((liste[3:4] or ('default',))[0])  # returns default

Possibly shorter:

liste = ['a', 'b', 'c']
value, = liste[:1] or ('default',)
print(value)  # returns a

It looks like you need the comma before the equal sign, the equal sign and the latter parenthesis.


More general:

liste = ['a', 'b', 'c']
f = lambda l, x, d: l[x:x+1] and l[x] or d
print(f(liste, 0, 'default'))  # returns a
print(f(liste, 1, 'default'))  # returns b
print(f(liste, 2, 'default'))  # returns c
print(f(liste, 3, 'default'))  # returns default

Tested with Python 3.6.0 (v3.6.0:41df79263a11, Dec 22 2016, 17:23:13)

qräbnö
  • 2,722
  • 27
  • 40
  • 1
    +1 for reasonable approach with compact syntax, but an exception safe `get()` or `get_at_index()` might be a favorable and intuitive alternative, at least for those not yet comfortable with [python slice notation](https://stackoverflow.com/a/63047385/42223) – dreftymac Feb 24 '22 at 02:41
22

Try this:

>>> i = 3
>>> a = [1, 2, 3, 4]
>>> next(iter(a[i:]), 'fail')
4
>>> next(iter(a[i + 1:]), 'fail')
'fail'
Vsevolod Kulaga
  • 666
  • 7
  • 8
  • 1
    i like this one, though it requires the creation of a new sub-list first. – Rick Mar 27 '18 at 13:09
  • It would be more efficient to do `next(iter(a[i:i+1]), 'fail')` than `next(iter(a[i:]), 'fail')` – Stef Mar 26 '23 at 18:02
21

A reasonable thing you can do is to convert the list into a dict and then access it with the get method:

>>> my_list = ['a', 'b', 'c', 'd', 'e']
>>> my_dict = dict(enumerate(my_list))
>>> print my_dict
{0: 'a', 1: 'b', 2: 'c', 3: 'd', 4: 'e'}
>>> my_dict.get(2)
'c'
>>> my_dict.get(10, 'N/A')
fab
  • 365
  • 2
  • 6
  • 27
    A reasonable workaround, but hardly "the best thing you can do". – tripleee Nov 19 '14 at 14:48
  • 5
    Very inefficient though. Note: Instead of that `zip range len` thing, one could just use `dict(enumerate(my_list))` – Marian Mar 04 '15 at 14:53
  • 4
    This is not the best thing, it's the worst thing you could do. – erikbstack Oct 29 '16 at 18:55
  • 9
    It's the worst thing if you consider performance... if you care about performance you don't code in an interpreted language like python. I find this solution using a dictionary rather elegant, powerful and pythonic. Early optimisations are evil anyway so let's have a dict and see later it it's a bottleneck. – Eric Jul 23 '17 at 10:46
9

So I did some more research into this and it turns out there isn't anything specific for this. I got excited when I found list.index(value), it returns the index of a specified item, but there isn't anything for getting the value at a specific index. So if you don't want to use the safe_list_get solution which I think is pretty good. Here are some 1 liner if statements that can get the job done for you depending on the scenario:

>>> x = [1, 2, 3]
>>> el = x[4] if len(x) > 4 else 'No'
>>> el
'No'

You can also use None instead of 'No', which makes more sense.:

>>> x = [1, 2, 3]
>>> i = 2
>>> el_i = x[i] if len(x) == i+1 else None

Also if you want to just get the first or last item in the list, this works

end_el = x[-1] if x else None

You can also make these into functions but I still liked the IndexError exception solution. I experimented with a dummied down version of the safe_list_get solution and made it a bit simpler (no default):

def list_get(l, i):
    try:
        return l[i]
    except IndexError:
        return None

Haven't benchmarked to see what is fastest.

radtek
  • 34,210
  • 11
  • 144
  • 111
  • 1
    Not really pythonic. – Eric Jul 23 '17 at 10:48
  • 1
    @Eric which snippet? I think the try, except makes most sense by looking at it again. – radtek Jul 24 '17 at 19:21
  • A standalone function is not pythonic. Exceptions are a little more pythonic indeed but not by much as it's such a common pattern in programming languages. What's much more pythonic is a new object that extends the builtin type `list` by subclassing it. That way the constructor can take a `list` or anything that behaves like a list, and the new instance behaves just like a `list`. See Keith's answer below which should be the accepted one IMHO. – Eric Jul 25 '17 at 20:06
  • Of course, that's true but it may not apply in some cases where functional programming makes more sense. These are just one off snippet examples to get the juices flowing. It really depends on your use case. If you're working with OOP, I agree with you there. – radtek Jul 25 '17 at 23:46
  • Well, the question is about OOP, not FP: "Why list doesn't have safe “get” method like dictionary?" so we're off-topic when dealing with FP so we're off-topic when dealing with FP. – Eric Jul 26 '17 at 08:21
  • But let's deal with FP: the OOP solution also happens to work in FP: `apply(lambda x: x.get(2), [safelist([1,2,3,4])])` (returns 3). On the other hand you'll have to tweak a little your function to make it FP compatible, for example in `apply(functools.partial(list_get, 2), ([1,2,3,4],))`. So we're not losing anything by using OOP, on the contrary. – Eric Jul 26 '17 at 08:41
  • The safelist solution does use the try/except on IndexError. This is just a lower level answer as to what works. How you implement it is up to you. You can wrap any of those snippets in OOP or FP. – radtek Jul 26 '17 at 18:08
  • An object is just more pythonic than a function in this case. I won't argue if it's low-level or whatnot. – Eric Jul 27 '17 at 19:22
  • 1
    @Eric I parsed the question not as OOP-specific but as "why do lists have no analogy to `dict.get()` to return a default value from a list index reference instead of having to catch `IndexError`? So it really is about language/library feature (and not OOP vs. FP context). Furthermore, one probably has to qualify your use of 'pythonic' as maybe WWGD (as his disdain for FP Python is well known) and not necessarily just satisfying PEP8/20. – cowbert Aug 09 '17 at 21:28
  • You've probably noticed that I've been dragged into FP by radtek because it helped his argumentation. Feels like trolling. STOP. – Eric Aug 10 '17 at 22:45
  • Not really, it started from your "not pythonic" argument which later you expanded into OOP. I was always stating these are low level examples to implement safe list get. – radtek Aug 11 '17 at 16:56
  • 1
    `el = x[4] if len(x) == 4 else 'No'` – do you mean `len(x) > 4`? `x[4]` is out-of-bounds if `len(x) == 4`. – mic Jun 14 '20 at 02:42
5

Dictionaries are for look ups. It makes sense to ask if an entry exists or not. Lists are usually iterated. It isn't common to ask if L[10] exists but rather if the length of L is 11.

Apprentice Queue
  • 2,036
  • 13
  • 13
  • Yes, agree with you. But I just parsed relative url of page "/group/Page_name". Split it by '/' and wanted to check if PageName is equal to certain page. It would be comfortable to write something like [url.split('/').get_from_index(2, None) == "lalala"] instead of making extra check for length or catch exception or write own function. Probably you are right it is simply considered unusual. Anyway I still disagree with this =) – Mikhail M. Feb 26 '11 at 07:48
  • @Nick Bastin: Nothing wrong. It is all about simplicity and speed of coding. – Mikhail M. Feb 26 '11 at 08:03
  • It would also be useful if you wanted to use lists as a more space efficient dictionary in cases where the keys are consecutive ints. Of course the existence of negative indexing already stops that. – Antimony Jun 19 '12 at 00:56
2

If you

  1. want a one liner,
  2. prefer not having try / except in your happy code path where you needn't, and
  3. want the default value to be optional,

you can use this:

list_get = lambda l, x, d=None: d if not l[x:x+1] else l[x]

Usage looks like:

>>> list_get(['foo'], 4) == None
True
>>> list_get(['hootenanny'], 4, 'ho down!')
'ho down!'
>>> list_get([''], 0)
''
Gus Bus
  • 369
  • 3
  • 7
2

This isn't an extremely general-purpose solution, but I had a case where I expected a list of length 3 to 5 (with a guarding if), and I was breaking out the values to named variables. A simple and concise way I found for this involved:

foo = (argv + [None, None])[3]
bar = (argv + [None, None])[4]

Now foo and bar are either the 4th and 5th values in the list, or None if there weren't that many values.

arantius
  • 1,715
  • 1
  • 17
  • 28
  • despite the disclaimer attached to this answer, it is precisely a general-purpose solution if the goal is to ensure that a list as at least N elements, where N is an upper limit that is known at design time – dreftymac Sep 10 '21 at 04:27
  • 1
    At the time I was thinking it would be impractical for large indexes, but I suppose you could e.g. `(argv + [None]*999)`. – arantius Sep 10 '21 at 17:34
  • exactly ^_^ dmid://uu966listinit1631296 – dreftymac Sep 10 '21 at 17:59
  • 1
    It’s simple to write but memory-wise it’s very inefficient – bfontaine Oct 05 '21 at 09:55
1

For small index values you can implement

my_list.get(index, default)

as

(my_list + [default] * (index + 1))[index]

If you know in advance what index is then this can be simplified, for example if you knew it was 1 then you could do

(my_list + [default, default])[index]

Because lists are forward packed the only fail case we need to worry about is running off the end of the list. This approach pads the end of the list with enough defaults to guarantee that index is covered.

Gordon Wrigley
  • 11,015
  • 10
  • 48
  • 62
-2

Your usecase is basically only relevant for when doing arrays and matrixes of a fixed length, so that you know how long they are before hand. In that case you typically also create them before hand filling them up with None or 0, so that in fact any index you will use already exists.

You could say this: I need .get() on dictionaries quite often. After ten years as a full time programmer I don't think I have ever needed it on a list. :)

Lennart Regebro
  • 167,292
  • 41
  • 224
  • 251
  • How about my example in comments? What is more simple and readable? (url.split('/').getFromIndex(2) == "lalala") OR (result = url.split(); len(result) > 2 and result[2] == "lalala"). And yes, I know I can write such function myself =) but I was surprised such function is not builtin. – Mikhail M. Feb 26 '11 at 08:40
  • 1
    Id' say in your case you are doing it wrong. URL handling should be done either by routes (pattern matching) or object traversal. But, to answer your specific case: `'lalala' in url.split('/')[2:]`. But the problem with your solution here is that you only look at the second element. What if the URL is '/monkeybonkey/lalala'? You'll get a `True` even though the URL is invalid. – Lennart Regebro Feb 26 '11 at 09:58
  • I took only second element because i needed only second element. But yes, slices seem good working alternative – Mikhail M. Feb 26 '11 at 16:02
  • @CSZ: But then the first element is ignored, and in that case you could skip it. :) See what I mean, the example doesn't work that well in real life. – Lennart Regebro Feb 26 '11 at 16:34