'Avoiding nested if else statement ladder in Python

It's hard to find answers to this problem when you don't know exactly how to describe it...

What is the most idiomatic way to deal with a fairly deep (but fixed) nested set of tests that you want to run sequentially but terminate as soon as the first one comes up with a successful result?

Instead of the following

Option 1: (results in way too much indentation)

def make_decision():

    results = ... some code or function that returns a list or None
    if results:
        decision = random.choice(results)
    else:
        results = ... other code or function
        if results:
            decision = random.choice(results)
        else:
            results = ... other code or function
            if results:
                decision = random.choice(results)
            else:
                results = ... other code or function
                if results:
                    decision = random.choice(results)

      ...etc.

                                else:
                                    decision = None

    print(decision)

    return decision

Option 2

Another option is to return from the function early but I'm not sure it's good practice to have so many returns scattered about and in this case I would prefer to do one final thing at the end (e.g. print(decision)) before returning:

def make_decision():

    results = ... some code or function that returns a list or None
    if results:
        return random.choice(results)
    
    results = ... other code or function
    if results:
        return random.choice(results)

    results = ... other code or function
    if results:
        return random.choice(results)
    
    ...etc.

    return None

Option 3

Finally I could make each test a separate function as described here and call them iteratively, provided each test function has the same set of arguments.

def test1(args):
    ...

def test2(args):
    ...

def test3(args):
    ...

def make_decision():

    decision = None
    for test in [test1, test2, test3, ...]:
        results = test(args)
        if results:
            decision = random.choice(results)
            break

    print(decision)

    return decision

This looks the best but I hadn't planned to make a function for every test and some tests can be done with the same function but with different arguments and some are just one-liners whereas others are multiple lines. So then would I have to build a list of functions and arguments before starting the loop? Or make a list of partial functions?

Any better suggestions welcome (before I go ahead with option 3 above)

UPDATE 2018-07-21:

A future potential option

Unbeknown to me as I was wrestling with this problem, PEP 572 was approved (and Mr van Rossum resigned as a consequence). If this PEP is implemented the following solution will also be possible I think:

def make_decision():

    if (results := ... some code or function) is not None:
        decision = random.choice(results)
    elif (results := ... some code or function) is not None:
        decision = random.choice(results)
    
    ...etc.

    else:
        decision = None

    return decision


Solution 1:[1]

You can choose any of the options, it all depends on your preference. On Option 2, I don't think it's a bad idea to have many returns since they are enclosed in conditional code. They will only be executed if that condition is True.

On Option 1, you can decide to switch to elif results: instead of:

if results:
        # some code
    else:
        if results:
               # some code

In your code it almost seem like you're checking for same results which looks like only one if block will execute. You should be checking against some value e.g. if results == something.

Finally, Option 3 looks cleaner. And to wrap up, pick the one you feel most comfortable with.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1