1
  1. Consider the following code:

    from itertools import chain
    list(chain(42))
    

    I am passing a non-iterable as an argument to chain and little surprisingly, I get exactly this error:

    TypeError: 'int' object is not iterable
    

    (Passing to list is only necessary because chain does not evaluate its arguments until the actual iteration.)

  2. If I use chain correctly, I can unpack the result as function argument:

    from itertools import chain
    foo = lambda x: x
    foo(*chain([42]))
    

    This runs without errors.

  3. Now, consider the combination of the two above cases, i.e., a chain with a non-iterable argument unpacked as function arguments:

    from itertools import chain
    foo = lambda x: x
    foo(*chain(42))
    

    As expected, this fails. In Python 3 this throws the same error as the first case. However, in Python 2.7.12, the error thrown is:

    TypeError: <lambda>() argument after * must be an iterable, not itertools.chain
    

    This does not make any sense to me. itertools.chain clearly is an iterable type: isinstance(chain(42),collections.Iterable) yields True. Also, it did not cause any problem in the second example. I would expect a similar error message as in case 2 or Python 3. What is the explanation for this error message?

Wrzlprmft
  • 4,234
  • 1
  • 28
  • 54
  • It's also worth noting that `chain(42)` doesn't throw an error in Python2 until you try to iterate over the result. – iafisher Apr 10 '17 at 14:27
  • @iafisher: That’s why I wrapped `list` around it. Also see my edit. – Wrzlprmft Apr 10 '17 at 14:36
  • *`itertools.chain` is an iterable type* <-- only when you've passed it iterables. Garbage in, garbage out. – wim Apr 10 '17 at 14:55
  • 1
    @wim That doesn't explain why Python 2 swallows the underlying exception whereas Python 3 does not. And `itertools.chain(x)` is always iterable, in the sense that it supports Python's iteration protocol. It's just that following that protocol fails if the original argument is not iterable itself. – Florian Brucker Apr 10 '17 at 15:01
  • @wim: `chain` is also an iterable with garbage arguments, otherwise the following would fail earlier than it does: `for x in chain(range(10),11): print(x)`. — *it's not obvious what exactly the problem is or what part needs any further explanation.* – The error message does not make any sense to me. If it makes sense to you, enlighten me with an answer (also see my edit). – Wrzlprmft Apr 11 '17 at 07:47
  • OK, so you just want to know why the error message is different? This is arguably a "RTFS" question, but I've posted an answer for you.. – wim Apr 11 '17 at 16:42

1 Answers1

1

The behaviour you are seeing is an attempt to give a clearer error message about what went wrong with the function call.

Python 2.7's way of determining if an object is iterable is just attempting to iterate it, and then catch the TypeError exception if necessary. It's not actually implemented in Python code, but that's still what happens in handling the function call syntax. Note: this has nothing to do with lambda, and a plain old def would have illustrated the example as well.

The function call is handled in CPython 2.7 by this C code:

static PyObject *
ext_do_call(PyObject *func, PyObject ***pp_stack, int flags, int na, int nk)
{
    ... snip ...

        t = PySequence_Tuple(stararg);
        if (t == NULL) {
            if (PyErr_ExceptionMatches(PyExc_TypeError) &&
                    /* Don't mask TypeError raised from a generator */
                    !PyGen_Check(stararg)) {
                PyErr_Format(PyExc_TypeError,
                             "%.200s%.200s argument after * "
                             "must be an iterable, not %200s",
                             PyEval_GetFuncName(func),
                             PyEval_GetFuncDesc(func),
                             stararg->ob_type->tp_name);
            }
            goto ext_call_fail;

    ... snip ...
}

I've truncated the code for brevity to show the relevant block: the starargs are iterated into a tuple, and if that fails with PyExc_TypeError then a new error is raised with the type and message matching what you've seen.

In Python 3, the function call C code was cleaned up and simplified significantly. Actually the ext_do_call function doesn't even exist any more, it was likely removed during implementation of PEP 3113. Now the exception from iterating a broken chain bubbles up unhandled. If you want to poke around in the current call code, you may start digging in Python/ceval.c::do_call_core.

wim
  • 338,267
  • 99
  • 616
  • 750