The observed behaviour results from the combination of two things: Properties of the random numbers to be generated, and the way Python hashes floats.
a. It is correct and desired that random.random() does not generate all possible, i.e. representable, values of data type float. In fact, it practically will produce a small subset only, in order to deliver a uniform distribution.
Technically, a random generator may produce a fix point value with 53 binary digits (and just convert them to the float format without loss of information). The resulting 2^53 numbers are spread out uniformly In the interval [0, 1[. Select each of them with the same likelihood and you'll have uniformly distributed random numbers (see also https://github.com/python/cpython/blob/9d38120e335357a3b294277fd5eff0a10e46e043/Lib/random.py#L822).
The float format in turn can represent many more different values, in particular within the interval [0, 2^(-53)[. This is a very small portion of [0, 1[ only, but most representable float values within [0, 1[ will be found within this small interval, e.g. all the values with any mantissa and exponents between -54 and -1023. (In theory, a generator of uniformly distributed numbers could be allowed to produce any of these small values as well, but with correspondingly small likelihood and you just may never observe any of these.)
b. Python's hashing algorithm for floats maps integer and fraction part bits to an 64 bit int value, virtually by using shift and or operations. For random numbers based exclusively on digits with positional values 2^(-1) to 2^(-53) this results in hash values using exclusively bits number 8 to 60. Bits 0 to 7 and above 60 always remain 0.
The following program may just serve as an illustration which produces hash values identical to the built-in hash function:
def my_hash(x: float) -> int:
num_bits = 61
neg, x = x < 0, abs(x)
# separate integer part (h) and fraction part (f)
h = int(x)
f = x - h
# fold integer part to bits no. 0...num_bits-1, aligning 2^0 to bit 0.
while h >= 2 ** num_bits:
h = (h & (2 ** num_bits - 1)) ^ (h >> num_bits)
# fold fraction part to bits no. 0...num_bits-1, aligning 2^-1 to bit num_bits-1.
while f != 0:
f *= 2 ** num_bits
h ^= int(f)
f -= int(f)
return h if not neg else -h
Note that numbers outside the fix point scope described in a. above will well lead to non-zero digits bellow bit 8 as well.