Indexable get method. [1,2,3].get(4) # None

\.get\( ~ 650 / 100K, but it includes all mappings and probably not mappings at all.

I have thought about list.get approximately 20 times / 100K lines of code.

If it was implemented, I would probably use it more, but hard to tell how much, maybe good use cases would turn up once it is there. Don’t think it would be more than 100.

1 Like

I went looking for a list.get within the last month. I cannot remember the exact details of it but when I typed pydoc list.get in my terminal and got nothing back it made me second think what I was doing and I believe I ended up using a different data structure than a list.

I’ll report back tomorrow when I can get the code

I think both the strongest argument for and against this proposal is duck typing.

Being able to pass both a list in addition to any Mapping or arbitrary object that implements a get, rather than just the former can be very convenient, since it reduces branches and leads to simpler interfaces without unions or duplicate methods.

But this is also its biggest danger if you don’t make use of static typing. Since get doesn’t raise, everything can look fine, without being fine, i.e. you can pass a list into function that can never yield a result from the list. So this can break your code in subtle ways, where you now need to either use static typing or be very vigilant in unit testing, in order to avoid that class of bug. Is get on list really convenient enough to outweigh the cost of this sharp edge? I personally don’t believe so, but I also tend to almost always use static typing, so I don’t feel that strongly about it. But considering that we can’t add it to Sequence, I’ll likely never use it, I certainly won’t bother using a separate SequenceWithGet protocol to exclude perfectly valid sequences.

Small rant about the previous discourse concerning `Mapping.get`

As an aside, some people have argued about the benefit of get in mappings as if it were only a shorthand for x[foo] if x in foo else None, which is complete nonsense if you know anything about data structures and how they are implemented.

In a mapping get is more or less required for efficiency and depending on the implementation is either an atomic operation, or very close to it, so __getitem__, __contains__ and get are usually implemented using the same common atomic function with a small extra step each. So doing both __contains__ and __getitem__ is a duplication of work[1], so get is a natural method to have on a Mapping, whereas anywhere else you’d have to justify it with a more compelling argument, than “we have decided to add it on dict”.


  1. some of this is mitigated by the fact that the hash of hashable objects is not supposed to change, so it theoretically only has to be calculated once and str does take advantage of that ↩︎

This has been proposed several times before, and each time only partial and unusable examples have been provided.

If first_face is equal to None, what are you going to do with the None value? Will you save it as None, or will the code flow change?

detected_faces = []

if first_face := detected_faces[0:1] or None:
    print(first_face)

else:
    print("No faces")

Note that detected_faces[0:1] is just a single use case of indexing brackets. You may need the first 2 faces detected_faces[:2] , the last 2 faces detected_faces[-2:] , etc.

In contrast to using a dict where you cannot retrieve multiple keys with square brackets, the get and setdefault methods provide added value beyond the basic use of square brackets for getting and setting a single value.

(The OP is no longer pursuing the proposal; I just added an argument for the record.)

What’s wrong with

try:
    first_face = detected_faces[0]
    print(first_face)
except IndexError:
    print("No faces")

If the code flow changes, just use a try…except. That’s what it’s for.

3 Likes

That stops working the moment you use a non-trivial function, as is the case with most EAFP, because what happens if whatever you replace print with throws an IndexError? EAFP is extremely prone to accidentally masking exceptions, unless you only ever use it on extremely tight expressions, at which point it’s usually much less readable than a proper method like get.

3 Likes

That’s what else: is for.

Well it can be either. How this is different from dict.get?

Maybe?

if first_face := (detected_faces[0:1] or [None])[0]:

Otherwise it doesn’t work.

And I don’t understand what this proves. Apart from the fact that list.get would be much simpler.

This is a good point. Doesn’t feel right to have list.get that only covers part of list.__getitem__ indices. It could be done with either:

Additional indexer:

a = [0, 1, 2]
print(list.get(None)[2:4])    # [2, None]

Or explicit slice argument:

list.get(idx: int | slice, default=None)
print(list.get(slice(2, 4), None))    # [2, None]

That would be useful for wider range of applications. E.g. padded centered rolling window.

3 Likes

I’m in favor of the explicit slice argument, didn’t even think about that option, that would be really cool.

A few weeks ago I wanted to separate a version to major, minor, patch without knowing if it has all parts (and didn’t want to install packaging just for that, can elaborate why), I wish I could have done:

major, minor, patch = version.split(".").get(slice(0, 3), None)
1 Like

That still requires an extremely tight expression in the try, so I don’t see how this addresses my criticism.

Why is it hard to add it to collections.abc.Sequence?

Because get can collide with methods on classes that inherit from Sequence? And it can break expectations about what get does on code that uses those objects?

It works, first_face is a list. The variable name is irrelevant:

default = None
if first_face := detected_faces[0:1] or default:
    print(first_face)

The code was intentionally constructed this way to enable using indexing brackets to retrieve multiple items.


I think this is beyond the scope of the list: “Get items from start to stop. If start >= length or stop >= length, fill with the default value.”

What I would often find useful is the set of functions

  • collections.keys(mapping_or_sequence),
  • collections.values(mapping_or_sequence),
  • collections.items(mapping_or_sequence),

that generalises keys(), values(), items() from mappings to mappings or sequences. (This allows, e.g., processing YAML data that can be list or dict in a uniform manner.)

Along the same lines, perhaps a functional API for collections could also include collections.get(), collections.setdefault(), etc., which then would not have the problem of a name clash.

But all these functions are so trivial that I usually just copy and paste the same small implementation I have :slight_smile:

1 Like

Not sure what your point is. If you’re concerned about the try being overly broad, you narrow it using tools like try/else, which will make the ‘try’ part tighter. But now you’d rather NOT have it so tight? In that case, go ahead and put more into the try block, especially since most code won’t raise IndexError.

PEP 463 was going to give an additional way to narrow the coverage of a try block, but it turns out, most people aren’t particularly bothered by the need to have entire statements in try blocks.

I think it is very much in line with with list:

In [69]: a = list()
In [70]: a[0:4] = [0, 1, 2, 3]
In [71]: a
Out[71]: [0, 1, 2, 3]

If a setter can go out of indexing scope, I can not see a good reason why such restriction should apply to get method.

1 Like

Yes, I think doing this in free functions is best. I’m not sure they would be popular enough yet to do in collections, but maybe somewhere outside of the stdlib for now?

I think people here are really undervaluing the cost of expanding interfaces. Interfaces should be small and focused. This ask reminds me of the person who wanted integers to be iterable so that he could do for x in 100.

1 Like

Do you have any particular classes in mind that inherit from Sequence and has a standard for implementing get method?

No, I don’t. But there is a lot of code that inherits from Sequence.

Rejection notice seems seems to give different reason why it didn’t go through. In a way, it even supports this proposal as it touches favourably on dict.get as opposed to try statement.

Where did you draw this conclusion from?

I completely agree. Don’t think anyone here thinks differently. The discussion is “given that interfaces should be small and focused, is this addition justifiable” and just stating the obvious is not a counter-argument.

Would like to hear how is this similar?

Considerable amount of subclasses of list that might be implementing it, don’t see how it is much different for Sequence. Without any proof this is an empty argument. I see that it might be more, but I don’t think it is as much of a difference as you conveyed.

2 Likes