Float contained in range

I have found that checking if a float number falls inside a range() doesn’t work, always giving False:

>>> 1.0 in range (0, 2)
>>> 1.5 in range (0, 2)

I can understand that range() can only accept integer values, but checking if it contains a float number should works. At worst, it could automatically convert the float number to an integer/floor, since range() is left-inclusive right-exclusive, so just the integer part of the float number would still be contained in the range, and correctly report True:

>>> int(1.5) in range (0, 2)

This would be a breaking change and as such is highly unlikely to happen. Also, range(x) is an iterable, and (x in iterable) == (x in list(iterable)) is a common assumption for iterables.

That would be a breaking change. Why not just call int() yourself?

That could be a solution, just only I was surprised that it didn’t work on the first place since both are numbers, and though __contains__() was just only checking for the being and end of the range, no matter the value type. If it would be such a huge breaking change, then maybe we can add this corner case to the documentation and show the usage of int() as an example of how to achieve this, what do you think?

I agree that, if you assume that a Python range and the mathematical concept of a range on the number line are the same, then yes this should work. However, they are not the same, range is a set of integers in a duck-patterned trenchcoat and thus cannot contain floats. This is one of those concepts that don’t translate neatly from math to programming, and I think you just have to accept that these are two different worlds. You could write your own class that behaves like both a range and mathematical range and use that wherever this property would be useful.

Edit: TIL out range is smart about contains, I assumed it had to iterate through the entire thing to find it. Documenting the cast seems like a nice idea

1 Like

It’s documented as “For a positive step , the contents of a range r are determined by the formula r[i] = start + step*i where i >= 0 and r[i] < stop .”

So it’s pretty clear it’s only ints in the range. I think any mention of non-ints would only confuse the documentation, which is already plenty long.


So, maybe it’s a misnomer, that leads to think that it could work on the mathematical concept, meanwhile we would see range() as a “list generator”, and so in keyword is checking the value is in the list, isn’t it? Then in that case, we should add the int() example in the docs, in case somebody else got to the same misconception.

From a mathematical perspective, range is more like an arithmetic sequence, and the documentation indeed refers to it as a sequence. It doesn’t represent an interval. range(0,2) represents the sequence “0, 1”, and 1.5 is not in that sequence.

I can see how just going off the name “range” one might expect the behavior mentioned in the OP, but no one should be going just off the name. :slight_smile: I think the documentation makes it quite clear there’s no reason to expect the mentioned behavior.


Apart from all the comments on range, what I don’t understand is why don’t you just use 0 <= x < 2 if you want to check if a float is in a given range?

I wouldn’t even use range like this for integers, as it’s slower than the chained comparison.


That’s what we are currently using, just seemed more clean to use the range(), and also we though that it would translate to 0 <= x < 2 under the hood.

I have sympathy for what the name implies for people who haven’t internalized how Python works, names are important. In other more weakly typed languages I would maybe make perfect sense to check if a float is contained in a range object.

As a matter of fact, no amount of documentation can prevent people from making false assumptions about the language.


It does work, and it can give True:

>>> 1.0 in range(0, 2)

The reason you get False for 1.5 is because 1.5 is conceptually actually not in that range.

It does accept floating point numbers, and it does work. As you saw in your example, it returns a result (False) - it doesn’t raise a TypeError nor a ValueError.

No, it should not and will not do that, because that is conceptually not a correct result.

range does not mean “every possible number between these values”. It very specifically represents only integer values.

Think about it: if range(0, 2) is supposed to contain 1.5, conceptually, then a loop like for i in range(0, 2): should make i be equal to 1.5 at some point, right? A loop is supposed to loop over everything that is in a sequence, right? Otherwise it can’t be considered a sequence. And if we do that, then the loop is going to have to consider all the other floating-point values, and take a very long time indeed (and also cause other problems, such as giving values for i that can’t be used to index into another sequence).

Please also remember that range allows a third step argument:

>>> 1 in range(0, 10, 2)

Surely you don’t think this result is also wrong?

Unfortunately, it is only “smart” for an integer input:

>>> import timeit
>>> timeit.timeit('0.1 in range(10000000)', number=1)
>>> timeit.timeit('-1 in range(10000000)', number=1)

Which reminds me, I was going to submit that as a bug…

Make sure you read Fast path for "float in range" checks first.

More specifically: A range() is a selection of integers, not even necessarily all of them. The default step is 1, meaning you’ll get every integer from start to just before stop, but any higher step and it isn’t even all integers.

Keeping stepped ranges in mind is a good way to remember that even an unstepped range is a disjoint collection of integers, not a continuous range of real numbers.

1 Like