Python Loops

· Seokhyeon Byun

Note: This post is based on my old programming study notes when I taught myself.

while Loop

While loop will never stop the loop as long as the condition is True. Once the condition is False, the loop stops. If condition is always True, while loop becomes an infinite loop. To exit infinite loop in the IDE, press Ctrl+C.

Basic Syntax

while condition:
    statement_1
    statement_2
    ...

Important Note

When using while loop, it’s necessary to define the initial state of variables used in the condition.

cookie = 0

while cookie < 10:
    cookie += 1
    print('He ate %d cookie' % cookie)
    if cookie == 10:
        print("He ate all the cookies.")

How to escape from while loop?

Use break: If while loop reaches break, the loop will stop regardless of whether the condition is True or False.

chocolate = 10  # Initial state of chocolate variable

while True:
    chocolate = chocolate - 1
    print('We have %d chocolates left' % chocolate)
    if chocolate == 0:
        print("Chocolate Factory is shutting down. We don't have any chocolate left...")
        break  # Escape infinite loop

How to go back to condition of while loop?

Use continue:

num = 0  # initial number is set to zero

while num < 10:
    num += 1  # num = num + 1
    if num % 2 == 0: 
        continue
    print(num)

# This will print:
# 1
# 3
# 5
# 7
# 9

Explanation: num starts from 0 and as long as num is below 10, the loop operates and adds 1 to num variable. When num is an even number, it goes back to the original condition (num < 10) of while loop and continues operating.


for Loop

Basic Syntax

for variable in List/Tuple/String:
    statement_1
    statement_2
    ...

Examples

# Iterating through a list
fruits = ['apple', 'banana', 'orange']
for fruit in fruits:
    print(f"I like {fruit}")

# Iterating through a string
word = "Python"
for letter in word:
    print(letter)

continue in for loop

Like while loop, if for loop reaches continue, it will go back to the original condition of for loop and keep operating from there.

numbers = [1, 2, 3, 4, 5]
for num in numbers:
    if num == 3:
        continue
    print(num)
# Output: 1, 2, 4, 5 (skips 3)

range() Function

Syntax

range(start, end, step)

By default, if we don’t set the start value and step value, Python automatically considers start=0 and step=1. range(n) means the range is from 0 to n-1.

Examples

# Basic range
for i in range(5):
    print(i)  # Output: 0, 1, 2, 3, 4

# Range with start and end
for i in range(2, 8):
    print(i)  # Output: 2, 3, 4, 5, 6, 7

# Range with step
for i in range(0, 10, 2):
    print(i)  # Output: 0, 2, 4, 6, 8

Practical Example: Multiplication Table

# 9 x 9 multiplication table with for loop
for i in range(2, 10):
    for j in range(1, 10):
        print(i * j, end=" ")
    print('')  # New line after each row

# Result:
# 2 4 6 8 10 12 14 16 18
# 3 6 9 12 15 18 21 24 27
# 4 8 12 16 20 24 28 32 36
# 5 10 15 20 25 30 35 40 45
# 6 12 18 24 30 36 42 48 54
# 7 14 21 28 35 42 49 56 63
# 8 16 24 32 40 48 56 64 72
# 9 18 27 36 45 54 63 72 81

Various Usage of for Loop

Case 1: Add new elements to empty list

A = []
for _ in range(9):  # from 0th index to 8th index, total 9
    value = int(input("Enter a number: "))
    A.append(value)
print(max(A))

Explanation: _ in for loop is used when we don’t need to care about the iterator of for loop.

Case 2: List Comprehension

General case:

numbers = [1, 2, 3, 4]
result = []  # empty list

for num in numbers:
    result.append(num * 3)
print(result)  # [3, 6, 9, 12]

Using List Comprehension:

numbers = [1, 2, 3, 4]
result = [num * 3 for num in numbers]
print(result)  # [3, 6, 9, 12]

More List Comprehension Examples

# With condition
even_squares = [x**2 for x in range(10) if x % 2 == 0]
# Result: [0, 4, 16, 36, 64]

# With string operations
words = ['hello', 'world', 'python']
uppercase = [word.upper() for word in words]
# Result: ['HELLO', 'WORLD', 'PYTHON']

# Nested list comprehension
matrix = [[i*j for j in range(1, 4)] for i in range(1, 4)]
# Result: [[1, 2, 3], [2, 4, 6], [3, 6, 9]]

enumerate() Function

By using enumerate(), you can get a counter and the value from the iterable simultaneously. Basic syntax is to put an iterable object inside the parentheses of enumerate().

It returns tuple type (pair of index and element).

Example 1: Basic enumerate

numbers = [1, 2, 3, 4, 5, 6]

for n in enumerate(numbers):
    print(n)

# Output:
# (0, 1)
# (1, 2)
# (2, 3)
# (3, 4)
# (4, 5)
# (5, 6)

Example 2: Unpacking enumerate

t = [1, 5, 7, 33, 39, 52]
for i, v in enumerate(t):
    print("index: {}, value: {}".format(i, v))

# Output:
# index: 0, value: 1
# index: 1, value: 5
# index: 2, value: 7
# index: 3, value: 33
# index: 4, value: 39
# index: 5, value: 52

Example 3: enumerate with start parameter

fruits = ['apple', 'banana', 'orange']
for i, fruit in enumerate(fruits, start=1):
    print(f"{i}. {fruit}")

# Output:
# 1. apple
# 2. banana
# 3. orange

Loop Control Summary

StatementPurposeUsage
breakExit loop completelyUse when you want to stop the loop
continueSkip current iterationUse when you want to skip to next iteration
passDo nothing (placeholder)Use as a placeholder for future code

Examples

# break example
for i in range(10):
    if i == 5:
        break
    print(i)
# Output: 0, 1, 2, 3, 4

# continue example  
for i in range(5):
    if i == 2:
        continue
    print(i)
# Output: 0, 1, 3, 4

# pass example
for i in range(5):
    if i == 2:
        pass  # TODO: implement special logic later
    print(i)
# Output: 0, 1, 2, 3, 4

Nested Loops

# Pattern printing
for i in range(5):
    for j in range(i + 1):
        print("*", end="")
    print()

# Output:
# *
# **
# ***
# ****
# *****

Loop with else

Python loops can have an else clause that executes when the loop completes normally (not via break).

# for-else
for i in range(5):
    print(i)
else:
    print("Loop completed normally")

# while-else with break
numbers = [1, 2, 3, 4, 5]
target = 7

for num in numbers:
    if num == target:
        print(f"Found {target}")
        break
else:
    print(f"{target} not found in the list")

---

## **Technical Interview Essentials**

### Time Complexity of Loops

**Critical Knowledge for Interviews:**
```python
# Single loop: O(n)
for i in range(n):
    print(i)  # O(n) total

# Nested loops: O(n²)
for i in range(n):
    for j in range(n):
        print(i, j)  # O(n²) total

# Asymmetric nested loops: O(n²) but fewer operations
for i in range(n):
    for j in range(i):  # Inner loop runs 0, 1, 2, ..., n-1 times
        print(i, j)     # Total: 0+1+2+...+(n-1) = n(n-1)/2 = O(n²)

# Loop with early termination: O(n) worst case, could be better
for i in range(n):
    if condition_met:
        break  # Best case: O(1), Worst case: O(n)
    process(i)

Common Interview Patterns

1. Two Pointers Pattern

def two_sum_sorted(arr, target):
    """
    Find two numbers that add up to target in sorted array.
    Time: O(n), Space: O(1)
    """
    left, right = 0, len(arr) - 1
    
    while left < right:
        current_sum = arr[left] + arr[right]
        
        if current_sum == target:
            return [left, right]
        elif current_sum < target:
            left += 1
        else:
            right -= 1
    
    return []

def reverse_array(arr):
    """Reverse array in-place using two pointers."""
    left, right = 0, len(arr) - 1
    
    while left < right:
        arr[left], arr[right] = arr[right], arr[left]
        left += 1
        right -= 1
    
    return arr

# Test
print(two_sum_sorted([1, 2, 3, 4, 6], 6))  # [1, 3] (2+4=6)
print(reverse_array([1, 2, 3, 4, 5]))      # [5, 4, 3, 2, 1]

2. Sliding Window Pattern

def max_subarray_sum(arr, k):
    """
    Find maximum sum of k consecutive elements.
    Time: O(n), Space: O(1)
    """
    if len(arr) < k:
        return None
    
    # Calculate sum of first window
    window_sum = sum(arr[:k])
    max_sum = window_sum
    
    # Slide the window
    for i in range(k, len(arr)):
        # Remove leftmost element, add rightmost element
        window_sum = window_sum - arr[i - k] + arr[i]
        max_sum = max(max_sum, window_sum)
    
    return max_sum

def min_window_with_sum(arr, target_sum):
    """
    Find minimum window length with sum >= target_sum.
    Time: O(n), Space: O(1)
    """
    min_length = float('inf')
    window_sum = 0
    window_start = 0
    
    for window_end in range(len(arr)):
        window_sum += arr[window_end]
        
        # Shrink window until sum is less than target
        while window_sum >= target_sum:
            min_length = min(min_length, window_end - window_start + 1)
            window_sum -= arr[window_start]
            window_start += 1
    
    return min_length if min_length != float('inf') else 0

# Test
print(max_subarray_sum([1, 2, 3, 4, 5], 3))      # 12 (3+4+5)
print(min_window_with_sum([1, 2, 3, 4], 6))      # 2 (3+4=7 >= 6)

3. Matrix Traversal Patterns

def spiral_matrix(matrix):
    """
    Traverse matrix in spiral order - classic interview question.
    Time: O(m*n), Space: O(1) excluding output
    """
    if not matrix or not matrix[0]:
        return []
    
    result = []
    top, bottom = 0, len(matrix) - 1
    left, right = 0, len(matrix[0]) - 1
    
    while top <= bottom and left <= right:
        # Traverse right
        for col in range(left, right + 1):
            result.append(matrix[top][col])
        top += 1
        
        # Traverse down
        for row in range(top, bottom + 1):
            result.append(matrix[row][right])
        right -= 1
        
        if top <= bottom:
            # Traverse left
            for col in range(right, left - 1, -1):
                result.append(matrix[bottom][col])
            bottom -= 1
        
        if left <= right:
            # Traverse up
            for row in range(bottom, top - 1, -1):
                result.append(matrix[row][left])
            left += 1
    
    return result

def diagonal_traverse(matrix):
    """Traverse matrix diagonally."""
    if not matrix or not matrix[0]:
        return []
    
    rows, cols = len(matrix), len(matrix[0])
    result = []
    
    # Upper diagonals (including main diagonal)
    for d in range(rows + cols - 1):
        diagonal = []
        
        # Determine start position for this diagonal
        if d < rows:
            start_row, start_col = d, 0
        else:
            start_row, start_col = rows - 1, d - rows + 1
        
        # Collect diagonal elements
        while start_row >= 0 and start_col < cols:
            diagonal.append(matrix[start_row][start_col])
            start_row -= 1
            start_col += 1
        
        # Reverse every other diagonal for zigzag pattern
        if d % 2 == 1:
            diagonal.reverse()
        
        result.extend(diagonal)
    
    return result

# Test
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
print(spiral_matrix(matrix))    # [1, 2, 3, 6, 9, 8, 7, 4, 5]
print(diagonal_traverse(matrix)) # [1, 2, 4, 7, 5, 3, 6, 8, 9]

Advanced Loop Techniques

1. Generator Expressions vs List Comprehensions

# Memory comparison for large datasets
n = 1000000

# List comprehension - creates entire list in memory
squares_list = [x**2 for x in range(n)]  # ~8MB memory

# Generator expression - lazy evaluation
squares_gen = (x**2 for x in range(n))   # ~200 bytes memory

# Usage patterns
def sum_of_squares_memory_efficient(n):
    """Memory-efficient calculation using generator"""
    return sum(x**2 for x in range(n))

def sum_of_squares_memory_intensive(n):
    """Memory-intensive approach using list"""
    return sum([x**2 for x in range(n)])

# For interview: generators are better for large datasets

2. Iterator Protocol Implementation

class Fibonacci:
    """Custom iterator for Fibonacci sequence - interview question"""
    
    def __init__(self, max_count):
        self.max_count = max_count
        self.count = 0
        self.current, self.next_val = 0, 1
    
    def __iter__(self):
        return self
    
    def __next__(self):
        if self.count >= self.max_count:
            raise StopIteration
        
        if self.count == 0:
            self.count += 1
            return self.current
        
        result = self.next_val
        self.current, self.next_val = self.next_val, self.current + self.next_val
        self.count += 1
        return result

# Usage
fib = Fibonacci(10)
print(list(fib))  # [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

# Can be used in loops
for num in Fibonacci(5):
    print(num, end=" ")  # 0 1 1 2 3

3. Loop Optimization Patterns

# SLOW - function call in every iteration
def slow_loop(items):
    for i in range(len(items)):  # len() called every iteration
        process(items[i])

# FAST - cache function result
def fast_loop(items):
    length = len(items)  # len() called once
    for i in range(length):
        process(items[i])

# FASTEST - direct iteration
def fastest_loop(items):
    for item in items:  # No indexing needed
        process(item)

# Loop with expensive condition check
def optimized_search(items, condition_func):
    """Move expensive checks outside loop when possible"""
    # Pre-filter if possible
    candidates = [item for item in items if cheap_check(item)]
    
    # Then apply expensive check
    for item in candidates:
        if condition_func(item):
            return item
    
    return None

Common Interview Gotchas

1. Modifying List While Iterating

# WRONG - modifying list while iterating
numbers = [1, 2, 3, 4, 5]
for num in numbers:
    if num % 2 == 0:
        numbers.remove(num)  # BUG: skips elements!

# CORRECT - iterate over copy
numbers = [1, 2, 3, 4, 5]
for num in numbers[:]:  # numbers[:] creates a copy
    if num % 2 == 0:
        numbers.remove(num)

# BETTER - use list comprehension
numbers = [1, 2, 3, 4, 5]
numbers = [num for num in numbers if num % 2 != 0]

# BEST - iterate backwards when removing by index
numbers = [1, 2, 3, 4, 5]
for i in range(len(numbers) - 1, -1, -1):
    if numbers[i] % 2 == 0:
        del numbers[i]

2. Variable Scope in Loops

# Common interview trap - variable scope
functions = []

# WRONG - all functions reference the same variable
for i in range(3):
    functions.append(lambda: i)  # All will return 2!

print([f() for f in functions])  # [2, 2, 2]

# CORRECT - capture variable value
functions = []
for i in range(3):
    functions.append(lambda x=i: x)  # Capture current value

print([f() for f in functions])  # [0, 1, 2]

# OR use closure properly
functions = []
for i in range(3):
    def make_func(value):
        return lambda: value
    functions.append(make_func(i))

print([f() for f in functions])  # [0, 1, 2]

3. Infinite Loop Detection

def safe_while_loop(start, condition_func, update_func, max_iterations=1000):
    """Safe while loop with iteration limit to prevent infinite loops"""
    current = start
    iterations = 0
    
    while condition_func(current) and iterations < max_iterations:
        current = update_func(current)
        iterations += 1
        
        # Safety check for interview scenarios
        if iterations >= max_iterations:
            raise RuntimeError(f"Loop exceeded {max_iterations} iterations")
    
    return current, iterations

# Example usage
def find_fixed_point(start_value):
    """Find fixed point where f(x) = x"""
    def condition(x):
        return abs(x - (x**2 + 1) / (2*x)) > 0.0001  # Not converged yet
    
    def update(x):
        return (x**2 + 1) / (2*x)  # Newton's method iteration
    
    try:
        result, iters = safe_while_loop(start_value, condition, update)
        return f"Converged to {result:.6f} in {iters} iterations"
    except RuntimeError as e:
        return f"Failed to converge: {e}"

print(find_fixed_point(2.0))

Performance Tips for Interviews

# 1. Use enumerate instead of range(len())
# SLOW
items = ['a', 'b', 'c', 'd']
for i in range(len(items)):
    print(f"{i}: {items[i]}")

# FAST
for i, item in enumerate(items):
    print(f"{i}: {item}")

# 2. Use zip for parallel iteration
# SLOW
names = ['Alice', 'Bob', 'Charlie']
ages = [25, 30, 35]
for i in range(len(names)):
    print(f"{names[i]} is {ages[i]} years old")

# FAST
for name, age in zip(names, ages):
    print(f"{name} is {age} years old")

# 3. Use itertools for efficient combinations
import itertools

# Generate all pairs efficiently
items = [1, 2, 3, 4]
for pair in itertools.combinations(items, 2):
    print(pair)  # (1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)

# Cartesian product
for combo in itertools.product([1, 2], ['a', 'b']):
    print(combo)  # (1, 'a'), (1, 'b'), (2, 'a'), (2, 'b')