Hi, I’m currently working on a Python project, and I’ve encountered a situation where I have several if-else statements within a function. I’m trying to determine the best practice for handling return statements in this context.
Here are 2 small examples to demonstrate my idea:
Multiple Returns
def check_number(x):
if x > 0:
return "Positive"
elif x < 0:
return "Negative"
else:
return "Zero"
Single Return at the End
def check_number(x):
result = None
if x > 0:
result = "Positive"
elif x < 0:
result = "Negative"
else:
result = "Zero"
return result
I’m looking to gather opinions from the community. Which would you prefer or is there a scenario where one case is better than the other?
I go back and forth on what style to use.
If the function returns an Optional[T], I think I tend to do the immediate return but in other cases I can’t say for sure what I prefer to do.
Just looking at you examples it I think the immediate return looks nicer but I’m sure someone can construct an example where returning at the end makes more sense and is nicer. I think it’s mostly about what you find nice, best practice be damned!
In this case I would prefer the second variant with only one return.
This is easier to debug (e.g. set a breakpoint on the single return statement to check the result) an also easier to extend (e.g. print / log the result prior return)
(BTW: you might omit the “result = None”, as all paths do set a result.)
But also multiple return statements have an advantage in some cases, e.g. they can avoid indentation levels for some code.
I would favour the first (shorter) in a simple case.
Some Pythonists would prefer you omit the else, so you just drop out to the default return.
In the second version, you do not need to assign None to begin with. In some languages you would need a declaration there (still without initialiser), but not Python.
It mainly comes down to considering the reader or maintainer. (Code is read more times than it is written.) A return deeply nested in complex logic can easily be missed, but so can an assignment, and at least the return says “we’re done”.
In a complex case I might need to do something with the result before returning, even if it is just logging, in which case the second version wins.
In your example, it probably doesn’t matter. However, in a larger context (e.g. a ball of mud), and in some other languages the first is easier to read than the second. If you use a variable, I need to verify that that variable isn’t global (I know, almost never an issue in Python). I also have to verify that the variable isn’t used somewhere else in the current context. Adding a variable may not increase coupling, but anyone reading or modifying the code needs to make sure that’s the case.
Hm I could still go either way in more complex cases. When the function gets large it can be tricky to find all the return statements. A variable can be easier to trace through the code.
But once it reaches that point, it is probably worth breaking out those sections into their own functions.
That should never be a problem. You shouldn’t ever be in a position of struggling to find all the return statements; either, because it’s irrelevant (in a lot of cases, you don’t NEED to find them all), or because it’s easy despite the function’s complexity (eg if there’s a “fail and bail” pattern, where you have a series of conditions that result in immediate returns). Otherwise, yes, refactor that thing!
I know, that word “should” is doing some heavy lifting. Still, my two easy cases do actually cover a remarkable spectrum of functions, and often it isn’t TOO hard a refactor to bring it to that point. Notably, it is almost certainly an entirely internal refactor - it doesn’t require pulling some of the code out into a helper function or anything like that.
I pretty much always immediately return as soon as I got what I wanted. The only time I would follow the second pattern is if the result is used elsewhere in the function
I usually prefer the latter one return approach. This way the single function has one overall flow to it. It might take different paths to get to the end but it all gets to the same path. Usually, like in this example, it is clear that the goal
of all the different cases is to get one or more specific variables defined and there may be different ways to do that. I can’t really defend this point of view that much, but this post is a call for opinions