Python, Concept by Concept

unit

Decisions

A community calendar needs to decide whether the given year 2024 gets a leap day. The finished program should print one exact message:

2024 is a leap year.

How can a program choose the right message instead of printing every possibility?

A leap year is divisible by 4, except a century year is not a leap year, except every 400th year is a leap year after all. That explains why 1900 is not a leap year, while 2000 and 2024 are leap years. In Python, the full test is (year % 4 == 0 and year % 100 != 0) or year % 400 == 0.

Run the single given year below.

Preview — you will build every part of this by the end of the unit.

year = 2024
is_leap_year = (year % 4 == 0 and year % 100 != 0) or year % 400 == 0

if is_leap_year:
    print(f"{year} is a leap year.")
else:
    print(f"{year} is not a leap year.")

Output

2024 is a leap year.

Notice

The boolean test becomes True for 2024, so only the first indented message runs. We will build this payoff in three lessons. Each idea climbs the same ladder: minimal, one twist, then realistic.

You will learn

  • Choose when code runs with if, elif, and else.
  • Combine yes-or-no tests with and, or, and not.
  • Put one decision inside another and order the branches so the right one runs.

Lesson 1: Choose with if

A one-way decision

An if statement runs its indented block only when its condition is True. A colon ends the condition line.

Minimal: print a message when a score reaches a target.

score = 92

if score >= 90:
    print("Target reached!")

Output

Target reached!

Notice

The indented line belongs to the decision. It runs because 92 >= 90 is True.

One twist: a false condition skips its block.

temperature = 65

if temperature > 80:
    print("Bring water.")

print("Check complete.")

Output

Check complete.

Notice

The indented reminder is skipped, but the unindented final line runs.

Realistic: use an equality test to recognize an exact score. A common broken version uses assignment inside the condition.

Read the error

score = 90
if score = 90:
    print("Exact score!")

The traceback ends with SyntaxError because = saves a value; it does not ask an equality question. Use == in the condition.

score = 90
if score == 90:
    print("Exact score!")

Output

Exact score!

Notice

if score == 90: asks a question and ends with a colon. The repaired program prints Exact score!.

Two possible paths

An else block supplies the path used when the if condition is False.

Minimal: choose between two messages.

has_ticket = True

if has_ticket:
    print("Enter")
else:
    print("Stop")

Output

Enter

Notice

Exactly one branch runs: the if branch for True, or the else branch for False.

One twist: the condition can compare a saved number.

items = 7

if items % 2 == 0:
    print("Even group")
else:
    print("Odd group")

Output

Odd group

Notice

The remainder decides which of the two labels is printed.

Realistic: include the given value in the chosen output.

balance = 12

if balance >= 10:
    print(f"Balance {balance}: ready")
else:
    print(f"Balance {balance}: add funds")

Output

Balance 12: ready

Notice

Both branches use the same fixed value, but only the matching message appears.

Real input: try the two-path score decision with a score supplied by a person.

Try it yourself

score = int(input("Score: "))
if score >= 10:
    print("Ready")
else:
    print("Keep practicing")

Notice

int() converts the typed score before the comparison, and only one message prints.

Lesson 2: Choose among Several Paths

Logical operators

and is True only when both joined tests are true. or is True when at least one joined test is true. not reverses a boolean: True becomes False, and False becomes True.

Minimal: join two booleans with and.

print(True and False)

Output

False

Notice

and prints False because both booleans must be True.

One twist: use or so one true boolean is enough.

print(True or False)

Output

True

Notice

or prints True because its first boolean is True.

Minimal: reverse one boolean.

print(not True)

Output

False

Notice

not True prints False because not flips the boolean.

One twist: use not in a condition.

has_ticket = False

if not has_ticket:
    print("Ticket needed")

Output

Ticket needed

Notice

has_ticket is False, so not has_ticket is True and the indented message runs.

Another twist: check that a value is in an inclusive range.

x = 7
in_range = x >= 0 and x <= 10
print(in_range)

Output

True

Notice

x >= 0 and x <= 10 makes both boundary tests visible. The chained form 0 <= x <= 10 asks the same inclusive-range question.

One twist: write the same inclusive range as a chained comparison.

x = 7
print(0 <= x <= 10)

Output

True

Notice

The chained comparison prints True because 7 is between both endpoints.

Realistic: accept either of two exact values.

day = "Saturday"

if day == "Saturday" or day == "Sunday":
    print("Weekend")
else:
    print("Weekday")

Output

Weekend

Notice

One true equality is enough for the or condition to choose Weekend.

One twist: compare two strings that differ only in capitalization.

print("Yes" == "yes")

Output

False

Notice

Equality is case-sensitive, so "Yes" == "yes" prints False.

Real input: try the weekend decision with a day supplied by a person.

Try it yourself

day = input("Day: ")
if day == "Saturday" or day == "Sunday":
    print("Weekend")
else:
    print("Weekday")

Notice

This cell asks for a day when you run it yourself; the decision still prints one label.

elif runs top-down

An elif adds another condition between if and else. Python checks the conditions from top to bottom, then runs the first matching branch and skips the rest.

Minimal: classify a score with the highest threshold first.

score = 95

if score >= 90:
    print("Level A")
elif score >= 80:
    print("Level B")
else:
    print("Keep practicing")

Output

Level A

Notice

95 also satisfies score >= 80, but the score >= 90 branch matches first. Python prints only Level A. Order matters.

One twist: add another threshold while keeping the checks highest-to-lowest.

score = 84

if score >= 90:
    print("Gold")
elif score >= 80:
    print("Silver")
elif score >= 70:
    print("Bronze")
else:
    print("Practice round")

Output

Silver

Notice

84 misses the first test and matches the second, so later branches are not checked.

One twist: an elif can require two tests at once with and.

score = 84

if score >= 90:
    print("Gold")
elif score >= 80 and score < 90:
    print("Silver")
else:
    print("Practice round")

Output

Silver

Notice

Both parts of the elif are true for 84, so the Silver branch runs. The < 90 half is already guaranteed by the top-down order — it is written here only to show and inside an elif; in your own ladders, leave it out.

Realistic: a triangle label must check the most specific equality pattern first.

side_a = 5
side_b = 5
side_c = 5

if side_a == side_b and side_b == side_c:
    print("Equilateral")
elif side_a == side_b or side_a == side_c or side_b == side_c:
    print("Isosceles")
else:
    print("Scalene")

Output

Equilateral

Notice

An equilateral triangle also has pairs of equal sides. Checking equilateral first prevents the broader isosceles branch from taking it. The elif condition uses or: any one of its three equal-side tests is enough.

Lesson 3: Build Clear Decision Trees

Nested decisions

A decision can contain another decision. The extra indentation shows that the inner question is asked only inside the chosen outer branch.

Minimal: check permission before checking a score.

has_permission = True
score = 8

if has_permission:
    if score >= 7:
        print("Activity unlocked")

Output

Activity unlocked

Notice

The score test is reached only after permission is confirmed.

One twist: give each decision its own alternative.

has_permission = True
score = 5

if has_permission:
    if score >= 7:
        print("Activity unlocked")
    else:
        print("Raise the score")
else:
    print("Permission needed")

Output

Raise the score

Notice

Each else lines up with the condition it belongs to. Here permission is present, so the inner else prints.

Realistic: use fixed stand-in text in a nested choice.

age_text = "12"
age = int(age_text)
has_pass = True

if age >= 10:
    if has_pass:
        print("Entry approved")
    else:
        print("Pass needed")
else:
    print("Age requirement not met")

Output

Entry approved

Notice

The fixed text "12" becomes an integer before the outer decision. Then the inner decision checks the pass.

Real input: try the nested entry decision with an age supplied by a person.

Try it yourself

age_text = input("Age: ")
age = int(age_text)
has_pass = True
if age >= 10:
    if has_pass:
        print("Entry approved")
    else:
        print("Pass needed")
else:
    print("Age requirement not met")

Notice

The typed age becomes an integer before the outer decision; the inner decision runs only when the age qualifies.

Return to the leap-year rule

The calendar rule joins two paths: a year passes when it is divisible by 4 and not divisible by 100, or when it is divisible by 400. Parentheses keep the first pair together.

Minimal: check whether one year is divisible by 4.

year = 2024
print(year % 4 == 0)

Output

True

Notice

2024 has no remainder after division by 4, so the comparison prints True.

One twist: exclude centuries from the divisible-by-4 rule.

year = 2024
print(year % 4 == 0 and year % 100 != 0)

Output

True

Notice

Both tests are true for 2024, so this partial rule prints True.

One twist: include years divisible by 400 again.

year = 2024
print((year % 4 == 0 and year % 100 != 0) or year % 400 == 0)

Output

True

Notice

The complete rule prints True; the or adds the every-400th-year exception. When and and or share a condition, and binds tighter than or; use parentheses to show the intended groups.

Minimal: save the boolean for one given year.

year = 2024
is_leap_year = (year % 4 == 0 and year % 100 != 0) or year % 400 == 0
print(is_leap_year)

Output

True

Notice

The full test prints True for the single given year 2024.

One twist: use the saved boolean to choose one exact label.

year = 2024
is_leap_year = (year % 4 == 0 and year % 100 != 0) or year % 400 == 0

if is_leap_year:
    label = "leap year"
else:
    label = "common year"

print(label)

Output

leap year

Notice

The calculation and the message choice have separate jobs, which makes the rule easier to read.

Realistic: print the full calendar message from the opening.

year = 2024
is_leap_year = (year % 4 == 0 and year % 100 != 0) or year % 400 == 0

if is_leap_year:
    print(f"{year} is a leap year.")
else:
    print(f"{year} is not a leap year.")

Output

2024 is a leap year.

Notice

One condition controls two possible messages, and exactly one appears.

Real input: try the full calendar decision with a year supplied by a person.

Try it yourself

year = int(input("Year: "))
is_leap_year = (year % 4 == 0 and year % 100 != 0) or year % 400 == 0
if is_leap_year:
    print(f"{year} is a leap year.")
else:
    print(f"{year} is not a leap year.")

Notice

Converting the typed year lets the same full rule choose one calendar sentence.

Final build

Run the calendar decision with the fixed year 2024. Read the condition aloud: divisible by 4 and not by 100, or divisible by 400.

# One given year; no repeated testing is needed.
year = 2024
is_leap_year = (year % 4 == 0 and year % 100 != 0) or year % 400 == 0

if is_leap_year:
    print(f"{year} is a leap year.")
else:
    print(f"{year} is not a leap year.")

Output

2024 is a leap year.

Notice

Comparisons create booleans, logical operators combine them, and the decision selects the one matching output.

Recap

  • if runs a block when its condition is true.
  • elif checks another path; else handles what remains.
  • and, or, and not combine or reverse yes-or-no tests.
  • An indented decision inside another decision asks a second question.
  • Branch order matters because Python takes the first matching path.

You can now build a calendar decision that identifies a leap year.

Go to the exercises (21)

For parents and teachers: report a problem