Introduction to Hands-on Practice
Welcome to our hands-on practice session with Python control structures! Now that we've covered the theoretical aspects of conditional statements and loops, it's time to put that knowledge into practice. As the legendary programmer Seymour Papert once said, "You can't learn to ride a bicycle by reading about it"—the same applies to programming.
In this session, we'll work through carefully designed exercises that will strengthen your understanding of control flow mechanisms in Python. We'll start with simpler problems and gradually move to more complex scenarios that combine different control structures.
The code for this session can be found in the /week2/day2/control_practice.py file in your course repository.
Practice with Basic Conditionals
Let's start with some straightforward exercises using if, elif, and else statements.
Exercise 1: Temperature Classifier
Write a program that takes a temperature value and classifies it as:
- Freezing: Below 0°C
- Cold: 0-10°C
- Cool: 10-20°C
- Warm: 20-30°C
- Hot: Above 30°C
# Solution
def classify_temperature(temp):
if temp < 0:
return "Freezing"
elif temp < 10:
return "Cold"
elif temp < 20:
return "Cool"
elif temp < 30:
return "Warm"
else:
return "Hot"
# Test with different temperatures
test_temperatures = [-5, 0, 5, 15, 25, 35]
for temp in test_temperatures:
classification = classify_temperature(temp)
print(f"{temp}°C is classified as {classification}")
Discussion
This simple example demonstrates the cascading nature of if-elif-else chains. Notice how we don't need to write 0 <= temp < 10 for the "Cold" category—since we've already checked that temp is not less than 0, we only need to check the upper bound.
This makes our code more concise and easier to read. When conditions have a natural progression, arranging them from smallest to largest (or vice versa) can simplify your logic.
Exercise 2: Grade Calculator
Create a program that converts numerical scores to letter grades according to the following scale:
- A: 90-100
- B: 80-89
- C: 70-79
- D: 60-69
- F: Below 60
Additionally, add a "+" suffix for scores in the top 3 points of each grade range (except A) and a "-" suffix for scores in the bottom 3 points.
# Solution
def calculate_grade(score):
# Input validation
if not (0 <= score <= 100):
return "Invalid score. Please enter a number between 0 and 100."
# Determine the base grade
if score >= 90:
base_grade = "A"
elif score >= 80:
base_grade = "B"
elif score >= 70:
base_grade = "C"
elif score >= 60:
base_grade = "D"
else:
return "F" # F has no +/- modifiers
# Add +/- modifiers (except for A+ and F+/F-)
if base_grade != "A" and score % 10 >= 7:
return base_grade + "+"
elif base_grade != "F" and score % 10 < 3:
return base_grade + "-"
else:
return base_grade
# Test with various scores
test_scores = [95, 92, 87, 83, 78, 72, 68, 62, 55]
for score in test_scores:
grade = calculate_grade(score)
print(f"Score: {score} → Grade: {grade}")
Discussion
This exercise combines two levels of decision-making: first determining the base grade, then applying modifiers. Notice how we handle special cases like "no A+" and "no F+/F-" with additional conditionals.
The score % 10 expression gives us the last digit of the score, which helps determine whether it falls in the top 3 or bottom 3 of its range. This is a common technique when you need to work with specific parts of a number.
Exercise 3: Leap Year Checker
Write a function that determines whether a given year is a leap year. The rules for leap years are:
- Years divisible by 4 are leap years
- Exception: years divisible by 100 are not leap years
- Exception to the exception: years divisible by 400 are leap years
# Solution
def is_leap_year(year):
# A year is a leap year if it's divisible by 4...
if year % 4 == 0:
# ...unless it's divisible by 100...
if year % 100 == 0:
# ...but it's still a leap year if it's divisible by 400
if year % 400 == 0:
return True
else:
return False
else:
return True
else:
return False
# More concise alternative
def is_leap_year_concise(year):
return (year % 4 == 0 and year % 100 != 0) or (year % 400 == 0)
# Test with various years
test_years = [1900, 2000, 2004, 2100, 2400]
for year in test_years:
is_leap = is_leap_year(year)
print(f"{year} is{' ' if is_leap else ' not '}a leap year")
Discussion
This exercise demonstrates how complex rules can be expressed using nested conditionals or combined into a single boolean expression. The first solution explicitly shows the logic step by step, while the second is more concise but may require more careful reading to understand.
The leap year problem is a classic example of rules with exceptions to exceptions, which is why it's a good exercise for practicing conditionals. Real-world problems often have these kinds of complex rule sets.
Practice with Loops
Now let's practice with loops, exploring both for and while loops in different scenarios.
Exercise 4: Sum of Even Numbers
Write a program that calculates the sum of all even numbers between 1 and 100.
# Solution using for loop and if statement
def sum_even_numbers_for():
total = 0
for num in range(1, 101):
if num % 2 == 0:
total += num
return total
# Alternative solution using range step parameter
def sum_even_numbers_range():
total = 0
for num in range(2, 101, 2): # Start at 2, step by 2
total += num
return total
# Solution using while loop
def sum_even_numbers_while():
total = 0
num = 2
while num <= 100:
total += num
num += 2
return total
# Compare the results (all should be the same)
print(f"Sum (for loop): {sum_even_numbers_for()}")
print(f"Sum (range): {sum_even_numbers_range()}")
print(f"Sum (while loop): {sum_even_numbers_while()}")
Discussion
This exercise shows three different approaches to the same problem. The first uses a for loop with an if statement to filter out odd numbers. The second is more efficient, using the step parameter of range() to only generate even numbers. The third uses a while loop, which can be appropriate when the number of iterations isn't known in advance (though that's not the case here).
In Python, the range() function is incredibly versatile, and learning to use it effectively can make your loop code much cleaner. The three parameters represent start, stop, and step, allowing you to generate sequences in various patterns.
Exercise 5: Prime Number Finder
Create a function that finds all prime numbers up to a given limit.
# Solution
def find_primes(limit):
primes = []
for num in range(2, limit + 1):
is_prime = True
# Check if num is divisible by any smaller number
for divisor in range(2, int(num ** 0.5) + 1):
if num % divisor == 0:
is_prime = False
break
if is_prime:
primes.append(num)
return primes
# Test the function
limit = 50
prime_numbers = find_primes(limit)
print(f"Prime numbers up to {limit}: {prime_numbers}")
Discussion
This example demonstrates nested loops and the use of break to exit the inner loop early. The algorithm is an inefficient version of the Sieve of Eratosthenes, but it illustrates the principle well.
Note the optimization where we only check divisors up to the square root of the number. This is a mathematical property: if num has a divisor larger than its square root, it must also have a corresponding divisor smaller than its square root.
We'll improve this function in a later exercise using a more sophisticated approach.
Exercise 6: Interactive Menu System
Create a simple interactive menu system using a while loop that presents options to the user and processes their choice.
# Solution
def simple_calculator():
print("Simple Calculator")
print("----------------")
running = True
while running:
print("\nOperations:")
print("1. Add")
print("2. Subtract")
print("3. Multiply")
print("4. Divide")
print("5. Exit")
choice = input("\nEnter your choice (1-5): ")
if choice == '5':
print("Thank you for using the calculator. Goodbye!")
running = False
continue
if choice not in ('1', '2', '3', '4'):
print("Invalid choice. Please enter a number between 1 and 5.")
continue
# Get operands
try:
num1 = float(input("Enter first number: "))
num2 = float(input("Enter second number: "))
except ValueError:
print("Invalid input. Please enter numeric values.")
continue
# Perform the selected operation
if choice == '1':
result = num1 + num2
operation = '+'
elif choice == '2':
result = num1 - num2
operation = '-'
elif choice == '3':
result = num1 * num2
operation = '*'
elif choice == '4':
if num2 == 0:
print("Error: Division by zero is not allowed.")
continue
result = num1 / num2
operation = '/'
print(f"\nResult: {num1} {operation} {num2} = {result}")
# Uncomment to run the calculator
# simple_calculator()
Discussion
This exercise demonstrates how a while loop can create an interactive program that continues running until the user decides to exit. The use of continue allows us to restart the loop when there are invalid inputs or when special conditions occur.
Note how we handle errors like invalid input and division by zero with appropriate error messages. This is an important aspect of writing robust interactive programs. In a later lesson, we'll learn about more advanced error handling techniques using exceptions.
Combining Control Structures
Now let's tackle more complex exercises that require combining different control structures.
Exercise 7: FizzBuzz
Write a program that prints numbers from 1 to 100, but:
- For multiples of 3, print "Fizz" instead of the number
- For multiples of 5, print "Buzz" instead of the number
- For multiples of both 3 and 5, print "FizzBuzz"
# Solution
def fizzbuzz(n):
results = []
for i in range(1, n + 1):
if i % 3 == 0 and i % 5 == 0:
results.append("FizzBuzz")
elif i % 3 == 0:
results.append("Fizz")
elif i % 5 == 0:
results.append("Buzz")
else:
results.append(str(i))
return results
# Generate and display the first 20 FizzBuzz values
fizzbuzz_values = fizzbuzz(20)
for i, value in enumerate(fizzbuzz_values, 1):
print(f"{i}: {value}")
Discussion
FizzBuzz is a classic programming interview question that combines loops and conditionals. The key insight is that we need to check for the "FizzBuzz" case first before checking for "Fizz" or "Buzz" individually. This demonstrates the importance of the order of conditions in an if-elif-else chain.
An alternative approach would be to build the output string gradually:
# Alternative approach
def fizzbuzz_alternative(n):
results = []
for i in range(1, n + 1):
output = ""
if i % 3 == 0:
output += "Fizz"
if i % 5 == 0:
output += "Buzz"
if output == "": # If neither condition was met
output = str(i)
results.append(output)
return results
This alternative approach is more extensible—if we needed to add a third condition (e.g., "Jazz" for multiples of 7), we could simply add another conditional check without changing the existing logic.
Exercise 8: Interactive Number Guessing Game
Create a number guessing game where the computer selects a random number between 1 and 100, and the player tries to guess it. The game should provide "higher" or "lower" hints and track the number of guesses.
import random
def guessing_game():
# Generate a random number between 1 and 100
secret_number = random.randint(1, 100)
attempts = 0
guessed = False
print("Welcome to the Number Guessing Game!")
print("I'm thinking of a number between 1 and 100.")
while not guessed:
# Get the player's guess
try:
guess = int(input("Enter your guess: "))
attempts += 1
except ValueError:
print("Please enter a valid number.")
continue
# Check the guess
if guess < 1 or guess > 100:
print("Please guess a number between 1 and 100.")
elif guess < secret_number:
print("Higher!")
elif guess > secret_number:
print("Lower!")
else:
guessed = True
print(f"Congratulations! You guessed the number in {attempts} attempts!")
# Ask if they want to play again
play_again = input("Would you like to play again? (yes/no): ")
if play_again.lower() in ('yes', 'y'):
guessing_game() # Recursive call to play again
else:
print("Thanks for playing! Goodbye!")
# Uncomment to play the game
# guessing_game()
Discussion
This exercise combines a while loop for the main game loop, conditionals for checking the player's guess, and exception handling for validating input. It also demonstrates a simple way to allow the player to play multiple games through a recursive function call.
One improvement to consider would be implementing a difficulty system where the player can choose different ranges or a limited number of guesses. This could be accomplished with additional conditionals and parameters to the main function.
Exercise 9: Password Validator
Create a function that validates passwords based on the following rules:
- At least 8 characters long
- Contains at least one uppercase letter
- Contains at least one lowercase letter
- Contains at least one digit
- Contains at least one special character from !@#$%^&*
def validate_password(password):
# Initialize result variables
valid = True
errors = []
# Check password length
if len(password) < 8:
valid = False
errors.append("Password must be at least 8 characters long")
# Check for uppercase letters
if not any(char.isupper() for char in password):
valid = False
errors.append("Password must contain at least one uppercase letter")
# Check for lowercase letters
if not any(char.islower() for char in password):
valid = False
errors.append("Password must contain at least one lowercase letter")
# Check for digits
if not any(char.isdigit() for char in password):
valid = False
errors.append("Password must contain at least one digit")
# Check for special characters
special_chars = "!@#$%^&*"
if not any(char in special_chars for char in password):
valid = False
errors.append("Password must contain at least one special character (!@#$%^&*)")
# Return validation result
return {
"valid": valid,
"errors": errors
}
# Test with various passwords
test_passwords = [
"password",
"Password",
"Password1",
"Pass@word1",
"p@s"
]
for pwd in test_passwords:
result = validate_password(pwd)
print(f"\nPassword: {pwd}")
if result["valid"]:
print("✅ Password is valid")
else:
print("❌ Password is invalid:")
for error in result["errors"]:
print(f" - {error}")
Discussion
This exercise demonstrates how to use list comprehensions with conditional expressions for validation tasks. The any() function returns True if at least one element in the iterable is true, making it perfect for checking if a password contains at least one character of a specific type.
Note how we collect all validation errors rather than returning at the first failure. This provides a better user experience, as the user can see all the issues with their password at once rather than fixing them one by one.
Advanced Practice
Let's finish with a few advanced exercises that combine multiple control structures and techniques.
Exercise 10: Improved Prime Number Generator (Sieve of Eratosthenes)
Implement the Sieve of Eratosthenes algorithm to find all prime numbers up to a given limit more efficiently.
def sieve_of_eratosthenes(limit):
# Initialize all numbers as potential primes
is_prime = [True] * (limit + 1)
# 0 and 1 are not prime
if limit >= 0:
is_prime[0] = False
if limit >= 1:
is_prime[1] = False
# Apply the sieve
for number in range(2, int(limit**0.5) + 1):
if is_prime[number]:
# Mark all multiples of the current prime as non-prime
for multiple in range(number*number, limit + 1, number):
is_prime[multiple] = False
# Collect the primes
primes = [number for number, prime in enumerate(is_prime) if prime]
return primes
# Compare performance with the naive approach
import time
limit = 10000
# Measure time for the naive approach
start_time = time.time()
naive_primes = find_primes(limit)
naive_time = time.time() - start_time
print(f"Naive approach found {len(naive_primes)} primes up to {limit}")
print(f"Time taken: {naive_time:.6f} seconds")
# Measure time for the sieve approach
start_time = time.time()
sieve_primes = sieve_of_eratosthenes(limit)
sieve_time = time.time() - start_time
print(f"Sieve approach found {len(sieve_primes)} primes up to {limit}")
print(f"Time taken: {sieve_time:.6f} seconds")
# Check that both methods found the same primes
print(f"Both methods found the same primes: {naive_primes == sieve_primes}")
# Calculate speedup
speedup = naive_time / sieve_time
print(f"The sieve approach was {speedup:.2f}x faster")
Discussion
The Sieve of Eratosthenes is a classic algorithm for finding all prime numbers up to a given limit. It's much more efficient than the naive approach of checking each number individually, especially for large limits.
The key insight is that instead of checking if each number is prime, we start with the assumption that all numbers are potentially prime, then systematically eliminate the non-primes. This approach leverages the fact that any multiple of a prime number cannot itself be prime.
This exercise demonstrates the power of efficient algorithms over brute-force approaches. It also shows how list comprehensions can be used to filter results based on a condition.
Exercise 11: Text Analyzer
Create a function that analyzes a text and returns statistics including word count, character count, average word length, and the most common words.
def analyze_text(text):
if not text:
return {
"error": "Empty text provided"
}
# Split the text into words
words = text.split()
# Count the total characters (excluding spaces)
char_count = sum(len(word) for word in words)
# Count the frequency of each word (case insensitive)
word_frequency = {}
for word in words:
# Remove punctuation and convert to lowercase
clean_word = ''.join(char for char in word if char.isalnum()).lower()
if clean_word: # Skip empty strings after cleaning
word_frequency[clean_word] = word_frequency.get(clean_word, 0) + 1
# Find the most common words
if word_frequency:
max_frequency = max(word_frequency.values())
most_common = [word for word, freq in word_frequency.items()
if freq == max_frequency]
else:
most_common = []
# Calculate average word length
avg_word_length = char_count / len(words) if words else 0
# Prepare and return the results
return {
"word_count": len(words),
"character_count": char_count,
"unique_words": len(word_frequency),
"average_word_length": round(avg_word_length, 2),
"most_common_words": most_common,
"most_common_frequency": max_frequency if word_frequency else 0
}
# Test the function
sample_text = """
Python is a high-level, general-purpose programming language. Its design philosophy
emphasizes code readability with the use of significant indentation. Python is
dynamically typed and garbage-collected. It supports multiple programming paradigms,
including structured, object-oriented, and functional programming.
"""
analysis = analyze_text(sample_text)
print("Text Analysis Results:")
print(f"Word count: {analysis['word_count']}")
print(f"Character count: {analysis['character_count']}")
print(f"Unique words: {analysis['unique_words']}")
print(f"Average word length: {analysis['average_word_length']} characters")
print(f"Most common word(s): {', '.join(analysis['most_common_words'])}")
print(f"Most common word frequency: {analysis['most_common_frequency']}")
Discussion
This exercise combines various control structures, dictionary operations, and string manipulation techniques. The solution demonstrates how to process text data and extract meaningful statistics using loops and conditionals.
The solution uses a dictionary to track word frequencies, which is a common technique in text analysis. It also demonstrates the use of list comprehensions for transforming and filtering data.
For a more advanced version, consider implementing additional features like:
- Sentence count and average sentence length
- Filtering out common "stop words" like "the", "and", "is"
- Calculating the readability score using formulas like Flesch-Kincaid
Exercise 12: Tic-Tac-Toe Game
Create a simple two-player Tic-Tac-Toe game that allows players to take turns marking positions on a 3x3 grid.
def tic_tac_toe():
# Initialize the board
board = [' ' for _ in range(9)] # 3x3 board as a flat list
def print_board():
"""Print the current state of the board."""
print("\n")
print(f" {board[0]} | {board[1]} | {board[2]} ")
print("---+---+---")
print(f" {board[3]} | {board[4]} | {board[5]} ")
print("---+---+---")
print(f" {board[6]} | {board[7]} | {board[8]} ")
print("\n")
def is_winner(player):
"""Check if the given player has won."""
# Check rows
for i in range(0, 9, 3):
if board[i] == board[i+1] == board[i+2] == player:
return True
# Check columns
for i in range(3):
if board[i] == board[i+3] == board[i+6] == player:
return True
# Check diagonals
if board[0] == board[4] == board[8] == player:
return True
if board[2] == board[4] == board[6] == player:
return True
return False
def is_board_full():
"""Check if the board is full (tie game)."""
return ' ' not in board
# Game loop
current_player = 'X'
print("Welcome to Tic-Tac-Toe!")
print("Enter positions as numbers 1-9 (left to right, top to bottom)")
while True:
print_board()
# Get player move
position = input(f"Player {current_player}, enter your move (1-9): ")
# Validate input
if not position.isdigit() or not 1 <= int(position) <= 9:
print("Invalid input! Please enter a number between 1 and 9.")
continue
position = int(position) - 1 # Convert to 0-based index
# Check if the position is already taken
if board[position] != ' ':
print("That position is already taken! Try again.")
continue
# Make the move
board[position] = current_player
# Check for a win
if is_winner(current_player):
print_board()
print(f"Player {current_player} wins!")
break
# Check for a tie
if is_board_full():
print_board()
print("It's a tie!")
break
# Switch player
current_player = 'O' if current_player == 'X' else 'X'
# Ask if they want to play again
play_again = input("Would you like to play again? (yes/no): ")
if play_again.lower() in ('yes', 'y'):
tic_tac_toe() # Recursive call to play again
else:
print("Thanks for playing! Goodbye!")
# Uncomment to play the game
# tic_tac_toe()
Discussion
This implementation of Tic-Tac-Toe combines multiple control structures and techniques we've learned:
- A
whileloop for the main game loop - Nested functions for organizing code
- Conditionals for validating moves and checking win conditions
- List comprehensions for initializing the board
The game demonstrates how to create an interactive program that maintains state (the board) and responds to user input. It also shows how to validate input and handle invalid cases gracefully.
For a more advanced version, consider adding features like:
- A computer player with simple AI
- Scoring to track wins across multiple games
- The ability to choose who goes first
Conclusion
In this hands-on practice session, we've explored a wide range of control structure applications in Python, from simple conditionals to complex interactive programs. Through these exercises, you've gained practical experience in:
- Using
if,elif, andelsefor decision-making - Implementing
forandwhileloops for repetition - Utilizing
breakandcontinuefor loop control - Combining multiple control structures for complex programs
- Applying these concepts to solve real-world problems
Remember that programming is a skill that improves with practice. I encourage you to modify these exercises, create your own variations, or combine elements from different examples to deepen your understanding.
As you continue your Python journey, you'll find that these fundamental control structures form the backbone of nearly every program you'll write. Mastering them now will give you a solid foundation for the more advanced topics we'll cover in later weeks.
For additional practice, try implementing these control structures to solve problems in your own areas of interest. Whether it's data analysis, game development, web scraping, or automation, the principles we've practiced today apply across all domains of programming.
Homework Challenges
To further reinforce your understanding of control structures, try these homework challenges:
-
Fibonacci Generator: Write a function that generates the first n numbers in the Fibonacci sequence.
-
Word Counter: Create a program that reads a text file and counts the occurrences of each word, then displays the top 10 most frequent words.
-
Rock Paper Scissors Game: Implement a game where the user plays against the computer, which makes random choices.
-
Password Generator: Create a function that generates strong random passwords of a specified length, ensuring they contain a mix of uppercase, lowercase, digits, and special characters.
-
Advanced Challenge: Implement Conway's Game of Life, a cellular automaton that simulates "life" according to simple rules. (Hint: Use nested lists to represent the grid.)
Submit your solutions through the course platform by the next session. We'll review selected submissions in class and discuss different approaches to solving these problems.