Making decisions

Beginner40 min

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 umbrella
else
wear sunglasses

Chains 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

tickets.py
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

tickets.js
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

Tickets.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

tickets.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: child

Notice 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

guard_first.py
items = []
if len(items) > 0 and items[0] > 5:
print("big first item")
else:
print("no big first item")

JavaScript

guard_first.js
const items = [];
if (items.length > 0 && items[0] > 5) {
console.log("big first item");
} else {
console.log("no big first item");
}

Java

GuardFirst.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.

guard_first.c
#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 item

The 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

parity.py
n = 7
parity = "even" if n % 2 == 0 else "odd"
print(n, "is", parity)

JavaScript

parity.js
const n = 7;
const parity = n % 2 === 0 ? "even" : "odd";
console.log(n, "is", parity);

Java

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

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 odd

Guard 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

describe_average.py
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

describe_average.js
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

DescribeAverage.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

describe_average.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.5
no data

Common 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 if chain, 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 < 65 and age <= 65 differ at exactly one value. Test the edges.
  • Comparing floats with ==. 0.1 + 0.2 == 0.3 is 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.
ExerciseFizzBuzz

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

fizzbuzz.py
# 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

fizzbuzz.js
// 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

FizzBuzz.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

fizzbuzz.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:

1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
ExerciseLeap-year checker with tests

A 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

leap_year.py
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

leap_year.js
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

LeapYear.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

leap_year.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 ok
1900 false ok
2024 true ok
2023 false ok
2100 false ok

Every year in the list is correct. The tests deliberately include the century years 1900, 2000 and 2100, which are where most first attempts fail.

ExerciseTruth table

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.