Python Programming

Chapter 3
Control Flow and Problem-Solving Logic

Diploma in Data science
Python Programming   •   Diploma

Control Flow and Problem-Solving Logic

Introduction

A Python program is useful not merely because it can store data or perform calculations, but because it can make decisions, repeat operations, and respond differently to changing conditions. These abilities are provided by control flow. Control flow determines the order in which instructions are executed.

In a simple sequential program, statements run from top to bottom. Real data-science tasks are rarely purely sequential. A program may need to reject invalid input, classify a customer, repeat calculations for every record, stop when a condition is met, or apply a different rule according to the value of a variable. These tasks require selection, iteration, pattern matching, and nested control structures.

Control flow must be designed as an algorithm before it is translated into Python. A well-designed algorithm is correct, readable, testable, and reasonably efficient. This chapter develops those qualities through pseudocode, Python syntax, problem-solving strategies, and practical data-oriented examples.

Learning Objectives

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

  1. Describe an algorithm using structured pseudocode and flow logic.
  2. Use Boolean expressions and comparison operators accurately.
  3. Implement selection with if, elif, and else.
  4. Apply structural pattern matching with match and case.
  5. Use for and while loops, including break, continue, and loop else.
  6. Design nested control structures without unnecessary complexity.
  7. Construct list, set, and dictionary comprehensions responsibly.
  8. Evaluate correctness, readability, and basic time and space efficiency.
  9. Apply control-flow techniques to a realistic data-science problem.

1. Algorithms and Pseudocode

An algorithm is a finite, ordered set of unambiguous instructions for solving a problem. An algorithm normally has:

  • Inputs: values supplied to the process;
  • Processing steps: operations performed on those values;
  • Outputs: results produced by the process;
  • Termination: a condition ensuring that execution eventually stops.

For example, consider a classification rule for a test score. Scores of 70 or above are classified as “Distinction”, scores from 50 to 69 as “Pass”, and lower scores as “Fail”.

ALGORITHM ClassifyScore
    INPUT score
    IF score < 0 OR score > 100 THEN
        OUTPUT "Invalid score"
    ELSE IF score >= 70 THEN
        OUTPUT "Distinction"
    ELSE IF score >= 50 THEN
        OUTPUT "Pass"
    ELSE
        OUTPUT "Fail"
    END IF
END ALGORITHM

Pseudocode is not executable code. It is a language-independent description that emphasizes logic rather than punctuation or library details. Good pseudocode uses consistent indentation, meaningful names, and explicit conditions.

The corresponding Python implementation is:

score = 74

if score < 0 or score > 100:
    result = "Invalid score"
elif score >= 70:
    result = "Distinction"
elif score >= 50:
    result = "Pass"
else:
    result = "Fail"

print(result)

The order of conditions matters. Once an if or elif condition is true, the remaining branches in that chain are skipped. Testing score >= 50 before score >= 70 would incorrectly classify a score of 74 as a pass.

1.1 Correctness, readability, and efficiency

An algorithm is correct if it produces the required result for every valid input and handles invalid inputs appropriately. Correctness includes both:

  • Partial correctness: if the algorithm terminates, its result is correct;
  • Termination: the algorithm eventually stops for every permitted input.

A practical correctness process is:

  1. Define the problem and expected output.
  2. Identify valid and invalid inputs.
  3. Write the normal case.
  4. Consider boundary cases, such as zero, an empty collection, or the exact threshold.
  5. Test representative examples.
  6. Trace execution when the result is unexpected.

Readability refers to how easily another programmer can understand and maintain the code. Meaningful variable names, consistent indentation, short functions, comments explaining decisions, and straightforward conditions all improve readability.

Efficiency concerns the resources used by an algorithm. The principal measures are:

  • Time complexity: how execution time grows as input size increases;
  • Space complexity: how additional memory use grows as input size increases.

If a loop examines every one of n records once, its time complexity is usually O(n). Two independent full loops are still O(n), because 2n grows proportionally to n. A loop nested inside another loop over the same collection may require O(n²) operations. These are growth-rate descriptions, not exact running times.

2. Conditions and Boolean Logic

A condition is an expression that evaluates to True or False. Python comparisons include:

OperatorMeaning
==equal to
!=not equal to
<less than
<=less than or equal to
>greater than
>=greater than or equal to

Logical operators combine conditions:

  • and is true only when both conditions are true;
  • or is true when at least one condition is true;
  • not reverses a Boolean value.
age =  twenty = 20
has_id = True

if age >= 18 and has_id:
    print("Access permitted")

The first assignment above would be invalid because twenty is not a number if used as written. A correct version is:

age = 20
has_id = True

if age >= 18 and has_id:
    print("Access permitted")

This illustrates an important professional practice: test code as written, not merely the intended logic.

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 improve efficiency and prevent errors:

if records and records[0]["status"] == "active":
    print("An active record exists")

The second condition is evaluated only when records is non-empty.

Truth values

In a Boolean context, False, None, numeric zero, and empty collections such as [] and "" are considered false. Most other values are true. Although this is convenient, explicit comparisons can improve clarity when the distinction matters:

if len(names) == 0:
    print("No names supplied")

Use is None when testing for the singleton value None; do not normally use == None.

3. Selection Structures

Selection chooses one path from several alternatives.

3.1 if, elif, and else

temperature = 31

if temperature >= 35:
    advice = "Avoid strenuous outdoor activity"
elif temperature >= 25:
    advice = "Stay hydrated"
else:
    advice = "Normal conditions"

Conditions should be arranged from the most specific or highest-priority rule to the least specific. Overlapping rules require deliberate ordering.

A guard clause handles an exceptional or invalid case early:

def calculate_average(values):
    if not values:
        return None
    return sum(values) / len(values)

This avoids an unnecessary nested block and prevents division by zero. The function assumes that values contains numeric values. If arbitrary input is permitted, validation should also be added.

3.2 Conditional expressions

For a short, simple choice, Python provides a conditional expression:

status = "pass" if score >= 50 else "fail"

It should not replace a multi-branch decision or contain complicated side effects. Readability is more important than saving one line.

3.3 Membership and range conditions

Membership tests are useful for categorical data:

valid_roles = {"admin", "analyst", "viewer"}
if role in valid_roles:
    print("Recognised role")

A set is generally preferable to a list for repeated membership tests because set lookup is approximately O(1) on average, whereas list lookup is O(n). This is an average-case property of Python’s hash-table implementation, not a guarantee for every unusual situation.

4. Structural Pattern Matching

Python 3.10 introduced match and case, known as structural pattern matching. It is useful when one value must be compared with several patterns or when the structure of a value matters.

command = "load"

match command:
    case "load":
        print("Loading data")
    case "clean":
        print("Cleaning data")
    case "report":
        print("Creating report")
    case _:
        print("Unknown command")

The underscore is a wildcard that matches any value and is commonly used as the default case. Only the first matching case is executed.

Pattern matching can unpack structured data:

record = ("A102", 87)

match record:
    case (student_id, score) if 0 <= score <= 100:
        print(f"{student_id}: valid score {score}")
    case _:
        print("Invalid record")

The if portion is called a guard. It adds a condition after the structural pattern matches.

For simple equality tests, if/elif may be more familiar. match becomes especially useful when dealing with tuples, dictionaries, commands, or alternative data shapes. It is not a universal replacement for dictionaries or polymorphic design.

5. Iteration

Iteration repeats a block of instructions. Python provides for loops for traversing an iterable and while loops for repeating while a condition remains true.

5.1 for loops

scores = [72, 64, 81]

for score in scores:
    print(score)

The loop variable takes each value in sequence. When both position and value are needed, use enumerate:

for position, score in enumerate(scores, start=1):
    print(position, score)

To iterate over pairs, use zip:

names = ["Amina", "Jon", "Li"]
scores = [72, 64, 81]

for name, score in zip(names, scores):
    print(name, score)

zip stops when the shortest iterable is exhausted. In Python versions supporting it, zip(..., strict=True) can detect unequal lengths and is useful when equal lengths are required.

The range function produces a sequence-like object of integers:

for number in range(2, 10, 2):
    print(number)       # 2, 4, 6, 8

The stop value is excluded. In general, range(start, stop, step) requires a non-zero step and produces values beginning at start, advancing by step, before reaching stop.

5.2 while loops

A while loop is appropriate when the number of repetitions is not known in advance:

attempts = 0

while attempts < 3:
    print("Attempt", attempts + 1)
    attempts += 1

The loop condition must eventually become false. Forgetting to update the control variable can produce an infinite loop.

A controlled input loop is often written as:

while True:
    response = input("Enter a positive number, or q to quit: ")
    if response.lower() == "q":
        break
    number = float(response)
    if number > 0:
        print("Accepted")
        break
    print("The number must be positive")

Production software should also handle non-numeric input with try/except; control flow and exception handling often work together.

5.3 break, continue, and loop else

  • break exits the nearest loop immediately.
  • continue skips the remaining body and begins the next iteration.
  • A loop’s else block runs if the loop ends normally, but not if it ends through break.
values = [4, 7, 9, 12]

for value in values:
    if value % 2 == 0:
        print("First even value:", value)
        break
else:
    print("No even value found")

This pattern is useful for searching. Excessive use of break and continue, however, can make execution difficult to trace. Use them when they express a clear search or skipping rule.

6. Nested Control Structures

A nested structure places one control structure inside another. For example, a data-quality process may examine each record and then evaluate several properties:

records = [
    {"name": "Ana", "score": 82},
    {"name": "Bo", "score": -4},
    {"name": "Cy", "score": 55},
]

for record in records:
    score = record["score"]
    if 0 <= score <= 100:
        if score >= 70:
            category = "distinction"
        elif score >= 50:
            category = "pass"
        else:
            category = "fail"
        print(record["name"], category)
    else:
        print(record["name"], "invalid score")

Nesting is legitimate when the inner decision depends on the outer condition. It becomes difficult to maintain when many levels are combined. Guard clauses can reduce nesting:

for record in records:
    score = record["score"]
    if not 0 <= score <= 100:
        print(record["name"], "invalid score")
        continue

    if score >= 70:
        category = "distinction"
    elif score >= 50:
        category = "pass"
    else:
        category = "fail"
    print(record["name"], category)

The second version separates validation from classification, making each decision easier to inspect.

7. Comprehensions

A comprehension creates a collection from an iterable using a compact expression. A list comprehension has the conceptual form:

[expression for item in iterable if condition]

Example:

scores = [45, 62, 78, 39, 91]
passed = [score for score in scores if score >= 50]

The equivalent loop is:

passed = []
for score in scores:
    if score >= 50:
        passed.append(score)

Comprehensions are often readable for one transformation and one simple filter. They should not contain complicated nested logic or side effects.

Other forms include:

squares = {n: n * n for n in range(1, 5)}
unique_lengths = {len(word) for word in ["data", "science", "data"]}
generator = (n * n for n in range(1_000_000))

The dictionary comprehension creates key-value pairs. The set comprehension removes duplicates. The generator expression produces values lazily rather than storing the entire result immediately. Consequently, a generator can reduce peak memory use when values are consumed once, although it cannot be indexed like a list and may require repeated computation if values are needed again.

A comprehension that creates a result of size n generally takes O(n) time and O(n) additional space. A generator expression generally takes O(n) time to consume but O(1) additional working space, excluding the memory used by the input and the current value.

8. A Systematic Problem-Solving Method

A reliable control-flow solution can be developed through the following stages.

Stage 1: Specify the problem

State the input, output, constraints, and assumptions. For example, “Given a list of non-negative transaction amounts, identify amounts above 10,000 and count them.”

Stage 2: Decompose the task

Separate validation, transformation, decision, and reporting. This reduces cognitive complexity.

Stage 3: Identify control structures

Use selection for alternatives, iteration for repeated work, and pattern matching for structured alternatives.

Stage 4: Write pseudocode

Pseudocode exposes missing cases before syntax obscures the logic.

Stage 5: Trace representative inputs

Include normal, boundary, empty, invalid, and unusually large inputs.

Stage 6: Implement and test

Use small functions and assertions where appropriate:

def count_large_amounts(amounts, threshold=10_000):
    if any(amount < 0 for amount in amounts):
        raise ValueError("Amounts must be non-negative")
    return sum(amount > threshold for amount in amounts)

assert count_large_amounts([5000, 12000, 10000]) == 1

In Python, True and False behave numerically as 1 and 0 in this sum. Although concise, the expression should be explained or rewritten if the audience may find it unclear.

Stage 7: Review complexity and maintainability

Ask whether the algorithm scans data unnecessarily, stores more than required, or contains avoidable nesting. Do not sacrifice clear correctness for a minor and unmeasured optimization.

9. Detailed Applied Case: Data-Quality Screening

The following is an illustrative case, not a report of a real organization.

Background

A training provider receives assessment records from several classes. Each record contains a learner identifier, a score, and an attendance percentage. The provider wants to classify each record as eligible, review, or invalid.

Problem

The rules are:

  • A score must be between 0 and 100 inclusive.
  • Attendance must be between 0 and 100 inclusive.
  • A learner is eligible if the score is at least 50 and attendance is at least 75.
  • A valid learner who fails either threshold is review.
  • A record outside either valid range is invalid.

Pseudocode

FOR each record
    IF score or attendance is outside 0 to 100
        classify as invalid
    ELSE IF score >= 50 AND attendance >= 75
        classify as eligible
    ELSE
        classify as review
    END IF
END FOR

Python solution

records = [
    {"id": "L01", "score": 82, "attendance": 91},
    {"id": "L02", "score": 48, "attendance": 88},
    {"id": "L03", "score": 70, "attendance": 61},
    {"id": "L04", "score": 105, "attendance": 90},
]

summary = {"eligible": 0, "review": 0, "invalid": 0}

for record in records:
    score = record["score"]
    attendance = record["attendance"]

    if not (0 <= score <= 100 and 0 <= attendance <= 100):
        category = "invalid"
    elif score >= 50 and attendance >= 75:
        category = "eligible"
    else:
        category = "review"

    summary[category] += 1
    print(record["id"], category)

print(summary)

The output categories are eligible, review, review, and invalid. The summary is therefore:

{'eligible': 1, 'review': 2, 'invalid': 1}

Analysis

The loop performs a constant amount of work per record, so the time complexity is O(n), where n is the number of records. The summary dictionary has a fixed number of categories, so its additional space is O(1), excluding the input records. The algorithm validates before classifying, preventing an invalid score from being treated as a legitimate failure.

A comprehension could produce a list of categories, but the explicit loop is preferable here because it updates a summary and prints an identifier. A more scalable production design would place classification in a function, validate required fields, log rejected records, and separate reporting from computation.

Outcome and learning points

The case demonstrates that effective control flow is not simply a matter of choosing syntax. The important design decisions were the order of validation and classification, explicit boundary handling, meaningful names, and a complexity assessment. These habits transfer directly to data cleaning, feature engineering, rule-based alerting, and model-monitoring pipelines.

10. Common Errors and Best Practices

Common errors

  1. Using assignment in a condition: = assigns; == compares.
  2. Incorrect branch ordering: a broad condition may capture cases intended for a later branch.
  3. Off-by-one errors: remember that range excludes its stop value.
  4. Infinite while loops: ensure that the loop condition can change.
  5. Mutating a collection while iterating over it: build a new collection or iterate over a copy.
  6. Confusing identity and equality: use is for None and object identity; use == for equivalent values.
  7. Overly dense comprehensions: expand complex logic into a normal loop or function.
  8. Ignoring invalid input: boundary and missing-value cases should be specified explicitly.

Best practices

  • Prefer clear conditions over clever expressions.
  • Use guard clauses to reject invalid cases early.
  • Keep each loop focused on one principal task.
  • Use enumerate, zip, and comprehensions when they improve clarity.
  • Name Boolean variables positively and clearly, such as is_valid or has_access.
  • Avoid duplicated rules; centralize repeated logic in functions.
  • Test boundary values, empty collections, and malformed records.
  • Measure performance before optimizing.
  • Document assumptions, especially data ranges and missing-value policy.

11. Professional, Ethical, and Contemporary Considerations

Control flow often implements decisions affecting people: eligibility, risk alerts, access, prioritization, or fraud screening. A technically correct program can still produce unfair or harmful outcomes if its rules encode biased assumptions or use poor-quality data.

Professional practice requires traceable rules, validation, appropriate access controls, and records of important decisions. If a rule rejects a record, the reason should be observable where privacy and security permit. Sensitive attributes should not be used casually, and developers should assess whether proxy variables reproduce protected characteristics.

Contemporary data systems may express control flow through vectorized operations, SQL queries, workflow engines, or machine-learning pipelines. Python loops remain valuable for irregular logic, validation, and orchestration, but large numerical workloads are often faster in specialized libraries because they move repeated operations into optimized compiled code. The underlying reasoning remains the same: define conditions, establish precedence, handle missing and invalid values, and evaluate cost.

Chapter Summary

Control flow determines the execution path of a Python program. Algorithms should first be specified in terms of inputs, processing, outputs, and termination, then represented with clear pseudocode. Boolean expressions support decisions, while if, elif, and else implement alternative paths. Structural pattern matching is useful for value and data-shape alternatives.

for loops traverse iterables, while while loops repeat until a condition changes. break, continue, and loop else provide additional control but should be used purposefully. Nested structures are appropriate when decisions depend on earlier conditions, although guard clauses can reduce unnecessary nesting. Comprehensions offer concise collection construction, while generators support lazy processing and lower peak memory use.

A professional solution must be correct, readable, testable, and appropriately efficient. Boundary testing, validation, meaningful names, and complexity analysis are essential. In data-science applications, control flow also carries ethical responsibilities because program rules may affect people and institutions.

Key Terms

Algorithm: A finite, ordered procedure for solving a problem.

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

Branch: One possible execution path in a selection structure.

Comprehension: A compact syntax for constructing a collection from an iterable.

Conditional expression: A single expression that selects between two values.

Control flow: The order in which program instructions execute.

Decision table: A structured representation of conditions and resulting actions.

Generator expression: An expression that produces values lazily.

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

Iteration: Repeated execution over data or while a condition holds.

Loop invariant: A property that remains true at a defined point during loop execution.

Pattern matching: Selection based on the value or structure of data.

Pseudocode: An informal, language-independent description of an algorithm.

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

Space complexity: The growth of additional memory use with input size.

Structural pattern matching: Python’s match/case mechanism for matching values and structures.

Time complexity: The growth of computational work with input size.

Validation: Checking whether input satisfies required rules and constraints.

Review Questions

Short Answer Questions

  1. What are the principal components of an algorithm?
  2. Explain the difference between break and continue.
  3. Why does the order of elif conditions matter?
  4. What is the purpose of a guard clause?
  5. State one difference between a list comprehension and a generator expression.

Long Answer Questions

  1. Explain how Boolean operators and short-circuit evaluation work in Python, using suitable examples.
  2. Compare for and while loops, including situations in which each is most appropriate.
  3. Describe structural pattern matching and compare it with an if/elif chain.
  4. Explain how correctness, readability, time complexity, and space complexity should influence algorithm design.
  5. Discuss the advantages and risks of nested control structures and provide strategies for reducing unnecessary nesting.

Analytical / Application Questions

  1. Design pseudocode and Python code to process a list of temperatures, classifying each as cold, moderate, hot, or invalid. State how boundary values are handled.
  2. A program searches a list for the first duplicate value. Propose an algorithm using a set, explain its expected time and space complexity, and identify an alternative nested-loop approach.
  3. Design a rule-based data-quality checker for records containing a required identifier, a numeric value, and a status. Include validation, classification, summary counts, and at least three test cases.

Multiple Choice Questions with Answers

Question 1

Which Python structure is most appropriate for choosing among several ordered score ranges?

A. A for loop

B. An if/elif/else chain

C. A generator expression

D. A try block

Correct Answer: B. An if/elif/else chain

Question 2

What does range(2, 10, 2) produce?

A. 2, 4, 6, 8

B. 2, 4, 6, 8, 10

C. 0, 2, 4, 6, 8

D. 10, 8, 6, 4, 2

Correct Answer: A. 2, 4, 6, 8

Question 3

Which statement about a loop else block is correct?

A. It always runs before the loop starts

B. It runs only when the loop ends through break

C. It runs when the loop completes normally without break

D. It can be used only with while loops

Correct Answer: C. It runs when the loop completes normally without break

Question 4

What is the usual average-case membership complexity for a Python set?

A. O(1)

B. O(log n)

C. O(n)

D. O(n²)

Correct Answer: A. O(1)

Question 5

Which expression selects scores at least 50 from scores?

A. [score for score in scores if score >= 50]

B. [score if score >= 50 for score]

C. (score in scores where score >= 50)

D. {for score >= 50 in scores}

Correct Answer: A. [score for score in scores if score >= 50]

Question 6

What is the main purpose of enumerate(scores, start=1)?

A. To sort scores

B. To provide each score together with a one-based position

C. To remove duplicate scores

D. To convert scores to strings

Correct Answer: B. To provide each score together with a one-based position

Question 7

For a list of n records, what is the usual time complexity of one loop that performs constant work per record?

A. O(1)

B. O(log n)

C. O(n)

D. O(n²)

Correct Answer: C. O(n)

Question 8

Which Python syntax provides structural pattern matching?

A. select and when

B. match and case

C. choose and option

D. switch and default

Correct Answer: B. match and case

Question 9

Why can a generator expression use less peak memory than a list comprehension?

A. It automatically compresses values

B. It evaluates values lazily instead of storing them all immediately

C. It never performs calculations

D. It stores values in a database

Correct Answer: B. It evaluates values lazily instead of storing them all immediately

Question 10

What is the safest usual test for whether variable value is None?

A. value = None

B. value == False

C. value is None

D. value in None

Correct Answer: C. value is None

Python Programming
Chapter 3 of 15