Welcome to Lesson 17! In this lesson, you will learn four powerful and practical Python topics that every programmer uses regularly:
- Iterators · How Python moves through data, one item at a time
- Modules · How to organise and reuse code across files
- Dates and Times · How to work with real-world time using Python
- Math · How to do advanced calculations using Python's built-in tools
These four topics connect naturally. You use iterators to walk through data, modules to import useful tools (like datetime and math), dates to track when things happen, and math to calculate important values in science, finance, and engineering.
By the end of this lesson you will be able to write a mini program that tracks student submissions, shows the current date, and calculates statistics · all from scratch.
Before we begin, make sure you are comfortable with these ideas:
- Variables · storing values like
x = 5 - Lists and Tuples · collections of items like
["apple", "banana"] - For loops · repeating actions with
for x in something: - Functions · blocks of reusable code created with
def - Classes · blueprints for objects (you will see them in the Iterator section)
If any of those feel unfamiliar, review your earlier lessons briefly before continuing.
What is an Iterator?
Think of an iterator like a book with a bookmark. The bookmark remembers which page you are on. Every time you want to read, you open to the bookmark, read one page, and move the bookmark forward. You never have to remember where you are · the bookmark does that for you.
In Python, an iterator is an object that remembers where you are in a collection of items. Each time you ask for the "next" item, it gives you one and moves forward automatically.
Why does this exist? Sometimes you have a very large amount of data and you cannot (or do not want to) load it all at once. An iterator lets you process one item at a time without needing to see the full list upfront. This saves memory and is faster for large data.
Two Important Words: Iterable vs Iterator
These sound similar but mean different things.
| Word | What it means | Examples |
|---|---|---|
| Iterable | An object you CAN loop through | list, tuple, dict, set, string |
| Iterator | An object that IS actively stepping through another object | Created by calling iter() on an iterable |
Analogy: A list is like a full bag of fruit. An iterator is the hand reaching into the bag and pulling out one fruit at a time.
The iter() and next() Functions
To turn an iterable into an iterator, you use iter(). To get the next item from an iterator, you use next().
Very simple example:
mytuple = ("apple", "banana", "cherry") # This is an iterable (a tuple)
myit = iter(mytuple) # This creates an iterator from it
print(next(myit)) # Step 1: get the first item
print(next(myit)) # Step 2: get the second item
print(next(myit)) # Step 3: get the third itemExpected Output:
apple
banana
cherryLine by line explanation:
mytuple = ("apple", "banana", "cherry")· creates a tuple with three fruitsmyit = iter(mytuple)· creates an iterator; it is now "pointing at" appleprint(next(myit))· asks the iterator: "what's the next item?" It returnsappleand moves the pointer tobananaprint(next(myit))· returnsbanana, pointer moves tocherryprint(next(myit))· returnscherry, pointer moves past the end
💡 Think about it: What would happen if you called
next(myit)a fourth time? There are no more items · Python would raise an error calledStopIteration. You will learn how to handle that shortly.
Strings Are Iterable Too
A string is just a sequence of characters. So strings are also iterable:
mystr = "banana"
myit = iter(mystr)
print(next(myit)) # b
print(next(myit)) # a
print(next(myit)) # n
print(next(myit)) # a
print(next(myit)) # n
print(next(myit)) # aExpected Output:
b
a
n
a
n
aEach call to next() returns one character, in order.
Looping Through an Iterator
You already know the for loop. Here is something interesting: when you use a for loop on a list, tuple, string, or other iterable, Python is secretly using an iterator under the hood!
mytuple = ("apple", "banana", "cherry")
for x in mytuple:
print(x)Expected Output:
apple
banana
cherryPython automatically calls iter() to create an iterator and then calls next() on every loop turn. The for loop knows when to stop because the iterator signals "done" by raising StopIteration.
Same thing with a string:
mystr = "banana"
for x in mystr:
print(x)Expected Output:
b
a
n
a
n
a💡 Real-world connection: Every time you write
for item in my_list:, Python is using the iterator protocol. Understanding this helps you understand how loops actually work inside Python.
Creating Your Own Iterator
Python lets you build your own custom iterator by creating a class with two special methods:
__iter__()· sets up the iterator (runs once wheniter()is called)__next__()· returns the next value (runs every timenext()is called)
What is a class? Think of a class as a blueprint. It defines what an object should do. __iter__ and __next__ are special "dunder" (double underscore) methods that Python calls automatically.
Simple custom iterator · counts upward from 1:
class MyNumbers:
def __iter__(self):
self.a = 1 # Start counting from 1
return self # Must return the iterator object itself
def __next__(self):
x = self.a # Save the current value
self.a += 1 # Move to the next number (a += 1 means a = a + 1)
return x # Return the current value
myclass = MyNumbers() # Create an instance of the class
myiter = iter(myclass) # Call __iter__, which sets self.a = 1
print(next(myiter)) # Calls __next__, returns 1, sets self.a = 2
print(next(myiter)) # Returns 2, sets self.a = 3
print(next(myiter)) # Returns 3, sets self.a = 4
print(next(myiter)) # Returns 4
print(next(myiter)) # Returns 5Expected Output:
1
2
3
4
5Line by line breakdown of __next__:
x = self.a· saves the current count (e.g., 1) intoxself.a += 1· increments the count for next time (now it will be 2)return x· sends back the saved value (1)
💡 What if you change
self.a = 1toself.a = 5? The sequence would start at 5, 6, 7, … Try it!
StopIteration · How to Stop Your Iterator
The example above would count forever if you kept calling next(). In real programs, that would be a problem. Python has a solution: you raise a special signal called StopIteration when the sequence should end.
Example · count from 1 to 20 only:
class MyNumbers:
def __iter__(self):
self.a = 1
return self
def __next__(self):
if self.a <= 20: # As long as the count is 20 or less...
x = self.a
self.a += 1
return x
else:
raise StopIteration # Signal: no more items!
myclass = MyNumbers()
myiter = iter(myclass)
for x in myiter: # for loop automatically stops at StopIteration
print(x)Expected Output:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20What raise StopIteration does:
raiseis Python's keyword for signalling an error or eventStopIterationis the specific signal that tells aforloop "we are done"- The
forloop catches this signal silently and stops · you never see an error
💡 Common beginner mistake: Forgetting to include the
raise StopIterationand having an infinite loop. Always include a stopping condition!
Guided Practice · Part 1: Iterators
Exercise 1A · Iterate manually
Objective: Practice using iter() and next() on a list.
Scenario: A school stores student names in a list. Print the first three names manually using next().
Steps:
- Create a list:
students = ["Amaka", "Chidi", "Emeka", "Ngozi", "Yemi"] - Create an iterator from it using
iter() - Print the first three names using three separate
next()calls
Expected Output:
Amaka
Chidi
EmekaSolution:
students = ["Amaka", "Chidi", "Emeka", "Ngozi", "Yemi"]
myit = iter(students)
print(next(myit)) # Amaka
print(next(myit)) # Chidi
print(next(myit)) # EmekaSelf-check questions:
- What would the 4th
next()call return? - What happens after the 5th
next()call?
Exercise 1B · Custom iterator for even numbers
Objective: Build a custom iterator that generates the first N even numbers.
Scenario: You need even numbers for a data-processing task.
class EvenNumbers:
def __iter__(self):
self.n = 0 # Start at 0 (first even number)
return self
def __next__(self):
if self.n <= 20: # Stop after 20
x = self.n
self.n += 2 # Move to next even number
return x
else:
raise StopIteration
myevens = EvenNumbers()
myiter = iter(myevens)
for x in myiter:
print(x)Expected Output:
0
2
4
6
8
10
12
14
16
18
20What-if challenge: What if you changed self.n += 2 to self.n += 3? What sequence would you get?
What is a Module?
Think of a module like a toolbox. When you are building something, you do not carry every tool you own in your hands · you keep them in a toolbox and pick out what you need. A Python module is a file of pre-written code (functions, variables, classes) that you can "pick up" and use in your own program.
Why do modules exist?
- To organise code: instead of one giant messy file, split code into neat files
- To reuse code: write a function once in a module, use it in many programs
- To share code: Python has thousands of ready-made modules others have built
A module is simply a Python file saved with the .py extension. Any .py file can be a module.
Creating a Module
To create a module, write your Python code in a file and save it as something.py.
Create a file called mymodule.py:
# mymodule.py
def greeting(name):
print("Hello, " + name)That is it! You have a module. It contains one function called greeting.
Using (Importing) a Module
To use a module in another file, you use the import keyword.
In a different file (e.g., main.py):
import mymodule
mymodule.greeting("Jonathan")Expected Output:
Hello, JonathanBreaking it down:
import mymodule· Python findsmymodule.pyand loads itmymodule.greeting("Jonathan")· accesses thegreetingfunction insidemymodule- The syntax is always:
module_name.function_name()
⚠️ Important rule: When calling a function from a module, you must use the format
module_name.function_name(). Forgetting themodule_name.part is a very common beginner mistake.
Variables in a Module
Modules can contain more than functions. They can hold variables of any type · strings, numbers, lists, dictionaries, and so on.
In mymodule.py, add a dictionary:
# mymodule.py
def greeting(name):
print("Hello, " + name)
person1 = {
"name": "John",
"age": 36,
"country": "Norway"
}In main.py, access the dictionary:
import mymodule
a = mymodule.person1["age"]
print(a)Expected Output:
36What happened:
mymodule.person1accesses the dictionary calledperson1inside the module["age"]gets the value associated with the"age"key- The result
36is stored inaand printed
Renaming a Module (Aliases)
Sometimes module names are long and you want a shorter name. Use the as keyword to create an alias (a nickname).
import mymodule as mx
a = mx.person1["age"]
print(a)Expected Output:
36Instead of typing mymodule every time, you type mx. This is especially useful with very common modules like NumPy (usually imported as np) or Pandas (usually imported as pd).
Built-in Modules
Python comes with dozens of modules already installed. You do not need to create them · just import them. Examples include platform, math, datetime, random, os, and more.
Example · using the platform module to check your operating system:
import platform
x = platform.system()
print(x)Expected Output (depends on your computer):
Windowsor
Linuxor
Darwin(Darwin = macOS)
The dir() Function · What's Inside a Module?
If you ever want to see all the functions and variables inside a module, use the dir() function. It returns a list of everything the module contains.
import platform
x = dir(platform)
print(x)Expected Output (shortened):
['DEV_NULL', '_UNIXCONFDIR', '__builtins__', ..., 'architecture', 'java_ver', 'machine', 'node', 'platform', 'processor', 'python_branch', 'python_build', ...]💡 Real-world use: When you discover a new module and want to explore it,
dir(module_name)is your quick guide to what tools it provides.
Importing Only Parts of a Module
Sometimes you only need one specific function or variable from a module. You can import just that one thing using the from keyword:
from mymodule import person1
print(person1["age"])Expected Output:
36Key difference: When using from module import something, you do NOT use the module name prefix. You just write person1["age"], not mymodule.person1["age"].
Two styles compared:
# Style 1: import the whole module
import mymodule
print(mymodule.person1["age"]) # must use prefix
# Style 2: import only what you need
from mymodule import person1
print(person1["age"]) # no prefix neededBoth produce the same result: 36.
⚠️ Common beginner mistake: Using
from mymodule import person1and then writingmymodule.person1["age"]· this causes aNameErrorbecause you did not import the full module, only the specific item.
Guided Practice · Part 2: Modules
Exercise 2A · Create and use a simple module
Objective: Create a module with useful school functions.
Step 1: Create a file called school.py:
# school.py
school_name = "Sunrise Academy"
total_students = 450
def welcome(student_name):
print("Welcome to " + school_name + ", " + student_name + "!")
def average(scores):
return sum(scores) / len(scores)Step 2: In your main file, use the module:
import school
school.welcome("Amaka")
scores = [78, 85, 92, 70, 88]
avg = school.average(scores)
print("Class average:", avg)
print("Total students:", school.total_students)Expected Output:
Welcome to Sunrise Academy, Amaka!
Class average: 82.6
Total students: 450Self-check: What would from school import welcome change about how you call the function?
Why Doesn't Python Have a Built-in Date Type?
Unlike numbers or strings, a date is not one of Python's built-in data types. Instead, Python gives you a module called datetime that you can import to work with dates and times.
This connects directly to what you just learned · datetime is a module, and you must import it before using it.
Importing and Using datetime
import datetime
x = datetime.datetime.now()
print(x)Expected Output (will match your current date and time):
2025-07-15 14:32:45.678901Breaking it down:
import datetime· loads the datetime moduledatetime.datetime· inside thedatetimemodule, there is a class also calleddatetime.now()· calls thenow()method, which captures the current date and time at this exact moment- The output format is:
YYYY-MM-DD HH:MM:SS.microseconds
The date output contains: year, month, day, hour, minute, second, and microsecond.
Accessing Individual Date Parts
A datetime object contains many parts. You can access them individually:
import datetime
x = datetime.datetime.now()
print(x.year) # just the year number
print(x.strftime("%A")) # full weekday nameExpected Output (example):
2025
Tuesdayx.year· accesses the year attribute directlyx.strftime("%A")· formats the date;%Ameans "full weekday name"
Creating a Specific Date Object
You do not have to use "today's date." You can create any date you want using datetime.datetime(year, month, day):
import datetime
x = datetime.datetime(2020, 5, 17)
print(x)Expected Output:
2020-05-17 00:00:00Parameters explained:
2020· the year5· the month (May)17· the day- The time defaults to
00:00:00(midnight) when not specified
You can also pass optional time parameters: datetime.datetime(year, month, day, hour, minute, second, microsecond, timezone). But for most uses, just year/month/day is enough.
The strftime() Method · Formatting Dates as Text
strftime stands for "string from time." It converts a date object into a human-readable string using format codes.
import datetime
x = datetime.datetime(2018, 6, 1)
print(x.strftime("%B"))Expected Output:
June%B means "full month name."
Complete format codes reference:
| Code | What it gives you | Example |
|---|---|---|
%a | Weekday, short | Wed |
%A | Weekday, full | Wednesday |
%w | Weekday as number (0=Sunday) | 3 |
%d | Day of month (01 · 31) | 31 |
%b | Month name, short | Dec |
%B | Month name, full | December |
%m | Month as number (01 · 12) | 12 |
%y | Year, 2-digit | 18 |
%Y | Year, 4-digit | 2018 |
%H | Hour (00 · 23) | 17 |
%I | Hour (00 · 12) | 05 |
%p | AM or PM | PM |
%M | Minute (00 · 59) | 41 |
%S | Second (00 · 59) | 08 |
%f | Microsecond | 548513 |
%j | Day number of the year (001 · 366) | 365 |
%U | Week number of year (Sunday first) | 52 |
%W | Week number of year (Monday first) | 52 |
%c | Local full date and time | Mon Dec 31 17:41:00 2018 |
%x | Local date | 12/31/18 |
%X | Local time | 17:41:00 |
Multiple Format Codes Together
You can combine many format codes to build any date string you want:
import datetime
x = datetime.datetime(2024, 3, 14)
# Format: "Day, DD Month YYYY"
print(x.strftime("%A, %d %B %Y"))
# Format: "MM/DD/YYYY"
print(x.strftime("%m/%d/%Y"))
# Format: "Year: YYYY | Month: MM | Day: DD"
print(x.strftime("Year: %Y | Month: %m | Day: %d"))Expected Output:
Thursday, 14 March 2024
03/14/2024
Year: 2024 | Month: 03 | Day: 14💡 Real-world use: In business systems, you often need to display dates in different formats for different regions.
strftime()is how you do that.
Guided Practice · Part 3: Dates
Exercise 3A · Birthday formatter
Objective: Create a date for a birthday and print it in three different formats.
Scenario: You are building a school records system. Format a student's birthday in three ways.
import datetime
birthday = datetime.datetime(2005, 8, 22)
# Format 1: Full written date
print(birthday.strftime("%A, %d %B %Y"))
# Format 2: Short numeric
print(birthday.strftime("%m/%d/%Y"))
# Format 3: Just the year and month
print(birthday.strftime("%B %Y"))Expected Output:
Monday, 22 August 2005
08/22/2005
August 2005Exercise 3B · Today's date display
import datetime
today = datetime.datetime.now()
print("Today is:", today.strftime("%A, %d %B %Y"))
print("Current time:", today.strftime("%H:%M:%S"))
print("Day of year:", today.strftime("%j"))Expected Output (example · will match your current date):
Today is: Tuesday, 15 July 2025
Current time: 09:45:22
Day of year: 196Self-check questions:
- What format code gives just the day name?
- How would you display only the hour and minute (like
09:45)?
Two Ways to Do Math in Python
Python gives you two levels of math tools:
- Built-in functions · always available, no import needed (
min(),max(),abs(),pow()) - The
mathmodule · powerful functions you must import first (math.sqrt(),math.ceil(),math.floor(),math.pi)
Built-in Math Functions
These functions are part of Python itself · no import needed.
min() and max() · find the lowest or highest value:
x = min(5, 10, 25)
y = max(5, 10, 25)
print(x)
print(y)Expected Output:
5
25min(5, 10, 25)· compares the three values and returns the smallest: 5max(5, 10, 25)· returns the largest: 25
They also work on lists:
scores = [72, 88, 65, 94, 51]
print(min(scores)) # 51
print(max(scores)) # 94abs() · absolute value (always positive):
The absolute value of a number is its distance from zero · always positive.
x = abs(-7.25)
print(x)Expected Output:
7.25-7.25 becomes 7.25. Think of it as "remove the minus sign."
Real-world use: In finance, you might have a balance of -500 (debt). abs(-500) gives you 500 · the amount owed without the sign.
debt = -15000
print("Amount owed:", abs(debt))Output:
Amount owed: 15000pow(x, y) · raise x to the power of y:
x = pow(4, 3) # same as 4 × 4 × 4
print(x)Expected Output:
64pow(4, 3)means 4³ = 4 × 4 × 4 = 64
More examples:
print(pow(2, 10)) # 2^10 = 1024
print(pow(5, 2)) # 5^2 = 25
print(pow(9, 0.5)) # 9^0.5 = square root of 9 = 3.0Expected Output:
1024
25
3.0💡 Real-world use:
pow()is used in compound interest formulas, physics (energy calculations), and computer science (hash functions, encryption).
The math Module
For more advanced mathematical operations, Python includes a dedicated math module.
import mathOnce imported, you have access to dozens of functions and constants.
math.sqrt() · square root:
The square root of a number is what you multiply by itself to get that number. For example, the square root of 64 is 8 (because 8 × 8 = 64).
import math
x = math.sqrt(64)
print(x)Expected Output:
8.0Note: sqrt() always returns a float (a decimal number).
More examples:
import math
print(math.sqrt(25)) # 5.0
print(math.sqrt(100)) # 10.0
print(math.sqrt(2)) # 1.4142135623730951💡 Real-world use: In physics, calculating the length of a diagonal (Pythagoras theorem):
c = math.sqrt(a2 + b2). In statistics, the standard deviation formula involves square roots.
math.ceil() · round UP to the nearest whole number:
ceil stands for "ceiling." Just like a ceiling is always above you, ceil() always goes upward.
import math
x = math.ceil(1.4) # 1.4 rounds UP to 2
y = math.ceil(7.001) # 7.001 rounds UP to 8
z = math.ceil(-1.4) # -1.4 rounds UP to -1
print(x) # 2
print(y) # 8
print(z) # -1Expected Output:
2
8
-1Real-world use: If you need 7.5 buses to transport students, you must order 8 buses · you cannot have half a bus. math.ceil(7.5) gives you 8.
math.floor() · round DOWN to the nearest whole number:
floor is the opposite. Just like a floor is always below you, floor() always goes downward.
import math
x = math.floor(1.4) # 1.4 rounds DOWN to 1
y = math.floor(7.999) # 7.999 rounds DOWN to 7
z = math.floor(-1.4) # -1.4 rounds DOWN to -2
print(x) # 1
print(y) # 7
print(z) # -2Expected Output:
1
7
-2Comparison of rounding functions:
| Value | round() | math.ceil() | math.floor() |
|---|---|---|---|
| 1.4 | 1 | 2 | 1 |
| 1.5 | 2 | 2 | 1 |
| 1.9 | 2 | 2 | 1 |
| -1.4 | -1 | -1 | -2 |
math.pi · the constant π (Pi):
Pi is the ratio of a circle's circumference to its diameter. It is approximately 3.14159... and goes on forever.
import math
x = math.pi
print(x)Expected Output:
3.141592653589793Real-world use · calculate the area of a circle:
import math
radius = 7
area = math.pi * radius ** 2 # Area = π × r²
print("Area of circle:", area)Expected Output:
Area of circle: 153.93804002589985Second Example Set · Reinforcing Math Concepts
Example: Physics · distance calculation with sqrt:
import math
# How far is a point (x=3, y=4) from the origin (0,0)?
# Pythagoras: distance = sqrt(x^2 + y^2)
x = 3
y = 4
distance = math.sqrt(x**2 + y**2)
print("Distance:", distance)Expected Output:
Distance: 5.0Example: Budgeting · ceil for packaging:
import math
items = 55
items_per_box = 12
boxes_needed = math.ceil(items / items_per_box)
print("Boxes needed:", boxes_needed)Expected Output:
Boxes needed: 5(55 ÷ 12 = 4.58… → round up → 5 boxes)
Guided Practice · Part 4: Math
Exercise 4A · Score analysis
Objective: Use built-in and math functions together on student scores.
Scenario: A teacher has a list of test scores and wants to analyze them.
import math
scores = [72, 85, 91, 63, 78, 88, 55, 94, 70, 82]
print("Highest score:", max(scores))
print("Lowest score:", min(scores))
print("Average:", sum(scores) / len(scores))
# How many boxes of certificates (5 per box) are needed?
boxes = math.ceil(len(scores) / 5)
print("Certificate boxes needed:", boxes)
# Variance-like: sqrt of average squared difference from 80
diffs_sq = [(s - 80)**2 for s in scores]
spread = math.sqrt(sum(diffs_sq) / len(diffs_sq))
print("Spread from 80:", round(spread, 2))Expected Output:
Highest score: 94
Lowest score: 55
Average: 77.8
Certificate boxes needed: 2
Spread from 80: 12.47Mistake 1 · Calling next() too many times
# Wrong — no StopIteration guard
mytuple = ("a", "b")
myit = iter(mytuple)
print(next(myit)) # a
print(next(myit)) # b
print(next(myit)) # ERROR: StopIterationFix: Use a for loop (it stops automatically) or wrap in a try/except:
mytuple = ("a", "b")
myit = iter(mytuple)
try:
while True:
print(next(myit))
except StopIteration:
print("Done!")Mistake 2 · Forgetting raise StopIteration in a custom iterator
# Wrong — infinite loop!
class Counter:
def __iter__(self):
self.n = 1
return self
def __next__(self):
x = self.n
self.n += 1
return x # ← no stopping condition!Fix: Always add a stopping condition:
def __next__(self):
if self.n <= 10:
x = self.n
self.n += 1
return x
else:
raise StopIterationMistake 3 · Using the module name after from ... import
# Wrong
from mymodule import greeting
mymodule.greeting("Amaka") # NameError: mymodule is not defined
# Correct
from mymodule import greeting
greeting("Amaka") # No prefix neededMistake 4 · Forgetting to import datetime or math
# Wrong
x = datetime.datetime.now() # NameError: datetime is not defined
# Correct
import datetime
x = datetime.datetime.now()Mistake 5 · Using datetime() directly instead of datetime.datetime()
# Wrong
import datetime
x = datetime(2020, 5, 17) # TypeError
# Correct
import datetime
x = datetime.datetime(2020, 5, 17) # datetime module → datetime class → constructorOr use a shortcut:
from datetime import datetime
x = datetime(2020, 5, 17) # Now you can use it directlyMistake 6 · Confusing math.ceil and math.floor
A quick reminder:
ceil= ceiling = always goes UPfloor= floor = always goes DOWN
import math
print(math.ceil(4.1)) # 5 (goes UP)
print(math.floor(4.9)) # 4 (goes DOWN)Mistake 7 · Forgetting import math before using math.sqrt
# Wrong
x = math.sqrt(16) # NameError
# Correct
import math
x = math.sqrt(16) # 4.0Now you will combine all four topics: iterators, modules, dates, and math.
Project Goal: Build a simple student submission tracker that:
- Stores student names and scores
- Iterates through them with a custom iterator
- Shows the current date of the report
- Calculates math statistics
Stage 1 · Setup: Create the data module
Create a file called student_data.py:
# student_data.py
students = ["Amaka", "Chidi", "Emeka", "Ngozi", "Yemi", "Bola", "Tunde"]
scores = {
"Amaka": 85,
"Chidi": 72,
"Emeka": 90,
"Ngozi": 68,
"Yemi": 88,
"Bola": 75,
"Tunde": 94
}
passing_score = 70Lesson 17 complete! 🎉
You covered:
- ✅ Lesson Introduction
- ✅ Prerequisite Concepts
- ✅ Part 1 · Python Iterators
- ✅ Part 2 · Python Modules
- ✅ Part 3 · Python Dates
- ✅ Part 4 · Python Math
- ✅ Common Beginner Mistakes