This issue tracker has been migrated to GitHub, and is currently read-only.
For more information, see the GitHub FAQs in the Python's Developer Guide.

classification
Title: reduce() masks exception
Type: behavior Stage: resolved
Components: Interpreter Core Versions: Python 3.2
process
Status: closed Resolution: accepted
Dependencies: Superseder:
Assigned To: belopolsky Nosy List: ajaksu2, akuchling, belopolsky, doerwalter, rhettinger, vstinner
Priority: low Keywords: patch

Created on 2003-01-10 14:41 by doerwalter, last changed 2022-04-10 16:06 by admin. This issue is now closed.

Files
File name Uploaded Description Edit
diff.txt doerwalter, 2003-01-28 20:38
issue665761.diff Alexander.Belopolsky, 2010-05-01 01:25 Patch against revision 80673
issue665761-release27.diff belopolsky, 2010-08-09 19:18 Patch for 2.7 branch, revision 83896
issue665761-py3k.diff belopolsky, 2010-08-11 00:35
Messages (12)
msg13979 - (view) Author: Walter Dörwald (doerwalter) * (Python committer) Date: 2003-01-10 14:41
In the following test script
-----
class Test:
   def __iter__(self):
      raise IOError

reduce(lambda x,y: x+y, Test())
-----
the real IOError exception is masked, i.e. the traceback is
-----
Traceback (most recent call last):
  File "test.py", line 5, in ?
    reduce(lambda x,y: x+y, Test())
TypeError: reduce() arg 2 must support iteration
-----
but IMHO should be
-----
Traceback (most recent call last):
  File "test.py", line 3, in ?
          raise IOError
IOError
-----
This can be fixed by removing the
PyErr_SetString(PyExc_TypeError, "reduce() arg 2 must
support iteration") call in
bltinmodule.c/buildtin_reduce().
msg13980 - (view) Author: Jean-Paul Calderone (exarkun) * (Python committer) Date: 2003-01-11 17:08
Logged In: YES 
user_id=366566

the __iter__ method is supposed to return an object that
defines a 'next' method.  The returned object is the one
used for iteration, not the original.  So I believe the
error message is correct - Test does not support iteration.
 If you change the code to:

>>> class test:
...   def __iter__(self):
...     return self
...   def next(self):
...     raise IOError
... 
>>> reduce(operator.add, test())

You get the expected result...

Traceback (most recent call last):
&nbsp;&nbsp;File "<stdin>", line 1, in ?
&nbsp;&nbsp;File "<stdin>", line 5, in next
IOError
msg13981 - (view) Author: Walter Dörwald (doerwalter) * (Python committer) Date: 2003-01-13 13:18
Logged In: YES 
user_id=89016

The point is that Python/bltinmodule.c/builtin_reduce()
masks the error returned from PyObject_GetIter(). Errors
from PyIter_Next() are not masked.

What about the following example:

class LazyFile:
   def __init__(self, name, mode="r"):
      self.name = name
      self.mode = mode
   def __iter__(self):
      return open(self.name, self.mode)
  
import operator
  
f = LazyFile("does not exist")
  
s = reduce(operator.add, f)

LazyFile *does* support iteration, but the underlying
problem of the non existing file is masked. Removing the
call PyErr_SetString(PyExc_TypeError, "reduce() arg 2 must
support iteration"); in builtin_reduce(), will produce the
original exception "IOError: [Errno 2] No such file or
directory: 'does not exist'" and when the second argument is
not iteratable, the original exception is just as good:

>>> reduce(lambda x,y: x+y, 42)
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
TypeError: iteration over non-sequence
msg13982 - (view) Author: Raymond Hettinger (rhettinger) * (Python committer) Date: 2003-01-25 04:00
Logged In: YES 
user_id=80475

There's a lot of this going around.  map() and zip() have 
the same issue.  I recommend fixing them all.
msg13983 - (view) Author: Walter Dörwald (doerwalter) * (Python committer) Date: 2003-01-28 20:38
Logged In: YES 
user_id=89016

Attached is a patch that fixes reduce(), map() and zip().
Unfortunately this loses the information about which
argument triggered the exception (in map() and zip())
msg13984 - (view) Author: Raymond Hettinger (rhettinger) * (Python committer) Date: 2003-01-28 23:59
Logged In: YES 
user_id=80475

One way to mitigate the information loss is to mimic the 
style in zip() which only traps TypeErrors but passes 
through things like the IOError in your original example.  
msg13985 - (view) Author: Walter Dörwald (doerwalter) * (Python committer) Date: 2003-01-30 11:29
Logged In: YES 
user_id=89016

Trapping only TypeError won't help:

class LazyFile:
   def __init__(self, name, mode="r"):
      self.name = name
      self.mode = mode
   def __iter__(self):
      return open(self.name, self.mode)
  
import operator
  
f = LazyFile(42)
  
s = reduce(operator.add, f)

Here the open call will raise a TypeError, that is totally
unrelated to the iterator protocol.

The cleanest solution would really be exception chaining.
msg13986 - (view) Author: A.M. Kuchling (akuchling) * (Python committer) Date: 2006-12-21 15:35
Preserving the argument number seems difficult without exception chaining; perhaps a middle group would be to only replace TypeError exceptions.  Walter's comment at 2003-01-30 06:29 shows this can still be fooled, but a fix for 90% of the cases is still better than a fix for 0% of them.

The patch looks OK, but I think it should be reworked to take the middle-ground approach, replacing only TypeErrors.
msg81849 - (view) Author: Daniel Diniz (ajaksu2) * (Python triager) Date: 2009-02-13 02:31
Confirmed in trunk, rev69550.
msg104685 - (view) Author: Alexander Belopolsky (Alexander.Belopolsky) Date: 2010-05-01 01:25
I am attaching a patch with unit tests that implements the "middle-ground approach" making map and reduce behave the way zip is now.

I my view this slightly preferable to the "all the way" approach of letting all exceptions to propagate up.  My reasoning is that out of the three functions, zip, reduce and map, zip is probably the most common and changing its behavior is more likely to affect somebody than a change to the other two.
msg114066 - (view) Author: Alexander Belopolsky (belopolsky) * (Python committer) Date: 2010-08-16 19:10
Committed to py3k in r84098.   Accepting this change for py3k was an easy decision to make because zip and map already behave this way in 3.x.

I am inclined to reject this for 2.7, however.  While I agree that this is a bug, fixing it has a potential of breaking users' code.  I also note that for zip and map, 2.7 users can switch to izip and imap which don't have this problem.  Arguably, switching to izip and imap in new code is a good idea regardless of this issue.  While there is no similar work-around for reduce, I don't think this bug is important enough to introduce backward incompatible change in the stable series.
msg116339 - (view) Author: Alexander Belopolsky (belopolsky) * (Python committer) Date: 2010-09-13 18:48
Since noone have spoken in favor of 2.7 backport, I am closing this issue as committed to py3k.
History
Date User Action Args
2022-04-10 16:06:07adminsetgithub: 37755
2010-09-13 18:48:29belopolskysetstatus: pending -> closed
versions: - Python 2.7
messages: + msg116339

keywords: - needs review
resolution: accepted
stage: patch review -> resolved
2010-08-16 19:10:36belopolskysetstatus: open -> pending

messages: + msg114066
2010-08-11 00:35:58belopolskysetfiles: + issue665761-py3k.diff
2010-08-09 19:18:26belopolskysetfiles: + issue665761-release27.diff
2010-07-19 02:40:03belopolskysetassignee: belopolsky
versions: + Python 3.2
nosy: + belopolsky, - Alexander.Belopolsky
2010-05-05 00:37:11vstinnersetnosy: + vstinner
2010-05-03 17:58:09belopolskysetkeywords: + needs review
stage: test needed -> patch review
2010-05-01 04:29:51exarkunsetnosy: - exarkun
2010-05-01 01:25:33Alexander.Belopolskysetfiles: + issue665761.diff
nosy: + Alexander.Belopolsky
messages: + msg104685

2009-02-13 02:31:03ajaksu2setversions: + Python 2.7
nosy: + ajaksu2
messages: + msg81849
keywords: + patch
type: behavior
stage: test needed
2003-01-10 14:41:13doerwaltercreate