Lecture 1 / 12
Lecture 01 · Fundamentals

Introduction to Python & Setup

Beginner ~45 min No prerequisites

What is Python?

Python is a high-level, interpreted, general-purpose programming language created by Guido van Rossum and first released in 1991. It emphasizes code readability and simplicity, using significant whitespace (indentation) to define code blocks instead of curly braces.

Python is one of the most widely used programming languages in the world, powering everything from web backends and data science pipelines to AI research and automation scripts.

Why Python?

Installing Python

Download the latest version (3.x) from python.org. As of 2026, Python 3.12+ is recommended.

ℹ️ Version Note
Always install Python 3. Python 2 reached end-of-life in 2020 and should not be used for new projects.

Windows

Download the installer from python.org and check "Add Python to PATH" during installation. Verify in Command Prompt:

cmd / terminal
python --version
# Expected: Python 3.12.x

macOS / Linux

macOS comes with Python 2 by default. Use Homebrew to install Python 3:

terminal
brew install python3
python3 --version

Your First Python Program

Open any text editor (VS Code is recommended) and create a file called hello.py:

hello.py
# This is a comment — Python ignores it
print("Hello, World!")
print("Welcome to Python Mastery!")
Output
Hello, World!
Welcome to Python Mastery!

The Python Interactive Shell (REPL)

Python includes an interactive shell where you type code and see immediate results. Type python (or python3) in your terminal:

Python REPL
>>> print("Hello")
Hello
>>> 2 + 3
5
>>> 10 / 3
3.3333333333333335
>>> exit()   # exits the REPL

Using VS Code (Recommended IDE)

VS Code is a free, powerful editor perfect for Python development. Install the Python extension by Microsoft from the Extensions panel. It provides:

Understanding Python Syntax Basics

Indentation is mandatory

Python uses indentation (4 spaces per level by convention) instead of curly braces to define code structure:

indentation.py
if True:
    print("This is indented — it's inside the if block")
    print("So is this")
print("This is outside — runs regardless")

Comments

comments.py
# Single-line comment — Python ignores this

"""
Multi-line string used as a docstring/comment block.
Often used to document functions and classes.
"""

x = 5  # Inline comment
✅ Best Practice
Write comments to explain why, not what. Code explains what it does; comments explain the reasoning.

How Python Runs Code

Python is an interpreted language. When you run python script.py, the Python interpreter reads your source code line by line, converts it to bytecode (.pyc files in __pycache__), and executes it via the Python Virtual Machine (PVM).

🎯 Exercise 1.1

Create a file called intro.py. Write a program that prints your name, your age, and your country on three separate lines. Run it from the terminal.

🎯 Exercise 1.2

Open the Python REPL and compute: what is 17 multiplied by 23? What is 2 to the power of 10? Use Python as a calculator.

Lecture 02 · Fundamentals

Variables & Data Types

Beginner ~50 min Requires: Lecture 01

Variables

A variable is a named reference to a value stored in memory. In Python, you create variables simply by assigning a value — no type declaration needed.

variables.py
name = "Alice"
age = 25
height = 5.6
is_student = True

print(name, age, height, is_student)
Output
Alice 25 5.6 True

Naming Rules

✅ Convention
Use snake_case for variables and functions: user_name, total_price. Use UPPER_CASE for constants: MAX_SIZE = 100.

Core Data Types

Type Example Description
int 42, -7, 0 Whole numbers (unlimited size)
float 3.14, -0.5 Decimal / floating point numbers
str "hello", 'world' Text (sequence of characters)
bool True, False Boolean (logical True/False)
NoneType None Represents the absence of a value

Integers

integers.py
x = 42
y = -100
big = 1_000_000   # underscores for readability
binary = 0b1010   # binary literal → 10
hexval = 0xFF    # hexadecimal → 255

print(type(x))   # <class 'int'>
print(big)        # 1000000

Floats

floats.py
pi = 3.14159
sci = 1.5e10     # scientific notation: 15000000000.0
neg = -2.7

print(type(pi))  # <class 'float'>

# Floating point precision caveat:
print(0.1 + 0.2)   # 0.30000000000000004 (IEEE 754)
⚠️ Float Precision
Floats have limited precision due to binary representation. For exact decimal arithmetic (e.g., money), use Python's decimal module.

Strings

strings.py
s1 = "Hello, World!"
s2 = 'Single quotes work too'
s3 = """Triple-quoted strings
span multiple lines"""

# Escape sequences
s4 = "Line 1\nLine 2"     # \n = newline
s5 = "Tab\there"         # \t = tab
s6 = "Quote: \"hi\""      # \" = literal quote

# f-strings (Python 3.6+) — the preferred way
name = "Bob"
age = 30
msg = f"My name is {name} and I am {age} years old"
print(msg)
Output
My name is Bob and I am 30 years old

Booleans

booleans.py
t = True
f = False

print(type(t))   # <class 'bool'>
print(True + True)  # 2 — bools are ints (True=1, False=0)

# Truthiness — these evaluate as False:
# 0, 0.0, "", [], {}, None
print(bool(0))    # False
print(bool(""))   # False
print(bool(42))   # True

Type Conversion

conversion.py
# Explicit conversion (casting)
x = int("42")        # str → int: 42
y = float("3.14")    # str → float: 3.14
z = str(100)          # int → str: "100"
b = bool(1)           # int → bool: True

# Getting user input (always returns str)
name = input("Enter your name: ")
age  = int(input("Enter your age: "))  # must cast!
print(f"Hello {name}, you are {age}")

Dynamic Typing & type()

Python is dynamically typed — the same variable can hold different types over its lifetime:

dynamic.py
x = 10
print(type(x))   # <class 'int'>
x = "hello"
print(type(x))   # <class 'str'>
x = [1, 2, 3]
print(type(x))   # <class 'list'>
🎯 Exercise 2.1

Write a program that asks the user for their name and birth year, then calculates and prints their approximate age with an f-string.

🎯 Exercise 2.2

Create variables of each core type (int, float, str, bool, None). Use type() to print each one's type.

Lecture 03 · Fundamentals

Operators & Expressions

Beginner ~45 min Requires: Lecture 02

Arithmetic Operators

arithmetic.py
a, b = 17, 5

print(a + b)    # Addition:       22
print(a - b)    # Subtraction:    12
print(a * b)    # Multiplication: 85
print(a / b)    # Division:       3.4   (always float!)
print(a // b)   # Floor division: 3     (truncates decimal)
print(a % b)    # Modulo:         2     (remainder)
print(a ** b)   # Exponentiation: 1419857

Comparison Operators

Comparison operators always return a boolean (True or False).

comparison.py
x = 10
print(x == 10)   # Equal:              True
print(x != 5)    # Not equal:          True
print(x > 8)     # Greater than:       True
print(x < 8)     # Less than:          False
print(x >= 10)   # Greater or equal:   True
print(x <= 9)    # Less or equal:      False

# Chained comparisons (Pythonic)
age = 20
print(18 <= age <= 65)    # True — very readable!

Logical Operators

logical.py
a = True
b = False

print(a and b)   # False — both must be True
print(a or b)    # True  — at least one True
print(not a)     # False — flips the boolean

# Short-circuit evaluation
x = 0
result = x or "default"
print(result)    # "default" — x is falsy, so "or" returns right side

name = "Alice"
greeting = name and f"Hello, {name}"
print(greeting)  # "Hello, Alice" — name is truthy

Assignment Operators

assignment.py
x = 10
x += 5    # x = x + 5  → 15
x -= 3    # x = x - 3  → 12
x *= 2    # x = x * 2  → 24
x //= 4   # x = x // 4 → 6
x **= 2   # x = x ** 2 → 36
print(x)  # 36

# Multiple assignment
a, b, c = 1, 2, 3
x = y = z = 0   # all set to 0

# Swap variables (Pythonic!)
a, b = b, a

Identity & Membership Operators

identity.py
# Identity: is / is not (checks memory identity)
x = [1, 2, 3]
y = x
z = [1, 2, 3]

print(x is y)      # True  — same object in memory
print(x is z)      # False — same values, different objects
print(x == z)      # True  — same values

# Membership: in / not in
fruits = ["apple", "banana", "cherry"]
print("banana" in fruits)     # True
print("grape" not in fruits)  # True

Operator Precedence

Operators follow a defined precedence order (highest first): ** → unary -/+* / // %+ - → comparisons → notandor. Use parentheses to be explicit:

precedence.py
print(2 + 3 * 4)      # 14 (multiplication first)
print((2 + 3) * 4)    # 20 (parentheses override)
print(2 ** 3 ** 2)    # 512 (** is right-associative: 2**(3**2))
🎯 Exercise 3.1

Write a program that takes a number from the user and determines whether it is even or odd using the modulo operator. Print an appropriate message.

🎯 Exercise 3.2

Calculate the area and circumference of a circle given a radius entered by the user. Use pi = 3.14159.

Lecture 04 · Fundamentals

Control Flow: if / elif / else

Beginner ~50 min Requires: Lecture 03

The if Statement

Conditional statements allow your program to make decisions and execute different code based on conditions.

if_basic.py
temperature = 35

if temperature > 30:
    print("It's hot outside!")
    print("Stay hydrated.")
Output
It's hot outside!
Stay hydrated.

if / else

if_else.py
age = 16

if age >= 18:
    print("You can vote.")
else:
    years_left = 18 - age
    print(f"You need to wait {years_left} more years.")

if / elif / else

grades.py
score = 78

if score >= 90:
    grade = "A"
elif score >= 80:
    grade = "B"
elif score >= 70:
    grade = "C"
elif score >= 60:
    grade = "D"
else:
    grade = "F"

print(f"Score: {score} → Grade: {grade}")
Output
Score: 78 → Grade: C

Nested if Statements

nested.py
username = "admin"
password = "secret123"

if username == "admin":
    if password == "secret123":
        print("Access granted!")
    else:
        print("Wrong password.")
else:
    print("Unknown user.")

Ternary (Conditional Expression)

A one-line if/else for simple assignments:

ternary.py
age = 20
status = "adult" if age >= 18 else "minor"
print(status)   # adult

# Equivalent to:
if age >= 18:
    status = "adult"
else:
    status = "minor"

match / case (Python 3.10+)

Python's structural pattern matching — similar to switch/case in other languages, but far more powerful:

match.py
command = "quit"

match command:
    case "start":
        print("Starting...")
    case "stop" | "quit":
        print("Stopping.")
    case _:
        print("Unknown command")
Output
Stopping.
🎯 Exercise 4.1 — BMI Calculator

Ask the user for their weight (kg) and height (m). Compute BMI = weight / height². Then categorize: Underweight (<18.5), Normal (18.5–24.9), Overweight (25–29.9), Obese (≥30).

🎯 Exercise 4.2 — Leap Year

Ask the user for a year. Determine if it's a leap year. A year is leap if: divisible by 4 AND (not divisible by 100 OR divisible by 400).

Lecture 05 · Fundamentals

Loops: for & while

Beginner ~55 min Requires: Lecture 04

The for Loop

Python's for loop iterates over any iterable (list, string, range, etc.).

for_basic.py
fruits = ["apple", "banana", "cherry"]

for fruit in fruits:
    print(f"I like {fruit}")

# Iterating a string
for char in "Python":
    print(char, end="-")   # P-y-t-h-o-n-

range()

range_demo.py
# range(stop)
for i in range(5):
    print(i, end=" ")   # 0 1 2 3 4

# range(start, stop)
for i in range(1, 6):
    print(i, end=" ")   # 1 2 3 4 5

# range(start, stop, step)
for i in range(0, 20, 5):
    print(i, end=" ")   # 0 5 10 15

# Counting backwards
for i in range(10, 0, -1):
    print(i, end=" ")   # 10 9 8 7 6 5 4 3 2 1

enumerate() and zip()

enumerate_zip.py
# enumerate gives index + value
animals = ["cat", "dog", "bird"]
for i, animal in enumerate(animals, start=1):
    print(f"{i}. {animal}")
# 1. cat  2. dog  3. bird

# zip iterates multiple lists simultaneously
names   = ["Alice", "Bob", "Carol"]
scores  = [95, 87, 92]

for name, score in zip(names, scores):
    print(f"{name}: {score}")

The while Loop

while_basic.py
count = 0
while count < 5:
    print(f"Count: {count}")
    count += 1

# Interactive while loop
while True:
    answer = input("Say 'quit' to exit: ")
    if answer == "quit":
        break
    print(f"You typed: {answer}")

break, continue, pass

loop_control.py
# break — exits the loop immediately
for i in range(10):
    if i == 5:
        break
    print(i, end=" ")  # 0 1 2 3 4

# continue — skips current iteration
for i in range(10):
    if i % 2 == 0:
        continue   # skip even numbers
    print(i, end=" ")  # 1 3 5 7 9

# pass — does nothing, placeholder
for i in range(3):
    pass   # useful for empty loops/stubs

List Comprehensions (Preview)

A concise way to create lists using a for loop in a single line:

comprehension.py
# Standard way
squares = []
for i in range(1, 6):
    squares.append(i ** 2)

# List comprehension — same thing, one line!
squares = [i ** 2 for i in range(1, 6)]
print(squares)   # [1, 4, 9, 16, 25]

# With a filter
evens = [x for x in range(20) if x % 2 == 0]
print(evens)     # [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
🎯 Exercise 5.1 — Multiplication Table

Use nested for loops to print the multiplication table from 1×1 to 10×10.

🎯 Exercise 5.2 — Number Guessing Game

Pick a secret number (e.g., 42). Use a while loop to keep asking the user to guess. Print "Too high", "Too low", or "Correct!" and track how many guesses they took.

Lecture 06 · Core Concepts

Functions

Beginner ~60 min Requires: Lecture 05

Defining and Calling Functions

Functions are reusable blocks of code that perform a specific task. Use the def keyword:

functions.py
def greet():
    """Prints a greeting message."""
    print("Hello! Welcome to Python.")

greet()     # Call the function
greet()     # Can be called many times

Parameters and Arguments

params.py
def greet(name, greeting="Hello"):   # default parameter
    print(f"{greeting}, {name}!")

greet("Alice")               # Hello, Alice!
greet("Bob", "Good morning") # Good morning, Bob!
greet(greeting="Hi", name="Carol")  # keyword args

Return Values

return.py
def add(a, b):
    return a + b

result = add(3, 5)
print(result)   # 8

# Return multiple values (as a tuple)
def min_max(numbers):
    return min(numbers), max(numbers)

lo, hi = min_max([3, 1, 7, 4, 9])
print(f"Min: {lo}, Max: {hi}")   # Min: 1, Max: 9

*args and **kwargs

args_kwargs.py
# *args: variable positional arguments (tuple)
def total(*numbers):
    return sum(numbers)

print(total(1, 2, 3))          # 6
print(total(10, 20, 30, 40))   # 100

# **kwargs: variable keyword arguments (dict)
def describe(**info):
    for key, value in info.items():
        print(f"{key}: {value}")

describe(name="Alice", age=30, city="Paris")

Scope: Local vs Global

scope.py
x = 10   # global variable

def my_func():
    y = 20   # local — only exists inside function
    print(x)  # can READ global
    print(y)

my_func()
# print(y)  # NameError — y doesn't exist here

# To MODIFY a global inside a function:
count = 0
def increment():
    global count
    count += 1

increment()
print(count)   # 1

Lambda Functions

Anonymous one-line functions using the lambda keyword:

lambda.py
# lambda arguments: expression
square = lambda x: x ** 2
print(square(5))   # 25

add = lambda a, b: a + b
print(add(3, 4))   # 7

# Most useful with map(), filter(), sorted()
numbers = [5, 2, 8, 1, 9]
sorted_nums = sorted(numbers, key=lambda x: -x)
print(sorted_nums)   # [9, 8, 5, 2, 1]

Recursion

recursion.py
def factorial(n):
    """Returns n! recursively."""
    if n <= 1:
        return 1
    return n * factorial(n - 1)

print(factorial(5))   # 120 (5*4*3*2*1)
print(factorial(10))  # 3628800
⚠️ Recursion Depth
Python has a default recursion limit of 1000. For very deep recursion, use iteration or sys.setrecursionlimit().
🎯 Exercise 6.1

Write a function is_palindrome(s) that returns True if the string s reads the same forwards and backwards (e.g., "racecar" → True).

🎯 Exercise 6.2

Write a recursive function fibonacci(n) that returns the nth Fibonacci number. Then optimise it with a cache (hint: look up functools.lru_cache).

Lecture 07 · Core Concepts

Data Structures

Intermediate ~70 min Requires: Lecture 06

Lists

Lists are ordered, mutable, allow duplicates. Defined with square brackets.

lists.py
fruits = ["apple", "banana", "cherry"]

# Indexing (0-based, negative counts from end)
print(fruits[0])    # apple
print(fruits[-1])   # cherry

# Slicing [start:stop:step]
print(fruits[0:2])  # ['apple', 'banana']
print(fruits[::-1]) # reversed: ['cherry', 'banana', 'apple']

# Modifying
fruits.append("date")       # add to end
fruits.insert(1, "avocado") # insert at index 1
fruits.remove("banana")    # remove first occurrence
popped = fruits.pop()        # remove and return last item

# Useful methods
nums = [3, 1, 4, 1, 5, 9]
nums.sort()                  # in-place sort
print(sorted(nums))         # returns new sorted list
print(len(nums))            # 6
print(nums.count(1))       # 2

Tuples

Tuples are ordered, immutable. Use parentheses. Great for fixed data like coordinates.

tuples.py
point = (3, 7)
rgb   = (255, 128, 0)

x, y = point         # tuple unpacking
r, g, b = rgb

# Tuples are faster and use less memory than lists
# Use tuples for data that shouldn't change
# point[0] = 5  # TypeError: tuple is immutable

single = (42,)      # single-element tuple needs trailing comma

Dictionaries

Dictionaries store key-value pairs. Keys must be unique and hashable. Python 3.7+ preserves insertion order.

dicts.py
person = {
    "name": "Alice",
    "age": 30,
    "city": "London"
}

# Access
print(person["name"])           # Alice
print(person.get("phone", "N/A")) # N/A (safe get)

# Modify
person["age"] = 31
person["email"] = "alice@example.com"
del person["city"]

# Iterate
for key, value in person.items():
    print(f"{key}: {value}")

# Dictionary comprehension
squares = {x: x**2 for x in range(1, 6)}
# {1: 1, 2: 4, 3: 9, 4: 16, 5: 25}

Sets

Sets are unordered collections of unique elements. Perfect for deduplication and set math.

sets.py
a = {1, 2, 3, 4}
b = {3, 4, 5, 6}

print(a | b)   # Union:        {1, 2, 3, 4, 5, 6}
print(a & b)   # Intersection: {3, 4}
print(a - b)   # Difference:   {1, 2}
print(a ^ b)   # Sym. diff:    {1, 2, 5, 6}

# Deduplicate a list
nums = [1, 2, 2, 3, 3, 3]
unique = list(set(nums))   # [1, 2, 3]

s = set()   # empty set (NOT {} — that's a dict!)
s.add(42)

Choosing the Right Structure

Structure Ordered Mutable Duplicates Use When
list Ordered, changeable collection
tuple Fixed data, dict keys, unpacking
dict keys: ❌ Key-value lookup
set Uniqueness, set operations
🎯 Exercise 7.1 — Word Frequency Counter

Write a program that takes a sentence from the user and prints how many times each word appears, sorted by frequency (most common first).

🎯 Exercise 7.2 — Phone Book

Build a simple phone book using a dictionary. Allow the user to: add a contact, look up a number, delete a contact, and list all contacts.

Lecture 08 · Core Concepts

Strings In Depth

Intermediate ~55 min Requires: Lecture 07

String Indexing & Slicing

str_index.py
s = "Hello, World!"

print(s[0])       # H
print(s[-1])      # !
print(s[7:12])    # World
print(s[::2])     # Hlo ol!
print(s[::-1])    # !dlroW ,olleH  (reversed)
print(len(s))     # 13

Essential String Methods

str_methods.py
s = "  Hello, World!  "

print(s.strip())         # "Hello, World!" (removes whitespace)
print(s.upper())         # "  HELLO, WORLD!  "
print(s.lower())         # "  hello, world!  "
print(s.title())         # "  Hello, World!  "

t = "Hello, World!"
print(t.replace("World", "Python"))  # Hello, Python!
print(t.split(", "))               # ['Hello', 'World!']
print(t.startswith("Hello"))       # True
print(t.endswith("!"))             # True
print(t.find("World"))             # 7
print(t.count("l"))                # 3

words = ["one", "two", "three"]
print(" | ".join(words))            # one | two | three

f-Strings (Formatted String Literals)

fstrings.py
name = "Alice"
score = 98.5678
items = 1000000

# Basic
print(f"Name: {name}")

# Number formatting
print(f"Score: {score:.2f}")     # 2 decimal places
print(f"Items: {items:,}")       # thousands separator
print(f"Score: {score:10.2f}")  # width 10, right-aligned

# Padding and alignment
print(f"{'Left':<10}")           # left-align in 10 chars
print(f"{'Right':>10}")          # right-align
print(f"{'Center':^10}")         # center

# Expressions in f-strings
print(f"2 + 2 = {2 + 2}")
print(f"Upper: {name.upper()}")

Raw Strings & Bytes

raw_bytes.py
# Raw strings: backslash is literal (useful for regex and paths)
path = r"C:\Users\Alice\Documents"
print(path)   # C:\Users\Alice\Documents

# Bytes: prefix b""
b = b"Hello"
print(type(b))        # <class 'bytes'>
print(b.decode())     # "Hello" — bytes to str
print("Hello".encode()) # b'Hello' — str to bytes

Regular Expressions (Intro)

regex.py
import re

text = "My email is alice@example.com and bob@test.org"

# Find all email addresses
emails = re.findall(r'\b[\w.+-]+@[\w-]+\.\w+\b', text)
print(emails)   # ['alice@example.com', 'bob@test.org']

# Check if a string is a valid phone number
phone = "123-456-7890"
match = re.match(r'^\d{3}-\d{3}-\d{4}$', phone)
print(bool(match))   # True
🎯 Exercise 8.1 — Sentence Analyser

Write a function that takes a sentence and returns: word count, character count (no spaces), most common letter, and whether it's a palindrome (ignoring spaces and case).

Lecture 09 · Advanced

Object-Oriented Programming

Intermediate ~75 min Requires: Lecture 08

Classes and Objects

OOP is a programming paradigm that organises code around objects — bundles of data (attributes) and behaviour (methods).

classes.py
class Dog:
    """A simple Dog class."""

    species = "Canis lupus familiaris"   # class variable

    def __init__(self, name, age):     # constructor
        self.name = name               # instance variable
        self.age  = age

    def bark(self):
        print(f"{self.name} says: Woof!")

    def __str__(self):                 # string representation
        return f"Dog({self.name}, {self.age})"

# Creating instances
rex  = Dog("Rex", 3)
luna = Dog("Luna", 5)

rex.bark()          # Rex says: Woof!
print(luna)         # Dog(Luna, 5)
print(rex.species)  # Canis lupus familiaris

Inheritance

inheritance.py
class Animal:
    def __init__(self, name):
        self.name = name

    def speak(self):
        raise NotImplementedError

    def __str__(self):
        return f"{self.__class__.__name__}({self.name})"


class Cat(Animal):   # inherits from Animal
    def speak(self):
        return f"{self.name} says: Meow!"


class Dog(Animal):
    def __init__(self, name, breed):
        super().__init__(name)   # call parent __init__
        self.breed = breed

    def speak(self):
        return f"{self.name} ({self.breed}) says: Woof!"


animals = [Cat("Whiskers"), Dog("Rex", "Labrador")]
for animal in animals:
    print(animal.speak())   # polymorphism!

Encapsulation & Properties

encapsulation.py
class BankAccount:
    def __init__(self, owner, balance=0):
        self.owner    = owner
        self._balance = balance   # _ = convention: "private"

    @property
    def balance(self):
        return self._balance

    @balance.setter
    def balance(self, amount):
        if amount < 0:
            raise ValueError("Balance cannot be negative")
        self._balance = amount

    def deposit(self, amount):
        self._balance += amount

    def withdraw(self, amount):
        if amount > self._balance:
            raise ValueError("Insufficient funds")
        self._balance -= amount

acc = BankAccount("Alice", 1000)
acc.deposit(500)
print(acc.balance)   # 1500

Dunder (Magic) Methods

dunder.py
class Vector:
    def __init__(self, x, y):
        self.x, self.y = x, y

    def __add__(self, other):    # +
        return Vector(self.x + other.x, self.y + other.y)

    def __len__(self):           # len()
        return 2

    def __repr__(self):          # repr()
        return f"Vector({self.x}, {self.y})"

v1 = Vector(1, 2)
v2 = Vector(3, 4)
print(v1 + v2)     # Vector(4, 6)
print(len(v1))     # 2
🎯 Exercise 9.1 — Library System

Create a Book class with title, author, and year. Create a Library class that holds a list of books, with methods: add_book(), remove_book(), search_by_author(), and list_all().

Lecture 10 · Advanced

File I/O & Exception Handling

Intermediate ~60 min Requires: Lecture 09

Reading & Writing Files

files.py
# Writing a file
with open("greetings.txt", "w") as f:
    f.write("Hello, World!\n")
    f.write("Python is awesome!\n")

# Reading entire file
with open("greetings.txt", "r") as f:
    content = f.read()
    print(content)

# Reading line by line
with open("greetings.txt", "r") as f:
    for line in f:
        print(line.strip())

# Appending to a file
with open("greetings.txt", "a") as f:
    f.write("Added a new line!\n")
✅ Always use with
The with statement (context manager) automatically closes the file even if an error occurs. Never use f = open(...) without closing it.

Working with CSV and JSON

csv_json.py
import csv
import json

# CSV writing
with open("data.csv", "w", newline="") as f:
    writer = csv.writer(f)
    writer.writerow(["Name", "Score"])
    writer.writerow(["Alice", 95])

# CSV reading
with open("data.csv") as f:
    for row in csv.DictReader(f):
        print(row)  # {'Name': 'Alice', 'Score': '95'}

# JSON serialization
data = {"name": "Alice", "scores": [95, 87, 92]}

with open("data.json", "w") as f:
    json.dump(data, f, indent=2)

with open("data.json") as f:
    loaded = json.load(f)
    print(loaded["name"])   # Alice

Exception Handling

exceptions.py
# try / except / else / finally
try:
    num = int(input("Enter a number: "))
    result = 100 / num
except ValueError:
    print("That's not a valid number!")
except ZeroDivisionError:
    print("Can't divide by zero!")
except Exception as e:
    print(f"Unexpected error: {e}")
else:
    print(f"Result: {result}")   # runs only if no exception
finally:
    print("This always runs")    # cleanup code

Raising Exceptions & Custom Exceptions

custom_exc.py
class InsufficientFundsError(Exception):
    """Raised when a withdrawal exceeds the balance."""
    def __init__(self, amount, balance):
        self.amount  = amount
        self.balance = balance
        super().__init__(
            f"Cannot withdraw {amount}; balance is {balance}"
        )

def withdraw(balance, amount):
    if amount > balance:
        raise InsufficientFundsError(amount, balance)
    return balance - amount

try:
    withdraw(100, 200)
except InsufficientFundsError as e:
    print(e)   # Cannot withdraw 200; balance is 100
🎯 Exercise 10.1 — Safe File Reader

Write a function read_file(path) that reads a file and returns its contents. Handle FileNotFoundError and PermissionError gracefully with informative messages. Test it with valid and invalid paths.

Lecture 11 · Advanced

Modules, Packages & Popular Libraries

Intermediate ~65 min Requires: Lecture 10

Importing Modules

imports.py
import math
import os
import sys
from datetime import datetime, timedelta
from random import randint, choice
import json as j   # alias

print(math.pi)          # 3.141592...
print(math.sqrt(16))   # 4.0
print(math.floor(3.9)) # 3

now = datetime.now()
print(now.strftime("%Y-%m-%d %H:%M"))

tomorrow = now + timedelta(days=1)
print(tomorrow)

Creating Your Own Module

mathutils.py
"""Custom math utilities module."""

def is_prime(n):
    if n < 2: return False
    for i in range(2, int(n**0.5) + 1):
        if n % i == 0: return False
    return True

def celsius_to_fahrenheit(c):
    return c * 9/5 + 32

PI = 3.14159265358979
main.py
from mathutils import is_prime, celsius_to_fahrenheit

print(is_prime(17))              # True
print(celsius_to_fahrenheit(100))  # 212.0

Installing Packages with pip

terminal
pip install requests          # HTTP requests
pip install numpy             # numerical computing
pip install pandas            # data analysis
pip install matplotlib        # plotting
pip install flask             # web framework
pip install pytest            # testing

pip list                      # see installed packages
pip freeze > requirements.txt # export dependencies
pip install -r requirements.txt  # install from file

Key Standard Library Modules

Module Purpose Key Functions
os OS interaction getcwd, listdir, path.join, mkdir
sys System-specific argv, exit, path, version
math Math functions sqrt, ceil, floor, log, sin
random Randomness randint, choice, shuffle, sample
datetime Date/time now, strftime, timedelta
collections Advanced containers Counter, defaultdict, deque
itertools Iterators chain, product, combinations
pathlib File paths Path, glob, mkdir, read_text

Virtual Environments

terminal
# Create a virtual environment
python -m venv venv

# Activate (macOS/Linux)
source venv/bin/activate

# Activate (Windows)
venv\Scripts\activate

# Deactivate
deactivate
✅ Best Practice
Always create a virtual environment per project. This isolates dependencies and avoids conflicts between different projects.
🎯 Exercise 11.1 — Random Quiz

Use the random module to build a quiz that picks 5 random questions from a list of 10 Q&A pairs, shuffles the answer options, and keeps score.

Lecture 12 · Capstone

Capstone Project: Task Manager CLI

Advanced ~90 min Requires: All Lectures

Congratulations on reaching the final lecture! We'll now build a fully functional command-line task manager that ties together everything covered in this course.

Project Features

Project Structure

project layout
task_manager/
├── main.py          # entry point & CLI loop
├── task.py          # Task and TaskManager classes
├── storage.py       # JSON file persistence
└── tasks.json       # auto-created data file

task.py — Core Classes

task.py
from datetime import datetime
import uuid

class Task:
    """Represents a single task."""

    def __init__(self, title, priority="medium"):
        self.id        = str(uuid.uuid4())[:8]
        self.title     = title
        self.priority  = priority
        self.done      = False
        self.created   = datetime.now().isoformat()

    def complete(self):
        self.done = True

    def to_dict(self):
        return vars(self)

    @classmethod
    def from_dict(cls, data):
        t = cls(data["title"], data["priority"])
        t.id      = data["id"]
        t.done    = data["done"]
        t.created = data["created"]
        return t

    def __str__(self):
        status = "✓" if self.done else "○"
        return f"[{status}] {self.id} | {self.title} ({self.priority})"


class TaskManager:
    """Manages a collection of tasks."""

    def __init__(self):
        self.tasks = []

    def add(self, title, priority="medium"):
        task = Task(title, priority)
        self.tasks.append(task)
        return task

    def complete(self, task_id):
        task = self._find(task_id)
        if task:
            task.complete()
            return task
        raise ValueError(f"Task {task_id} not found")

    def delete(self, task_id):
        task = self._find(task_id)
        if task:
            self.tasks.remove(task)
        else:
            raise ValueError(f"Task {task_id} not found")

    def filter(self, status="all"):
        if status == "done":
            return [t for t in self.tasks if t.done]
        elif status == "pending":
            return [t for t in self.tasks if not t.done]
        return self.tasks

    def _find(self, task_id):
        return next((t for t in self.tasks if t.id == task_id), None)

storage.py — JSON Persistence

storage.py
import json
from pathlib import Path
from task import Task

FILE = Path("tasks.json")

def load():
    if not FILE.exists():
        return []
    with FILE.open() as f:
        return [Task.from_dict(d) for d in json.load(f)]

def save(tasks):
    with FILE.open("w") as f:
        json.dump([t.to_dict() for t in tasks], f, indent=2)

main.py — CLI Entry Point

main.py
from task import TaskManager
import storage

def show_menu():
    print("\n── Task Manager ──────────────")
    print("  1. Add task")
    print("  2. List tasks")
    print("  3. Complete task")
    print("  4. Delete task")
    print("  5. Quit")
    print("──────────────────────────────")

def main():
    mgr = TaskManager()
    mgr.tasks = storage.load()

    while True:
        show_menu()
        choice = input("Choice: ").strip()

        if choice == "1":
            title    = input("Task title: ")
            priority = input("Priority (low/medium/high): ") or "medium"
            task = mgr.add(title, priority)
            print(f"Added: {task}")

        elif choice == "2":
            filt = input("Filter (all/pending/done): ") or "all"
            tasks = mgr.filter(filt)
            if not tasks:
                print("No tasks found.")
            else:
                for t in tasks:
                    print(" ", t)

        elif choice == "3":
            tid = input("Task ID to complete: ")
            try:
                t = mgr.complete(tid)
                print(f"Completed: {t}")
            except ValueError as e:
                print(f"Error: {e}")

        elif choice == "4":
            tid = input("Task ID to delete: ")
            try:
                mgr.delete(tid)
                print("Deleted.")
            except ValueError as e:
                print(f"Error: {e}")

        elif choice == "5":
            storage.save(mgr.tasks)
            print("Goodbye! Tasks saved.")
            break

        storage.save(mgr.tasks)   # auto-save after every action


if __name__ == "__main__":
    main()

What You've Learned

This capstone project applied every major concept from the course:

🚀 What's Next?

You've completed the core Python curriculum! Consider exploring:

  • Web development — Flask or FastAPI
  • Data science — NumPy, Pandas, Matplotlib
  • Machine learning — scikit-learn, TensorFlow, PyTorch
  • Automation — Selenium, requests, BeautifulSoup
  • Testing — pytest, unittest
  • Async programming — asyncio, aiohttp
🎯 Final Challenge — Extend the Task Manager

Add these features to the task manager: (1) due dates for tasks, (2) search tasks by keyword, (3) export tasks to a CSV report, (4) a statistics command showing counts per priority and completion rate.