PY

Python Fundamentals

18 lessons

Progress0%
1. Introduction to Python
1What is Python?2Setting Up Python
2. Variables and Data Types
1Variables in Python2Data Types
3. Control Flow
Conditional StatementsLoops
4. Functions
Function Basics
5. Data Structures
Lists Deep DiveTuples & SetsDictionaries & Comprehensions
6. Advanced Functions
Closures & Higher-Order FunctionsDecorators & Lambda
7. Object-Oriented Python
Classes & InstancesInheritance & Dunder Methods
8. Exception Handling
try / except / finallyCustom Exceptions & Context Managers
9. Modules & File I/O
Modules & PackagesFile I/O & JSON
All Tutorials
PythonAdvanced Functions
Lesson 11 of 18 min
Chapter 6 · Lesson 1

Closures & Higher-Order Functions

Closures & Higher-Order Functions

Functions as First-Class Objects

In Python, functions are first-class citizens — they can be assigned to variables, stored in data structures, passed as arguments, and returned from other functions:

python
def greet(name):
    return f"Hello, {name}!"

say_hello = greet          # assign to variable
print(say_hello("Alice"))  # Hello, Alice!

funcs = [abs, str, len]    # store in a list

Higher-Order Functions

A higher-order function either accepts a function as an argument or returns a function (or both).

Built-in Higher-Order Functions

python
nums = [1, -2, 3, -4, 5]

# map — apply function to every item
squares   = list(map(lambda x: x**2, nums))

# filter — keep items where function returns True
positives = list(filter(lambda x: x > 0, nums))

# zip — pair items from two iterables
names  = ["Alice", "Bob"]
scores = [95, 87]
paired = list(zip(names, scores))   # [("Alice", 95), ("Bob", 87)]

# enumerate — add index to iteration
for i, val in enumerate(["a", "b", "c"], start=1):
    print(i, val)

functools.reduce

reduce(f, iterable) collapses an iterable to a single value by applying f cumulatively:

python
from functools import reduce
product = reduce(lambda acc, x: acc * x, [1, 2, 3, 4, 5])  # 120

Closures

A closure is a nested function that remembers variables from its enclosing scope even after the outer function has returned:

python
def make_counter(start=0):
    count = start
    def counter():
        nonlocal count
        count += 1
        return count
    return counter

c = make_counter()
print(c())  # 1
print(c())  # 2

The inner function counter closes over the variable count. Each call to make_counter() creates an independent closure with its own count.

The nonlocal Keyword

Without nonlocal, an assignment inside the inner function creates a new local variable, shadowing the outer one. nonlocal declares that a name refers to a variable in the nearest enclosing scope:

python
def outer():
    x = 10
    def inner():
        nonlocal x
        x += 5      # modifies outer's x
    inner()
    return x        # 15

Practical Closure Patterns

  • Factories: generate specialized functions (e.g., multipliers, validators)
  • Memoization: cache return values inside the enclosing scope
  • Callbacks: pass state-carrying functions to event handlers

Code Examples

map, filter, zip, enumeratepython
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

# map — square each number
squares = list(map(lambda x: x**2, numbers))
print("Squares:", squares)

# filter — keep only even numbers
evens = list(filter(lambda x: x % 2 == 0, numbers))
print("Evens:", evens)

# zip — combine two lists element-wise
names  = ["Alice", "Bob", "Carol"]
scores = [92, 85, 78]
results = list(zip(names, scores))
print("Results:", results)

# enumerate — index + value
print("\nLeaderboard:")
for rank, (name, score) in enumerate(results, start=1):
    print(f"  {rank}. {name}: {score}")

map() and filter() return lazy iterators in Python 3, so list() is needed to materialise them. zip() stops at the shortest iterable. enumerate() with start= lets you control the initial index counter.

functools.reduce & First-Class Functionspython
from functools import reduce

# reduce — fold a list into one value
numbers = [1, 2, 3, 4, 5]
product = reduce(lambda acc, x: acc * x, numbers)
print("Product:", product)

total = reduce(lambda acc, x: acc + x, numbers, 0)
print("Sum:", total)

# Functions stored in a data structure
operations = {
    "add":      lambda a, b: a + b,
    "subtract": lambda a, b: a - b,
    "multiply": lambda a, b: a * b,
}

for op_name, func in operations.items():
    print(f"10 {op_name} 3 = {func(10, 3)}")

reduce() can take an optional initializer (third argument) used as the starting accumulator. Storing functions in a dict is a common pattern for dispatching operations without lengthy if/elif chains.

Closures & nonlocalpython
# Factory function returning a closure
def make_multiplier(factor):
    def multiply(x):
        return x * factor   # 'factor' is closed over
    return multiply

double = make_multiplier(2)
triple = make_multiplier(3)
print("Double 7:", double(7))
print("Triple 7:", triple(7))

# Counter with nonlocal
def make_counter(start=0):
    count = start
    def increment(step=1):
        nonlocal count
        count += step
        return count
    return increment

counter = make_counter(10)
print(counter())    # 11
print(counter(5))   # 16
print(counter())    # 17

Each call to make_multiplier() creates a new closure with its own 'factor'. Without nonlocal, assigning to 'count' inside increment() would create a local variable, causing an UnboundLocalError on the += step.

Quick Quiz

1. What is a closure in Python?

2. What does the nonlocal keyword do?

3. What does filter(func, iterable) return?

Was this lesson helpful?

PreviousNext