Skip to main content

🔄 Lesson 5.3: Iterating Over Collections

You know how to create and modify collections. Now it's time to master the art of processing every element — transforming data, filtering results, accumulating totals, and combining collections. These patterns are the backbone of real-world programming.

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Choose the right loop type for each situation
  • Use enumerate to get both index and value
  • Apply map, filter, and reduce to transform collections
  • Use any/all checks, zip, and other common patterns
  • Chain operations for elegant data pipelines
  • Know when to use loops vs. functional methods

Estimated Time: 45 minutes

Project: Build a grade report processor that filters, transforms, and summarizes student data

📑 In This Lesson

Basic Collection Loops

The simplest way to process every element: a for loop. You've seen this in Module 3 — now let's focus on the patterns specific to collections.

# for...in — the standard Python loop
colors = ["red", "green", "blue"]

for color in colors:
    print(color)
# red, green, blue

# When you need the index too, DON'T do this:
# for i in range(len(colors)):  ← works but not Pythonic
#     print(i, colors[i])

# DO use enumerate() instead (next section!)

# Looping over a dictionary
scores = {"Alice": 95, "Bob": 87, "Charlie": 92}

# Keys only (default)
for name in scores:
    print(name)

# Key-value pairs
for name, score in scores.items():
    print(f"{name}: {score}")
let colors = ["red", "green", "blue"];

// for...of — iterates over VALUES (use this for arrays!)
for (let color of colors) {
    console.log(color);
}
// red, green, blue

// .forEach() — callback style
colors.forEach(color => console.log(color));

// Classic for loop — when you need the index
for (let i = 0; i < colors.length; i++) {
    console.log(i, colors[i]);
}

// ⚠️ for...in — iterates over KEYS (use for objects, NOT arrays)
let scores = { Alice: 95, Bob: 87, Charlie: 92 };

for (let name in scores) {
    console.log(`${name}: ${scores[name]}`);
}

// for...of with Object.entries() (cleaner for objects)
for (let [name, score] of Object.entries(scores)) {
    console.log(`${name}: ${score}`);
}
List<string> colors = new List<string> { "red", "green", "blue" };

// foreach — the standard C# collection loop
foreach (string color in colors)
{
    Console.WriteLine(color);
}

// Classic for loop — when you need the index
for (int i = 0; i < colors.Count; i++)
{
    Console.WriteLine($"{i}: {colors[i]}");
}

// Looping over a dictionary
Dictionary<string, int> scores = new Dictionary<string, int>
{
    { "Alice", 95 }, { "Bob", 87 }, { "Charlie", 92 }
};

foreach (var (name, score) in scores)
{
    Console.WriteLine($"{name}: {score}");
}

⚠️ JavaScript: for...of vs. for...in

for...of gives you values — use it for arrays. for...in gives you keys (indices for arrays, property names for objects) — use it for objects. Using for...in on arrays can produce unexpected results because it also iterates over inherited properties.

🎓 Instructor Note: Delivery Guidance

The for...of vs. for...in distinction is a perennial JavaScript confusion point. A simple mnemonic: "of = values of an array" and "in = keys in an object." Write both on the board side by side with examples. Python and C# don't have this ambiguity — their for...in / foreach always gives values for lists and key-value pairs for dicts.

Enumerate: Index + Value

Often you need both the position and the value during iteration. Each language has a clean way to do this.

fruits = ["apple", "banana", "cherry", "date"]

# enumerate() gives you (index, value) pairs
for i, fruit in enumerate(fruits):
    print(f"{i}: {fruit}")
# 0: apple
# 1: banana
# 2: cherry
# 3: date

# Start counting from a different number
for i, fruit in enumerate(fruits, start=1):
    print(f"{i}. {fruit}")
# 1. apple
# 2. banana
# 3. cherry
# 4. date

# Useful for finding items
scores = [85, 92, 78, 95, 88]
for i, score in enumerate(scores):
    if score >= 90:
        print(f"High score at position {i}: {score}")
# High score at position 1: 92
# High score at position 3: 95
let fruits = ["apple", "banana", "cherry", "date"];

// .forEach() provides (value, index)
fruits.forEach((fruit, i) => {
    console.log(`${i}: ${fruit}`);
});

// for...of with .entries() — gives [index, value] pairs
for (let [i, fruit] of fruits.entries()) {
    console.log(`${i}: ${fruit}`);
}

// Classic for loop also works
for (let i = 0; i < fruits.length; i++) {
    console.log(`${i + 1}. ${fruits[i]}`);
}

// Finding items with index
let scores = [85, 92, 78, 95, 88];
scores.forEach((score, i) => {
    if (score >= 90) {
        console.log(`High score at position ${i}: ${score}`);
    }
});
List<string> fruits = new List<string>
    { "apple", "banana", "cherry", "date" };

// Classic for loop (most common in C# for indexed access)
for (int i = 0; i < fruits.Count; i++)
{
    Console.WriteLine($"{i}: {fruits[i]}");
}

// LINQ Select with index
foreach (var (fruit, i) in fruits.Select((f, i) => (f, i)))
{
    Console.WriteLine($"{i}: {fruit}");
}

// Finding items with index
List<int> scores = new List<int> { 85, 92, 78, 95, 88 };
for (int i = 0; i < scores.Count; i++)
{
    if (scores[i] >= 90)
        Console.WriteLine($"High score at position {i}: {scores[i]}");
}

Enumerate Quick Reference

Pattern 🐍 Python ⚡ JavaScript 🔷 C#
Index + value enumerate(list) .forEach((val, i))
or .entries()
for (int i = 0; ...)
Custom start enumerate(list, 1) Manual i + 1 Manual i + 1

Map and Filter

Two of the most important collection operations: map transforms every element, and filter keeps only elements that pass a test. We previewed these in Lesson 13 — now let's go deeper.

prices = [10.99, 24.50, 7.25, 15.00, 32.99]

# List comprehension (preferred in Python)
with_tax = [price * 1.08 for price in prices]
print(with_tax)  # [11.87, 26.46, 7.83, 16.20, 35.63]

# Round them
with_tax = [round(price * 1.08, 2) for price in prices]

# map() function (alternative)
with_tax2 = list(map(lambda p: round(p * 1.08, 2), prices))

# Transform strings
names = ["alice smith", "bob jones", "charlie brown"]
titled = [name.title() for name in names]
print(titled)  # ['Alice Smith', 'Bob Jones', 'Charlie Brown']

# Extract a field from dicts
students = [
    {"name": "Alice", "grade": 95},
    {"name": "Bob", "grade": 87},
    {"name": "Charlie", "grade": 92}
]
names_only = [s["name"] for s in students]
print(names_only)  # ['Alice', 'Bob', 'Charlie']
let prices = [10.99, 24.50, 7.25, 15.00, 32.99];

// .map() — returns a new array with transformed values
let withTax = prices.map(price => Math.round(price * 1.08 * 100) / 100);
console.log(withTax);  // [11.87, 26.46, 7.83, 16.20, 35.63]

// Transform strings
let names = ["alice smith", "bob jones", "charlie brown"];
let titled = names.map(name =>
    name.split(" ").map(w => w[0].toUpperCase() + w.slice(1)).join(" ")
);
console.log(titled);  // ['Alice Smith', 'Bob Jones', 'Charlie Brown']

// Extract a field from objects
let students = [
    { name: "Alice", grade: 95 },
    { name: "Bob", grade: 87 },
    { name: "Charlie", grade: 92 }
];
let namesOnly = students.map(s => s.name);
console.log(namesOnly);  // ['Alice', 'Bob', 'Charlie']
List<double> prices = new List<double>
    { 10.99, 24.50, 7.25, 15.00, 32.99 };

// .Select() — LINQ's map
List<double> withTax = prices
    .Select(p => Math.Round(p * 1.08, 2))
    .ToList();
Console.WriteLine(string.Join(", ", withTax));

// Transform strings
List<string> names = new List<string>
    { "alice smith", "bob jones", "charlie brown" };
var titled = names
    .Select(n => System.Globalization.CultureInfo.CurrentCulture
        .TextInfo.ToTitleCase(n))
    .ToList();

// Extract a field (with anonymous types or tuples)
var students = new List<(string Name, int Grade)>
{
    ("Alice", 95), ("Bob", 87), ("Charlie", 92)
};
List<string> namesOnly = students.Select(s => s.Name).ToList();
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

# List comprehension with condition
evens = [n for n in numbers if n % 2 == 0]
print(evens)  # [2, 4, 6, 8, 10]

big = [n for n in numbers if n > 5]
print(big)  # [6, 7, 8, 9, 10]

# filter() function
evens2 = list(filter(lambda n: n % 2 == 0, numbers))

# Filter objects
students = [
    {"name": "Alice", "grade": 95},
    {"name": "Bob", "grade": 67},
    {"name": "Charlie", "grade": 92},
    {"name": "Diana", "grade": 55}
]

passing = [s for s in students if s["grade"] >= 70]
print([s["name"] for s in passing])  # ['Alice', 'Charlie']

# Filter + transform in one comprehension
honor_names = [s["name"] for s in students if s["grade"] >= 90]
print(honor_names)  # ['Alice', 'Charlie']
let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

// .filter() — returns elements that pass the test
let evens = numbers.filter(n => n % 2 === 0);
console.log(evens);  // [2, 4, 6, 8, 10]

let big = numbers.filter(n => n > 5);
console.log(big);  // [6, 7, 8, 9, 10]

// Filter objects
let students = [
    { name: "Alice", grade: 95 },
    { name: "Bob", grade: 67 },
    { name: "Charlie", grade: 92 },
    { name: "Diana", grade: 55 }
];

let passing = students.filter(s => s.grade >= 70);
console.log(passing.map(s => s.name));  // ['Alice', 'Charlie']

// Filter + transform (chain them)
let honorNames = students
    .filter(s => s.grade >= 90)
    .map(s => s.name);
console.log(honorNames);  // ['Alice', 'Charlie']
List<int> numbers = new List<int>
    { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

// .Where() — LINQ's filter
List<int> evens = numbers.Where(n => n % 2 == 0).ToList();
Console.WriteLine(string.Join(", ", evens));  // 2, 4, 6, 8, 10

// Filter objects
var students = new List<(string Name, int Grade)>
{
    ("Alice", 95), ("Bob", 67), ("Charlie", 92), ("Diana", 55)
};

var passing = students.Where(s => s.Grade >= 70).ToList();

// Filter + transform (chain)
var honorNames = students
    .Where(s => s.Grade >= 90)
    .Select(s => s.Name)
    .ToList();
Console.WriteLine(string.Join(", ", honorNames));  // Alice, Charlie

Reduce: Accumulating a Result

Reduce (also called fold or aggregate) processes every element and combines them into a single result — a total, a string, a dictionary, or anything else.

from functools import reduce

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

# Sum (built-in is simpler, but reduce shows the concept)
total = reduce(lambda acc, n: acc + n, numbers, 0)
print(total)  # 15

# But Python prefers built-in functions:
print(sum(numbers))       # 15
print(max(numbers))       # 5
print(min(numbers))       # 1

# Join strings
words = ["Hello", "beautiful", "world"]
sentence = " ".join(words)
print(sentence)  # "Hello beautiful world"

# Build a frequency map (reduce-style thinking)
letters = ["a", "b", "a", "c", "b", "a"]
freq = {}
for letter in letters:
    freq[letter] = freq.get(letter, 0) + 1
print(freq)  # {'a': 3, 'b': 2, 'c': 1}

# Group items
students = [
    {"name": "Alice", "grade": "A"},
    {"name": "Bob", "grade": "B"},
    {"name": "Charlie", "grade": "A"},
    {"name": "Diana", "grade": "B"}
]
by_grade = {}
for s in students:
    by_grade.setdefault(s["grade"], []).append(s["name"])
print(by_grade)  # {'A': ['Alice', 'Charlie'], 'B': ['Bob', 'Diana']}
let numbers = [1, 2, 3, 4, 5];

// .reduce(callback, initialValue)
let total = numbers.reduce((acc, n) => acc + n, 0);
console.log(total);  // 15

// Find max
let max = numbers.reduce((a, b) => a > b ? a : b);
console.log(max);  // 5

// Join strings
let words = ["Hello", "beautiful", "world"];
let sentence = words.join(" ");
console.log(sentence);  // "Hello beautiful world"

// Build a frequency map
let letters = ["a", "b", "a", "c", "b", "a"];
let freq = letters.reduce((acc, letter) => {
    acc[letter] = (acc[letter] ?? 0) + 1;
    return acc;
}, {});
console.log(freq);  // {a: 3, b: 2, c: 1}

// Group items
let students = [
    { name: "Alice", grade: "A" },
    { name: "Bob", grade: "B" },
    { name: "Charlie", grade: "A" },
    { name: "Diana", grade: "B" }
];
let byGrade = students.reduce((acc, s) => {
    (acc[s.grade] ??= []).push(s.name);
    return acc;
}, {});
console.log(byGrade);  // {A: ['Alice','Charlie'], B: ['Bob','Diana']}
List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };

// .Aggregate() — LINQ's reduce
int total = numbers.Aggregate(0, (acc, n) => acc + n);
Console.WriteLine(total);  // 15

// Built-in LINQ methods (usually preferred)
Console.WriteLine(numbers.Sum());   // 15
Console.WriteLine(numbers.Max());   // 5
Console.WriteLine(numbers.Min());   // 1

// Join strings
List<string> words = new List<string> { "Hello", "beautiful", "world" };
string sentence = string.Join(" ", words);
Console.WriteLine(sentence);

// Group items
var students = new List<(string Name, string Grade)>
{
    ("Alice", "A"), ("Bob", "B"), ("Charlie", "A"), ("Diana", "B")
};

var byGrade = students
    .GroupBy(s => s.Grade)
    .ToDictionary(g => g.Key, g => g.Select(s => s.Name).ToList());

foreach (var (grade, names) in byGrade)
    Console.WriteLine($"{grade}: {string.Join(", ", names)}");
// A: Alice, Charlie
// B: Bob, Diana

💡 When to Use Reduce

Reduce is the most powerful iteration method — any operation you can do with a loop, you can express as a reduce. But power comes with complexity. Use built-in functions (sum, max, join) when they exist; reach for reduce when you need custom accumulation logic like building frequency maps or grouping data.

Common Patterns

Beyond map, filter, and reduce, here are patterns you'll use regularly.

Any / All — Check Conditions

scores = [85, 92, 78, 95, 88]

# any() — is at least one True?
has_perfect = any(s == 100 for s in scores)
has_failing = any(s < 60 for s in scores)
print(has_perfect)  # False
print(has_failing)  # False

# all() — are ALL True?
all_passing = all(s >= 70 for s in scores)
print(all_passing)  # True

all_honor = all(s >= 90 for s in scores)
print(all_honor)  # False
let scores = [85, 92, 78, 95, 88];

// .some() — at least one passes?
let hasPerfect = scores.some(s => s === 100);
let hasFailing = scores.some(s => s < 60);
console.log(hasPerfect);  // false
console.log(hasFailing);  // false

// .every() — ALL pass?
let allPassing = scores.every(s => s >= 70);
console.log(allPassing);  // true

let allHonor = scores.every(s => s >= 90);
console.log(allHonor);  // false
List<int> scores = new List<int> { 85, 92, 78, 95, 88 };

// .Any() — at least one?
bool hasPerfect = scores.Any(s => s == 100);  // false
bool hasFailing = scores.Any(s => s < 60);    // false

// .All() — every one?
bool allPassing = scores.All(s => s >= 70);   // true
bool allHonor = scores.All(s => s >= 90);     // false

Find — Get the First Match

users = [
    {"name": "Alice", "role": "admin"},
    {"name": "Bob", "role": "user"},
    {"name": "Charlie", "role": "admin"}
]

# next() with generator expression
first_admin = next((u for u in users if u["role"] == "admin"), None)
print(first_admin)  # {'name': 'Alice', 'role': 'admin'}

# With a default if not found
first_mod = next((u for u in users if u["role"] == "mod"), None)
print(first_mod)  # None
let users = [
    { name: "Alice", role: "admin" },
    { name: "Bob", role: "user" },
    { name: "Charlie", role: "admin" }
];

// .find() — first element that passes
let firstAdmin = users.find(u => u.role === "admin");
console.log(firstAdmin);  // {name: 'Alice', role: 'admin'}

// .findIndex() — index of first match
let idx = users.findIndex(u => u.role === "admin");
console.log(idx);  // 0

// Not found = undefined / -1
let firstMod = users.find(u => u.role === "mod");
console.log(firstMod);  // undefined
var users = new List<(string Name, string Role)>
{
    ("Alice", "admin"), ("Bob", "user"), ("Charlie", "admin")
};

// .First() — throws if none found
var firstAdmin = users.First(u => u.Role == "admin");
Console.WriteLine(firstAdmin.Name);  // Alice

// .FirstOrDefault() — returns default if none found
var firstMod = users.FirstOrDefault(u => u.Role == "mod");
Console.WriteLine(firstMod.Name ?? "None");  // None

// .FindIndex()
int idx = users.FindIndex(u => u.Role == "admin");
Console.WriteLine(idx);  // 0

Zip — Combine Two Collections

names = ["Alice", "Bob", "Charlie"]
scores = [95, 87, 92]

# zip() pairs elements by position
for name, score in zip(names, scores):
    print(f"{name}: {score}")
# Alice: 95, Bob: 87, Charlie: 92

# Create a dictionary from two lists
grade_book = dict(zip(names, scores))
print(grade_book)  # {'Alice': 95, 'Bob': 87, 'Charlie': 92}
let names = ["Alice", "Bob", "Charlie"];
let scores = [95, 87, 92];

// No built-in zip — use .map() with index
let paired = names.map((name, i) => [name, scores[i]]);
console.log(paired);  // [['Alice',95], ['Bob',87], ['Charlie',92]]

// Create an object from two arrays
let gradeBook = Object.fromEntries(
    names.map((name, i) => [name, scores[i]])
);
console.log(gradeBook);  // {Alice: 95, Bob: 87, Charlie: 92}
List<string> names = new List<string> { "Alice", "Bob", "Charlie" };
List<int> scores = new List<int> { 95, 87, 92 };

// .Zip() pairs elements
foreach (var (name, score) in names.Zip(scores))
{
    Console.WriteLine($"{name}: {score}");
}

// Create a dictionary from two lists
var gradeBook = names.Zip(scores)
    .ToDictionary(pair => pair.First, pair => pair.Second);

Chaining Operations

The real power of map/filter/reduce is chaining them together to build data processing pipelines.

# Scenario: Process student records
students = [
    {"name": "Alice", "grade": 95, "subject": "Math"},
    {"name": "Bob", "grade": 67, "subject": "Math"},
    {"name": "Charlie", "grade": 92, "subject": "Science"},
    {"name": "Diana", "grade": 88, "subject": "Math"},
    {"name": "Eve", "grade": 55, "subject": "Science"},
    {"name": "Frank", "grade": 91, "subject": "Math"}
]

# Pipeline: Math students → passing → names → sorted
math_honor = sorted([
    s["name"]
    for s in students
    if s["subject"] == "Math" and s["grade"] >= 70
], reverse=True)

print(math_honor)  # ['Frank', 'Diana', 'Alice']

# Pipeline: Average grade per subject
from collections import defaultdict
by_subject = defaultdict(list)
for s in students:
    by_subject[s["subject"]].append(s["grade"])

averages = {
    subject: round(sum(grades) / len(grades), 1)
    for subject, grades in by_subject.items()
}
print(averages)  # {'Math': 85.3, 'Science': 73.5}
let students = [
    { name: "Alice", grade: 95, subject: "Math" },
    { name: "Bob", grade: 67, subject: "Math" },
    { name: "Charlie", grade: 92, subject: "Science" },
    { name: "Diana", grade: 88, subject: "Math" },
    { name: "Eve", grade: 55, subject: "Science" },
    { name: "Frank", grade: 91, subject: "Math" }
];

// Pipeline: Math students → passing → names → sorted
let mathHonor = students
    .filter(s => s.subject === "Math")
    .filter(s => s.grade >= 70)
    .map(s => s.name)
    .sort()
    .reverse();

console.log(mathHonor);  // ['Frank', 'Diana', 'Alice']

// Pipeline: Average grade per subject
let bySubject = students.reduce((acc, s) => {
    (acc[s.subject] ??= []).push(s.grade);
    return acc;
}, {});

let averages = Object.fromEntries(
    Object.entries(bySubject).map(([subject, grades]) => [
        subject,
        Math.round(grades.reduce((a, b) => a + b, 0) / grades.length * 10) / 10
    ])
);
console.log(averages);  // {Math: 85.3, Science: 73.5}
var students = new List<(string Name, int Grade, string Subject)>
{
    ("Alice", 95, "Math"), ("Bob", 67, "Math"),
    ("Charlie", 92, "Science"), ("Diana", 88, "Math"),
    ("Eve", 55, "Science"), ("Frank", 91, "Math")
};

// Pipeline: Math students → passing → names → sorted
var mathHonor = students
    .Where(s => s.Subject == "Math")
    .Where(s => s.Grade >= 70)
    .Select(s => s.Name)
    .OrderByDescending(n => n)
    .ToList();

Console.WriteLine(string.Join(", ", mathHonor));  // Frank, Diana, Alice

// Pipeline: Average grade per subject
var averages = students
    .GroupBy(s => s.Subject)
    .ToDictionary(g => g.Key, g => Math.Round(g.Average(s => s.Grade), 1));

foreach (var (subject, avg) in averages)
    Console.WriteLine($"{subject}: {avg}");

✅ Read Chained Operations Top to Bottom

When you see a chain like .filter().filter().map().sort(), read it as a step-by-step process: "Start with all students, keep only Math students, keep only passing, extract names, sort alphabetically." Each step feeds its result into the next. This is called a data pipeline.

Loops vs. Functional Methods

You can accomplish the same thing with either approach. Here's guidance on when to use which.

Use Case Recommendation Why
Transform every element map / comprehension Clear intent: "make a new collection from this one"
Keep some elements filter / comprehension Clear intent: "keep items matching this condition"
Accumulate a single result reduce or loop Use built-ins (sum, max) when possible; loops for complex logic
Side effects (print, modify external state) for loop Loops make side effects visible; map/filter should be pure
Break early on condition for loop map/filter process everything; loops can break
Complex multi-step processing chained methods Pipeline reads like a recipe; nested loops get confusing

💡 The Pragmatic Rule

Use whichever is easier to read. If a chain of map/filter makes the intent clear in one line, use it. If you're struggling to cram logic into a lambda, use a for loop. Readability beats cleverness every time.

Exercises

🏋️ Exercise 1: Data Transformer

Objective: Given a list of temperatures in Fahrenheit, produce:

  • A list of temperatures converted to Celsius (formula: (F - 32) × 5/9)
  • Only temperatures above freezing (Celsius > 0)
  • The average of all Celsius temperatures

Input: [32, 50, 68, 14, 86, 23, 104]

✅ Solution
fahrenheit = [32, 50, 68, 14, 86, 23, 104]

celsius = [round((f - 32) * 5/9, 1) for f in fahrenheit]
print(celsius)  # [0.0, 10.0, 20.0, -10.0, 30.0, -5.0, 40.0]

above_freezing = [c for c in celsius if c > 0]
print(above_freezing)  # [10.0, 20.0, 30.0, 40.0]

average = round(sum(celsius) / len(celsius), 1)
print(f"Average: {average}°C")  # 12.1°C
let fahrenheit = [32, 50, 68, 14, 86, 23, 104];

let celsius = fahrenheit.map(f => Math.round((f - 32) * 5/9 * 10) / 10);
console.log(celsius);  // [0, 10, 20, -10, 30, -5, 40]

let aboveFreezing = celsius.filter(c => c > 0);
console.log(aboveFreezing);  // [10, 20, 30, 40]

let average = Math.round(celsius.reduce((a, b) => a + b, 0) / celsius.length * 10) / 10;
console.log(`Average: ${average}°C`);  // 12.1°C
List<int> fahrenheit = new List<int> { 32, 50, 68, 14, 86, 23, 104 };

var celsius = fahrenheit.Select(f => Math.Round((f - 32) * 5.0 / 9, 1)).ToList();
var aboveFreezing = celsius.Where(c => c > 0).ToList();
double average = Math.Round(celsius.Average(), 1);

Console.WriteLine($"Average: {average}°C");  // 12.1°C

🏋️ Exercise 2: Grade Report Processor

Objective: Given student records, write functions to:

  • get_passing(students) — return students with grade >= 70
  • get_honor_roll(students) — return names of students with grade >= 90, sorted alphabetically
  • average_by_subject(students) — return a dict of subject → average grade
  • highest_per_subject(students) — return the top student per subject
✅ Solution
students = [
    {"name": "Alice", "grade": 95, "subject": "Math"},
    {"name": "Bob", "grade": 67, "subject": "Math"},
    {"name": "Charlie", "grade": 92, "subject": "Science"},
    {"name": "Diana", "grade": 88, "subject": "Science"},
    {"name": "Eve", "grade": 91, "subject": "Math"}
]

def get_passing(students):
    return [s for s in students if s["grade"] >= 70]

def get_honor_roll(students):
    return sorted([s["name"] for s in students if s["grade"] >= 90])

def average_by_subject(students):
    from collections import defaultdict
    by_subj = defaultdict(list)
    for s in students:
        by_subj[s["subject"]].append(s["grade"])
    return {subj: round(sum(g)/len(g), 1) for subj, g in by_subj.items()}

def highest_per_subject(students):
    best = {}
    for s in students:
        subj = s["subject"]
        if subj not in best or s["grade"] > best[subj]["grade"]:
            best[subj] = s
    return best

print("Passing:", [s["name"] for s in get_passing(students)])
print("Honor Roll:", get_honor_roll(students))
print("Averages:", average_by_subject(students))
print("Top per subject:", {k: v["name"] for k, v in highest_per_subject(students).items()})
let students = [
    { name: "Alice", grade: 95, subject: "Math" },
    { name: "Bob", grade: 67, subject: "Math" },
    { name: "Charlie", grade: 92, subject: "Science" },
    { name: "Diana", grade: 88, subject: "Science" },
    { name: "Eve", grade: 91, subject: "Math" }
];

const getPassing = (students) =>
    students.filter(s => s.grade >= 70);

const getHonorRoll = (students) =>
    students.filter(s => s.grade >= 90).map(s => s.name).sort();

const averageBySubject = (students) => {
    let groups = students.reduce((acc, s) => {
        (acc[s.subject] ??= []).push(s.grade);
        return acc;
    }, {});
    return Object.fromEntries(
        Object.entries(groups).map(([subj, grades]) =>
            [subj, Math.round(grades.reduce((a,b) => a+b,0) / grades.length * 10) / 10])
    );
};

const highestPerSubject = (students) =>
    students.reduce((acc, s) => {
        if (!acc[s.subject] || s.grade > acc[s.subject].grade)
            acc[s.subject] = s;
        return acc;
    }, {});

console.log("Passing:", getPassing(students).map(s => s.name));
console.log("Honor Roll:", getHonorRoll(students));
console.log("Averages:", averageBySubject(students));
console.log("Top:", highestPerSubject(students));
var students = new List<(string Name, int Grade, string Subject)>
{
    ("Alice", 95, "Math"), ("Bob", 67, "Math"),
    ("Charlie", 92, "Science"), ("Diana", 88, "Science"),
    ("Eve", 91, "Math")
};

var passing = students.Where(s => s.Grade >= 70).ToList();
var honorRoll = students.Where(s => s.Grade >= 90)
    .Select(s => s.Name).OrderBy(n => n).ToList();
var avgBySubj = students.GroupBy(s => s.Subject)
    .ToDictionary(g => g.Key, g => Math.Round(g.Average(s => s.Grade), 1));
var topPerSubj = students.GroupBy(s => s.Subject)
    .ToDictionary(g => g.Key, g => g.OrderByDescending(s => s.Grade).First());

Console.WriteLine($"Passing: {string.Join(", ", passing.Select(s => s.Name))}");
Console.WriteLine($"Honor Roll: {string.Join(", ", honorRoll)}");
foreach (var (subj, avg) in avgBySubj)
    Console.WriteLine($"{subj} avg: {avg}");
foreach (var (subj, top) in topPerSubj)
    Console.WriteLine($"{subj} top: {top.Name} ({top.Grade})");
🎓 Instructor Note: Delivery Guidance

Exercise 1 is a gentle warm-up — convert, filter, average. Exercise 2 is the full capstone that ties together everything from Module 5: lists, dicts, filter, map, reduce, grouping, and chaining. Walk through the JavaScript .reduce() grouping pattern carefully — it's the hardest concept here. Note how C# LINQ shines for this kind of data processing with GroupBy and Average. Challenge fast students: add a generate_report(students) that produces a formatted multi-line string summary.

Summary

🎉 Key Takeaways

  • Basic loops: Python for...in, JS for...of (values) / for...in (keys), C# foreach
  • Enumerate: Python enumerate(), JS .forEach((val, i)) or .entries(), C# classic for
  • Map transforms every element: Python comprehension, JS .map(), C# .Select()
  • Filter keeps elements that pass: Python comprehension with if, JS .filter(), C# .Where()
  • Reduce accumulates a single result: Python reduce(), JS .reduce(), C# .Aggregate()
  • Any/All: Python any()/all(), JS .some()/.every(), C# .Any()/.All()
  • Zip pairs two collections: Python zip(), JS .map() with index, C# .Zip()
  • Chaining builds readable data pipelines: filter → transform → sort → extract

🚀 What's Next?

That wraps up Module 5: Collections! You've learned to store, access, transform, and process data with lists and dictionaries. In Module 6, we step into Object-Oriented Programming — modeling real-world concepts with classes, objects, and methods.

🎯 Quick Check

Question 1: In JavaScript, which array method transforms every element into a new value?

Question 2: What does Python's any([False, False, True, False]) return?

Question 3: Which LINQ method is C#'s equivalent of JavaScript's .filter()?