Making decisions
Write branching logic with if, else if and else, combine conditions with boolean operators, and use guard clauses to keep decisions readable.
What you will learn
- Write if, else-if and else chains that cover every case exactly once
- Combine conditions with and, or and not, and check them with a truth table
- Use short-circuiting to guard risky checks
- Flatten nested decisions with guard clauses
On this page
Until now every program you have seen runs its lines top to bottom, once. Real programs have to react: charge a child a different price from an adult, refuse a negative number, stop when the file ends. The tool for that is the conditional, a piece of code that runs only if some boolean expression is true.
The shape of a decision
A conditional has a condition (any expression that produces a boolean, as in the expressions lesson) and a block of code that runs when the condition is true. Add else for the other case:
if it is raining take an umbrellaelse wear sunglassesChains handle more than two cases. The computer tests the conditions from the top and runs the block of the first one that is true; everything below is skipped. If none is true, the final else runs.
The example below picks a ticket type from an age. The order of the tests matters: because age < 18 is only reached when age < 5 was false, we never need to write age >= 5 and age < 18.
Python
age = 15
if age < 0: ticket = "invalid age"elif age < 5: ticket = "free"elif age < 18: ticket = "child"elif age < 65: ticket = "adult"else: ticket = "senior"
print(f"Age {age}: {ticket}")JavaScript
const age = 15;let ticket;
if (age < 0) { ticket = "invalid age";} else if (age < 5) { ticket = "free";} else if (age < 18) { ticket = "child";} else if (age < 65) { ticket = "adult";} else { ticket = "senior";}
console.log(`Age ${age}: ${ticket}`);Java
public class Tickets { public static void main(String[] args) { int age = 15; String ticket;
if (age < 0) { ticket = "invalid age"; } else if (age < 5) { ticket = "free"; } else if (age < 18) { ticket = "child"; } else if (age < 65) { ticket = "adult"; } else { ticket = "senior"; }
System.out.println("Age " + age + ": " + ticket); }}C
#include <stdio.h>
int main(void) { int age = 15; const char *ticket;
if (age < 0) { ticket = "invalid age"; } else if (age < 5) { ticket = "free"; } else if (age < 18) { ticket = "child"; } else if (age < 65) { ticket = "adult"; } else { ticket = "senior"; }
printf("Age %d: %s\n", age, ticket); return 0;}All four print:
Age 15: childNotice what differs: Python marks blocks with a colon and indentation, the others use braces and put the condition in parentheses; Python writes elif, the others write else if. The logic is identical. Change age and run it again to see each branch.
Combining conditions
Often one test is not enough. The boolean operators from the expressions lesson join conditions together: and (both must hold), or (at least one), not (flip it). Python spells them out; JavaScript, Java and C use &&, || and !.
A truth table lists every combination of inputs, and it is the most reliable way to check you have written the logic you meant. For “a person may enter if they are an adult or they are accompanied”, with adult and with_adult as the inputs:
adult |
with_adult |
adult or with_adult |
|---|---|---|
| false | false | false |
| false | true | true |
| true | false | true |
| true | true | true |
Two rules save a lot of bugs. First, not (a and b) is the same as (not a) or (not b), and not (a or b) is the same as (not a) and (not b). These are De Morgan’s laws, and they let you untangle a condition that has become hard to read. Second, never compare a boolean to true: write if is_ready, not if is_ready == true.
Short-circuiting
As you saw in the expressions lesson, and and or stop evaluating as soon as the answer is known. That turns the order of a condition into a safety feature:
Here the list of items is empty, so asking for its first item would fail. The length check on the left protects the item check on the right.
Python
items = []if len(items) > 0 and items[0] > 5: print("big first item")else: print("no big first item")JavaScript
const items = [];if (items.length > 0 && items[0] > 5) { console.log("big first item");} else { console.log("no big first item");}Java
public class GuardFirst { public static void main(String[] args) { int[] items = {}; if (items.length > 0 && items[0] > 5) { System.out.println("big first item"); } else { System.out.println("no big first item"); } }}C
C arrays do not know their own length, so the program keeps a separate count.
#include <stdio.h>
int main(void) { int items[1] = {0}; int count = 0; if (count > 0 && items[0] > 5) { printf("big first item\n"); } else { printf("no big first item\n"); } return 0;}All four print:
no big first itemThe condition never touches items[0] when the list is empty, because the length test is already false and and stops. Written the other way round, the same condition would crash in Python and Java, and read outside the array in C. Put the cheap safety check on the left.
Conditional expressions
A conditional expression is a compact form that chooses between two values. Python writes a if test else b; JavaScript, Java and C write test ? a : b. Use it for simple picks; use a full if when either branch does more than produce a value.
Python
n = 7parity = "even" if n % 2 == 0 else "odd"print(n, "is", parity)JavaScript
const n = 7;const parity = n % 2 === 0 ? "even" : "odd";console.log(n, "is", parity);Java
public class Parity { public static void main(String[] args) { int n = 7; String parity = n % 2 == 0 ? "even" : "odd"; System.out.println(n + " is " + parity); }}C
#include <stdio.h>
int main(void) { int n = 7; const char *parity = n % 2 == 0 ? "even" : "odd"; printf("%d is %s\n", n, parity); return 0;}All four print:
7 is oddGuard clauses
Deeply nested ifs are hard to read. A guard clause deals with the unusual case first and leaves the function early, so the main logic stays at the left margin. Compare the two shapes in pseudocode:
nested guarded------ -------if count is not zero if count is zero compute and return average return "no data"else compute and return average return "no data"With one guard the difference is small; with four checks it is the difference between a staircase and a flat list. Here is the guarded shape as a real function. (Functions get their own lesson shortly; for now read each one as a named, reusable block that gives back a value.)
Python
def describe_average(total, count): if count == 0: return "no data" return f"average {total / count}"
print(describe_average(10, 4))print(describe_average(0, 0))JavaScript
function describeAverage(total, count) { if (count === 0) { return "no data"; } return `average ${total / count}`;}
console.log(describeAverage(10, 4));console.log(describeAverage(0, 0));Java
public class DescribeAverage { static String describeAverage(double total, int count) { if (count == 0) { return "no data"; } return "average " + total / count; }
public static void main(String[] args) { System.out.println(describeAverage(10, 4)); System.out.println(describeAverage(0, 0)); }}C
#include <stdio.h>
static void describe_average(double total, int count) { if (count == 0) { printf("no data\n"); return; } printf("average %g\n", total / count);}
int main(void) { describe_average(10, 4); describe_average(0, 0); return 0;}The C version prints inside the function instead of returning text, because returning a string from C needs memory management that comes much later.
All four print:
average 2.5no dataCommon pitfalls
=where==belongs. In C,if (x = 5)assigns 5 and is always true. Python and Java refuse it in most cases, but the habit is worth breaking early.- Overlapping or reordered branches. In an
else ifchain, a broad test placed too early swallows the cases below it. Put the most specific test first. - Forgetting the boundary. Is a 65-year-old a senior?
age < 65andage <= 65differ at exactly one value. Test the edges. - Comparing floats with
==.0.1 + 0.2 == 0.3is false, as the expressions lesson showed. - Dangling else in brace-optional languages. In C, Java and JavaScript you may omit braces around a single statement, and then a later line you add is silently outside the
if. Always use braces. - Repeating a test. If you write the same condition in two places, one will eventually be updated without the other. Store the result in a named boolean.
Print the numbers 1 to 15. For multiples of 3 print Fizz instead, for multiples of 5 print Buzz, and for multiples of both print FizzBuzz. (The for line repeats the body fifteen times; the next lesson explains it. Focus on the if-chain.) There is no input. Expected output: fifteen lines, starting 1, 2, Fizz, 4, Buzz and ending 14, FizzBuzz.
Show solution
The multiple-of-both case must be tested first. If n % 3 == 0 came first, 15 would print Fizz and never reach the others.
Python
# The loop arrives in lesson 8; here it just repeats the if-chain for 1..15.for n in range(1, 16): if n % 15 == 0: print("FizzBuzz") elif n % 3 == 0: print("Fizz") elif n % 5 == 0: print("Buzz") else: print(n)JavaScript
// The loop arrives in lesson 8; here it just repeats the if-chain for 1..15.for (let n = 1; n <= 15; n++) { if (n % 15 === 0) { console.log("FizzBuzz"); } else if (n % 3 === 0) { console.log("Fizz"); } else if (n % 5 === 0) { console.log("Buzz"); } else { console.log(n); }}Java
public class FizzBuzz { public static void main(String[] args) { // The loop arrives in lesson 8; here it just repeats the if-chain for 1..15. for (int n = 1; n <= 15; n++) { if (n % 15 == 0) { System.out.println("FizzBuzz"); } else if (n % 3 == 0) { System.out.println("Fizz"); } else if (n % 5 == 0) { System.out.println("Buzz"); } else { System.out.println(n); } } }}C
#include <stdio.h>
int main(void) { /* The loop arrives in lesson 8; here it just repeats the if-chain for 1..15. */ for (int n = 1; n <= 15; n++) { if (n % 15 == 0) { printf("FizzBuzz\n"); } else if (n % 3 == 0) { printf("Fizz\n"); } else if (n % 5 == 0) { printf("Buzz\n"); } else { printf("%d\n", n); } } return 0;}All four print:
12Fizz4BuzzFizz78FizzBuzz11Fizz1314FizzBuzzA year is a leap year if it is divisible by 4, except years divisible by 100 are not, except years divisible by 400 are. Write a checker and run it on 2000, 1900, 2024, 2023 and 2100, comparing each result with the expected answer. Expected: 2000 and 2024 are leap years; 1900, 2023 and 2100 are not.
Show solution
Guard clauses express the exceptions from most to least specific:
Python
def is_leap(year): if year % 400 == 0: return True if year % 100 == 0: return False return year % 4 == 0
tests = [(2000, True), (1900, False), (2024, True), (2023, False), (2100, False)]for year, expected in tests: result = is_leap(year) status = "ok" if result == expected else "WRONG" print(year, result, status)JavaScript
function isLeap(year) { if (year % 400 === 0) { return true; } if (year % 100 === 0) { return false; } return year % 4 === 0;}
const tests = [[2000, true], [1900, false], [2024, true], [2023, false], [2100, false]];for (const [year, expected] of tests) { const result = isLeap(year); const status = result === expected ? "ok" : "WRONG"; console.log(year, result, status);}Java
public class LeapYear { static boolean isLeap(int year) { if (year % 400 == 0) { return true; } if (year % 100 == 0) { return false; } return year % 4 == 0; }
public static void main(String[] args) { int[] years = {2000, 1900, 2024, 2023, 2100}; boolean[] expected = {true, false, true, false, false}; for (int i = 0; i < years.length; i++) { boolean result = isLeap(years[i]); String status = result == expected[i] ? "ok" : "WRONG"; System.out.println(years[i] + " " + result + " " + status); } }}C
#include <stdbool.h>#include <stdio.h>
static bool is_leap(int year) { if (year % 400 == 0) { return true; } if (year % 100 == 0) { return false; } return year % 4 == 0;}
int main(void) { int years[] = {2000, 1900, 2024, 2023, 2100}; bool expected[] = {true, false, true, false, false}; for (int i = 0; i < 5; i++) { bool result = is_leap(years[i]); const char *status = result == expected[i] ? "ok" : "WRONG"; printf("%d %s %s\n", years[i], result ? "true" : "false", status); } return 0;}Python prints True and False with a capital; the other three print true and false. Apart from that the output is the same:
2000 true ok1900 false ok2024 true ok2023 false ok2100 false okEvery year in the list is correct. The tests deliberately include the century years 1900, 2000 and 2100, which are where most first attempts fail.
Fill in the truth table for not (a and b) and show that it matches (not a) or (not b) on every row.
Show solution
a |
b |
a and b |
not (a and b) |
(not a) or (not b) |
|---|---|---|---|---|
| false | false | false | true | true |
| false | true | false | true | true |
| true | false | false | true | true |
| true | true | true | false | false |
The last two columns agree on all four rows, which is De Morgan’s law in action.
Summary
- A conditional runs a block only when its boolean condition is true; a chain runs the first matching block and skips the rest.
- Combine conditions with and, or and not; a truth table or De Morgan’s laws will check or simplify them.
- Short-circuiting lets an earlier test protect a later one.
- Guard clauses handle odd cases first and keep the main path flat.
Next: Repetition, where you make code run many times.