← Back to Python series
⚙️
Intermediate
lambda · Closures · *args · **kwargs · functools · List · Dict · Set · Generator expressions · yield · import · from · pip · __all__ · __init__.py

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.

lambdaclosureargskwargsfunctoolspartialcomprehensiongeneratoryieldlazy evaluationmodulepackageimportpip__init__
Duration
⏱ 2.5 hours + 2.5 hours + 2 hours
Level
📊 Intermediate
Prerequisite
🎯 Basic track complete
OUTCOME
Write higher-order functions and use functools.lru_cache for memoization / Replace verbose for-loops with comprehensions and write a generator pipeline / Create a multi-file package with a public API defined via __all__

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.

python
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 age

2. Closures

A closure is a function that remembers variables from its enclosing scope, even after that scope has finished.

python
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())  # 3

3. *args and **kwargs

python
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

python
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))  # 49

5. Common Mistakes

  1. Lambda with multiple statements — not allowed. If you need multiple lines, write a regular def.
  2. Late binding in closures: all closures in a loop capture the same variable. Use default arg: lambda i=i: i.
  3. 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

python
# 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

python
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

python
# 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 1

4. Common Mistakes

  1. Deeply nested comprehensions hurt readability — if you need three levels, use a regular loop.
  2. Generators are exhausted after one pass. If you need to iterate twice, convert to list first.
  3. 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

python
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))    # 120

2. Creating a Module

Any .py file is a module. Create utils.py and import from it.

python
# 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.

text
mypackage/
  __init__.py      # makes it a package
  geometry.py      # module with shapes
  statistics.py    # module with stats
python
from mypackage import geometry
from mypackage.statistics import mean

4. pip

bash
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 file

5. Common Mistakes

  1. Circular imports: A imports B, B imports A. Restructure to break the cycle.
  2. Naming your file the same as a standard library module (e.g., random.py) shadows it.
  3. from module import * pollutes the namespace. Use explicit imports or __all__ in the module.

💻 Examples

Run these examples and check the output yourself.

01_lambda_sort.py— Multi-key sort with lambda
CODE
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']}")
▶ Output
Alice: grade=90, age=20
Bob: grade=90, age=22
Carol: grade=85, age=21
02_closure_adder.py— Closure factory: make_adder
CODE
def 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]
▶ Output
8
13
[6, 7, 8]
03_variadic.py— *args and **kwargs in practice
CODE
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")
▶ Output
Result: 1, 2, 3
Letters: a | b | c
01_comprehensions.py— List / dict / set comprehensions
CODE
numbers = 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)
▶ Output
[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}
02_flatten.py— Flatten a nested list with a nested comprehension
CODE
nested = [[1, 2, 3], [4, 5], [6, 7, 8, 9]]
flat = [item for sublist in nested for item in sublist]
print(flat)
▶ Output
[1, 2, 3, 4, 5, 6, 7, 8, 9]
03_generator_pipeline.py— Lazy pipeline for large data
CODE
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))
▶ Output
[110, 120, 130, 140, 150, 160, 170, 180, 190]
geometry.py— A simple geometry module
CODE
"""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)
main.py— Using the geometry module
CODE
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}")
▶ Output
Circle r=5:  78.54
Rect 4×6:    24
Distance:    5.00

📝 Exercises

Try them yourself first, then open the solution to compare.

Exercise 1

Pipeline

Goal: Write apply_pipeline(value, *functions) that applies each function in sequence.

Requirements
  • 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
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
▶ Output
169
Exercise 2

Memoized Fibonacci

Goal: Implement Fibonacci with lru_cache and compare performance.

Requirements
  • Plain recursive fib vs @lru_cache version
  • Time both for n=35 with time.perf_counter
  • Print results and elapsed time
▶Toggle solution
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')
Exercise 3

Matrix Transpose

Goal: Transpose a 3×3 matrix using a list comprehension.

Requirements
  • 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
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)
▶ Output
[1, 4, 7]
[2, 5, 8]
[3, 6, 9]
Exercise 4

Infinite Fibonacci Generator

Goal: Write fib_gen() that yields Fibonacci numbers indefinitely.

Requirements
  • Yields 0, 1, 1, 2, 3, 5, 8, ... infinitely
  • Use islice from itertools to take first N values
▶Toggle solution
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)))
▶ Output
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
Exercise 5

math_utils package

Goal: Create a package with two modules: algebra.py and stats.py.

Requirements
  • 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
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
Example code / lecture materials

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