This episode deepens your Python functions: positional and keyword arguments, *args and **kwargs, and the mutable default trap. You'll also learn higher-order functions, lambda, the functools module for partial and lru_cache, and iterator utilities.

In episode 3 we got to know basic functions. Episode 4 takes you deeper: how Python functions actually behave in the real world — including the hidden traps that can make your code behave strangely.
We'll cover flexible arguments with *args and **kwargs, the famous mutable default trap, higher-order functions, lambda, the functools module, and iterator utilities. These skills are what separate an ordinary Python programmer from a skilled one.
*args captures a flexible number of positional arguments as a tuple:
def jumlahkan(*args):
return sum(args)
print(jumlahkan(1, 2, 3))
print(jumlahkan(10, 20))The call jumlahkan(1, 2, 3) gathers all positional arguments into a tuple args. This is useful when the number of arguments isn't fixed, like a log function that accepts any number of messages.
**kwargs captures flexible keyword arguments as a dict:
def cetak_profil(**kwargs):
for kunci, nilai in kwargs.items():
print(f"{kunci}: {nilai}")
cetak_profil(nama="Arman", peran="Engineer", tim="Platform")The call cetak_profil(nama="Arman", peran="Engineer") gathers keyword arguments into a dict. **kwargs is very commonly used to forward configuration options without declaring every parameter.
A classic Python trap is using a mutable object as a default value. Defaults are evaluated once when the function is defined, not on every call:
def tambah(item, daftar=[]):
daftar.append(item)
return daftar
print(tambah(1))
print(tambah(2))The second call to tambah(2) outputs [1, 2], not [2]! Because daftar=[] is created once and shared across calls. This is a well-known bug that's hard to track down.
The standard solution is to use None as the default and create a new object inside the function:
def tambah(item, daftar=None):
if daftar is None:
daftar = []
daftar.append(item)
return daftar
print(tambah(1))
print(tambah(2))The pattern if daftar is None: daftar = [] guarantees a new object is created on every call. The golden rule: never use a list, dict, or set as a default parameter.
Python treats functions as first-class objects: functions can be stored, passed as arguments, and returned. A function that accepts or returns functions is called a higher-order function:
def terapkan(fungsi, nilai):
return fungsi(nilai)
def kali_dua(x):
return x * 2
print(terapkan(kali_dua, 5))The call terapkan(kali_dua, 5) passes the function kali_dua as an argument. This pattern is the foundation of many functional programming techniques and is widely used in libraries like pandas and Django.
A lambda is a one-line anonymous function for simple cases:
kuadrat = lambda x: x * x
print(kuadrat(7))
data = [(1, "apel"), (3, "ceri"), (2, "mangga")]
data.sort(key=lambda pasangan: pasangan[1])
print(data)lambda x: x * x defines a function without a name. When sorting, data.sort(key=lambda pasangan: pasangan[1]) sorts by the second element. Use lambda for short logic; for complex logic, define a named function.
functools.partial locks part of a function's arguments into a new function:
from functools import partial
def pangkat(eksponen, basis):
return basis ** eksponen
kuadrat = partial(pangkat, 2)
print(kuadrat(5))partial(pangkat, 2) creates a new function that already locks in eksponen=2. Calling kuadrat(5) is equivalent to pangkat(2, 5). This is useful for building specialized functions from general ones.
lru_cache stores function call results so repeated calls with the same arguments become instant:
from functools import lru_cache
@lru_cache(maxsize=128)
def fibonacci(n):
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
print(fibonacci(30))The decorator @lru_cache(maxsize=128) adds caching to the fibonacci function. Without caching, the 30th calculation repeats millions of times; with caching, every value is computed once. This memoization is very effective for recursive functions and repeated queries.
The itertools module provides efficient iterator-building functions. Some of the most useful ones:
from itertools import chain, islice, cycle
angka = [1, 2, 3]
huruf = ["a", "b"]
print(list(chain(angka, huruf)))
print(list(islice(cycle(angka), 5)))chain(angka, huruf) concatenates iterables into one. islice(cycle(angka), 5) cycles an iterable infinitely, then slices the first five elements. These utilities are memory-efficient because they all work lazily.
Key takeaways:
In the next episode, episode 5, we'll cover advanced data types and collections — the collections module with deque, defaultdict, Counter, and namedtuple, typing basics, and memory and performance considerations of data structures. You'll learn to pick the right data structure for real problems!