Instead of
def do_something(a, b, c): return res_fn( fn(a, b), fn(b), c )
I do:
def do_something(a, b, c): inter_1 = fn(a, b) inter_2 = fn(b) result = res_fn(inter_1, inter_2, c) return result
The first version is much shorter, and when formatted properly, equally readable.
But the reason I prefer the second approach is because all intermediate steps are saved to local variables.
Exception tracking tools like Sentry, and even Django's error page that pops up when DEBUG=True is set, capture the local context. On top of that, if you ever have to step through the function with a debugger, you can see the exact return value before stepping out from the function. This is the reason why I even save the final result in a local variable, just before returning it.
At the performance cost of couple of extra variable assignments, and couple of extra lines of code, this makes debugging much easier.