Part 2 — Control Flow and Core Data Structures
In this chapter, continuing directly from Part 1: Python Fundamentals, we expand beyond linear, top-to-bottom execution to build dynamic, intelligent programs. You will learn how Python makes decisions through branching conditionals, repeats work through loops, and organizes complex data using Python’s four foundational built-in data structures: Lists, Tuples, Sets, and Dictionaries, culminating in Pythonic Comprehensions.
Topics Covered in This Guide
- Conditional Statements & Truthiness — Branching logic (
if,elif,else), ternary operators, and Python’s strict truthiness evaluation model. - Loops:
for,while, and Loop Control — Repetition paradigms, generator-backedrange(), loop jumps (break,continue,pass), and the loopelseclause. - Lists: Dynamic, Mutable Sequences — In-depth indexing, slicing, mutability, in-place methods, aliasing vs shallow vs deep copying.
- Tuples: Immutable Sequences — Immutability, hashability, tuple packing, and advanced star unpacking (
*rest). - Sets: Unique, Unordered Hash Collections — Hash-table backed membership testing, set theory operations (union, intersection, difference, symmetric difference).
- Dictionaries: Key-Value Hash Maps — Key-value mapping, hashable key constraints, safe retrieval with
.get(), dict iteration, and nesting. - Comprehensions: The Pythonic Data Pipeline — Transforming and filtering lists, sets, and dictionaries with concise, high-performance syntax.
- Master Comparison Matrix — Comprehensive cheat sheet comparing order, mutability, duplicate handling, and Big-O performance.
- Common Mistakes & Misconceptions — Mutating lists during iteration, mutable default arguments, set literal gotchas, and
KeyErroravoidance. - Summary — Core takeaways, memory mechanics, and architectural mental models.
- Quick Reference — High-yield syntax summary table and operation cheat sheet.
- Practice Problems (3 Levels) — Hands-on problem sets from basic to challenge level with hints.
- Mini-Project: Student Academic Performance Analyzer — Full interactive end-to-end Python CLI application.
1. Conditional Statements & Truthiness
1. What is it?
Conditional statements control execution paths based on boolean evaluations (True or False).
2. Syntax
if condition_1:
# Executes when condition_1 is True
...
elif condition_2:
# Executes when condition_1 is False AND condition_2 is True
...
else:
# Executes when ALL preceding conditions are False
...
3. Truth Value Testing (Truthy vs Falsy)
In Python, every object has an inherent boolean truth value. You do not need to write if len(items) > 0: or if flag == True:.
Objects Considered Falsy in Python:
- Constants:
None,False - Numeric zeros:
0,0.0,0j,Decimal(0),Fraction(0, 1) - Empty sequences and collections:
"",(),[],{},set(),range(0)
Everything else is considered Truthy.
# Idiomatic Truthiness Check
cart = []
if not cart:
print("Your shopping cart is empty!") # Executes because [] is falsy
4. Ternary Conditional Expression
Python provides a clean inline conditional expression:
# syntax: value_if_true if condition else value_if_false
age = 19
status = "Adult" if age >= 18 else "Minor"
2. Loops: for, while, and Loop Control
1. The for Loop & range()
Python’s for loop is actually a for-each iterator that traverses elements of any iterable.
# range(start, stop[, step]) -> generates numbers from start to (stop - 1)
for i in range(1, 10, 2):
print(i, end=" ")
# Output: 1 3 5 7 9
2. The while Loop
Repeats as long as a condition remains true. Always ensure an update step occurs to avoid infinite loops:
countdown = 3
while countdown > 0:
print(countdown)
countdown -= 1
print("Blast off!")
3. Loop Control: break, continue, pass
break: Terminates the innermost loop immediately.continue: Skips the rest of the current iteration and jumps to the next loop cycle.pass: A null statement used as a syntactic placeholder where code is required.
for num in range(1, 10):
if num % 2 == 0:
continue # Skip even numbers
if num == 7:
break # Stop loop when 7 is reached
print(num, end=" ")
# Output: 1 3 5
4. The Loop else Clause
Python provides a unique and powerful construct: an else block attached directly to a for or while loop.
Rule: The loop
elseblock executes ONLY if the loop completed normally without encountering abreak.
# Prime number search
target = 13
for divisor in range(2, target):
if target % divisor == 0:
print(f"{target} is divisible by {divisor}, not prime.")
break
else:
# This runs only if no 'break' was hit!
print(f"{target} is a prime number!")
3. Lists: Dynamic, Mutable Sequences
1. What is a List?
A list is an ordered, mutable collection of arbitrary Python objects. Internally, CPython implements lists as dynamic arrays of pointers.
2. Creation, Indexing, and Slicing
fruits = ["apple", "banana", "cherry", "date"]
# Indexing (0-based and negative)
print(fruits[0]) # 'apple'
print(fruits[-1]) # 'date'
# Slicing: [start:stop:step]
print(fruits[1:3]) # ['banana', 'cherry'] (stop index 3 is non-inclusive)
print(fruits[::-1]) # ['date', 'cherry', 'banana', 'apple'] (reversed copy)
3. List Mutation Methods
items = [10, 20, 30]
# Adding items
items.append(40) # [10, 20, 30, 40] (Appends single item to end)
items.extend([50, 60]) # [10, 20, 30, 40, 50, 60] (Appends all iterable items)
items.insert(1, 15) # [10, 15, 20, 30, 40, 50, 60] (Inserts at index 1)
# Removing items
items.remove(15) # Removes first matching value 15
popped_val = items.pop()# Removes and returns last element (60)
popped_at = items.pop(0)# Removes and returns element at index 0 (10)
del items[0] # Deletes element at index 0
# Ordering
scores = [42, 12, 88, 55]
scores.sort() # In-place sort: [12, 42, 55, 88]
scores.sort(reverse=True) # In-place descending sort
scores.reverse() # In-place reversal
Warning (
appendvsextend):items.append([1, 2])nests the list inside:[..., [1, 2]].items.extend([1, 2])flattens the items:[..., 1, 2].
4. Aliasing vs Shallow Copy vs Deep Copy
# 1. Aliasing (Both variables point to the exact same list in memory)
original = [1, 2, [3, 4]]
alias = original
alias.append(99)
print(original) # [1, 2, [3, 4], 99] -> Original was modified!
# 2. Shallow Copy (Copies top-level references, but nested objects remain shared)
shallow = original.copy() # or original[:]
shallow[0] = 500 # Modifies only shallow
shallow[2].append(777) # Modifies nested list in BOTH shallow and original!
print(original[2]) # [3, 4, 777]
# 3. Deep Copy (Recursively clones every nested container)
import copy
deep = copy.deepcopy(original)
deep[2].append(999)
print(original[2]) # [3, 4, 777] -> Original remains completely isolated!
4. Tuples: Immutable Sequences
1. What is a Tuple?
A tuple is an ordered, immutable collection. Once created, elements cannot be added, removed, or reassigned.
2. Creation and the Single-Element Gotcha
# Standard creation
point = (10, 20)
empty_tuple = ()
# CRITICAL GOTCHA: Single-element tuple requires a trailing comma!
not_a_tuple = (42) # Evaluates to int: 42
is_a_tuple = (42,) # Correct tuple of length 1
3. Tuple Packing and Unpacking
# Packing
user_data = "Shafin", 23, "Engineer" # Parentheses are optional
# Unpacking
name, age, profession = user_data
print(name, age) # Shafin 23
# Star Unpacking (Python 3+)
numbers = [1, 2, 3, 4, 5, 6]
first, *middle, last = numbers
print(first) # 1
print(middle) # [2, 3, 4, 5] (Captured as a list)
print(last) # 6
4. Why Use Tuples Instead of Lists?
- Safety: Guarantee data cannot be accidentally mutated by downstream code.
- Performance: Tuples use less memory than lists and optimize small-tuple allocations.
- Hashability: Because they are immutable, tuples can be used as dictionary keys and set elements (provided their contents are also immutable).
5. Sets: Unique, Unordered Hash Collections
1. What is a Set?
A set is an unordered collection of unique, hashable objects. Sets are backed by hash tables, giving them average-case time complexity for additions, deletions, and membership checks (val in my_set).
2. Creation and Modification
# Creating sets
numbers = {1, 2, 3, 3, 2, 1}
print(numbers) # {1, 2, 3} (Duplicates automatically dropped)
# Empty set gotcha:
empty_dict = {} # This is a dict!
empty_set = set() # This is an empty set
# Adding and removing
colors = {"red", "green"}
colors.add("blue")
colors.discard("yellow") # Safe: does nothing if item does not exist
# colors.remove("yellow") # Raises KeyError if item is not found
3. Mathematical Set Operations
a = {1, 2, 3, 4}
b = {3, 4, 5, 6}
# Union: items in either a or b
print(a | b) # {1, 2, 3, 4, 5, 6}
print(a.union(b))
# Intersection: items in both a and b
print(a & b) # {3, 4}
print(a.intersection(b))
# Difference: items in a but not in b
print(a - b) # {1, 2}
print(a.difference(b))
# Symmetric Difference: items in a or b, but not both
print(a ^ b) # {1, 2, 5, 6}
print(a.symmetric_difference(b))
# Subsets and Supersets
c = {1, 2}
print(c.issubset(a)) # True (c <= a)
print(a.issuperset(c)) # True (a >= c)
6. Dictionaries: Key-Value Hash Maps
1. What is a Dictionary?
A dictionary (dict) is an associative container that maps unique, hashable keys to arbitrary values. In Python 3.7+, dictionaries are guaranteed to maintain their insertion order.
2. Key Requirements
Keys must be hashable (immutable types like str, int, float, tuple containing immutable items). Lists and dicts cannot be keys.
3. Accessing and Modifying
student = {
"name": "Shafin",
"major": "Computer Science",
"gpa": 3.92
}
# Accessing keys
print(student["name"]) # 'Shafin'
# print(student["id"]) # Raises KeyError if key is missing!
# Safe retrieval using .get(key, default)
student_id = student.get("id", "N/A")
print(student_id) # 'N/A'
# Adding / Updating
student["gpa"] = 3.95 # Updates existing key
student["semester"] = "Fall" # Adds new key
student.update({"id": "ST-101", "campus": "Main"})
# Deletion
removed_gpa = student.pop("gpa")# Removes key and returns value
del student["campus"] # Deletes key
4. Iterating Over Dictionaries
inventory = {"laptops": 12, "monitors": 25, "keyboards": 40}
# Iterating keys (default)
for item in inventory:
print(item, end=" ")
# laptops monitors keyboards
# Iterating values
for count in inventory.values():
print(count, end=" ")
# Iterating key-value pairs (idiomatic)
for item, count in inventory.items():
print(f"{item} -> {count}")
7. Comprehensions: The Pythonic Data Pipeline
Comprehensions provide a concise syntax for transforming, filtering, and constructing collections in a single expressive line.
1. List Comprehensions
# Traditional approach
squares = []
for x in range(1, 6):
squares.append(x ** 2)
# Idiomatic List Comprehension
# Syntax: [expression for item in iterable if condition]
squares = [x ** 2 for x in range(1, 6)]
print(squares) # [1, 4, 9, 16, 25]
# With Filtering: Even squares only
even_squares = [x ** 2 for x in range(1, 10) if x % 2 == 0]
print(even_squares) # [4, 16, 36, 64]
# Conditional Expression inside Comprehension
# [val_if_true if cond else val_if_false for item in iterable]
labels = ["EVEN" if x % 2 == 0 else "ODD" for x in range(5)]
print(labels) # ['EVEN', 'ODD', 'EVEN', 'ODD', 'EVEN']
2. Set & Dictionary Comprehensions
# Set Comprehension (extract unique lengths of words)
words = ["python", "is", "awesome", "python", "code"]
unique_lengths = {len(w) for w in words}
print(unique_lengths) # {2, 4, 6, 7}
# Dictionary Comprehension (swap keys and values)
original_dict = {"a": 1, "b": 2, "c": 3}
inverted_dict = {value: key for key, value in original_dict.items()}
print(inverted_dict) # {1: 'a', 2: 'b', 3: 'c'}
8. Master Comparison Matrix
| Data Structure | Syntax | Ordered? | Mutable? | Allows Duplicates? | Element Lookup Cost | Practical Best Use Case |
|---|---|---|---|---|---|---|
| List | [1, 2, 3] | Yes | Yes | Yes | linear scan | Dynamic ordered collections, stacks, queues |
| Tuple | (1, 2, 3) | Yes | No | Yes | linear scan | Fixed records, dict keys, function multi-returns |
| Set | {1, 2, 3} | No | Yes | No | constant | De-duplication, fast membership checking |
| Dictionary | {"k": "v"} | Yes (3.7+) | Yes | Keys: No, Values: Yes | constant | Keyed indexing, caches, entity representations |
9. Common Mistakes & Misconceptions
-
=vs==:=is assignment;==is equality comparison. -
Modifying a collection while iterating over it:
# BUG: Modifying list during iteration skips elements! numbers = [1, 2, 3, 4, 5] for n in numbers: if n % 2 == 0: numbers.remove(n) # FIX: Iterate over a copy or use a comprehension: numbers = [n for n in numbers if n % 2 != 0] -
Empty Collection Creation Bug: Remember that
{}creates an empty dictionary, not an empty set. Always useset(). -
Defaulting
dict[key]instead of.get(): Accessing missing keys directly throwsKeyError. Always use.get(key, default)when the key is uncertain.
Summary
- Truthiness governs conditional branching: empty containers and zero values evaluate to
False. - The loop
elseblock runs only when a loop finishes naturally without triggering abreak. - Lists are mutable dynamic arrays; modifying copies requires
copy.deepcopy()when nested structures are present. - Tuples are immutable and hashable, making them ideal for fixed records and dictionary keys.
- Sets and dictionaries leverage hash tables to provide instantaneous lookups.
- Comprehensions replace cumbersome multi-line
forloops with expressive, high-speed data pipelines.
Quick Reference
| Operation | Code Snippet | Output / Behavior |
|---|---|---|
| Ternary operator | x = "yes" if flag else "no" | Conditional assignment |
| Unpack with star | a, *rest, b = [1, 2, 3, 4] | a=1, rest=[2, 3], b=4 |
| Safe Dict Retrieval | d.get("missing", 0) | Returns 0 without KeyError |
| Remove from Set safely | s.discard("unknown") | Does not raise KeyError |
| List Comprehension | [x*2 for x in nums if x > 0] | Transforms and filters |
| Dict Comprehension | {k: v for k, v in pairs} | Constructs dict from pairs |
Practice Problems
Level 1 — Basic
- Given a list of numbers
[12, 45, 2, 9, 88, 34], use a singleforloop to find and print the minimum and maximum values without usingmin()ormax(). - Take a string with duplicate characters (e.g.
"abracadabra") and print the unique characters in alphabetical order using a set andsorted().
Level 2 — Intermediate
- Given a dictionary of student grades
scores = {"Alice": 85, "Bob": 62, "Charlie": 91, "Diana": 74}, write a dictionary comprehension that creates a new dictionary containing only students with passing grades (>= 70), mapping their names to"PASS". - Implement a palindrome checker that ignores spaces, punctuation, and letter casing (e.g.,
"A man, a plan, a canal: Panama"should returnTrue) using list comprehension or string filtering.
Level 3 — Challenge
- Write a two-sum lookup algorithm: Given a list of integers
nums = [2, 7, 11, 15]and a targettarget = 9, use a dictionary to find the indices of the two numbers that add up totargetin a single pass.
Hints
- Problem 3:
{name: "PASS" for name, score in scores.items() if score >= 70}. - Problem 4: Use
clean = [c.lower() for c in text if c.isalnum()], then checkclean == clean[::-1]. - Problem 5: For each number, calculate
complement = target - num. Ifcomplementexists inseen_dict, return[seen_dict[complement], current_index]. Otherwise, storeseen_dict[num] = current_index.
Mini-Project: Student Academic Performance Analyzer
Save and run this complete Python script as grade_analyzer.py:
"""
Part 2 Mini-Project: Academic Performance & Grading CLI Engine
Demonstrates: conditionals, loops, dictionaries, lists, sets, and comprehensions.
"""
# Student records dataset
classroom_data = [
{"id": "S101", "name": "Alice Johnson", "scores": [88, 92, 79, 95]},
{"id": "S102", "name": "Bob Smith", "scores": [55, 60, 58, 62]},
{"id": "S103", "name": "Charlie Davis", "scores": [95, 98, 92, 100]},
{"id": "S104", "name": "Diana Prince", "scores": [72, 75, 80, 68]},
{"id": "S105", "name": "Evan Wright", "scores": [40, 52, 48, 45]},
]
def calculate_letter_grade(avg: float) -> str:
"""Computes academic letter grade based on standard scale."""
if avg >= 90:
return "A"
elif avg >= 80:
return "B"
elif avg >= 70:
return "C"
elif avg >= 60:
return "D"
else:
return "F"
print("=" * 65)
print(" ACADEMIC PERFORMANCE & GRADE BOOK REPORT ")
print("=" * 65)
# Process students and collect analytics
processed_records = []
all_student_averages = []
for student in classroom_data:
scores = student["scores"]
average_score = sum(scores) / len(scores)
letter_grade = calculate_letter_grade(average_score)
is_honor_roll = average_score >= 90
student_summary = {
"id": student["id"],
"name": student["name"],
"average": round(average_score, 2),
"grade": letter_grade,
"honor": is_honor_roll
}
processed_records.append(student_summary)
all_student_averages.append(average_score)
# Formatted Table Output
print(f"{'ID':<6} | {'Student Name':<16} | {'Average':<8} | {'Grade':<6} | {'Honor Roll':<10}")
print("-" * 65)
for record in processed_records:
honor_flag = "★ YES" if record["honor"] else " NO"
print(f"{record['id']:<6} | {record['name']:<16} | {record['average']:<8.2f} | {record['grade']:<6} | {honor_flag:<10}")
# Class-level Analytics using Comprehensions
class_mean = sum(all_student_averages) / len(all_student_averages)
honor_students = [r["name"] for r in processed_records if r["honor"]]
at_risk_students = [r["name"] for r in processed_records if r["grade"] == "F"]
unique_grades = sorted({r["grade"] for r in processed_records})
print("=" * 65)
print(f"Class Average Performance : {class_mean:.2f}%")
print(f"Unique Grades Assigned : {', '.join(unique_grades)}")
print(f"Honor Roll Recipients : {', '.join(honor_students) if honor_students else 'None'}")
print(f"At-Risk Students (Need Help): {', '.join(at_risk_students) if at_risk_students else 'None'}")
print("=" * 65)
What Comes Next?
In Part 3 — Functions and Modular Programming, we introduce code reuse, function signatures, *args and **kwargs, the LEGB scope rule, closures, first-class functions, recursion, and packaging reusable modules.
← Part 1 — Python Fundamentals | Part 2 — Control Flow & Data Structures | Explore Python Topic Hub →