def greet(name):
return f"Hello, {name}"
hello = greet # assigning function to a variable
print(hello("Alice")) # Hello, Alice
# Hint: takes two int args and returns an int
def add(a:int, b:int=1)->int:
"""Add two int args with second arg default value = 1, and return a int."""
return a + b
def example(a, b, c=10):
print(a, b, c)
example(1, 2) # 1 2 10
example(1, b=5, c=15) # 1 5 15
example(1, c=15, b=5) # 1 5 15
Use None as default to make an argument optional:
def example(a, b, c=None):
print(a, b, c)
def demo(*args, **kwargs):
print("args:", args)
print("kwargs:", kwargs)
demo(1, 2, 3, x=10, y=20)
# args: (1, 2, 3)
# kwargs: {'x': 10, 'y': 20}
*args → collects extra positional arguments into a tuple
**kwargs → collects extra keyword arguments into a dict
Python functions can return multiple values
def get_position():
return 10, 20
x, y = get_position()
print(x, y) # 10 20
A closure occurs when an inner function remembers variables from an outer function — even after the outer function finishes executing.
def multiplier(factor):
def inner(n):
return n * factor
return inner
times3 = multiplier(3)
print(times3(10)) # 30
This is equivalent to the following class implementation:
class Multiplier:
def __init__(self, factor):
self.factor = factor
def __call__(self, n):
return n * self.factor
times3 = Multiplier(3)
print(times3(10)) # 30
Syntax: lambda parameters: expression
Useful for short, throwaway functions:
double = lambda x: x * 2
print(double(4)) # 8
nums = [1, 2, 3, 4]
print(list(map(lambda x: x*x, nums))) # [1, 4, 9, 16]
map syntax: map(function, iterable) and it's lazy (doesn't compute immediately; hence passed to list)
Generators produce values lazily instead of storing everything in memory.
They are iterable, and produce values on demand -- so can be used in for loop
def countdown(n):
while n > 0:
yield n
n -= 1
for i in countdown(5):
print(i)
Calling the function does not run the function body:
gen = countdown(5)
It simply creates a generator object (if you print):
<generator object countdown at 0x...>
👉 Calling next() yields/returns the value to the caller; but the countdown() function pauses at the yield line and continues from the next line when next() called again.
gen = countdown(5)
print(next(gen)) # 5
print(next(gen)) # 4
print(next(gen)) # 3
print(next(gen)) # 2
print(next(gen)) # 1
print(next(gen)) # ERROR: StopIteration (generator is finished)
👉 The for loop takes care of calling next() and stopping it gracefully.
def forever():
n = 1
while True:
yield n
n += 1
# Two independent generators
a = forever()
b = forever()
# Generating two streams
next(a)
next(a)
next(b)
def gen():
yield 1
yield 2
return # stops the generator
yield 3 # never runs
# Only prints 1, 2
for x in gen():
print(x)
The return simply ends the generator