Though I could not find the source code, based on the results I posted in my question it is very likely that __str__ and __repr__ for float are implemented along the lines:
class float:
def __repr__(self):
return ??? # core implementation, maybe from C???
def __str__(self):
return self.__repr__() # falls back to __repr__()
This logic explains all four cases. For example, in case 2b, calling repr(storage) calls Storage.__repr__(storage), which then calls float.__str__(storage), which falls back to float.__repr__(storage), which is finally resolved to Storage.__repr__(storage) (because of the method override and OOP). The loop closes and goes into an infinite recursion, or at least up until the max depth is reached and we get a RecursionError: maximum recursion depth exceeded while calling a Python object