Unit 2: Conditional and Iterative Statements

INT108 — Python Programming 7 min read

I. Orientation: Control Flow in Python

A Python program executes statements top-to-bottom unless a control flow statement alters that order. This unit covers the two alterations available: selection (run a block only if a condition holds) and iteration (run a block repeatedly). Both depend on evaluating an expression to a truth value, so the unit also covers the operators that produce truth values, the % operator that supplies most of the arithmetic tests, and the random module that supplies unpredictable data to loop over.

Defining properties and conventions assumed throughout:

  • Block structure by indentation: Python has no begin/end or braces. A compound statement consists of a header line ending in a colon and an indented body (conventionally 4 spaces). Consistent indentation is syntax, not style.
  • The bool type: Has exactly two values, True and False (capitalised), which are subclasses of int with values 1 and 0 — True + True is 2.
  • Truthiness: Any object may be used where a condition is expected. Falsy values are False, None, 0, 0.0, "", [], (), {}; everything else is truthy.
  • Statement versus expression: if, while and for are statements — they perform an action and have no value. x % 2 == 0 is an expression — it has the value True or False.
  • Flow of execution: The order in which lines actually run. Tracing this order by hand (a desk check) is the primary debugging technique for this unit.
  • Loop vocabulary: iteration = one pass through the body; loop variable = the name updated each pass; terminating condition = the state that ends the loop; infinite loop = a loop whose condition never becomes false.

II. Operators and Values that Drive Decisions

Selection and iteration are only as expressive as the tests written into them. This section covers the arithmetic operator, the data source, and the two operator families that build those tests.

A. Modulus Operator

The modulus operator % yields the remainder after integer division of the left operand by the right.

  • Definition and companion: a % b is the remainder, a // b is the floor (integer) quotient. They satisfthe identity (a // b) * b + a % b == a.
    PYTHON
      7 % 3     # 1
      7 // 3    # 2
      -7 % 3    # 2   (sign follows the divisor in Python)
      6.5 % 2   # 0.5 (works on floats too)
  • Divisibility test: x % n == 0 is true exactly when n divides x. Basis of even/odd tests (x % 2 == 0), leap-year rules, and FizzBuzz-style logic.
  • Digit extraction: n % 10 gives the last decimal digit, n // 10 removes it — the standard pair for digit-sum, palindrome and reverse-number loops.
  • Wrapping/cycling: (i + 1) % k cycles indices 0,1,…,k−1; clock arithmetic uses hour % 12, day-of-week uses d % 7.
  • Error condition: b % 0 raises ZeroDivisionError, so guard with a conditional before dividing by a user-supplied value.

B. Random Numbers

Programs are deterministic, so "random" numbers come from a pseudo-random generator — a deterministic algorithm whose output passes statistical tests for randomness.

  • Import and seed: import random. The generator holds internal state; random.seed(42) fixes it so a run is reproducible for testing. Without a seed, state is initialised from the system clock/OS entropy.
  • Core functions:
    PYTHON
      random.random()            # float in [0.0, 1.0)
      random.randint(1, 6)       # int in 1..6 inclusive (a die)
      random.randrange(0, 10, 2) # like range(): 0,2,4,6,8
      random.uniform(1.5, 3.5)   # float in the given range
      random.choice(['H','T'])   # one element of a sequence
      random.shuffle(deck)       # in-place permutation
  • Scaling by hand: int(random.random() * n) gives 0..n-1; low + int(random.random() * (high - low + 1)) generalises it — the arithmetic randint performs internally.
  • Why it belongs here: Random values give conditionals and loops non-trivial input, enabling simulation (dice, coin tosses, Monte Carlo estimates) without file or user input.

C. Boolean Expressions

A Boolean expression is any expression whose value is True or False; the relational operators are its main source.

  • The six comparisons: == (equal), != (not equal), >, <, >=, <=. Note = is assignment; == is comparison — confusing them is a SyntaxError in a condition.
  • Chaining: Python permits mathematical chaining: 0 <= x <= 100 is evaluated as 0 <= x and x <= 100, with x computed once.
  • Membership and identity: in / not in test containment ('a' in 'cat'True); is / is not test object identity, correct only for singletons such as None.
  • Float caution: 0.1 + 0.2 == 0.3 is False due to binary representation; compare with a tolerance, abs(a - b) < 1e-9.

D. Logic Operators

The three logical operators combine Boolean expressions into compound conditions.

  • and: True only if both operands are true — n % 2 == 0 and n % 3 == 0 tests divisibility by 6.
  • or: True if at least one operand is true — ch == 'y' or ch == 'Y'.
  • not: Unary negation — not (x > 10) is equivalent to x <= 10 (De Morgan-style rewriting keeps conditions readable).
  • Short-circuit evaluation: and stops at the first false operand, or at the first true one. This makes guards possible: if n != 0 and total / n > 5: never divides by zero.
  • Return value: The operators return an operand, not a bool — 0 or 'x' is 'x', 'a' and 'b' is 'b'.
  • Precedence: not > and > or, and all comparisons bind tighter than all three; parenthesise mixed expressions for clarity.

III. Selection: Conditional Statements

Selection chooses among alternative blocks by evaluating conditions in order.

A. Conditional

The if statement executes its body only when the condition is truthy.

  • Three forms:
    PYTHON
      if x > 0:                 # simple: one branch
          print('positive')
    
      if x % 2 == 0:            # alternative: exactly one of two runs
          print('even')
      else:
          print('odd')
    
      if score >= 90:           # chained: first true branch wins, rest skipped
          grade = 'A'
      elif score >= 80:
          grade = 'B'
      else:
          grade = 'F'
  • Branch: Each possible path is a branch; else has no condition and is the catch-all. Only one branch of a chain ever executes.
  • Order matters: In a chained conditional, place the most restrictive condition first — testing score >= 80 before score >= 90 would label every A as a B.
  • Empty body: Use pass as a placeholder body, since an indented block cannot be omitted.
  • Conditional expression: parity = 'even' if n % 2 == 0 else 'odd' compresses a two-branch assignment into one line.

B. Nested Conditionals

A conditional whose body contains another conditional; the outer condition is a precondition for the inner test.

  • Structure and cost: Each level adds an indentation level; readability degrades quickly past two levels.
    PYTHON
      if x == y:
          print('equal')
      else:
          if x < y:
              print('x is less')
          else:
              print('x is greater')
  • Flattening with elif: The above is identical to if x == y: … elif x < y: … else: … — prefer the flat form.
  • Flattening with and: if 0 < x: if x < 10: becomes if 0 < x < 10:.
  • Guardian pattern: Nesting is legitimate when the outer test protects the inner one: if y != 0: wrapping if x / y > 1:.

IV. Iteration: Loop Statements

Iteration repeats a block. Choose while when the number of repetitions is unknown, for when it is known or a sequence is available.

A. While Statements

A while loop re-tests its condition before every iteration and stops when the condition becomes false.

  • Semantics: Evaluate condition → if false, exit; if true, run body, repeat. Zero iterations occur if the condition is false initially.
    PYTHON
      n = 27; count = 0
      while n != 1:                  # Collatz sequence length
          n = n // 2 if n % 2 == 0 else 3 * n + 1
          count += 1
  • Three obligations: initialise the loop variable before the loop, test it in the header, update it inside the body. Omitting the update gives an infinite loop.
  • Termination argument: For the countdown while n > 0: n = n - 1, n strictly decreases and is bounded below, so the loop must end.
  • Sentinel/validation loop: Repeat until input is acceptable — while True: with if valid: break.
  • break, continue, else: break exits immediately; continue skips to the next test; a loop else clause runs only if the loop ended without break (useful in prime testing).

B. For Loop Statement

A for loop performs definite iteration over the items of an iterable, assigning each in turn to the loop variable.

  • Form and range: range(start, stop, step) produces start … stop-1; stop is exclusive.
    PYTHON
      total = 0
      for i in range(1, 11):        # 1..10
          total += i                # accumulator pattern → 55
      for ch in 'python':           # traversal of a string
          print(ch, end=' ')
  • Traversal by index vs by item: for i in range(len(s)) gives positions; for c in s gives characters; for i, c in enumerate(s) gives both.
  • Counter and accumulator idioms: initialise count = 0 / total = 0 before the loop; update inside; report after.
  • Contrast with while: for cannot loop forever over a finite iterable and needs no manual update, so it is preferred whenever the iteration count is determinable in advance.

V. Nested Loops and Randomised Repetition

A. Nested For

A for loop inside another; the inner loop completes fully for each single iteration of the outer loop.

  • Iteration count: Outer m × inner n = m·n executions of the innermost body — a 3×4 nest runs 12 times.
  • Grid/table pattern:
    PYTHON
      for i in range(1, 4):             # rows
          for j in range(1, 4):         # columns
              print(i * j, end='\t')
          print()                        # newline after each row
  • Dependent bounds: for j in range(i) makes the inner range grow with the outer variable — the basis of triangular star patterns and pair-comparison loops (for j in range(i+1, n)).
  • Scope of break: break leaves only the innermost loop; use a flag or a function return to leave both.

B. Nested While

The same containment with condition-controlled loops, used when neither bound is known in advance.

  • Critical rule: the inner loop's control variable must be re-initialised inside the outer body, otherwise the inner condition is already false on the second outer pass and the inner loop never runs again.
    PYTHON
      i = 1
      while i <= 3:
          j = 1                 # re-initialised each outer pass
          while j <= 3:
              print(i, j)
              j += 1
          i += 1
  • Typical use: input validation inside a menu loop, or digit-processing (while n > 0) inside a per-number loop.

C. Random Numbers in Loops

Loops turn a single random draw into a data set, which is the basis of simulation.

  • Fixed-trial simulation: for plus a counter estimates a probability.
    PYTHON
      import random
      sixes = 0
      for _ in range(1000):
          if random.randint(1, 6) == 6:   # conditional inside the loop
              sixes += 1
      print(sixes / 1000)                 # ≈ 0.167
  • Random termination: while random.random() < 0.5: gives a loop whose trip count is unknown — the natural while case; the expected number of iterations is 1.
  • Reproducibility: call random.seed(0) once before the loop so a failing simulation can be re-run identically while debugging.
  • Pitfall: drawing the random value outside the loop freezes it; the call must be inside the body to vary per iteration.

VI. Encapsulation and Generalization

These are the two refactoring steps that convert working loop-and-conditional code into reusable code, applied in sequence.

A. Encapsulation

Wrapping a working fragment of code in a function so it can be invoked by name.

  • Procedure: take a tested loop, indent it under a def header, add a docstring, replace printed results with a return where appropriate.
  • Effect: local variables (i, count) become private to the function, removing name clashes; the code becomes a single named unit rather than a block to be re-typed.
    PYTHON
      def print_squares():
          """Print squares of 1..5."""
          for i in range(1, 6):
              print(i * i)

B. Generalization

Replacing a constant inside the encapsulated code with a parameter, so one function covers a family of cases.

  • Procedure: identify the hard-coded literal, add it as a parameter, substitute it in the body; supply a default to keep old call sites working.
    PYTHON
      def print_squares(n=5, base=2):
          """Print i**base for i in 1..n."""
          for i in range(1, n + 1):
              print(i ** base)
  • Keyword arguments: print_squares(base=3, n=4) documents intent at the call site and frees the caller from argument order.
  • Interface versus implementation: the interface is the parameter list and return value; the implementation is the loop inside. A well-generalized function lets the body change (e.g. whilefor) without breaking callers.
  • Discipline of the pair: encapsulate first (proving the code works in isolation), generalize second (widening its applicability); repeated together, they build a program from a set of small tested functions rather than one long script.