# 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)
True
>>> 1.5 in range (0, 2)
False
``````

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)
True
``````

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.

3 Likes

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. I think the documentation makes it quite clear there’s no reason to expect the mentioned behavior.

2 Likes

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.

7 Likes

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.

4 Likes

It does work, and it can give `True`:

``````>>> 1.0 in range(0, 2)
True
``````

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)
False
``````

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)
0.3919527439866215
>>> timeit.timeit('-1 in range(10000000)', number=1)
6.4820051193237305e-06
``````

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