Weeks 1–3 — Advanced Functions · Comprehensions & Generators · Modules & Packages
Go beyond basic def: write anonymous functions with lambda, understand closures, handle variable-length arguments, and use functools tools like partial and lru_cache. Write concise, readable data transformations with list, dict, and set comprehensions. Then step into generators and yield for memory-efficient iteration. Organize code into reusable modules and packages. Understand how Python's import system works, create your own packages, and manage dependencies with pip.
What you'll learn
- 1Write lambda expressions for short anonymous functions
- 2Explain how closures capture enclosing scope
- 3Accept variable positional (*args) and keyword (**kwargs) arguments
- 4Use functools.partial and functools.lru_cache
- 5Pass functions as arguments to map, filter, sorted
- 6Write list, dict, and set comprehensions with filtering
- 7Use nested comprehensions for 2D data
- 8Explain the difference between a list comprehension and a generator expression
- 9Write generator functions with yield
- 10Chain generators for memory-efficient pipelines
- 11Import standard library modules and third-party packages
- 12Create your own module (.py file) and import it
- 13Organise a package with __init__.py
- 14Control public API with __all__
- 15Install and use third-party packages with pip
Week 1 — Advanced Functions
Go beyond basic def: write anonymous functions with lambda, understand closures, handle variable-length arguments, and use functools tools like partial and lru_cache.
1. Lambda Expressions
lambda creates an anonymous function in a single expression. Best used for short, one-off operations.
square = lambda x: x ** 2
add = lambda a, b: a + b
print(square(5)) # 25
print(add(3, 4)) # 7
# Common use: sort key
people = [("Alice", 30), ("Bob", 25), ("Charlie", 35)]
people.sort(key=lambda p: p[1])
print(people) # sorted by age2. Closures
A closure is a function that remembers variables from its enclosing scope, even after that scope has finished.
def make_counter(start=0):
count = start
def increment():
nonlocal count
count += 1
return count
return increment
counter = make_counter()
print(counter()) # 1
print(counter()) # 2
print(counter()) # 33. *args and **kwargs
def my_sum(*args):
return sum(args)
print(my_sum(1, 2, 3, 4)) # 10
def log_event(event, **kwargs):
print(f"[{event}]", " ".join(f"{k}={v}" for k, v in kwargs.items()))
log_event("login", user="alice", ip="127.0.0.1")4. functools
from functools import lru_cache, partial
@lru_cache(maxsize=None)
def fib(n):
if n <= 1: return n
return fib(n-1) + fib(n-2)
print(fib(50)) # fast thanks to memoization
double = partial(pow, exp=2) # pow with exp fixed to 2
# Note: partial works with positional; use lambda for keyword only
double = lambda x: x ** 2
print(double(7)) # 495. Common Mistakes
- Lambda with multiple statements — not allowed. If you need multiple lines, write a regular def.
- Late binding in closures: all closures in a loop capture the same variable. Use default arg: lambda i=i: i.
- Forgetting nonlocal when modifying an enclosing variable — Python treats it as a new local otherwise.
Week 2 — Comprehensions & Generators
Write concise, readable data transformations with list, dict, and set comprehensions. Then step into generators and yield for memory-efficient iteration.
1. List Comprehensions
# Basic: [expression for item in iterable if condition]
squares = [x**2 for x in range(1, 11)]
evens = [x for x in range(20) if x % 2 == 0]
matrix = [[row*col for col in range(1,4)] for row in range(1,4)]
print(squares)
print(evens)
print(matrix)If the resulting list is only iterated once, prefer a generator expression () instead of [] to save memory.
2. Dict & Set Comprehensions
words = ["apple", "banana", "cherry"]
lengths = {w: len(w) for w in words}
print(lengths) # {'apple': 5, 'banana': 6, 'cherry': 6}
unique_lengths = {len(w) for w in words}
print(unique_lengths) # {5, 6}3. Generator Expressions & yield
# Generator expression (lazy — doesn't compute all at once)
gen = (x**2 for x in range(1_000_000))
print(next(gen)) # 0
print(next(gen)) # 1
# Generator function
def countdown(n):
while n > 0:
yield n
n -= 1
for v in countdown(5):
print(v, end=' ') # 5 4 3 2 14. Common Mistakes
- Deeply nested comprehensions hurt readability — if you need three levels, use a regular loop.
- Generators are exhausted after one pass. If you need to iterate twice, convert to list first.
- Using a walrus operator (:=) in comprehensions is valid but can make code hard to read.
Week 3 — Modules & Packages
Organize code into reusable modules and packages. Understand how Python's import system works, create your own packages, and manage dependencies with pip.
1. Importing Modules
import math
print(math.sqrt(16)) # 4.0
print(math.pi) # 3.14159...
from math import sqrt, pi
print(sqrt(25)) # 5.0
import math as m # alias
print(m.factorial(5)) # 1202. Creating a Module
Any .py file is a module. Create utils.py and import from it.
# utils.py
def greet(name):
return f"Hello, {name}!"
PI = 3.14159
# main.py
from utils import greet, PI
print(greet("Alice"))
print(PI)3. Packages
A package is a directory with an __init__.py file. It groups related modules.
mypackage/
__init__.py # makes it a package
geometry.py # module with shapes
statistics.py # module with statsfrom mypackage import geometry
from mypackage.statistics import mean4. pip
pip install requests # install
pip install requests==2.31.0 # specific version
pip uninstall requests # remove
pip list # show installed
pip freeze > requirements.txt # export for sharing
pip install -r requirements.txt # install from file5. Common Mistakes
- Circular imports: A imports B, B imports A. Restructure to break the cycle.
- Naming your file the same as a standard library module (e.g., random.py) shadows it.
- from module import * pollutes the namespace. Use explicit imports or __all__ in the module.
💻 Examples
Run these examples and check the output yourself.
students = [
{"name": "Alice", "grade": 90, "age": 20},
{"name": "Bob", "grade": 90, "age": 22},
{"name": "Carol", "grade": 85, "age": 21},
]
# Sort by grade descending, then by age ascending
students.sort(key=lambda s: (-s["grade"], s["age"]))
for s in students:
print(f"{s['name']}: grade={s['grade']}, age={s['age']}")
Alice: grade=90, age=20
Bob: grade=90, age=22
Carol: grade=85, age=21def make_adder(n):
def adder(x):
return x + n
return adder
add5 = make_adder(5)
add10 = make_adder(10)
print(add5(3)) # 8
print(add10(3)) # 13
print(list(map(add5, [1, 2, 3]))) # [6, 7, 8]
8
13
[6, 7, 8]def report(*values, sep=", ", label="Result"):
print(f"{label}: {sep.join(str(v) for v in values)}")
report(1, 2, 3)
report("a", "b", "c", sep=" | ", label="Letters")
Result: 1, 2, 3
Letters: a | b | cnumbers = list(range(1, 21))
squares = [n**2 for n in numbers]
even_sq = [n**2 for n in numbers if n % 2 == 0]
sq_dict = {n: n**2 for n in range(1, 6)}
uniq = {n % 5 for n in range(20)}
print(squares[:5])
print(even_sq)
print(sq_dict)
print(uniq)
[1, 4, 9, 16, 25]
[4, 16, 36, 64, 100, 144, 196, 256, 324, 400]
{1: 1, 2: 4, 3: 9, 4: 16, 5: 25}
{0, 1, 2, 3, 4}nested = [[1, 2, 3], [4, 5], [6, 7, 8, 9]]
flat = [item for sublist in nested for item in sublist]
print(flat)
[1, 2, 3, 4, 5, 6, 7, 8, 9]def read_lines(n):
for i in range(n):
yield f"log line {i}: value={i*10}"
def parse_value(lines):
for line in lines:
yield int(line.split('=')[1])
def filter_large(values, threshold):
for v in values:
if v > threshold:
yield v
result = filter_large(parse_value(read_lines(20)), 100)
print(list(result))
[110, 120, 130, 140, 150, 160, 170, 180, 190]"""Geometry helpers."""
import math
__all__ = ["circle_area", "rect_area", "distance"]
def circle_area(r):
return math.pi * r ** 2
def rect_area(w, h):
return w * h
def distance(p1, p2):
return math.sqrt((p2[0]-p1[0])**2 + (p2[1]-p1[1])**2)
from geometry import circle_area, rect_area, distance
print(f"Circle r=5: {circle_area(5):.2f}")
print(f"Rect 4×6: {rect_area(4, 6)}")
print(f"Distance: {distance((0,0),(3,4)):.2f}")
Circle r=5: 78.54
Rect 4×6: 24
Distance: 5.00📝 Exercises
Try them yourself first, then open the solution to compare.
Pipeline
Goal: Write apply_pipeline(value, *functions) that applies each function in sequence.
- Takes a value and any number of functions
- Returns result of applying all functions left to right
- Test with a pipeline of lambda math operations
▶Toggle solution
from functools import reduce
def apply_pipeline(value, *funcs):
return reduce(lambda v, f: f(v), funcs, value)
result = apply_pipeline(5,
lambda x: x * 2, # 10
lambda x: x + 3, # 13
lambda x: x ** 2, # 169
)
print(result) # 169
169Memoized Fibonacci
Goal: Implement Fibonacci with lru_cache and compare performance.
- Plain recursive fib vs @lru_cache version
- Time both for n=35 with time.perf_counter
- Print results and elapsed time
▶Toggle solution
import time
from functools import lru_cache
def fib_slow(n):
if n <= 1: return n
return fib_slow(n-1) + fib_slow(n-2)
@lru_cache
def fib_fast(n):
if n <= 1: return n
return fib_fast(n-1) + fib_fast(n-2)
n = 35
t0 = time.perf_counter()
print(fib_slow(n), f'slow: {time.perf_counter()-t0:.4f}s')
t0 = time.perf_counter()
print(fib_fast(n), f'fast: {time.perf_counter()-t0:.6f}s')
Matrix Transpose
Goal: Transpose a 3×3 matrix using a list comprehension.
- Input: [[1,2,3],[4,5,6],[7,8,9]]
- Output: [[1,4,7],[2,5,8],[3,6,9]]
- Use a single comprehension expression
▶Toggle solution
matrix = [[1,2,3],[4,5,6],[7,8,9]]
transposed = [[row[i] for row in matrix] for i in range(len(matrix[0]))]
for row in transposed:
print(row)
[1, 4, 7]
[2, 5, 8]
[3, 6, 9]Infinite Fibonacci Generator
Goal: Write fib_gen() that yields Fibonacci numbers indefinitely.
- Yields 0, 1, 1, 2, 3, 5, 8, ... infinitely
- Use islice from itertools to take first N values
▶Toggle solution
from itertools import islice
def fib_gen():
a, b = 0, 1
while True:
yield a
a, b = b, a + b
print(list(islice(fib_gen(), 10)))
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]math_utils package
Goal: Create a package with two modules: algebra.py and stats.py.
- algebra.py: quadratic(a,b,c) returns both roots
- stats.py: mean, median, mode
- __init__.py exports all four functions
- Test from a separate main.py
▶Toggle solution
# math_utils/__init__.py
from .algebra import quadratic
from .stats import mean, median, mode
# math_utils/algebra.py
import cmath
def quadratic(a, b, c):
d = cmath.sqrt(b**2 - 4*a*c)
return (-b+d)/(2*a), (-b-d)/(2*a)
# math_utils/stats.py
from statistics import mean, median, mode
All lecture materials and example code are openly available on GitHub.
View on GitHub ↗Comments 0
No comments yet. Be the first.
Only members can comment.
Comments with ads, abuse, personal information or illegal content are hidden or removed. Comments are shown with your nickname. Privacy Policy