16

suppose you have a function that can return some object or None:

def foobar(arg):
   if arg == 'foo':
       return None
   else:
       return 'bar'

Now you call this method and you want to do something with the object, for this example i get a str, so i may want to call the upper() function. There are now two cases that can happen, where the second one will fail, because None has no method upper()

foobar('sdfsdgf').upper()
foobar('foo').upper() 

of course this is now easy to fix:

tmp = foobar('foo')
if tmp is not None:
    tmp.upper()
# or
try:
    foobar('foo').upper()
except ArgumentError:
    pass
# or
caller = lambda x: x.upper() if type(x) is str else None
caller(foobar('foo'))

but the exception handling is maybe not specfic enough and it can happen that i catch an exception that could be important (= debugging gets harder), while the first method is good but can result in larger code and the third option looks quite nice but you have to do it for all possible functions so method one is probably the best.

I wonder if there is any syntactic sugar that handle this problem nicely?

reox
  • 5,036
  • 11
  • 53
  • 98
  • 2
    You could return an empty string instead of 'None'. – Eddie Jan 24 '14 at 09:56
  • yes of course, but suppose the function i call is a library and i don't wan't to change the API – reox Jan 24 '14 at 09:57
  • You can only have a syntactic aproach, since must be a call:ret = foobar(arg); ret.upper() if ret else pass – cox Jan 24 '14 at 09:58
  • @cox: `ret.upper()` **returns** the new string. And you cannot use `pass` in a conditional expression. – Martijn Pieters Jan 24 '14 at 10:02
  • I know, it was as exemple; I did not want to put it in an answer with [code][/code] and indentation;generic programming – cox Jan 24 '14 at 10:28

4 Answers4

18

You can use:

(foobar('foo') or '').upper()

The expression inside the parenthesis returns a string, even if foobar() returns a falsy value.

This does result in None being replaced by the empty string. If your code relies on None being left in place, your best choice still is to use a separate if statement to call .upper() only if the return value is not None.

Afriza N. Arief
  • 7,696
  • 5
  • 47
  • 74
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
  • of course! i never think of all the language features at once :) – reox Jan 24 '14 at 10:01
  • 2
    That `or` trick is neat unless you've got _several_ falsy values in the possible returns (`0`, `""`, `None`, `[]`, etc.). Just a reminder … – Alfe Jan 24 '14 at 10:11
  • @Alfe: Absolutely, but since you want to be able to call `.upper()` here, that looks like an *excellent* result of using `or`. :-) – Martijn Pieters Jan 24 '14 at 10:12
  • I thought of `.upper()` as just an example for a more general question. Btw, I'm still considering the question of how to _avoid_ calling that `.upper()` at all. In the general case (calling `f(foobar('foo') or '')`) it might be a costly call, depending on the `f()`, and it would be nice to avoid calling it at all. but I can't think of a solution without a tmp variable or a special function for this task. – Alfe Jan 24 '14 at 10:16
  • 1
    @Alfe: You cannot *avoid* the call unless you can know, a priori, when it'll return `None`. Then you just don't call the function. But if you cannot predict if it'll return `None`, *you have no choice* but to call it and then test the return value. – Martijn Pieters Jan 24 '14 at 10:18
  • Of course I have to call `foobar()` and will have to test its result. But I wanted to avoid calling `foobar()` _and_ `.upper()` in all cases, even if `foobar()` returned `None`. Anyway, I guess this gets too philosophical. Pragmatism dictates to use a higher order function for solving this if need be: `def callIfNotNone(f, v): return v if v is None else f(v)` and `callIfNotNone(lambda s: s.upper(), foobar('foo'))`. – Alfe Jan 24 '14 at 10:26
  • `lambda s: s.upper()`, assuming we are expecting a `str` instance, is more neatly spelled `str.upper` (no parens). – Karl Knechtel Jan 24 '14 at 10:40
11

Since this question was answered, there are several new developments that make this pattern a bit easier.

The pymaybe package allows you to simply wrap your values with maybe to silently propagate None values:

from pymaybe import maybe
maybe(foobar('foo')).upper()

This is very similar to the answer by Martijn Pieters except that it preserves None values instead of replacing them with an empty string and it'll be more robust in the presence of multiple falsey values.

Furthermore, it looks like there's a standards track draft proposal at PEP 505 that looks to add syntaxes akin to C#'s ?. accessors to the language itself. I'm not sure how much support it has, but it's a place to look for possible further enhancements.

mbauman
  • 30,958
  • 4
  • 88
  • 123
1

An alternate approach - the first/third options can be wrapped up:

def forward_none(func):
    def wrapper(arg):
        return None if arg is None else func(arg)
    return wrapper

And keep in mind that methods don't have to be used as methods - they're still attributes of the class, and when looked up in the class, they're plain functions:

forward_none(str.upper)(foobar('foo'))

And we can also use this as a decorator:

@forward_none
def do_interesting_things(value):
    # code that assumes value is not None...

do_interesting_things(None) # ignores the original code and evaluates to None
do_interesting_things("something") # as before decoration

Although in practice, if I had to, I would probably do what Martijn suggests. And I would try really hard not to have to; getting into this situation is a code smell suggesting that we should have raised an exception rather than returning None in the first place. :)

Karl Knechtel
  • 62,466
  • 11
  • 102
  • 153
1

With the walrus operator python 3.8:

if x := foobar("b"):  # Truthy if x is not None
    x.upper()

... or more explicit:

if (x := foobar("b")) is not None:
    x.upper()
Greedo
  • 4,967
  • 2
  • 30
  • 78