Course Content
Module 1 – Getting Started with Python
introduced the fundamentals of Python, giving beginners a clear understanding of how the language works and how to start writing simple programs. Python was highlighted as a beginner-friendly language with simple syntax, making it easy to read and write code.
0/7
Module 2 – Introduction to Python Programming
In this Introduction to Python module, learners explore Python’s clear, readable syntax and powerful features. Beginning with installation and a simple “Hello, World!” script, you will progress through variables, control flow and functions using step-by-step examples. By the end, you will be equipped to write your own Python programmes, automate routine tasks and tap into an extensive library ecosystem for real-world projects.
0/7
Basic Command for Command prompt, PowerShell, Zsh(macOS)
0/1
Module 3 – Variables, Data Types and Basic Operations
In the Variables, Data Types and Basic Operations in Python module, learners explore how to store and manage data using variables, master fundamental types such as integers, floats, strings and booleans, and perform arithmetic, comparison and logical operations step by step. Clear explanations, real world examples and hands on exercises guide you through writing and debugging code. By the end of this module, you will be ready to build dynamic Python programs and automate everyday tasks.
0/6
Module 4 – Control Flow – Conditions and Loops
Control flow structures determine the order in which your program’s code executes. With conditional statements, you can make decisions and execute certain code blocks only when specific conditions are met. Loops allow you to repeat actions efficiently without writing redundant code. In this module, we will explore fundamental control flow concepts in Python in a step-by-step manner, similar to Microsoft’s learning curriculum. By the end, you’ll understand how to use if, elif, and else statements (including nested conditions) for decision-making, how truthy and falsy values work in Boolean logic, how to construct for loops (using range() and iterating over collections), how to use while loops along with loop control statements (break and continue), and how to leverage list comprehensions and generator expressions for concise looping. Finally, we’ll apply these concepts in a practical exercise to build an interactive decision-making system. Each section below includes explanations, code examples, and mini-exercises to reinforce the concepts, all formatted for clarity and easy follow-along.
0/8
Day 1 Summary
We covered Modules 1, 2 & Module 3 (Lesson 1 & 2)
0/1
Module 5 – Functions and Code Organisation
Imagine you need to clean up a messy data set or send a personalised email to each customer. Instead of writing the same steps over and over, you can create a function and call it whenever you need. In this lesson on Functions and Code Organisation, you will learn how to define functions, pass and return information, document your work and group related code into modules for easy reuse and maintenance.
0/10
Day 2 Summary
Summary for Day 21 Aug 2025
0/1
Day 3 Summary
Summary of Day 28 Aug 2025
0/1
Module 7 – Working with Files and Folders
In this lesson, we will learn how to manipulate files and directories using Python. We’ll explore common file operations using the os module, and see how the pathlib module provides an object-oriented way to handle file paths. We’ll also use the glob module for pattern-based file searches and learn file I/O operations for text, CSV, and binary files. Additionally, we’ll introduce the calendar and time modules to work with dates and timestamps. Finally, an interactive lab will tie everything together by automating a folder backup and cleanup task. Follow the step-by-step sections below for each subtopic, try out the code examples, and explore the guided lab at the end.
0/9
Module 8 – Error Handling and Debugging Techniques
In this lesson, we will learn how to handle errors in Python programs and how to debug code effectively. Errors are inevitable, but knowing how to manage them ensures our programs don't crash unexpectedly. We will cover the difference between syntax errors and exceptions, how to use try, except, else, and finally blocks to catch and handle exceptions, and how to raise your own exceptions (including creating custom exception classes). We’ll also explore debugging strategies: using simple print statements or the logging module to trace your program’s execution, and using Python’s interactive debugger pdb to step through code. By following best practices for error handling and debugging, you can write resilient, maintainable code. Throughout this lesson, try the examples and exercises to practice these techniques.
0/9
Day 4 Summary
0/1
Module 9 – Automating Excel and PDFs with Python
In this lesson, you will learn how to automate common communication and reporting tasks using Python. We will cover sending notifications via email, messaging platforms, and SMS, as well as manipulating Excel spreadsheets and PDF files programmatically. Each section below includes step-by-step explanations, code examples, and interactive exercises to reinforce your understanding. By the end of this lesson, you’ll be able to send emails with attachments, integrate with Slack/Microsoft Teams, send SMS alerts, and automate Excel/PDF workflows.
0/9
Day 5 Summary
0/1
Mini Project: Build your own Automation Tool
The project incorporates two common automation tasks – Contact Management and Student Tasks Tracking
0/2
Day 6 Summary
0/1
Introduction to Python Programming (Copy 1)

List comprehensions and generator expressions

Python offers concise constructs called list comprehensions and generator expressions to create sequences or iterate in a single line of code. These are advanced applications of loops and conditionals that allow for clearer, more compact code in many cases. They essentially compress a loop and an optional condition into a single expression.

List Comprehensions

A list comprehension is a way to build a new list by iterating over a sequence and optionally filtering items, all in one expression. It’s often described as “syntactic sugar” for a for loop that appends to a list. The basic syntax is:

[<expression> for <item> in <iterable> if <condition_optional>]

The result is a new list.

  • <expression> defines the value to put in the new list for each <item>.

  • The for <item> in <iterable> is like a loop, going through the iterable.

  • The optional if <condition> (placed at the end) can filter which items get included (only those where the condition is True will be processed.

Example 1: Create a list of squares of numbers:

numbers = [1, 2, 3, 4, 5]
squares = [num * num for num in numbers]
print(squares)  
# Output: [1, 4, 9, 16, 25]

This comprehension iterates over each num in numbers and computes num * num (square) to produce a new list of squared values. It achieves in one line what would take 3-4 lines using a standard loop (initialising an empty list, looping, appending).

Example 2: Add a filter condition – suppose from the same list we want only the squares of even numbers:

even_squares = [num * num for num in numbers if num % 2 == 0]
print(even_squares)  # Output: [4, 16]

The added if num % 2 == 0 clause ensures that the expression num * num is executed only for even numbers, so the resulting list is [4, 16] (squares of 2 and 4). This is equivalent to looping and using an if inside the loop to decide whether to append.

List comprehensions make code more concise and often more readable for simple transformations. They are also quite fast in Python (usually faster than an equivalent Python loop with append, because the looping and list construction happen in C under the hood). However, overly complex comprehensions (with multiple conditions or nested loops) can become hard to read, so it’s about finding a balance.

Some key features:

  • You can nest loops in a comprehension (e.g., flattening a matrix with [elem for row in matrix for elem in row]).

  • You can have multiple if conditions or even an if…else in expression part (ternary-like) if needed.

  • Python also has similar comprehensions for other collections: set comprehensions (using {} braces) and dict comprehensions (using {key: value ...} syntax), but list comprehension is the most common, and the concept is similar for all.

One must be mindful that a list comprehension creates the entire list in memory immediately. This is generally fine for moderately sized lists, but if you’re dealing with a huge range or data stream, the memory usage could be a concern. That’s where generator expressions come in.

Generator Expressions

A generator expression is like a list comprehension, but it creates a generator object that yields items one by one instead of building the whole list at once. In syntax, it looks similar to a list comprehension, except it uses parentheses (…) instead of square brackets.

Example: Using a generator expression for the same squares calculation:

numbers = [1, 2, 3, 4, 5]
squares_gen = (num * num for num in numbers)
print(squares_gen)        # Output: <generator object <genexpr> at 0x7f8e9c1cdd60>
print(list(squares_gen))  # Output: [1, 4, 9, 16, 25]

When we print squares_gen we see it’s a generator object, not the actual list of squares. We can iterate over this generator to retrieve the values. In the example, list(squares_gen) exhausts the generator to produce a list of results.

We could also loop:

for sq in (num * num for num in numbers):
    print(sq)

This would print 1, 4, 9, 16, 25 each on a separate line.

The key difference: the generator does not calculate all squares up front. Instead, each time you iterate and need the next value, it computes that value on the fly (lazy evaluation).

Memory Efficiency: Generator expressions are memory-efficient. Using a generator, you could iterate over a sequence of billions of numbers without holding them all in memory at once, because it yields them one at a time as needed. In contrast, a list comprehension over that range would attempt to create a giant list, likely running out of memory. The generator “remembers” how to generate the next value (essentially, it encapsulates the loop logic internally), and it yields values on demand until it’s exhausted. Once a generator is exhausted (all values produced), it can’t be reused or reset (unless you create it again).

Example with condition: You can include conditions similarly in a generator expression:

python
odd_squares_gen = (num*num for num in numbers if num % 2 != 0)

This creates a generator of squares of odd numbers (1,3,5 -> 1,9,25). It works analogously to the list comp example, but in generator form.

Important: If you convert a generator to a list (or otherwise consume it fully), you’ll end up using memory for that list. So, to truly benefit, you would iterate over the generator directly. For instance:

# Processing a large generator without keeping all results
gen = (x*x for x in range(1, 1000000000))
total = 0
for val in gen:
    total += val
    if total > 1000000:
        break
print("Reached over one million in partial sum")

This loop will run until the sum of squares exceeds 1,000,000 and then break. We never stored all the squares; we just generated and added them one by one, stopping early. A list comp for the same would have been wasteful (and we didn’t even need all that data).

When to use list comprehension vs generator expression? It depends on your needs:

Use a list comprehension when you need to actually produce a list object to use later (e.g., you need to index into it, or use it multiple times) and the dataset is of a manageable size. It’s straightforward and eager.

Use a generator expression when you are iterating through results once or feeding them into something that can consume an iterator (like a for loop, a sum() function, etc.), especially if the sequence is large or potentially infinite. Generators are also ideal for pipelines – feeding data through multiple steps without intermediate storage.

As a rule of thumb, if you find yourself writing [... for ... in ...] just to loop, and you don’t actually need the list container, using ( ... ) might be more efficient.

Let’s compare quickly with a memory usage example (just conceptual, not actual code here):

List comprehension nums = [i for i in range(1000000)] will create a list of 1,000,000 integers in memory.

Generator expression nums_gen = (i for i in range(1000000)) will create a generator that knows how to produce those integers but at a given moment only holds one (or a few) in memory. The generator itself is lightweight (as shown by sys.getsizeof in an example: a generator object might be only ~88 bytes, whereas a list of a million ints could be many megabytes).

One caution: generator expressions can only be iterated once. If you need to iterate multiple times, either recreate the generator or use a list.

Example to illustrate one-time usage:

gen = (i*i for i in range(5))
for val in gen:
    print(val, end=" ")
# Output: 0 1 4 9 16

for val in gen:
    print(val, end=" ")
# Output:  (nothing, the generator is exhausted)

After the first loop, gen has yielded all its values and is exhausted, so the second loop produces nothing. A list, on the other hand, could be reused multiple times.

Recap:

  • List comprehension: Use [ ... ] syntax to get a list. Great for creating transformed or filtered lists in a concise way. E.g., [x**2 for x in data if x > 0].

  • Generator expression: Use ( ... ) for a memory-efficient iterator that yields results on the fly. Good for large data streams or when you only need to read the sequence once, or feed it into something like sum() or any(). E.g., sum(x**2 for x in data) will use a generator internally.

Both forms support conditional filtering and can loop over any iterable. They improve code conciseness and in many cases performance (especially list comps vs manual loops).

Don’t overuse comprehensions for very complex logic. If you have nested loops and multiple conditions, sometimes a plain loop is easier to understand. But for most one-liner transformations, they are excellent tools.

Mini-Exercise:

  1. Write a list comprehension that takes a list of words and produces a list of the lengths of those words. For example
    words = ["Python", "loops", "awesome"][6, 5, 7].

  2. Given a list nums = [3, -4, 2, 0, -1, 8], use a list comprehension with a filter to create a new list of only the positive numbers squared (ignoring negatives and zero).
    The result should be [9, 4, 64] for the given list.

  3. Convert the above comprehension into a generator expression. Use a loop or the list() function to retrieve the results from the generator and confirm it matches the list.

  4. (Challenge) Using a single list comprehension, generate all pairs of two dice rolls (values 1-6) that sum to 7.
    Output should be a list of tuples, e.g. (1,6), (2,5), ... (6,1).
    This requires a nested comprehension or two for clauses in one comprehension.