You can check what the bytecode looks like for those conditions:
In [1]: import dis
In [2]: dis.dis(lambda: 'Hi' in x)
1 0 LOAD_CONST 1 ('Hi')
3 LOAD_GLOBAL 0 (x)
6 COMPARE_OP 6 (in)
9 RETURN_VALUE
In [3]: dis.dis(lambda: x.find('Hi') != -1)
1 0 LOAD_GLOBAL 0 (x)
3 LOAD_ATTR 1 (find)
6 LOAD_CONST 1 ('Hi')
9 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
12 LOAD_CONST 3 (-1)
15 COMPARE_OP 3 (!=)
18 RETURN_VALUE
As you can see the find
versions does a lot more, in particular it is doing an attribute lookup which is not needed for the in
operator.
I must also say that in
makes it more explicit that you are checking for the presence of a substring and not of its position, and thus it is more readable.
In terms of speed they should be perfectly equal for any reasonable size of strings. Only for the smallest strings the attribute lookup has a significant impact, but in that case the condition is checked very fast anyway.
The third option would be to use index
and catch the exception:
try:
string.index(substring)
except IndexError:
# not found
else:
# found
Although this cannot be expressed as a simple expression.