Programming in Python

Chapter 3
Control Flow and Problem Solving

Bachelors Program in AI & ML
Programming in Python   •   Bachelor

Control Flow and Problem Solving

Introduction

A Python program is useful not merely because it can store data or perform calculations, but because it can make decisions and repeat actions systematically. Control flow is the order in which statements are executed. By default, Python follows a sequential flow: statements run from top to bottom. Real programs, however, must respond to conditions, process collections of unknown size, and choose among alternative actions. Selection, pattern matching, iteration, ranges, loop control, and nested logic provide the fundamental mechanisms for doing this.

These mechanisms are also central to artificial intelligence and machine learning applications. Data-cleaning pipelines use conditions to identify invalid records; training programs repeat operations across epochs; search algorithms explore alternatives; and evaluation programs classify outputs according to rules. Although modern AI systems often use sophisticated libraries, their surrounding software still depends on precise control flow.

This chapter develops a disciplined approach to translating problem specifications and pseudocode into correct Python programs. It emphasizes not only syntax but also reasoning: identifying cases, defining stopping conditions, preserving invariants, testing boundary values, and selecting an appropriate control structure.

Learning Objectives

By the end of this chapter, you should be able to:

1. Explain sequential, conditional, and iterative control flow.

2. Construct Boolean expressions using comparisons and logical operators.

3. Use `if`, `elif`, and `else` to implement mutually exclusive decisions.

4. Apply structural pattern matching with `match` and `case` when it improves clarity.

5. Use `for` loops, `while` loops, and `range` accurately.

6. Control loops with `break`, `continue`, and loop `else` clauses.

7. Design and trace nested conditional statements and nested loops.

8. Translate pseudocode and informal specifications into tested Python programs.

9. Analyze common errors, limitations, and professional implications of control-flow decisions.

1. From Problem Specification to Program Logic

A problem specification describes what a program must accomplish. Before writing Python code, the programmer should identify:

  • Inputs: values supplied by a user, file, sensor, or another program.
  • Outputs: values, messages, files, or system actions produced.
  • State: information that changes while the program runs.
  • Conditions: facts that determine which action should occur.
  • Repetition: work that must be performed multiple times.
  • Termination: the condition under which execution stops.
  • Constraints: requirements such as valid ranges, performance, or safety.

A useful translation process is:

1. Restate the problem in precise language.

2. List normal cases and exceptional cases.

3. Write pseudocode using indentation and clear control words.

4. Identify variables and their initial values.

5. Translate each pseudocode structure into Python.

6. Trace the program with representative and boundary inputs.

7. Test invalid inputs and termination behavior.

For example, a specification might state: “Read examination marks, reject values outside 0–100, assign a grade, and report the result.” The specification contains validation, selection, and output. A direct implementation is clearer when these responsibilities are separated rather than hidden in one complicated expression.

```python

mark = float(input("Enter mark: "))

if not 0 <= mark <= 100:

print("Invalid mark")

elif mark >= 70:

print("Distinction")

elif mark >= 50:

print("Pass")

else:

print("Fail")

```

The order of conditions matters. Once `mark >= 70` is false, the later tests need not reconsider that case. The chained comparison `0 <= mark <= 100` is equivalent to `0 <= mark and mark <= 100`; it does not compare a Boolean result with a number.

2. Boolean Logic and Selection

A Boolean expression evaluates to `True` or `False`. Comparisons include:

  • `==` equality
  • `!=` inequality
  • `<`, `<=`, `>`, `>=` ordering
  • `is` and `is not` identity tests, normally used for objects such as `None`
  • `in` and `not in` membership tests

The assignment operator `=` must not be confused with equality `==`.

Python combines conditions with:

  • `and`: true only when both operands are true;
  • `or`: true when at least one operand is true;
  • `not`: reverses a Boolean value.

Python uses short-circuit evaluation. In `A and B`, Python does not evaluate `B` if `A` is false. In `A or B`, it does not evaluate `B` if `A` is true. This can prevent unnecessary work or unsafe evaluation, but expressions should remain readable.

Parentheses are recommended when logical precedence could be misunderstood. For example:

```python

if (age >= 18 and has_id) or is_authorised:

print("Access permitted")

```

The `if` statement selects a block when its condition is true. An optional `else` handles all remaining cases, while `elif` supports additional alternatives.

```python

if temperature < 0:

state = "solid risk"

elif temperature < 100:

state = "liquid range"

else:

state = "vapour range"

```

This is a multiway selection. The conditions are evaluated from top to bottom, and only the first true branch executes. A common error is writing independent `if` statements when the cases are intended to be mutually exclusive:

```python

May print more than one message

if score >= 50:

print("Pass")

if score >= 70:

print("Distinction")

```

If exactly one result is required, use `if`–`elif`–`else` or return immediately from a function.

Guard Clauses

A guard clause handles an exceptional or terminating case early. It reduces nesting and makes the normal path easier to see.

```python

def average(values):

if not values:

return None

return sum(values) / len(values)

```

Here, the empty-list case is handled before division. Guard clauses are valuable in validation, input processing, and security-sensitive code because invalid states can be rejected early.

Truth Values and Common Pitfalls

In a Boolean context, `False`, `None`, numeric zero, and empty collections are false; most other objects are true. Thus, `if items:` tests whether a collection is non-empty. This is concise, but the programmer must distinguish “empty” from “missing” when the application treats them differently.

The expression `x == 1 or 2` is a frequent error. It is interpreted as `(x == 1) or 2`, and `2` is truthy. The correct expression is `x == 1 or x == 2`, or more clearly, `x in (1, 2)`.

3. Structural Pattern Matching

Python’s `match` statement, introduced in Python 3.10, supports structural pattern matching. It is useful when a program must compare a value or inspect the shape of data across several alternatives. It is not simply a renamed `switch` statement.

```python

command = input("Command: ").strip().lower()

match command:

case "start":

print("System starting")

case "stop":

print("System stopping")

case "status":

print("System ready")

case _:

print("Unknown command")

```

The underscore is a wildcard pattern that matches anything and is usually placed last. A case may include a guard:

```python

match result:

case (status, value) if status == "ok" and value >= 0:

print("Valid result:", value)

case (status, _):

print("Result status:", status)

case _:

print("Unexpected format")

```

Patterns can match sequences, mappings, classes, and literal values. Alternatives can be combined using `|`:

```python

match response_code:

case 200 | 201:

message = "Successful response"

case 400 | 404:

message = "Client-side problem"

case _:

message = "Other response"

```

Use `if`–`elif` when conditions are primarily arbitrary Boolean tests. Use `match` when alternatives have a recognizable structure or a finite set of patterns. Pattern matching can make command interpreters, parsers, and protocol handlers more expressive, but it requires a recent Python version and should not be used merely for novelty.

4. Iteration and Repetition

Iteration executes a block repeatedly. Python provides two principal loop forms.

The `for` Loop

A `for` loop visits items in an iterable, such as a list, string, tuple, set, dictionary, file, or range.

```python

features = ["age", "income", "score"]

for feature in features:

print(feature)

```

The loop variable receives each item in turn. When iterating over a dictionary, iteration visits keys by default:

```python

counts = {"cat": 3, "dog": 5}

for animal, count in counts.items():

print(animal, count)

```

A `for` loop is generally preferred when the number or source of iterations is determined by an iterable. It avoids manually maintaining an index and reduces off-by-one errors.

The `while` Loop

A `while` loop repeats while a condition remains true.

```python

attempts = 0

password = ""

while password != "correct" and attempts < 3:

password = input("Password: ")

attempts += 1

if password == "correct":

print("Authenticated")

else:

print("Access denied")

```

A correct `while` loop must establish an initial condition, test the condition, update relevant state, and eventually reach a false condition or an explicit termination statement. If no state affecting the condition changes, the loop may be infinite.

The `range` Object

`range` represents an arithmetic progression of integers without immediately constructing a list. Its forms are:

```python

range(stop)

range(start, stop)

range(start, stop, step)

```

The `stop` value is excluded. The default start is `0`, and the default step is `1`.

```python

for number in range(2, 8):

print(number) # 2, 3, 4, 5, 6, 7

for number in range(10, 1, -2):

print(number) # 10, 8, 6, 4, 2

```

The step must not be zero. A negative step is required when counting downward. The expression `range(1, 10, -1)` produces no values because the starting value is already below the stopping direction.

The half-open interval convention—include the start, exclude the stop—supports useful properties. For example, `range(len(items))` produces valid list indices, and adjacent ranges such as `range(0, 5)` and `range(5, 10)` do not overlap.

Prefer Direct Iteration

When the index is unnecessary, direct iteration communicates intent better:

```python

for name in names:

print(name)

```

When both an index and a value are needed, use `enumerate`:

```python

for index, name in enumerate(names, start=1):

print(index, name)

```

When two sequences must be processed together, use `zip`:

```python

for name, mark in zip(names, marks):

print(name, mark)

```

By default, `zip` stops when the shortest iterable is exhausted. If unequal lengths indicate an error, validate them explicitly rather than silently losing data.

5. Loop Control and the Loop `else` Clause

`break` terminates the nearest enclosing loop. `continue` skips the remainder of the current iteration and proceeds to the next iteration.

```python

for value in values:

if value < 0:

continue

if value == 0:

break

print(value)

```

Use these statements for clear control decisions, not to obscure complicated logic. In particular, a loop with many `break` and `continue` statements can be difficult to test.

Python also permits an `else` clause on loops. The loop `else` executes if the loop finishes normally, but not if it terminates through `break`.

```python

def contains_duplicate(values):

seen = set()

for value in values:

if value in seen:

return True

seen.add(value)

else:

return False

```

The `else` expresses “no early termination occurred.” It is useful for searches, but many teams prefer a Boolean variable or a function return when that is more immediately readable.

6. Nested Logic and Nested Loops

A control structure placed inside another is nested. Nested logic is necessary when a decision depends on earlier decisions or when each item in one collection must be compared with items in another.

```python

for row in range(3):

for column in range(4):

print(f"({row}, {column})", end=" ")

print()

```

The inner loop completes all four iterations for each iteration of the outer loop, producing 3 × 4 = 12 coordinate pairs. In general, if an outer loop performs `m` iterations and an inner loop performs `n` iterations for each outer iteration, the body executes `m × n` times, assuming fixed counts.

Nested loops can therefore become expensive. Searching every pair in a collection of `n` items may require approximately `n²` comparisons, whereas a set-based method can often reduce membership checking to expected constant time per item. The exact performance depends on data structures and implementation, but the design principle is broadly important: use the simplest structure that satisfies the specification.

Nested conditionals should be designed around a decision tree. Excessive nesting is often a signal that validation, classification, and processing should be separated into functions.

```python

def classify_temperature(value):

if value < 0:

return "below freezing"

if value < 100:

return "ordinary liquid range"

return "above boiling point"

```

This flat structure is easier to read than deeply nested alternatives and is straightforward to unit-test.

7. Worked Example: Translating Pseudocode

Specification

Given a positive integer `n`, display every integer from 1 through `n`, replacing multiples of 3 with `Fizz`, multiples of 5 with `Buzz`, and multiples of both with `FizzBuzz`.

Reasoning

The repeated action is naturally represented by a `for` loop over `range(1, n + 1)`. The combined condition must be checked before the individual conditions; otherwise, a number such as 15 would be labelled only `Fizz`.

Pseudocode

```text

read n

for number from 1 through n

if number is divisible by 3 and 5

print FizzBuzz

else if number is divisible by 3

print Fizz

else if number is divisible by 5

print Buzz

else

print number

```

Python Implementation

```python

n = int(input("Enter a positive integer: "))

if n <= 0:

print("n must be positive")

else:

for number in range(1, n + 1):

if number % 3 == 0 and number % 5 == 0:

output = "FizzBuzz"

elif number % 3 == 0:

output = "Fizz"

elif number % 5 == 0:

output = "Buzz"

else:

output = str(number)

print(output)

```

The `%` operator gives the remainder after integer division. A number is divisible by a positive integer when its remainder is zero. The program validates the input before entering the loop, uses the inclusive upper bound `n + 1`, and orders the overlapping case first.

Trace for `n = 16`

The values 3, 6, 9, 12, and 16 produce `Fizz` except that 15 produces `FizzBuzz`; 5 and 10 produce `Buzz`; all other values are printed numerically. This trace tests ordinary values, each individual condition, the overlapping condition, and the final boundary.

8. Applied Case: Validating and Summarising Sensor Readings

Background

This illustrative case concerns a small environmental-monitoring device that receives temperature readings. The device must ignore missing readings, reject physically impossible values outside a configured range, count warning readings, and compute an average of accepted readings.

Problem

Given a sequence that may contain numbers and `None`, accept values from -50 to 60 degrees Celsius inclusive. Count readings at or above 35 degrees as warnings. If no valid readings exist, report that an average cannot be calculated.

Analysis

The specification contains several decisions. Missing values must be distinguished from numeric zero, so `reading is None` is appropriate. Range validation should occur before classification. A single pass through the data is sufficient, so a `for` loop is preferable to repeated searches. The running total and count form the required state.

Solution

```python

def summarise_readings(readings):

total = 0.0

valid_count = 0

warning_count = 0

for reading in readings:

if reading is None:

continue

if not -50 <= reading <= 60:

continue

total += reading

valid_count += 1

if reading >= 35:

warning_count += 1

if valid_count == 0:

return {"valid": 0, "warnings": 0, "average": None}

return {

"valid": valid_count,

"warnings": warning_count,

"average": total / valid_count,

}

sample = [20.0, None, 37.5, -60.0, 35.0, 18.5]

print(summarise_readings(sample))

```

For this sample, the accepted values are 20.0, 37.5, 35.0, and 18.5. Their sum is 111.0, so the average is 111.0 / 4 = 27.75 °C. There are two warnings. The value -60.0 is rejected, and `None` is skipped.

Outcome and Learning Points

The function performs one pass and maintains explicit state. It distinguishes missing, invalid, and valid readings; handles the zero-valid-reading case; and returns structured data rather than mixing calculation with user-interface output. In a production system, the acceptable range and warning threshold should be documented configuration values, and units should be verified at the system boundary.

9. Testing, Correctness, and Professional Practice

Control-flow programs should be tested against equivalence classes and boundary values. For a mark range from 0 to 100, useful tests include values below the range, exactly 0, an ordinary interior value, exactly 100, and values above the range. For a loop, test zero iterations, one iteration, multiple iterations, and termination at the final permitted value.

A trace table records variable values after each iteration. It is particularly useful for finding errors in counters, accumulators, and loop conditions. Assertions can document assumptions:

```python

assert 0 <= mark <= 100

```

Assertions are useful during development, but they should not be the sole mechanism for validating untrusted user input because Python can disable assertions with an interpreter option.

Important common errors include:

  • using `=` instead of `==`;
  • forgetting that `range` excludes its stop value;
  • creating an infinite `while` loop;
  • testing a broad condition before a more specific one;
  • using `is` for value equality instead of identity;
  • modifying a collection while iterating over it;
  • assuming `zip` reports unequal lengths;
  • allowing malformed input to reach later calculations;
  • relying on accidental operator precedence;
  • placing `break` in a nested loop while intending to exit all loops.

Professional code should favour meaningful names, small functions, consistent indentation, explicit validation, and tests that document expected behavior. Static analysis tools, formatters, type checkers, and automated tests can identify many control-flow defects before deployment.

10. Ethical, Safety, and AI-Related Considerations

Control flow may encode consequential decisions. A rule such as “reject an application when a score is below a threshold” can affect people if used in admissions, lending, employment, or healthcare. Programmers must therefore document decision criteria, validate data quality, monitor error cases, and ensure that automated rules comply with applicable law and institutional policy. A technically correct conditional can still implement an unfair or unjustified policy.

In AI systems, control flow often surrounds statistical models. For example, a program may route low-confidence predictions to human review. The threshold should be justified using validation evidence, and the fallback path must be tested. Programs should not silently discard records, conceal exceptions, or treat missing values as negative evidence without a domain-based rationale. Logging and audit trails are important when decisions may need explanation or review.

11. Contemporary Developments and Best Practices

Modern Python development increasingly combines basic control flow with comprehensions, generator expressions, iterators, asynchronous loops, and pattern matching. These features can improve expressiveness, but they do not replace the need to understand conditions and termination. A compact comprehension should be avoided when a normal loop is clearer, especially when it contains side effects or nested logic.

Best practices include:

1. Choose `for` for traversal of known iterables and `while` for condition-controlled repetition.

2. Make interval boundaries explicit and test them.

3. Place specific cases before general cases.

4. Use guard clauses to reject invalid states early.

5. Keep loop bodies small and separate calculations from presentation.

6. Avoid unnecessary nesting; use functions and early returns.

7. Ensure every `while` loop has a demonstrable termination path.

8. Use `match` for structural alternatives, not arbitrary conditions.

9. Test empty, singleton, boundary, invalid, and unusually large inputs.

10. Document assumptions, units, thresholds, and failure behavior.

Chapter Summary

Control flow determines the execution order of a Python program. Selection uses Boolean expressions with `if`, `elif`, and `else`; structural pattern matching uses `match` and `case` to handle recognizable data patterns. Iteration uses `for` to traverse iterables and `while` to repeat while a condition remains true. The `range` object represents integer sequences using an inclusive start and exclusive stop, while `enumerate` and `zip` support clear traversal of indexed and parallel data.

`break`, `continue`, and loop `else` provide additional loop control, but they should be used transparently. Nested logic can express complex decisions and pairwise processing, yet excessive nesting and quadratic loops may harm readability and performance. Reliable problem solving requires translating specifications into pseudocode, identifying boundary cases, maintaining explicit state, proving termination where possible, and testing normal and exceptional inputs. In professional and AI-related systems, control-flow rules must also be documented, auditable, and evaluated for safety and fairness.

Key Terms

Control flow: The order in which program statements execute.

Boolean expression: An expression that evaluates to `True` or `False`.

Selection: Choosing among alternative execution paths.

Conditional statement: A statement that executes code according to a condition.

Guard clause: An early check that handles an exceptional or invalid case.

Short-circuit evaluation: Stopping Boolean evaluation when the result is already determined.

Structural pattern matching: Matching values or data structures against patterns using `match` and `case`.

Iteration: Repeated execution of a block of code.

Iterable: An object whose elements can be visited sequentially.

`for` loop: A loop that traverses an iterable.

`while` loop: A loop that continues while a condition is true.

Range: A lazy integer progression represented by Python’s `range` object.

Half-open interval: An interval that includes its start but excludes its stop.

`break`: A statement that terminates the nearest enclosing loop.

`continue`: A statement that skips to the next loop iteration.

Loop invariant: A property that remains true at a defined point during every iteration.

Sentinel: A special value used to signal termination or a particular state.

Nested logic: A control structure placed inside another control structure.

Boundary value: An input at or immediately near a permitted limit.

Pseudocode: Informal, language-independent notation for expressing an algorithm.

Review Questions

Short Answer Questions

1. What is the difference between `if`–`elif`–`else` and several independent `if` statements?

2. Why does `range(2, 8)` produce six values rather than seven?

3. When is a `while` loop more appropriate than a `for` loop?

4. What is the purpose of the wildcard pattern `_` in a `match` statement?

5. Explain the difference between `break` and `continue`.

Long Answer Questions

1. Explain how Boolean operators and short-circuit evaluation affect conditional execution. Include suitable Python examples.

2. Describe a systematic method for translating a problem specification into Python control flow.

3. Compare `for` loops, `while` loops, and `range`, including common errors associated with each.

4. Explain how nested loops affect execution count and potential performance. Give an example where an alternative data structure would be preferable.

5. Discuss how validation, boundary testing, and guard clauses contribute to reliable software.

Analytical / Application Questions

1. Design a Python function that accepts a list of marks, ignores missing values, rejects values outside 0–100, and returns the count, highest valid mark, and average. Specify behavior when no valid marks exist.

2. A program must process commands represented as tuples such as `("ADD", item)` and `("REMOVE", item)`. Decide whether `if`–`elif` or `match` is more appropriate and justify your choice with a code outline.

3. A nested-loop program compares every pair of 10,000 records. Analyze the likely scalability problem and propose a control-flow or data-structure redesign.

Multiple Choice Questions with Answers

Question 1

What values are produced by range(2, 6)?

A. 2, 3, 4, 5

B. 2, 3, 4, 5, 6

C. 1, 2, 3, 4, 5

D. 3, 4, 5, 6

Correct Answer: A. 2, 3, 4, 5

Question 2

Which condition correctly tests whether x is either 1 or 2?

A. x == 1 or 2

B. x == (1 or 2)

C. x == 1 or x == 2

D. x in 1, 2, 3

Correct Answer: C. x == 1 or x == 2

Question 3

What happens when a break statement executes inside a loop?

A. The current iteration is restarted

B. The nearest enclosing loop terminates

C. All functions in the program terminate

D. The loop condition is permanently changed to True

Correct Answer: B. The nearest enclosing loop terminates

Question 4

Which Python structure is most suitable for traversing every item in a list?

A. A for loop

B. A match statement only

C. An assignment statement only

D. A while loop with no condition

Correct Answer: A. A for loop

Question 5

What does the underscore pattern _ usually represent in a match statement?

A. A pattern that matches nothing

B. A wildcard that can match any remaining value

C. A syntax error in every case

D. A pattern matching only integers

Correct Answer: B. A wildcard that can match any remaining value

Question 6

Which condition should normally be tested first in a FizzBuzz implementation?

A. The number is not divisible by 3

B. The number is divisible by 3 only

C. The number is divisible by 5 only

D. The number is divisible by both 3 and 5

Correct Answer: D. The number is divisible by both 3 and 5

Question 7

What is the main danger of a while loop whose condition-related state is never updated?

A. It always executes exactly once

B. It may become an infinite loop

C. It automatically converts to a for loop

D. It skips the first iteration

Correct Answer: B. It may become an infinite loop

Question 8

Which expression tests that a reading is not missing when missing values are represented by None?

A. reading == None only

B. reading is None

C. reading is not None

D. reading != 0

Correct Answer: C. reading is not None

Question 9

What does continue do in a loop?

A. Terminates every enclosing loop

B. Skips the remaining body of the current iteration

C. Repeats the current iteration forever

D. Executes the loop else clause immediately

Correct Answer: B. Skips the remaining body of the current iteration

Question 10

If an outer loop runs 3 times and an inner loop runs 4 times for each outer iteration, how many times does the inner body execute?

A. 4

B. 7

C. 12

D. 16

Correct Answer: C. 12

Programming in Python
Chapter 3 of 15