[Python-Dev] PEP 463: Exception-catching expressions

Yury Selivanov yselivanov.ml at gmail.com
Fri Feb 21 20:04:45 CET 2014


Thank you for writing this PEP, Chris. I'm impressed by the
quality of this PEP, and how you handled the discussion on
python-ideas.

I initially liked this idea, however, after reading the PEP
in detail, my vote is: -1 on the current syntax; -1 on the
whole idea.

On 2/20/2014, 10:15 PM, Chris Angelico wrote:
> [snip]
> * dict.get(key, default) - second positional argument in place of
>    KeyError
>
> * next(iter, default) - second positional argument in place of
>    StopIteration
>
> * list.pop() - no way to return a default
We can fix that in 3.5.
> * seq[index] - no way to handle a bounds error
We can add 'list.get(index, default)' method, similar to
'Mapping.get'. It's far more easier than introducing new
syntax.

> * min(sequence, default=default) - keyword argument in place of
>    ValueError
>
> * sum(sequence, start=default) - slightly different but can do the
>    same job
'sum' is not a very frequently used builtin.

I think the Motivation section is pretty weak.

Inconvenience of dict[] raising KeyError was solved by
introducing the dict.get() method. And I think that

dct.get('a', 'b')

is 1000 times better than

dct['a'] except KeyError: 'b'

I don't want to see this (or any other syntax) used by
anyone.

I also searched how many 'except IndexError' are in
the standard library code.  Around 60.  That's a rather
low number, that can justify adding 'list.get' but not
advocate a new syntax.

Moreover, I think that explicit handling of IndexError is
rather ugly and error prone, using len() is usually
reads better.

> [snip]
>
> Consider this example of a two-level cache::
>      for key in sequence:
>          x = (lvl1[key] except KeyError: (lvl2[key] except KeyError: f(key)))
>          # do something with x

I'm sorry, it took me a minute to understand what your
example is doing.  I would rather see two try..except blocks
than this.

>
> Retrieve an argument, defaulting to None::
>          cond = args[1] except IndexError: None
>
>          # Lib/pdb.py:803:
>          try:
>              cond = args[1]
>          except IndexError:
>              cond = None

cond = None if (len(args) < 2) else args[1]

> Attempt a translation, falling back on the original::
>          e.widget = self._nametowidget(W) except KeyError: W
>
>          # Lib/tkinter/__init__.py:1222:
>          try:
>              e.widget = self._nametowidget(W)
>          except KeyError:
>              e.widget = W
I'm not sure this is a good example either.
I presume '_nametowidget' is some function,
that might raise a KeyError because of a bug in
its implementation, or to signify that there is
no widget 'W'. Your new syntax just helps to work
with this error prone api.

>
> Read from an iterator, continuing with blank lines once it's
> exhausted::
>          line = readline() except StopIteration: ''
>
>          # Lib/lib2to3/pgen2/tokenize.py:370:
>          try:
>              line = readline()
>          except StopIteration:
>              line = ''
Handling StopIteration exception is more common in standard
library than IndexError (although not much more), but again,
not all of that code is suitable for your syntax. I'd say
about 30%, which is about 20-30 spots (correct me if I'm
wrong).

>
> Retrieve platform-specific information (note the DRY improvement);
> this particular example could be taken further, turning a series of
> separate assignments into a single large dict initialization::
>          # sys.abiflags may not be defined on all platforms.
>          _CONFIG_VARS['abiflags'] = sys.abiflags except AttributeError: ''
>
>          # Lib/sysconfig.py:529:
>          try:
>              _CONFIG_VARS['abiflags'] = sys.abiflags
>          except AttributeError:
>              # sys.abiflags may not be defined on all platforms.
>              _CONFIG_VARS['abiflags'] = ''
Ugly.
_CONFIG_VARS['abiflags'] = getattr(sys, 'abiflags', '')
Much more readable.

> Retrieve an indexed item, defaulting to None (similar to dict.get)::
>      def getNamedItem(self, name):
>          return self._attrs[name] except KeyError: None
>
>      # Lib/xml/dom/minidom.py:573:
>      def getNamedItem(self, name):
>          try:
>              return self._attrs[name]
>          except KeyError:
>              return None
_attrs there is a dict (or at least it's something that quaks
like a dict, and has [] and keys()), so

return self._attrs.get(name)

> Translate numbers to names, falling back on the numbers::
>              g = grp.getgrnam(tarinfo.gname)[2] except KeyError: tarinfo.gid
>              u = pwd.getpwnam(tarinfo.uname)[2] except KeyError: tarinfo.uid
>
>              # Lib/tarfile.py:2198:
>              try:
>                  g = grp.getgrnam(tarinfo.gname)[2]
>              except KeyError:
>                  g = tarinfo.gid
>              try:
>                  u = pwd.getpwnam(tarinfo.uname)[2]
>              except KeyError:
>                  u = tarinfo.uid
This one is a valid example, but totally unparseable by
humans. Moreover, it promotes a bad pattern, as you
mask KeyErrors in 'grp.getgrnam(tarinfo.gname)' call.

>
> Perform some lengthy calculations in EAFP mode, handling division by
> zero as a sort of sticky NaN::
>
>      value = calculate(x) except ZeroDivisionError: float("nan")
>
>      try:
>          value = calculate(x)
>      except ZeroDivisionError:
>          value = float("nan")
>
> Calculate the mean of a series of numbers, falling back on zero::
>
>      value = statistics.mean(lst) except statistics.StatisticsError: 0
>
>      try:
>          value = statistics.mean(lst)
>      except statistics.StatisticsError:
>          value = 0

I think all of the above more readable with try statement.
>
> Retrieving a message from either a cache or the internet, with auth
> check::
>
>      logging.info("Message shown to user: %s",((cache[k]
>          except LookupError:
>              (backend.read(k) except OSError: 'Resource not available')
>          )
>          if check_permission(k) else 'Access denied'
>      ) except BaseException: "This is like a bare except clause")
>
>      try:
>          if check_permission(k):
>              try:
>                  _ = cache[k]
>              except LookupError:
>                  try:
>                      _ = backend.read(k)
>                  except OSError:
>                      _ = 'Resource not available'
>          else:
>              _ = 'Access denied'
>      except BaseException:
>          _ = "This is like a bare except clause"
>      logging.info("Message shown to user: %s", _)

If you replace '_' with a 'msg' (why did you use '_'??)
then try statements are *much* more readable.

> [snip]
>
> Lib/ipaddress.py:343::
>              try:
>                  ips.append(ip.ip)
>              except AttributeError:
>                  ips.append(ip.network_address)
> Becomes::
>              ips.append(ip.ip except AttributeError: ip.network_address)
or it may become:

ips.append(getattr(ip, 'ip', ip.network_address))

or

address = getattr(ip, 'ip', ip.network_address)
ips.append(address)

---

All in all, your proposal scares me. I doesn't make python
code readable, it doesn't solve the problem of overbroad
exceptions handling (you have couple examples of overbroad
handling in your PEP examples section).

Yes, some examples look neat. But your syntax is much easier
to abuse, than 'if..else' expression, and if people start
abusing it, Python will simply loose it's readability
advantage.


Yury


More information about the Python-Dev mailing list