The iterator protocol doesn't actually require a type to support __iter__
. It requires a type to either support __iter__
, or __getitem__
with sequential integer arguments starting from 0. See the iter
function for the best explanation of this in the docs.
So, hasattr(x, "__iter__")
will give you false negatives if testing whether something is iterable.
So, how can you do this? Well, the right way, even if you don't like it, is:
try:
i = iter(x)
except TypeError:
# not iterable
Also, note that, as the docs for hasattr
explain:
This is implemented by calling getattr(object, name)
and seeing whether it raises an exception or not.
So, really, you're not avoiding exceptions at all; you're just coming up with a more convoluted way to raise an exception and hide that fact from yourself.
But meanwhile, iteration is a red herring in the first place. The in
operator is implemented with the __contains__
method. Container types that don't define a __contains__
method will fall back to iterating and comparing, but types aren't required to implement it that way. You can have a __contains__
that's much faster than iterating could be (as with dict
and set
); you can even be a container without being an iterable. (Note that the collections
module ABCs have separate Container
and Iterable
bases; neither one depends on the other.)
So, if you really wanted to do this without any exception handling, how could you?
Well, you have to check that at least one of the following is true:
x
has a __contains__
method.
x
has an __iter__
method.
x
has a __getitem__
method that, when called with the number 0
, either returns successfully or raises IndexError
.
Even if you accept that the last one can't possibly be tested without actually trying to call it with the number 0
and just assume that having __getitem__
is "close enough", how can you test for this without relying on exceptions?
You really can't. You could, e.g., iterate over dir(x)
, but that won't work for classes that define __contains__
dynamically, e.g., in a __getattr__
method that delegates to self.real_sequence
.
And, even if you could, what happens if you have, say, a class that defines __contains__
as taking no arguments? The attribute is there, but in
is still going to raise a TypeError
.
And all of this is ignoring the (implementation-dependent) rules on which special methods are looked up on the object and which on the type itself. For example, in CPython 2.7:
>>> class C(object): pass
>>> c = C()
>>> c.__contains__ = lambda self, x: return True
>>> hasattr(c, '__contains__')
True
>>> c.__contains__(2)
True
>>> 2 in c
TypeError: argument of type 'C' is not iterable