Proposal: Add a `limit` parameter to `enumerate()`

I propose adding an optional limit parameter to the built-in enumerate() function to restrict the number of iterations.

Limiting iteration is a common task, but current solutions are verbose:

# Current approach: requires import and nesting
from itertools import islice

for i, item in islice(enumerate(data), 5):
    ...

Alternatives have downsides: slicing creates copies, and zip(range(n), data) ignores the start parameter.

Update the signature to: enumerate(iterable, start=0, limit=None)

Examples:

# Stop after 10 items
for i, line in enumerate(file, limit=10):
    print(i, line)
# Works with start
for i, item in enumerate(data, start=1, limit=3):
    print(i, item)

It makes a common pattern more readable and removes the need for itertools.islice in simple loops.

Thoughts?

You could zip(range(start, start+n), data) perhaps?

9 Likes

No thanks. Enumerate is for enumerating, not limiting anything regarding the length of the iteration.

If anything, we should add itertools with clearer intent for take, skip, step_by, nth, sllice, which are all things that islice can do but are verbose to do.

But a parameter on an unrelated iterator? At that point just add everuthing as parameters to map (or worse, find a way to add even more new special syntax to comprehensions)

12 Likes

But I think enumerate(data, s, n) is Superior. Below are my test results just now:

Test Code

from dis import dis
from itertools import islice
from sys import version
print("Version:", version)
print("Note: n = e - s")
xxx = ("zip(range(s, e), data)",
       "enumerate(data[:n], s)",
       "islice(enumerate(data, s), n)",
       "enumerate(data, s, n)  # Hypothetical syntax")
for i, x in enumerate(xxx, 1):
    print("=" * 48)
    print("#", i, ".", x)
    print("-" * 48)
    dis(x)

Bytecode Output

Version: 3.14.2 (tags/v3.14.2:df79316, Dec  5 2025, 17:18:21) [MSC v.1944 64 bit (AMD64)]
Note: n = e - s
================================================
# 1 . zip(range(s, e), data)
------------------------------------------------
  0           RESUME                   0
  1           LOAD_NAME                0 (zip)
              PUSH_NULL
              LOAD_NAME                1 (range)
              PUSH_NULL
              LOAD_NAME                2 (s)
              LOAD_NAME                3 (e)
              CALL                     2
              LOAD_NAME                4 (data)
              CALL                     2
              RETURN_VALUE
================================================
# 2 . enumerate(data[:n], s)
------------------------------------------------
  0           RESUME                   0
  1           LOAD_NAME                0 (enumerate)
              PUSH_NULL
              LOAD_NAME                1 (data)
              LOAD_CONST               0 (None)
              LOAD_NAME                2 (n)
              BINARY_SLICE
              LOAD_NAME                3 (s)
              CALL                     2
              RETURN_VALUE
================================================
# 3 . islice(enumerate(data, s), n)
------------------------------------------------
  0           RESUME                   0
  1           LOAD_NAME                0 (islice)
              PUSH_NULL
              LOAD_NAME                1 (enumerate)
              PUSH_NULL
              LOAD_NAME                2 (data)
              LOAD_NAME                3 (s)
              CALL                     2
              LOAD_NAME                4 (n)
              CALL                     2
              RETURN_VALUE
================================================
# 4 . enumerate(data, s, n)  # Hypothetical syntax
------------------------------------------------
  0           RESUME                   0
  1           LOAD_NAME                0 (enumerate)
              PUSH_NULL
              LOAD_NAME                1 (data)
              LOAD_NAME                2 (s)
              LOAD_NAME                3 (n)
              CALL                     3
              RETURN_VALUE

Key Findings

Method 1, zip(range(s, e), data), requires two function calls and involves no slicing or imports. Method 2, enumerate(data[:n], s), uses one function call but introduces BINARY_SLICE, creating memory overhead. Method 3, islice(enumerate(data, s), n), involves two function calls and requires an import. The hypothetical Method 4, enumerate(data, s, n), achieves the objective with a single function call, zero slicing, no imports, and the fewest instructions

Why Method 4 is Better

Minimal function calls: Methods 1 and 3 use nested calls (2 CALL instructions), whereas Method 4 uses just one. This reduces stack frame creation and execution time

Zero slice overhead: Unlike Method 2, which utilizes BINARY_SLICE to copy data, Method 4 avoids memory allocation and data duplication, making it suitable for infinite iterators

No import dependency: Method 3 requires itertools.islice, while Method 4 uses the built-in function directly

Cleanest bytecode: Method 4 produces the shortest instruction sequence, simply loading arguments and executing a single call

I hope you can accept my proposal. Thank you

Yes, although it’s possible, it may not be very good. I think it should be like my new reply :slight_smile:

I’ve addressed this in the comment above, please take a look :slight_smile:

Of course, changing ‘limit’ to ‘end’ is also possible

Yep, just read it. I see your point about performance, both for raw execution or import time. They are always valid concerns, but not enough for me to agree with you.

First of all, unless you do that in a nested loop, the function call overhead will be dwarfed by the actual iteration time.

Furthermore, in a general sense, I’m strongly convinced that if you choose to use python, you made the conscious trade-off of prioritizing code clarity and dev speed versus raw speed for every LOC.

Sure you can use polars or numpy for “large” work, but as a whole, your python program will be very slow anyways compared to any compiled language.

And since clarity of code != conciseness of code, I do think that prioritizing said clarity over micro optimisations like this is very often a preferable choice.

Bit of a rant on iterators below, feel free to skip.

I have a strong view on iterators: they should all do a single job, with the least possible amount of parameters. I really like Rust approach with clearly named methods for every behavior. fold and reduce, sort and sort_by, for example, without “hidden” parameters that completely transform the behavior of the function. So I’m won’t be the easiest one to convince unfortunately. I even do think , for example, that the map approach with multiple iterables is confusing and should have been separated in a new object.

Since python doesn’t have fluent syntax as the standard (e.g range(10).iter().map().enumerate().take(4).collect(list), and instead prioritized comprehensions and imperative for loops, iterators already feel kind of a black box: you are encouraged to inline your logic in a list comprehension, but at the same time to use provided builtins like zip or enumerate that are, at the core, a completely dofferent way of writing iterations. Thus, the more and more we add additional parameters, the more and more we encourage a way of writing iterations were maybe most of the logic is imperative, or functional, which is readed in a completely different way

3 Likes

For simple loops I tend to use an imperative style:

for i, item in enumerate(data):
    if i > 10:
        break
    print(i, item)
7 Likes

Overheads are only relevant when they make a material difference in context. Otherwise, you’re playing around with loose nanoseconds while ignoring microseconds. Clean bytecode doesn’t matter much - what your proposal does is move the functionality into the enumerate call, so you don’t see it. (For an extreme example of clean bytecode, look up the HQ9+ language, which has VERY clean bytecode for a VERY specific set of operations. It’s also completely useless for doing actual work.)

Everything is a tradeoff. Having complicated builtins might help your specific case, but it means everyone has to pay the price for them. Instead, simple tools can be built into larger patterns in much more reasonable ways.

2 Likes

I think runtime would be more useful.

islice will not be far off - it doesn’t have start - no wasted work.

And range will likely be just as fast.

I can’t say I’ve ever found myself needing to enumerate with a limit like this. Is it really that common of a pattern?

4 Likes

-1. Stopping iteration early is not specific to enumerate. Orthogonal generic solutions are better.

islice: Python intentionally does not have thousands of builtins, so as to avoid imports. Composition is fundamental to programming.

if stop_condition: break: fine if stop condition needs to be dynamically calculated

5 Likes

Just this week I was thinking about “I wish enumerate had a ‘step’ argument, like range does”, so that I could change:

class WordsToBytes(codecs.IncrementalEncoder):
    def encode(self, input: tuple[int], final = False):
        ret_array = bytearray(2 * len(input))

-        for idx, word in (
-            (2 * i, w)
-            for (i, w) in enumerate(input)
-        ):
+        for idx, word in enumerate(input, step = 2):
            ret_array[idx + 0] = (word >> 8) & FULL_BYTE_MASK
            ret_array[idx + 1] = (word >> 0) & FULL_BYTE_MASK
            # Tiny bit wasteful, but demonstrates the pattern

        ret = bytes(ret_array)

        return ret

    def feed_eof(self) -> None:
        # button_that_does_nothing.jpg
        self.encode(tuple(), final = True)

Seems related / an okay idea, but it was rejected ~20 years ago: issue892804.

The point being if limit were added (which I’m -1 on, and it seems unlikely), could it be a keyword-only argument? Thanks.

1 Like

You can use:

for idx, word in zip(itertools.count(step=2), input):

The iterator tools in Python are really good at providing a little bit each, and being combinable in powerful ways.

10 Likes

The slice subscript might be a syntax of choice for iterators, similarly as for the lists.
But for example, enumerate(items)[0:10:2] would create a different type than enumerate
Also consuming my_iterator[0:10:2] would basically consume the 10 first elements of my_iterator while yielding 5 of them…
This might yield caveats and confusion.

really, I believe this is some confusign semantics here.

If I want to iterate over a subset of the data, I have to throw something to subset the data - quite often, a slice - if it is interactive, islice will do.

THEN…after that, if I want indexes to that data, I will wrap my subset iterable in enumerate.
Why do the “thing to give me an index of the item” should control 'how many items I get out of this"??

6 Likes

Is there a large use on existing code? I usually search on sourcegraph. The same applies to the proposal.