Question
Why Membership Testing in Python range() Is So Fast
Question
In Python 3, range() returns a range object rather than a fully built list. Since it produces values lazily, I would expect checking membership in a huge range to take a very long time.
For example, I would assume Python would need to generate many values before deciding whether this number is present:
1_000_000_000_000_000 in range(1_000_000_000_000_001)
However, this evaluates almost instantly, even with much larger numbers.
The same seems true for stepped ranges:
1_000_000_000_000_000_000_000 in range(0, 1_000_000_000_000_000_000_001, 10)
I also tried writing a simple generator-based version of range(), and membership checks are much slower:
def my_crappy_range(n):
i = 0
while i < n:
yield i
i += 1
What is the Python 3 range() object doing internally that makes membership testing so fast?
Short Answer
By the end of this page, you will understand that Python 3's range is not just a lazy generator-like object. It is a full sequence type that stores only start, stop, and step, and it can test membership with arithmetic instead of iterating through every value. That is why x in range(...) is usually extremely fast, even for enormous ranges.
Concept
Python 3's range is a compact arithmetic sequence, not a generator that computes values one by one for every operation.
A range object stores just three core pieces of information:
startstopstep
From those three numbers, Python can determine:
- the length of the range
- the value at a given index
- whether a value belongs to the range
For membership testing, Python does not need to generate all values. Instead, it uses math.
For a value x to be in:
range(start, stop, step)
all of these must be true:
xmust be within the range boundsxmust land exactly on one of the stepping points
That second rule can be checked with modulo arithmetic:
(x - start) % step == 0
So instead of checking one value after another, Python can answer the question in roughly constant time.
That matters because many real programs work with large numeric intervals:
- checking if an index belongs to a batch window
- validating IDs or timestamps against stepped rules
- iterating over ranges without storing large lists
- slicing and indexing efficiently
If range had to generate each value during membership checks, it would be unusable for very large spans.
Mental Model
Think of range as a mathematical rule, not a bucket of numbers.
For example:
range(0, 20, 5)
is not really storing:
- 0
- 5
- 10
- 15
Instead, it is more like a rule that says:
"Start at 0, keep adding 5, and stop before 20."
Now imagine someone asks, "Is 15 in there?"
Python does not walk through the whole sequence. It asks:
- Is 15 between the bounds?
- Starting from 0, can I reach 15 by adding 5 some whole number of times?
If yes, then 15 is in the range.
So range behaves more like a formula than a generated list.
Syntax and Examples
Basic syntax
range(stop)
range(start, stop)
range(start, stop, step)
Examples:
range(5) # 0, 1, 2, 3, 4
range(2, 8) # 2, 3, 4, 5, 6, 7
range(0, 20, 5) # 0, 5, 10, 15
Membership testing
print(10 in range(0, 20, 5)) # True
print(11 in range(0, 20, 5)) # False
Python can answer these quickly because it checks the arithmetic pattern.
Example: huge range
x = 1_000_000_000_000_000
print(x in range(x + 1))
This is fast because Python checks:
- Is
xbetween0andx + 1? - Does it fit the step pattern of
1?
Both are simple arithmetic checks.
Comparing with a generator
def my_range(n):
i = 0
while i < n:
yield i
i += 1
print(10 in my_range(20))
A generator does not know its full sequence structure in the same way a range object does. To test membership, Python must iterate through yielded values until it finds a match or runs out.
That means generator membership is generally linear time, while range membership is typically constant time for integers.
Step by Step Execution
Consider this example:
x = 14
r = range(2, 20, 3)
print(x in r)
The range represents:
2, 5, 8, 11, 14, 17
Python can determine membership without generating all of them.
Step 1: Check the bounds
The range starts at 2 and stops before 20, so valid values must be:
- greater than or equal to
2 - less than
20
14 passes this test.
Step 2: Check the step pattern
The step is 3, so every valid number must be reachable by repeatedly adding 3 to 2.
Python effectively checks:
(14 - 2) % 3 == 0
That becomes:
12 % 3 == 0
Which is true.
Step 3: Return the result
Since 14 is in bounds and matches the stepping pattern, the result is:
True
Now try a value that is not in the range:
x = 13
r = range(2, 20, 3)
print(x in r)
Python checks:
(13 - 2) % 3 == 0
That becomes:
11 % 3 == 2
So the result is False.
No iteration over every element is required for these integer checks.
Real World Use Cases
range membership logic is useful anywhere you are working with predictable numeric patterns.
Input validation
def is_valid_even_port(port):
return port in range(1024, 65536, 2)
This checks whether a port number is in an allowed even-numbered range.
Scheduling or batching
minute = 30
if minute in range(0, 60, 15):
print("Run scheduled task")
Useful for time slots, polling intervals, or repeating jobs.
Game development
if player_x in range(100, 201):
print("Player is inside the zone")
This can be used for numeric zones, score bands, or level thresholds.
Data processing
if row_index in range(0, total_rows, 1000):
print("Process checkpoint")
Helpful for checkpoints, chunking, and periodic actions.
API and business rules
def discount_tier(quantity):
if quantity in range(10, 20):
return 0.10
return 0.0
Useful when business logic depends on numeric intervals or stepped categories.
Real Codebase Usage
In real codebases, developers often use range as a lightweight sequence object, not just in for loops.
Validation and guard clauses
def set_retry_count(n):
if n not in range(1, 6):
raise ValueError("retry count must be between 1 and 5")
This is clean and expressive for integer validation.
Stepped rules
def is_valid_page_size(size):
return size in range(10, 101, 10)
Good for values that must follow a pattern.
Index and slicing support
range supports indexing and length efficiently:
r = range(10, 100, 10)
print(len(r)) # 9
print(r[2]) # 30
That makes it useful in code that needs sequence behavior without building a list.
Configuration-style checks
ALLOWED_RETRIES = range(1, 4)
if retries in ALLOWED_RETRIES:
...
This communicates intent clearly.
Why not use a generator?
Generators are great when values are produced dynamically and only consumed once. But they do not provide the same sequence features:
- no cheap membership test in general
- no direct indexing
- no length
- values are consumed as you iterate
range is better when the sequence follows a simple arithmetic rule.
Common Mistakes
Mistake 1: Thinking range is just a generator
range is lazy in the sense that it does not store all numbers, but it is not a generator.
r = range(5)
print(type(r))
It is a special sequence type.
Mistake 2: Assuming membership always iterates
Beginners often assume:
x in range(...)
must check each item one by one. For integer membership in range, Python uses arithmetic instead.
Mistake 3: Forgetting that stop is excluded
Broken expectation:
print(10 in range(0, 10)) # False
Why? Because range(0, 10) includes 0 through 9, not 10.
Mistake 4: Misunderstanding stepped ranges
print(11 in range(0, 20, 5)) # False
Even though 11 is between 0 and 20, it does not match the step pattern.
Mistake 5: Replacing range with a generator and expecting the same behavior
def my_range(n):
i = 0
while i < n:
yield i
i += 1
This can generate values one by one, but it does not support fast arithmetic membership checks like range does.
Mistake 6: Forgetting negative steps work differently
print(5 in range(10, 0, -1)) # True
print(0 in range(10, 0, -1)) # False
The direction and stop boundary still matter.
Comparisons
| Concept | Stores all values? | Supports indexing? | Membership speed | Reusable? |
|---|---|---|---|---|
range | No | Yes | Very fast for integers | Yes |
list(range(...)) | Yes | Yes | Fast, but uses more memory | Yes |
generator (yield) | No | No | Usually linear scan | Often one-pass |
range vs generator
rangeis a sequence with structure.- A generator produces values over time.
rangecan often answer questions with math.- A generator usually has to iterate.
range vs list
- A list stores every element in memory.
- A
rangestores only the rule. - A list is useful if you need mutation or all values materialized.
- A
rangeis better for large numeric sequences.
range in Python 3 vs older xrange in Python 2
In Python 3, range behaves like a proper sequence object with richer behavior than many beginners expect. That is part of why it feels more powerful than a simple lazy iterator.
Cheat Sheet
Quick facts
rangeis a sequence type, not a generator.- It stores
start,stop, andstep. - It does not build the whole list in memory.
- Membership for integers is fast because Python uses arithmetic.
Syntax
range(stop)
range(start, stop)
range(start, stop, step)
Membership idea
For:
x in range(start, stop, step)
Python checks roughly:
- Is
xwithin the bounds? - Does it fit the stepping rule?
Equivalent idea:
start <= x < stop and (x - start) % step == 0
For negative steps, the bounds logic is reversed.
Examples
10 in range(20) # True
10 in range(0, 20, 5) # True
11 in range(0, 20, 5) # False
0 in range(10, 0, -1) # False
Good use cases
- loop counts
- integer validation
- stepped numeric rules
- large sequences without memory waste
Important edge cases
stopis excludedstepcannot be0- negative steps count downward
- generators do not behave like
rangefor membership
FAQ
Why is x in range(...) so fast in Python?
Because Python does not iterate through every value. For integer membership, it checks bounds and step alignment using arithmetic.
Does range generate all numbers lazily like a generator?
Not exactly. It does not store all numbers, but it is a sequence object, not a generator.
Is range membership always constant time?
For normal integer membership checks, it is typically very fast because it uses math instead of scanning values.
Why is my custom generator-based range much slower?
A generator usually has to yield values one by one until it finds a match or reaches the end.
Does range use less memory than a list?
Yes. A range stores only the arithmetic rule, while a list stores every element.
Can I index into a range?
Yes.
r = range(10, 20)
print(r[3]) # 13
Does range work with negative steps?
Yes.
range(10, 0, -2)
This counts downward: 10, 8, 6, 4, 2.
Is the stop value included in a range?
No. The stop value is excluded.
Mini Project
Description
Build a small validator that checks whether user input belongs to allowed numeric ranges. This demonstrates how range can be used for fast membership testing in practical code, such as validating menu choices, retry counts, or stepped configuration values.
Goal
Create a Python script that validates integers against multiple range rules and prints whether each value is allowed.
Requirements
- Create at least two
rangeobjects with different steps. - Check several test values using the
inoperator. - Print clear output showing whether each value is valid.
- Include one descending range example.
- Do not convert the ranges to lists.
Keep learning
Related questions
@staticmethod vs @classmethod in Python Explained
Learn the difference between @staticmethod and @classmethod in Python with clear examples, use cases, mistakes, and a mini project.
Add Rows to a Pandas DataFrame in Python
Learn how to add rows to a Pandas DataFrame, why repeated row appends are slow, and when to use loc, concat, or record lists.
Call a Function by Name in a Python Module
Learn how to call a function by name in a Python module using strings, getattr, and safe patterns for dynamic function dispatch.