A string S is a substring of the string T if and only if there exist an index i such that T[i:i+len(S)] == S. When S is the empty string you have T[i:i] = '' = S so all the results are correct.
Also note that T.index('') returns 0 because index returns the first index in which the substring appears, and T[0:0] = '' so that's definitely the correct result.
In summary, the empty string is a substring of every string, and all those results are a direct consequence of this.
Also note that this is peculiar to strings, because strings are sequences of characters, which are themselves strings of length one. For other kind of sequences(such as lists or tuples) you do not get the same results:
>>> (1,2,3).index(())
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: tuple.index(x): x not in tuple
>>> [1,2,3].index([1,2])
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: [1, 2] is not in list
>>> [] in [1,2,3]
False
That's because list and tuple only check for members, and not for sub-lists or sub-tuples, because their elements can be of arbitrary type.
Imagine the case ((1,2),1,2).index((1,2)). Should index check for "sub-tuples"(and thus return 1), for members(and thus return 0) or do some ugly mixture(e.g. first check for sub-tuples and then for members)? In python it was decided to search for members only, since it is simpler and it's usually what you want. Checking for sub-tuples only would give really odd results in the general case and doing "mixtures" would often yield unpredictable results.