5

When working with built-in types like int and float in Python, it's common to employ exception handling in cases where input might be unreliable:

def friendly_int_convert(val):
    "Convert value to int or return 37 & print an alert if conversion fails"
    try:
        return int(val)
    except ValueError:
        print('Sorry, that value doesn\'t work... I chose 37 for you!')
        return 37

Are there any prominent edge-cases to be aware of when using str()?

def friendly_str_convert(val):
    "Convert value to str or return 'yo!' & print an alert if conversion fails"
    try:
        return str(val)
    except Exception: # Some specific Exception here
        print('Sorry, that value doesn\'t work... I chose \'yo!\' for you!')
        return 'yo!'

I really don't like using a broad Exception since there are cases like NameError that signify a problem with the code and should raise an error. I've considered UnicodeError as a candidate but I'm not sure whether str() causes it (vs. foo.encode() and foo.decode() where it's easier to understand) and would love an example of what input, if any, would trigger it.

In summary: Is it generally safe to use str() without a try / except block even with unreliable input?

Alec
  • 1,399
  • 4
  • 15
  • 27
  • 1
    `Exception` isn't a base class for `KeyboardInterrupt`, which is why using `except Exception` is preferable to using a bare `except`. – chepner Jul 14 '16 at 18:20
  • @chepner that's a good point, edited to remove the reference to `KeyboardInterrupt`. – Alec Jul 14 '16 at 18:24
  • 2
    This is the best answer IMO https://stackoverflow.com/a/38982099/6260170 – Chris_Rands Mar 03 '18 at 12:17
  • @Chris_Rands I hadn’t considered the case of exceeding maximum recursion depth when typecasting a deeply nested object to a string. Thanks for the link! – Alec Mar 03 '18 at 13:22

3 Answers3

3

In summary: Is it generally safe to use str() without a try / except block even with unreliable input?

That depends on what kind of input we're talking about. You've tagged this question Python 3, so you don't need to worry about the UnicodeEncodeErrors you'd get with Python 2 and Unicode input, but the object you're receiving could do pretty much anything in its __str__ or __repr__, raising pretty much any kind of exception. For example,

In [18]: import weakref

In [19]: class Foo(object): pass

In [20]: str(weakref.proxy(Foo()))
---------------------------------------------------------------------------
ReferenceError                            Traceback (most recent call last)
<ipython-input-20-396b2ab40052> in <module>()
----> 1 str(weakref.proxy(Foo()))

ReferenceError: weakly-referenced object no longer exists
user2357112
  • 260,549
  • 28
  • 431
  • 505
  • Yeah, I figured that Python 2 would likely involve some additional Unicode trickery. Is there a way to detect if `__str__` or `__repr__` have been modified before calling `str()`? – Alec Jul 14 '16 at 17:50
  • @Alec: Modified? No, and it wouldn't help you anyway. We don't need to modify any `__str__` methods to get crazy exceptions. – user2357112 Jul 14 '16 at 17:52
  • Yeah, I guess that the `weakref` case is actually a good example of your point. My objective is more to catch strange user input so hopefully I can avoid needing to handle any of these more interesting exceptions. – Alec Jul 14 '16 at 18:03
2

There's a huge difference between str and int in this regard. int can definitely raise TypeError and ValueError.

As far as I can think, the only exception that str can raise for normal objects is UnicodeEncodeError:

>>> s = u"a\xac\u1234\u20ac\U00008000"
>>> str(s)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
UnicodeEncodeError: 'ascii' codec can't encode characters in position 1-4: ordinal not in range(128)

And that only happens on python2.x.

Of course, I can easily make a class that fails with just about any exception imaginable:

>>> class MyError(Exception):
...   pass
... 
>>> class Foo(object):
...   def __str__(self):
...     raise MyError
... 
>>> f = Foo()
>>> str(f)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in __str__
__main__.MyError

For the most part, I would question some of the implicit assumptions that all exceptions need to be handled at this point. Generally, it's best to only handle exceptions that you know how to handle. In this case, exotic exceptions that happen because the user put junk into the function should probably be handled at the level where the junk is going in -- not within the function itself. Catching the error and returning some value which is likely nonsense isn't going to be super helpful for debugging issues, etc.

mgilson
  • 300,191
  • 65
  • 633
  • 696
  • Interestingly enough, trying your example above doesn't reproduce the error on my end: `>>> str(s) ... 'a¬ሴ€耀'`! Great point with the custom exceptions. – Alec Jul 14 '16 at 17:43
  • 1
    @Alec -- I neglected to see that you're working on python3.x. `str` is different there, but you can _might_ end up with a similar issue when encoding/decoding bytes. – mgilson Jul 14 '16 at 18:17
2

Due to concerns you've raised, I'd do except Exception as e:. Exception is generic "catch-all" in Python 3 for "normal" exceptions (other than "system-level" exceptions resulting from process getting signals, KeyboardInterrupt, etc).

If I were you I'd log the actual exception (e in my example above) at the very least, to see what actually happen (your code silently drops actual exception object by doing except Exception:).

Alec
  • 1,399
  • 4
  • 15
  • 27
LetMeSOThat4U
  • 6,470
  • 10
  • 53
  • 93
  • Interesting, I'd never heard of StandardError before (it doesn't show up in the [documentation](https://docs.python.org/3.5/library/exceptions.html)) – Alec Jul 14 '16 at 17:57
  • 1
    [`StandardError` doesn't exist in Python 3.](https://docs.python.org/3.0/whatsnew/3.0.html#changes-to-exceptions) – user2357112 Jul 14 '16 at 18:09
  • @user2357112 Thanks for pointing that out, I guess that explains why it's not in the docs :) – Alec Jul 14 '16 at 18:25