PEP Proposal: list.first() and list.last() for accessing most important list elements

Proposal

Create methods mylist.first() and mylist.last() for list as aliases for mylist[0] and mylist[-1] respectively.

Example

mylist = [0, 1, 2, 3]
assert mylist.first() == 0
assert mylist.last() == 3

empty_list = []
empty_list.first()  # raises "IndexError: list index out of range" or "IndexError: list is empty"

Why?

The first and last elements of a list are accessed the most by index directly. By aliasing them through clearly denoted functions readability is improved especially for new Python programmers.

Some other programming languages that already implement this:

What do you think? Should I create a PEP for that?

This seems totally unnecessary? Using the indexes is shorter and will be faster (since these methods are going to do the same thing but with the overhead of a method call)

21 Likes

New programmers are better advised to learn Python’s features directly. Indexing is powerful and flexible, and a feature like this would simply delay new users getting access to that power.

Consistency with other languages is not a useful goal in itself. You need to demonstrate that these methods would be useful in Python itself, not just claim “consistency” is sifficient. For example, a first method on lists would not work on tuples or deques. In Rust, first is generic, so it’s useful on all collection types.

No, I think you’d be better dropping the idea, to be honest.

16 Likes

Readability is one of Python’s basic principles. I would argue that the shortness of mylist[0] can hinder readability compared to mylist.first().

And yes, these methods would be functionally redundant just like mylist[0] is functionally redundant when you can use mylist.__getitem__(0). It is not about functional necessity but ergonomics.

As Paul pointed out, mylist[0] should be very readable for anyone who is comfortable in Python, because indexing is so common (as you pointed out!). Giving newcomers an idiosyncratic method for a single use-case, rather than moving them towards fluency, doesn’t seem like an improvement.

In any case, you certainly shouldn’t write a PEP without a core developer who is willing to sponsor it (which I suspect is…very unlikely to happen).

2 Likes

Not for reading however. Seeing code like mylist.last() should immediately convey the intention (especially with a docstring). mylist[-1] on the other hand takes some experience to interpret.

That is true. I made that point to show that first()and last() would not be radically new concepts in programming languages.

That is why I am asking here first :slight_smile:

1 Like

Your proposed version of list.first() would work identically to [0] indexing: an IndexError would be raised if the list was empty. This would actually be inconsistent with Rust’s implementation of these methods for Vec. In Rust, indexing into an empty vec panics (analogous to IndexError in Python), but Vec<T>::first returns Option<T>, for which the closest analogy in Python would be T | None.

6 Likes

Somewhat related:

3 Likes

first and last are the kind of methods an abstract data type implemented by lists might provide, but there’s no particular reason the list itself needs to expose them.

1 Like

I would rather like Iterable.first and Iterable.last.

  • Although iteraTOR.first is just another way to spell next(iterator), iteraBLE.first would forgo the need to do next(iter(iterable), default) or for-break trick.
  • Whereas, Iterator.last would be quite handy to consume an iterator in C speed, without needing to import collections.deque.
  • Ideally both first and last would throw exception if not provided with a default in case of empty iterables.

Sequence.last would probably make more sense than Iterator.last.

3 Likes

Do you say that because Iterator.last would fail to stop in case of infinite iterators? Or because it has a potential of people not understanding the fact that it will always be O(n)? Or the fact that it will consume the entire iterator?

If it’s any of the above, I think it is the same for list(iterator) or deque(iterator, maxlen=1|0). But I agree, the only benefit of iterator.last is avoiding deque import, which is not a huge win. Also, I now see, how iterator.last could be a footgun, whereas at least the deque trick is a better indicator of the fact that user knows what they are doing. And in case of IteraBLE.first/last, the deal-breaker is that Set and Mapping are are also iterable.

Another case I just thought of, that could be deemed problematic is: iterable.first/last may break expectation; it would modify Iterator, but not Sequence. So if there are more operations to follow, type narrowing becomes necessary.

For Sequence, personally, I can’t remember having to deal with a sequence where I couldn’t get the first and the last by [0|-1]. At least whenever I write a Sequence, I implement support for [-1]. But I have felt the need of first and last for iterables on multiple occasions (mainly for elegance). I think it all comes down to Iterators not having functionalities like peek and advance (which have been brought up and shot down multiple times). But I realize I am going horribly off-topic, and throwing a lot of ideas all at once, so I will stop.

Hi, and whilst I would like to extend my welcome to our community… …I don’t think this is a good idea. Python’s indexing capabilities are powerful, succinct, and not too hard to learn I think. Python language community try not to have too many , (sanctioned), ways of doing one thing to avoid confusion. Whilst mention of first and last might be necessary in absolute beginners code, you could augment it with a comment before it becomes unneccessary.
If your audience is none-Python programmers then write pseudo-code which should expand single-language idioms for clarity, and should not expect to be directly runnable in and of themselves.
If your audience are fellow Python programmers then expecting them to know accessing first and last elements of a list by index is OK. If you know your “Python” audience would have problems reading this then you should add extra comments describing this - but use the indexing to help them learn; and probably need more care in other aspects of Python shown too.

1 Like

I just want to remind people that it’s technology impossible to add methods to either of iterables or iterators, since these are duck typed protocols defined by having __iter__/__next__[1], with no further requirements.

While it might be fun to ponder what the semantics of these methods could be, the best we could hope for without a fundamental change to python semantics is a builtin function, or more likely, a function in itertools. (Or a method on a few builtin iterators/-ables, but I don’t think anyone considers that a benefit)


  1. or a compatible __getitem__ ↩︎

2 Likes

Yes, but to my understanding raising an IndexError is more pythonic than returning None. This is where the philosophies of Python and Rust clash. Rust expects you to handle the Option type while it would be easy to miss this silent failure in Python.

Or would you prefer returning None instead of raising an exception?

I agree that it’s more common in Python for failure to be indicated by raising exceptions rather than returning None. But if we say it’s going to work exactly the same way as indexing in Python, that removes one of the key reasons why Rust has both indexing and a Vec::first method (so that the language offers a type-safe method of accessing the first element of a maybe-empty vec, as well as the possibly-panicking method that is indexing)

A method adds overhead and if it contributes no additional functionality, and someone thinks using 0 or -1 as indices is too difficult, then why not do something trivial like set up some constants of sorts like:

FIRST = 0
LAST = -1
SECOND = 1
THIRD = 2
PENULTIMATE = -2

# Now use these instead of mylist.first(), ...
mylist[FIRST]
mylist[LAST]
mylist[SECOND]
mylist[PENULTIMATE]

I will say anyone wanting to learn a language like python really should learn the indexing techniques as asking for something like all but the first does not lend itself to this approach.

Sometimes making things simpler can merely make it more complicated.

2 Likes

Slightly shifting from what the OP raised, I am asking for examples of what value may be added by having or adding sort of getters for the first or last elements of the list while leaving the list unchanged?

I mean, for example, if I had my own data structure and I wanted to intercept access to the data and do something such as check if request should be honored, or log that it happened or slow them down by only allowing one request per minute, or record how many times it has been asked for, or even note if the underlying content has changed since the last time and return a cached copy if not, I can imagine such interventions making sense.

But a python list is simply a list. It is designed to do very little and do it fairly well. If you want other functionality, you may be able to subclass it or use some other data construct.

What the OP, who may be un-excellent, has not mentioned may actually be valid. If the focus is on new users, and especially those whose background (or intial programming language) has a prejudice that numbering start with the number 1, then he may have a point.

mylist[1] is how many languages would access the first. Some do not really have any way to access the last except by some stratagem like mylist[len(mylist)] and a possible crutch when starting to use python lists might help them avoid having to learn the bitter reality of a zero based left-to right indexing with a negative number based right to left indexing that we all know and love!

i have a use-case where the .first and .last properties of an empty sequence are fine to be None values. but it would be wrong beahviour for sequences generally. context matters with such higher level (regarding the expressivenes compared to index notation) attributes and it’s thus recommedable to implement case-related extensions based on abstract base classes.

1 Like