Learning If-Else by Inventing Branching Logic

You won't be taught — you will discover. Every step builds on the last. Trust the process.

Python Quick Reference

This chapter uses if/elif/else to make decisions and comparisons to test conditions.

# Comparison operators: ==  !=  <  >  <=  >=
x = 7
x > 5           # True
x == 7          # True
x != 3          # True

# if / elif / else
if x > 10:
    print("big")
elif x > 5:
    print("medium")    # this runs
else:
    print("small")

# Combining conditions: and, or, not
if x > 0 and x < 100:
    print("in range")

# Returning different values from a function
def classify(n):
    if n > 0:
        return "positive"
    elif n < 0:
        return "negative"
    else:
        return "zero"
  • if, elif, else must end with a colon; the body is indented.
  • == tests equality; = assigns a value.
  • and/or/not combine boolean conditions.

Part 1: Branching with Randomness

A Random Die Roll

random.random() returns a random decimal number between 0 (inclusive) and 1 (exclusive) — something like 0.3741....

Before coding:

  1. If you multiply a number in [0, 1) by 6, what range does the result fall in?
  2. If you then take the integer part (int(...)), what range of whole numbers do you get?
  3. How would you shift that range so it becomes 1 to 6 instead of 0 to 5 — like a die?

Your derivation:

  • random.random() * 6 is in the range: ...
  • int(random.random() * 6) is in the range: ...
  • To shift to 1--6: ...

Write `roll_die`

Write a function roll_die() that returns a random integer between 1 and 6 (inclusive), using only random.random() — no if needed yet, just the expression you derived above.

Run it a bunch of times to make sure you only ever see 1 through 6.

Your First Branch: Coin Toss

Now for an actual decision. random.random() < 0.5 is True about half the time, False the other half.

Write a function coin_toss() that returns the string "Heads" or "Tails", using an if/else on random.random() < 0.5.

Tasks:

  1. Implement coin_toss().
  2. Call it 20 times and print the results.
  3. Call it 10,000 times and count how many times you got "Heads". Is it close to 5,000?

A Three-Way Branch

if/else only gives you two outcomes. if/elif/else gives you more.

Write a function classify_roll(n) that takes a die result n (1 through 6) and returns:

  • "Low" if n is 1 or 2
  • "Mid" if n is 3 or 4
  • "High" if n is 5 or 6

Tasks:

  1. Implement it using if/elif/else.
  2. Roll the die 20 times with roll_die() and classify each result. Print both the roll and its class.

Part 2: Comparing Two Distances — Closer to A or B?

Set Up the Comparison

You're given three points on a 2D plane: P, A, and B. You want to know whether P is closer to A or to B.

You could compute the real distances (with a square root), but for comparing two distances, you don't need to — the one with the smaller squared distance is also the one with the smaller actual distance. (Square root is increasing, so it never flips the order.)

Before coding:

For P=(1, 2), A=(0, 0), B=(5, 5):

  1. Compute the squared distance from P to A: (px - ax)**2 + (py - ay)**2.
  2. Compute the squared distance from P to B.
  3. Which is smaller? So which point is P closer to?

Your derivation:

  • squared distance P to A: ...
  • squared distance P to B: ...
  • closer point: ...

Write `closer_point`

Write a function closer_point(p, a, b) where p, a, b are each (x, y) tuples. It should return:

  • 'A' if p is closer to a
  • 'B' if p is closer to b
  • 'Equal' if the distances are the same

This needs a three-way branch — if/elif/else.

Example:

closer_point((1, 2), (0, 0), (5, 5))     # Output: 'A'
closer_point((4, 4), (0, 0), (8, 8))     # Output: 'Equal'
closer_point((7, 3), (2, 3), (10, 3))    # Output: 'B'

Try More Cases

Tasks:

  1. Pick three new points where you can predict the answer just by sketching them on paper. Verify your function agrees.
  2. Pick a point exactly halfway between A and B. What does closer_point say?

Part 3: Is a Point Between Two Others? (1D)

The Order Problem

On a number line, you have two points a and b, and a third point p. You want to know if p lies between a and b (including the endpoints).

The tricky part: you're not told whether a < b or a > b.

Before coding:

  1. If a = 5 and b = 2, what are the actual lower and upper bounds of the segment?
  2. Python's built-in min() and max() can take two arguments. How would you use them to get the lower and upper bounds regardless of which of a, b is bigger?

Your derivation:

  • lower bound = ...
  • upper bound = ...

Write `is_point_on_line_1d`

Write is_point_on_line_1d(a, b, p) that returns True if p lies between a and b (inclusive), regardless of which of a, b is larger.

Example:

is_point_on_line_1d(2, 5, 3)      # Output: True
is_point_on_line_1d(5, 2, 3)      # Output: True   (order doesn't matter)
is_point_on_line_1d(2, 5, 6)      # Output: False
is_point_on_line_1d(4, 4, 4)      # Output: True   (a single point)
is_point_on_line_1d(4, 4, 5)      # Output: False

Test with Negative Numbers

A good function shouldn't need special-casing for negative numbers if the logic is right.

Tasks: test is_point_on_line_1d on these and check the results match the comments.

is_point_on_line_1d(-5, -2, -3)   # Output: True
is_point_on_line_1d(-5, -2, -6)   # Output: False
is_point_on_line_1d(-2, -5, -4)   # Output: True
is_point_on_line_1d(-1, 3, 0)     # Output: True

Part 4: Do Two Segments Touch or Overlap? (1D)

Think About Separation, Not Overlap

You now have two segments on the number line: segment 1 from start1 to end1, and segment 2 from start2 to end2 (and again, you can't assume start < end for either one).

It's easier to reason about when they're completely separate first, then negate it.

Before coding:

  1. After sorting each segment so you know its true lo and hi, when is segment 1 entirely to the left of segment 2? Write the condition.
  2. When is segment 1 entirely to the right of segment 2?
  3. The segments touch or overlap exactly when neither of those is true. Using not and or (or by flipping the logic directly), write the combined condition for "touching or overlapping".

Your derivation:

  • segment 1 entirely left of segment 2 when: ...
  • segment 1 entirely right of segment 2 when: ...
  • touching or overlapping when: ...

Write `are_lines_touching_or_overlapping`

Write are_lines_touching_or_overlapping(start1, end1, start2, end2) that returns True if the two segments touch or overlap, False if they're completely separate. Remember to handle the case where start > end for either segment, just like in Part 3.

Example:

are_lines_touching_or_overlapping(1, 4, 3, 6)        # Output: True   (overlap 3 to 4)
are_lines_touching_or_overlapping(1, 3, 3, 5)        # Output: True   (touch at 3)
are_lines_touching_or_overlapping(1, 2, 3, 4)        # Output: False  (separate)

Test with Negative and Reversed Inputs

Tasks: verify these all match the comments — pay special attention to the last one, where both segments are given "backwards".

are_lines_touching_or_overlapping(-3, 1, 0, 4)       # Output: True   (overlap 0 to 1)
are_lines_touching_or_overlapping(-5, -2, -2, 3)     # Output: True   (touch at -2)
are_lines_touching_or_overlapping(-10, -6, -5, -1)   # Output: False  (no touch or overlap)
are_lines_touching_or_overlapping(-2, -7, -4, -3)    # Output: True   (overlap, even with reversed inputs)

Separation vs Overlap Logic

You wrote are_lines_touching_or_overlapping by checking whether the segments are NOT separated. Why is it easier to check for separation instead of directly checking for overlap?

A. Separation is faster to compute

B. Overlap has many sub-cases (partial overlap, containment, touching at endpoint) but separation has only two simple conditions

C. Python can only check inequality, not equality

D. Separation works in 2D but overlap doesn't

Part 5: Is a Point Inside a Rectangle? (2D)

Reduce 2D to Two 1D Problems

A rectangle with sides parallel to the axes is defined by its bottom-left corner (x1, y1) and top-right corner (x2, y2) (with x1 < x2 and y1 < y2). A point (px, py) is inside it if its x is between x1 and x2, and its y is between y1 and y2.

Look closely — "is px between x1 and x2" is exactly the function you wrote in Part 3!

Before coding: write out, in words, how is_point_inside_rectangle could be built using two calls to is_point_on_line_1d instead of writing brand-new comparison logic.

Your plan:

is_point_inside_rectangle(x1, y1, x2, y2, px, py) = is_point_on_line_1d(...) and is_point_on_line_1d(...)

Write `is_point_inside_rectangle`

Write is_point_inside_rectangle(x1, y1, x2, y2, px, py) by reusing is_point_on_line_1d — don't rewrite the comparison logic from scratch.

Example:

is_point_inside_rectangle(0, 0, 10, 5, 3, 2)   # Output: True
is_point_inside_rectangle(0, 0, 10, 5, 10, 5)  # Output: True  (on the corner)
is_point_inside_rectangle(0, 0, 10, 5, 11, 5)  # Output: False
is_point_inside_rectangle(-5, -5, 5, 5, 0, 0)  # Output: True

Visualise It

Use the plotting helper below to see a rectangle and a point together, colour-coded by whether the point is inside.

Tasks: Try a few of your own points and rectangles to build intuition.

plot_rectangle_and_point(0, 0, 10, 5, 3, 2)
plot_rectangle_and_point(0, 0, 10, 5, 11, 5)

Part 6: Do Two Rectangles Intersect? (2D)

Project onto Each Axis

A rectangle is just two independent ranges glued together: an x-range and a y-range. Two rectangles intersect exactly when their x-ranges touch/overlap and their y-ranges touch/overlap.

This is the same trick as Part 5 — except now you reuse the Part 4 function (segment overlap) instead of the Part 3 function (point between).

A rectangle here is represented as ((x1, y1), (x2, y2)) — bottom-left and top-right corners.

Before coding: write out, in words, how are_rectangles_intersecting(rect1, rect2) could be built using two calls to are_lines_touching_or_overlapping — one for the x-coordinates of both rectangles, one for the y-coordinates.

Your plan:

are_rectangles_intersecting(rect1, rect2) = are_lines_touching_or_overlapping(...) and are_lines_touching_or_overlapping(...)

Write `are_rectangles_intersecting`

Write are_rectangles_intersecting(rect1, rect2) by reusing are_lines_touching_or_overlapping on the x-coordinates and on the y-coordinates.

Example:

are_rectangles_intersecting(((0, 0), (3, 3)), ((2, 2), (5, 5)))   # Output: True
are_rectangles_intersecting(((0, 0), (1, 1)), ((2, 2), (3, 3)))   # Output: False
are_rectangles_intersecting(((0, 0), (2, 2)), ((2, 2), (4, 4)))   # Output: True  (touching at a corner)
are_rectangles_intersecting(((0, 0), (5, 5)), ((1, 1), (2, 2)))   # Output: True  (one inside another)

Visualise It

Use the plotting helper below to see both rectangles, colour-coded by whether they intersect.

Tasks: Try a few of your own rectangle pairs to build intuition.

plot_rectangles(((0, 0), (3, 3)), ((2, 2), (5, 5)))
plot_rectangles(((0, 0), (1, 1)), ((2, 2), (3, 3)))

Dimension Decomposition

To check if two rectangles intersect in 2D, you checked whether their projections overlap on BOTH axes. What happens if you only check one axis?

A. You get the correct answer for squares, but not rectangles

B. You might say they intersect when they actually don't — projections can overlap on one axis while the rectangles are side by side on the other

C. You miss some valid intersections

D. It works fine for 2D — one axis is enough

Part 7: Bonus Challenges

Devise Your Own Impurity Formula

In machine learning (you'll meet this properly in the Decision Trees chapter), we often need a number that says how "mixed" a set is between two classes.

Write my_impurity(c1, c2) where c1 and c2 are counts (not fractions) of two classes. Your formula must satisfy:

  1. If c1 = 0 or c2 = 0 (all one class), impurity should be 0.
  2. Impurity should increase as the counts become more evenly balanced.
  3. Impurity should be maximum when c1 == c2.

There's no single right answer — invent a formula, test it against the three conditions, and adjust if it doesn't satisfy them. (One possible formula: (2 * min(c1, c2)) / (c1 + c2).)

3D Boxes

You generalised "point between two numbers" into "point inside a rectangle" by checking the x-axis and y-axis independently. Push it one step further.

A 3D box is ((x1, y1, z1), (x2, y2, z2)) — its two opposite corners. Write are_boxes_intersecting(box1, box2) by reusing are_lines_touching_or_overlapping three times — once per axis.

Part 8: Reflection — What Did You Just Build?

Reflection

Take a moment to answer these questions in your own words.

  1. What's the difference between if/else and if/elif/else? When do you need the second form?
  2. Why is comparing squared distances enough, without ever computing a square root?
  3. In Part 3, why did you need min()/max() instead of just assuming a < b?
  4. What was the key trick that let you turn the 1D "is point between" function into the 2D "is point inside a rectangle" function?
  5. What was the key trick that let you turn the 1D "segments overlap" function into the 2D "rectangles intersect" function?
  6. In your own words: what general strategy do Parts 5 and 6 both use to turn a 2D (or 3D) problem into something easier?