If, Elif, and Else
Branch with if, elif, and else. Return early. Guard unknown tools, empty text, and errors before the happy path.
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.
If the guard is true, take that path. Else is the leftover path. Order is the policy.
The first true test winsobs = {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
| Value | Counts as | Agent meaning |
|---|---|---|
None | false | missing field, no error object |
"" | false | empty text |
" " | true until you strip | model sent only spaces |
[] | false | no hits |
0 | false | a count of zero — often valid |
False | false | explicit no |
{"ok": False} | true | a 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.
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.
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.
| Pattern | Use it when |
|---|---|
if / elif / else | One of several cases |
Early return | You already know the answer |
if name not in allowed | Unknown tool |
if not text.strip() | Empty or blank text |
if error is not None | An error message is present |
if used >= max_steps | Budget 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.
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"># retryIf 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 resultwhen0is valid, or whenresultis a dict withok: False.- Putting the wide case first so specific cases never run (more in match/case).
- Forgetting that
elifonly runs if earlier tests were false. - Combining two failures into one
ifso tests cannot tell them apart. - Calling
.strip()onNonebecause 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.
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