The constant costs are different. Just because two algorithms are O(1) does not make them equivalent. For one, dictionaries are only O(1) on average, while list lookup is O(1), always.
You need analyse the differences, but you are making a wrong assumption here. f1
looks up an element by index in list, then tests for equality. The f2
tests an element in a dictionary. However, a dictionary membership test which involves hashing and a test for equality (if there is an object at that location).
So the real difference here is that of hashing versus a list lookup. And the list lookup wins as its cost is still O(1), but hashing can be O(N), based on the size of the object being hashed. The difference explains the timings you see:
>>> import timeit
>>> a = [j * j for j in range(100000)]
>>> timeit.timeit('a[5000]', 'from __main__ import a', number=10**7)
0.26793562099919654
>>> timeit.timeit('_h(5000)', '_h = hash', number=10**7)
0.4080043680005474
The actual cost of hashing is normally averaged out over all dictionary membership lookups; together with the possible worst-case scenario of O(N) lookups for dictionaries, that makes dictionary lookups only average O(1). List lookups on the other hand are always O(1).