JJoeven

Curriculum/Python

If, Elif, and Else

Branch with if, elif, and else. Return early. Guard unknown tools, empty text, and errors before the happy path.

beginner20 min13 / 37

An if runs a block of code only when a test is true. elif means "else if": try this test if the ones above were false. else runs if none of the tests were true.

Python uses spaces at the start of a line to show which lines belong to the if. Use four spaces. End the if line with a colon. Forget the colon, and you get a SyntaxError. Forget the indent, and you get an IndentationError.

Agents use these tests as rules: unknown tool? reject. Empty query? ask again. Error? retry. Budget gone? stop. The model can suggest. Your ifs decide whether the suggestion is legal.

if, elif, else

Only one branch runs. Python checks from the top. The first true test wins. Later tests are skipped even if they would also be true.

The first true test wins
if errorretrythink

If the guard is true, take that path. Else is the leftover path. Order is the policy.

The first true test wins
python
obs = {class="tok-s">"error": None, class="tok-s">"done": False}
if obs.get(class="tok-s">"error"):
    action = class="tok-s">"retry"
elif obs.get(class="tok-s">"done"):
    action = class="tok-s">"stop"
else:
    action = class="tok-s">"think"
print(action)

You can chain many elif lines. else is optional. If you omit else and no test is true, nothing in those branches runs. That can be a bug if you assumed one path always ran.

A test can be any value. Empty values count as false: "", [], 0, None. Prefer a clear check when that might surprise you. if obs.get("error"): is fine when error is None or a non-empty string. It is a lie if error could be 0. Use if obs.get("error") is not None: when you need that exact idea.

Nested if inside if works. It gets hard to read fast. Prefer early return in a function instead of nesting four levels.

Truthiness you will actually hit

ValueCounts asAgent meaning
Nonefalsemissing field, no error object
""falseempty text
" "true until you stripmodel sent only spaces
[]falseno hits
0falsea count of zero — often valid
Falsefalseexplicit no
{"ok": False}truea non-empty dict is true even if ok is False

That last row bites. if result: is true for {"ok": False, "error": "timeout"}. You wanted if result.get("ok"):. Truth of the container is not truth of a field inside it.

python
result = {class="tok-s">"ok": False, class="tok-s">"error": class="tok-s">"timeout"}
if result:
    print(class="tok-s">"this still runs — the dict is not empty")
if result.get(class="tok-s">"ok"):
    print(class="tok-s">"this does not run")

Early return

Early return means leave a function as soon as you know the answer. The rest of the function does not run.

A guard is a check that stops bad input. Put guards first. The happy path stays flat and easy to read.

python
def next_action(obs):
    if obs.get(class="tok-s">"error"):
        return class="tok-s">"retry"
    if obs.get(class="tok-s">"done"):
        return class="tok-s">"stop"
    return class="tok-s">"think"

That is easier to extend than a tall pile of nested ifs. Add a new guard as a new block at the top. Do not wrap the whole function in another indent.

return exits the function, not only the if. Lines after the if / return still belong to the function, and they run when the test was false. That is the flat style. Use elif when cases exclude each other and share one result name. Use stacked early returns when each guard is a different reason to leave. Agent policy is usually the second: unknown tool, empty query, over budget, then run.

Guard unknown tools and empty strings

An unknown tool is a name that is not in the allowlist. Reject it. Do not run it. Do not try to guess a close name unless you have a dedicated, tested mapper. Guessing read from reed is how you run the wrong tool.

An empty string is "". In an if text: test, empty text is false. A model may also send only spaces. strip() removes spaces. Then " " becomes "" and you can treat it as empty.

PatternUse it when
if / elif / elseOne of several cases
Early returnYou already know the answer
if name not in allowedUnknown tool
if not text.strip()Empty or blank text
if error is not NoneAn error message is present
if used >= max_stepsBudget is gone

if not text.strip(): is a common guard. If text might not be a string, check isinstance(text, str) first, or the strip call crashes. That check belongs at the trust edge, where model JSON arrives.

Write failure cases first: unknown tool, empty query, error. What is left is the normal path. if tests can use and / or. Split them if the return value should differ: unknown tool vs empty query. The model (and your tests) need distinct error strings.

and / or short-circuit. if name and name not in allowed: does not evaluate the in test when name is missing. That saves a TypeError if name is None and you later call a method on it. Put the cheap, failing-closed check first.

A missing colon after if is a SyntaxError. An extra colon on the next line is also a syntax error. Copy the shape from a working function until your fingers know it: test, colon, newline, four spaces, body.

Walkthrough: overlapping tests

Order is policy. Pass both error and done in one dict. The first true guard wins.

python
def next_action(obs):
    if obs.get(class="tok-s">"error"):
        return class="tok-s">"retry"
    if obs.get(class="tok-s">"done"):
        return class="tok-s">"stop"
    return class="tok-s">"think"

print(next_action({class="tok-s">"error": class="tok-s">"timeout", class="tok-s">"done": True}))  class="tok-c"># retry

If you swap those two ifs, a failed-but-finished run would stop and never retry. There is no universal right order. There is only the order you chose and the tests that lock it.

= inside a test is a SyntaxError (or, in rare old patterns, an assignment that is not a comparison). You want == for equality. if name = "search": does not run the search. It fails to parse.

What goes wrong

  • Missing colon after if.
  • Mixing tabs and spaces.
  • Using = inside the test instead of ==.
  • if result when 0 is valid, or when result is a dict with ok: False.
  • Putting the wide case first so specific cases never run (more in match/case).
  • Forgetting that elif only runs if earlier tests were false.
  • Combining two failures into one if so tests cannot tell them apart.
  • Calling .strip() on None because the JSON field was missing.

A budget guard belongs in the loop, not only in the model prompt. if used >= max_steps: return "stop" is a hard stop. The model does not get a vote. If you only ask the model to stop, it may keep calling tools.

Live PythonOpen full playgroundpython
Output
Run to execute this in your browser. Nothing is sent to a server.

Pass both error and done in one dict. Which return wins? The first guard. Order is policy. The last two prints show why if result: is the wrong test for a result dict.

How agents use this

Before a model runs, if statements are the rules. Unknown tool? Reject. Empty query? Ask again. Error? Retry. Budget gone? Stop. Early return keeps each rule in one short block. Replacing the policy with a model does not remove these guards.

A production loop still has a hard if step >= max_steps: return. The model does not get a vote on the budget. A production tool runner still has if name not in TOOLS: return error. The model does not get a vote on os.system.

When you read an agent and cannot find these guards, they are missing, not implied. Write them as functions with names: gated, handle_query, next_action. Tests can call those functions with tiny dicts. That is cheaper than waiting for a live model to hit the bad path.

Keep the error strings distinct. "unknown tool" vs "empty query" vs "budget" is how you score a test suite. One "bad" return hides which rule fired.

Guards also belong after tools return. if not obs.get("ok"): decides retry vs give up. The model can suggest “search again.” Your if used < 3 decides whether that suggestion is legal.

Check your understanding

What does handle_query("") return if empty text is treated as false?