Possibility to exclude ranges from range

Hi everyone,

I recently came across a scenario where I needed to iterate over a large range of numbers but completely exclude a specific sub-range in the middle (for example, looping from 1 to 100, but skipping 50 to 60).

Furthermore, a common extension of this problem is needing to exclude multiple, arbitrary sub-ranges at once (e.g., skipping 50..60, 75..80, etc.).

Currently, the idiomatic ways to solve this in Python are:

An explicit if/continue block:

Python

for i in range(1, 101):
    if 50 <= i <= 60 or 75 <= i <= 80:
        continue
    # do something

Chaining multiple split ranges via itertools.chain:

Python

from itertools import chain

for i in chain(range(1, 50), range(61, 75), range(81, 101)):
    # do something

Or using an inline list comprehension to filter dynamically:

Python

for i in [x for x in range(1, 101) if not (50 <= x <= 60 or 75 <= x <= 80)]:
    # do something

While these approaches work , they quickly become hard to read, require precise mental math regarding the exclusive stop boundaries , or create unnecessary intermediate lists in memory just for filtering.

To improve expressiveness, I would love to propose a way to exclude one or multiple ranges. Taking inspiration from Python’s argument unpacking (*args), the feature could accept a variable amount of exclusion ranges:

Variant 1: A .skip() method on the range object (Backward-compatible)

This uses a *args style signature (def skip(self, *ranges)), allowing developers to pass as many exclusion ranges or tuples as they need without introducing a new keyword.

Python

# Proposed syntax with a single exclusion:
for i in range(1, 101).skip(range(50, 61)):
    print(i)

# Proposed syntax with multiple exclusions (*args style):
for i in range(1, 101).skip(range(50, 61), range(75, 81)):
    print(i)

Variant 2: A new skip operator/keyword (Syntactic Sugar)

An alternative would be an intuitive infix operator that accepts either a single range or a collection/tuple of ranges.

Python

# Proposed syntax:
for i in range(1, 101) skip (range(50, 61), range(75, 81)):
    print(i)

I am aware that introducing a new keyword like skip comes with major backward-compatibility hurdles. However, the .skip(*ranges) method approach seems like a highly ergonomic, non-breaking addition to Python’s built-in range type.

What are your thoughts on extending range to natively support one or multiple exclusions? Has something similar been proposed or rejected in the past?

Thanks for your feedback!

1 Like

I don’t think it’s necessary for the builtin range to do this, but you could define your own. Part of the reason here is that this would get extremely messy if you had different steps, so this would be easiest done with a stepless range alternative. And rather than design new syntax, there’s an obvious one available to you: subtraction!

So here’s what I would recommend:

class MultiRange:
    def __init__(self, start, stop=None):
        if stop is None:
            start, stop = 0, start
        self.sections = [(start, stop)]
    def __sub__(self, other):
        # Go through all sections and exclude everything that's covered by other's sections
        # This may involve splitting a range, shortening it at one end, or leaving it as is.
        new_range = type(self)(0)
        # TODO: mutate new_range
        return new_range
    def __iter__(self):
        for start, stop in self.sections:
            yield from range(start, stop)

You may also want to be able to add ranges, too.

Something along these lines should work fairly well. You would be able to do something like:

for i in MultiRange(1, 101) - range(50, 61) - range(75, 81):
    print(i)
3 Likes

or with sets ?

for i in frozenset(range(1, 101)) - frozenset(range(50, 60)) - frozenset(range(70, 80)):
    print(i)

EDIT:

for i in sort(list(
        set(...)
        - set(...)
        - set(...)
    )):

Why frozen? And it might change the order of the resulting elements, which might be a problem.

2 Likes

This may be suitable for sets of integers in CPython, but I have to caution against iterating sets in any situation where the order matters. That’s not an implementation detail I would rely on

Thanks for the suggestion! A custom MultiRange class is definitely a clever workaround and would solve the problem for an individual project or a custom utility library.

However, the core motivation behind my proposal was to address this at the built-in language level.

If every developer has to implement their own MultiRange class or carry it around in a personal library across different projects, it defeats the goal of improving Python’s out-of-the-box ergonomics. Since range() is one of the most fundamental and frequently used built-ins for loops, having a native way to exclude sub-ranges would eliminate this repeated boilerplate entirely.

That’s why I felt a native .skip(*ranges) method on the existing range object would be so powerful—it keeps the standard library expressive and prevents developers from reinventing the wheel for a very common looping pattern.

And what does your AI say about having this on PyPI?

9 Likes

I’m actually sad there is no way to like your post multiple times now, but you are right, perhaps it would have been better not to use AI for the OP.

Nothing here seems overtly AI-generated to me. Whatever you think of AI, it’s not great to use it as a witch-hunt and possibly falsely accuse people.

10 Likes

Regardless of whether the OP is using AI to write posts, the more relevant thing is that the idea is not particularly useful.

If you want to skip a subset of a range there are lots of ways to achieve that with plain Python. If you want it to be super-performant then it’s time to implement your own extension, but again this is not a huge obstacle.

I don’t think I’ve ever wished for something like this, because it’s just not a common a use-case in real code–it sounds like a shortcut just for coding challenges.

1 Like

Agreed. I cannot remember that I ever wanted to remove certain ranges, rather than some indices contained in some set or satisfying some condition. If I could write (analogously to what is possible in comprehensions)

for i in range(a,b) if i is not in excluded_indices:
    ...

I probably would do that, but I am also completely fine with

for i in range(a,b):
    if i is in excluded_indices:
        continue
    ...

Well, you can write (removing the erroneous is)

for i in (i for i in range(a,b) if i not in excluded_indices):
    ...

But I think the version with a separate if statement is clearer to the reader.

That still supposes that this actually is a common case enough that it would need to be repeated.

I question both the hard to read argument for the itertools.chain based solution as well as the assumption that some skip parameters for the range function would somehow avoid the ambiguities around boundaries (rather than just define it’s own rules people would need to lookup or remember)

I find the itertools.chain method very readable and flexible and it also avoids building intermediate lists. I actually have a hard time coming up with an api for range that would be meaningfully shorter or compact. The one negative is really discoverability but I would balance that against the need actually being a lot rarer than assumed here.

That wouldn’t be the first on this forum but then respond to that. I agree that the second post has signs enough that I would be very surprised if it wasn’t generated It’s not egregrious enough to be that dismissive about it without addressing the question at all.

That said, please try to use AI where it helps clarity and make the arguments yourself if possible. We’re all here since we enjoy discussing with other people.

2 Likes

I actually think “ask your favourite chatbot whether this deserves in the Python standardlib” is genuine good advice. AI should be able to tell you ‘no’ and also explain why with more patience that you’re likely to find on a forum.
But then chatGPT does give me useful answers, and I can’t know it would give the OP equally useful answers.

I’ve not ever needed a range() with gaps in it ever, so I don’t think this is a very common desire. And you can achieve what you want very tidily with a 5-line function on a case-by-case basis, or using a class that would require about 20-30 lines to write.
Python does not add new syntax so that some people can avoid writing 30 lines of code per project. (Though the Annotated syntax that’s being discussed wouldn’t save that many lines per project would it? And it is still being seriously considered.)

2 Likes

It’s not really new syntax being added. Similar to e.g. strings supporting +, it’s just using existing syntax that’s being exposed via a method for the operator.

But this is a bit off-topic, so I’d like to get back to the topic. I think the general message most people here want to get to the OP stays the same, as adding ±2 lines for some feature that’s not used often doesn’t hurt a lot.

yes I should have use the word “features” instead of “syntax”. My bad.

For what it’s worth, I think you could write the desired class approximately like this

class Range:
    ranges: list[range]

    def __init__(self, start=None, stop=None, ranges=None):
        if ranges is None:
            ranges = []
        if start is not None:
            ranges += [range(start, stop)]
        self.ranges=ranges

    def __iter__(self):
        for r in self.ranges:
            yield from r

    def skip(self, start, stop):
        bot_ranges = [
            range(r.start, min(r.stop, start))
            for r in self.ranges
            if r.start < start
        ]
        top_ranges = [
            range(max(r.start, stop), r.stop)
            for r in self.ranges
            if r.stop > stop
        ]
        return Range(ranges=bot_ranges + top_ranges)

    def __repr__(self):
        return f"Range({self.ranges})"

The itertools approach remains the simplest way to concatenate generators generally.

The proposed “skip” keyword/method is likely complexifying the python API without being an essential addition.

For your insight, this is a cleaner syntax for a list concatenation :

list(range(5)) + list(range(8, 13))

Yet it would create temporary lists before the final one, thus is not the best performance option.

@hprodh Or:

*range(5), *range(8, 13)
3 Likes

Yes, I am using an LLM to help me structure my thoughts and translate them into clear English, as it is not my native language. I apologize if it sounds or ist AI-style. However, the idea for .skip() is entirely my own—I just found the current workarounds a bit cumbersome and wanted to see if others felt the same way.

I really want to thank my supporters here who stepped in to keep the focus on the actual technical concept rather than the drafting tools!

At the same time, I completely understand the perspective of the skeptics who want to keep the core language and the range object as minimal and pure as possible. Python’s simplicity is its greatest strength, and adding features blindly can clutter the language.

However, to address the feedback regarding itertools.chain vs. .skip():

While the typing effort for a single exclusion seems similar, the real power of a native .skip(*ranges) method lies in reducing cognitive load, avoiding off-by-one errors with Python’s exclusive upper boundaries, and—crucially—maintainability.

Consider this direct comparison when excluding the exact same three blocks:

Python

# The current way using itertools.chain:
# Highly prone to off-by-one errors; the original intent (1 to 100) is completely lost visually.
for i in chain(range(1, 30), range(41, 55), range(66, 80), range(91, 101)):
    ...

# The proposed ergonomic way:
# The main intent (1 to 100) remains perfectly clear, followed by a clean 'blacklist'.
for i in range(1, 101).skip(range(30, 41), range(55, 66), range(80, 91)):
    ...

Now, imagine you want to change the upper limit of the main loop later on (e.g., from 100 to 500).

  • With the chain approach, you have to find and modify the very last range at the end of the expression (range(91, 501)), which feels counter-intuitive and disconnected.

  • With the .skip() approach, you simply change the main range at the very beginning (range(1, 501)), while your blacklist remains completely untouched.

Python has a beautiful history of adding syntactic sugar and ergonomic features purely to make code more expressive and human-readable, even if a functional alternative already existed—think of the ternary operator (x if condition else y), the walrus operator (:=), or string .join().

The string .strip() method is another great example: we use it because it’s convenient and expressive, even though we could technically achieve the same result with manual string slicing. A .skip() method on range feels like a natural extension of that same pragmatic Python philosophy: making common, repetitive logic clear at a single glance without forcing developers to reinvent the wheel every time."

I think the principle “not every 3-line function needs to be a builtin” applies here. The usefulness of the feature is limited (I know you claim you’ve needed it often, but in my experience I’ve never seen anyone else express a need for this functionality), and it’s relatively easy to implement yourself in a few lines of code (you can make the solution as complex and general as you like, but the simple form is straightforward).

Yes, chain() obscures the upper limit to an extent. But there’s also:

for i in (i for i in range(1,101) if not (30 <= i < 40 or 55 <= i < 65 or 80 <= i < 91)):
    ...

That directly expresses the intent (i in the given range unless it’s in one of the excluded subranges). It’s a little verbose (“for i in (i for i in...” is clumsy) but that’s a known aspect of a standard language feature, and hardly a disaster.

Add to that the fact that supporting this within the existing range() object would probably add extra complexity, and hence worsen performance, even in the typical case of no exclusions. For something as commonly used as range, that’s not a good trade-off.

Sorry, but I’m -1 on the proposal.

5 Likes