What are if, elif and else?

Pick one outcome with if, elif and else, and see how the order of the tests decides which one you get.

if runs a block of code when its test is True. else catches everything the if missed. elif (short for “else if”) adds more tests in between, and Python runs the first block whose test is True and skips the rest.

I show two rules built this way: one that labels a day up, flat or down, and one that turns an RSI reading into a position of +1, -1 or 0.

Step 1. Label a day up, flat or down

Four outcomes need if, two elif tests, and else. Python reads the tests top to bottom.

returns = [None, 0.1, 0.0, -0.1]   # daily returns; None means no prior day

for r in returns:
    if r is None:        # test 1
        label = "no prior day"
    elif r > 0:          # test 2, reached only if test 1 was False
        label = "up"
    elif r == 0:         # test 3, reached only if tests 1 and 2 were False
        label = "flat"
    else:                # nothing above matched
        label = "down"
    print(r, "->", label)
# -> None -> no prior day
# -> 0.1 -> up
# -> 0.0 -> flat
# -> -0.1 -> down
None -> no prior day
0.1 -> up
0.0 -> flat
-0.1 -> down

Each value lands in exactly one branch. A day cannot come out both up and flat.

Step 2. The first true test wins

Once a test passes, Python stops looking. Here 0.06 passes both r > 0 and r > 0.05, and the one written first runs.

r = 0.06

if r > 0:             # 0.06 > 0 is True, so this block runs
    size = "gain"
elif r > 0.05:        # never reached for 0.06
    size = "big gain"
else:
    size = "loss or flat"

print(size)           # -> gain
gain

Swap the two tests and the same number comes out differently.

r = 0.06

if r > 0.05:          # the tighter test first
    size = "big gain"
elif r > 0:
    size = "gain"
else:
    size = "loss or flat"

print(size)           # -> big gain
big gain

The narrower test goes above the wider one. Otherwise the wider one swallows every value the narrower one was meant to catch.

Step 3. Put the None check first

None marks a missing value, and comparing it with a number raises an error.

r = None

try:
    r > 0
except TypeError as e:
    print("TypeError:", e)
# -> TypeError: '>' not supported between instances of 'NoneType' and 'int'
TypeError: '>' not supported between instances of 'NoneType' and 'int'

if r is None: at the top of the chain sends missing values into their own branch, so the comparisons below only see numbers. That is the order used in Step 1.

Step 4. Turn an RSI reading into a position

RSI is an indicator bounded between 0 and 100. Below 30 counts as oversold and above 70 as overbought. I map each reading to a position: +1 for long, -1 for short, 0 for flat.

rsi = [None, 22.0, 48.0, 74.0, 55.0]   # RSI at each day's close; None before there is enough history

positions = []
for x in rsi:
    if x is None:        # no reading yet, so hold nothing
        pos = 0.0
    elif x < 30:         # oversold, go long
        pos = 1.0
    elif x > 70:         # overbought, go short
        pos = -1.0
    else:                # 30 to 70 inclusive
        pos = 0.0
    positions.append(pos)

print(positions)   # -> [0.0, 1.0, 0.0, -1.0, 0.0]
[0.0, 1.0, 0.0, -1.0, 0.0]

The else gives every reading a value. Drop it and pos keeps whatever it held on the previous pass through the loop.

p = None
kept = []
for x in [22.0, 48.0, 74.0]:
    if x < 30:
        p = 1.0
    elif x > 70:
        p = -1.0
    # no else here
    kept.append(p)

print(kept)   # -> [1.0, 1.0, -1.0]
[1.0, 1.0, -1.0]

The middle reading is 48, which matches neither test, so it reuses the 1.0 set by the reading before it.

Your turn

Label a single RSI reading x = 25.0 as "very oversold" (below 20), "oversold" (below 30) or "neutral", and give None its own branch. Which test has to come before which?

x = 25.0

if x is None:         # first, so the comparisons below only see numbers
    label = "no reading"
elif x < 20:          # tighter test above the wider one
    label = "very oversold"
elif x < 30:
    label = "oversold"
else:
    label = "neutral"

print(label)          # -> oversold

With the last two tests swapped, x = 15.0 prints "oversold", because 15.0 < 30 passes first.