Python Zero to Advanced #4: Functions, *args, **kwargs, Scope & Lambda
Copy-pasting the same five lines in three places is a recipe for bugs. Functions let you give a name to a piece of logic and reuse it anywhere. In this part you will learn how to define functions, pass arguments in every way Python allows, understand where variables live (scope), and write tiny anonymous functions with lambda.
Defining and calling functions
Use def, a name, parentheses with parameters, and a colon. The body is indented. return sends a value back to the caller; a function without return returns None.
def greet(name):
"""Return a greeting for the given name."""
return f"Hello, {name}!"
def log(message):
print(f"[LOG] {message}")
result = greet("Siti")
print(result)
print(log("started"))
print(greet.__doc__)
Output:
Hello, Siti!
[LOG] started
None
Return a greeting for the given name.
The triple-quoted string right below def is a docstring. Editors show it as a tooltip and help(greet) prints it, so get into the habit of writing one.
A function can return several values at once. Python packs them into a tuple, which you can unpack on the caller side:
def min_max(numbers):
return min(numbers), max(numbers)
low, high = min_max([7, 3, 9, 1])
print(low, high)
Output:
1 9
Positional, keyword, and default arguments
Arguments can be passed by position or by name. Parameters can have default values, which makes them optional.
def make_order(item, qty=1, express=False):
mode = "express" if express else "regular"
return f"{qty}x {item} ({mode})"
print(make_order("Coffee"))
print(make_order("Tea", 3))
print(make_order("Juice", express=True))
print(make_order(qty=2, item="Water"))
Output:
1x Coffee (regular)
3x Tea (regular)
1x Juice (express)
2x Water (regular)
Keyword arguments make calls self-documenting: make_order("Juice", express=True) is much clearer than make_order("Juice", 1, True).
The mutable default trap
Default values are evaluated once, when the function is defined. A mutable default such as a list is therefore shared between calls:
def add_tag_bad(tag, tags=[]):
tags.append(tag)
return tags
print(add_tag_bad("python"))
print(add_tag_bad("web")) # surprise!
def add_tag(tag, tags=None):
if tags is None:
tags = []
tags.append(tag)
return tags
print(add_tag("python"))
print(add_tag("web"))
Output:
['python']
['python', 'web']
['python']
['web']
*args and **kwargs
Sometimes you do not know how many arguments will be passed. *args collects extra positional arguments into a tuple, and **kwargs collects extra keyword arguments into a dict. The names are a convention; the stars are what matter.
def total(*args):
print("args =", args)
return sum(args)
print(total(1, 2, 3))
def profile(name, **kwargs):
print("name =", name)
for key, value in kwargs.items():
print(f" {key}: {value}")
profile("Budi", city="Jakarta", role="engineer")
def everything(a, *args, b=0, **kwargs):
print(a, args, b, kwargs)
everything(1, 2, 3, b=4, c=5)
Output:
args = (1, 2, 3)
6
name = Budi
city: Jakarta
role: engineer
1 (2, 3) 4 {'c': 5}
Stars also work in the other direction: they unpack a list or dict into arguments when calling a function.
def describe(name, age, city):
return f"{name}, {age}, from {city}"
data = ["Siti", 30, "Surabaya"]
info = {"name": "Andi", "age": 25, "city": "Medan"}
print(describe(*data))
print(describe(**info))
Output:
Siti, 30, from Surabaya
Andi, 25, from Medan
Parameters after a bare * are keyword-only, a great way to force clarity for boolean flags: def send(to, *, urgent=False) can only be called as send("budi@example.com", urgent=True).
Scope: where variables live
Python resolves names using the LEGB rule: Local (inside the current function), Enclosing (outer functions), Global (module level), and Built-in (names like len). Assigning to a name inside a function creates a local variable unless you declare otherwise.
counter = 0 # global
def increment_local():
counter = 100 # new local variable, global untouched
return counter
def increment_global():
global counter
counter += 1
print(increment_local(), counter)
increment_global()
increment_global()
print(counter)
def make_counter():
count = 0 # enclosing scope
def step():
nonlocal count
count += 1
return count
return step
c = make_counter()
print(c(), c(), c())
Output:
100 0
2
1 2 3
make_counter returns a closure: the inner function remembers count even after the outer function has finished. Use global sparingly; passing values in and returning results is almost always cleaner.
Lambda and functions as values
In Python, functions are objects: you can store them in variables, put them in lists, and pass them to other functions. A lambda is a one-expression anonymous function, most useful as a short argument to sorted(), map(), or filter().
square = lambda x: x * x
print(square(7))
users = [("Budi", 28), ("Siti", 22), ("Andi", 35)]
print(sorted(users, key=lambda u: u[1]))
print(sorted(users, key=lambda u: u[0]))
nums = [1, 2, 3, 4, 5, 6]
print(list(map(lambda n: n * 10, nums)))
print(list(filter(lambda n: n % 2 == 0, nums)))
def apply(func, value):
return func(value)
print(apply(str.upper, "python"))
Output:
49
[('Siti', 22), ('Budi', 28), ('Andi', 35)]
[('Andi', 35), ('Budi', 28), ('Siti', 22)]
[10, 20, 30, 40, 50, 60]
[2, 4, 6]
PYTHON
If a lambda grows beyond a simple expression, give it a proper name with def. Readability wins.
Tips
- Keep functions small and focused on one task; a good name often removes the need for comments.
- Prefer returning values over printing inside functions, so the result can be reused and tested.
- Never use a mutable object (list, dict) as a default value; use
Noneinstead. - Forgetting
returnis a common bug: the caller silently getsNone.
Exercise
Write average(*numbers, precision=2) that returns the rounded average, or None if no numbers are given.
def average(*numbers, precision=2):
if not numbers:
return None
return round(sum(numbers) / len(numbers), precision)
print(average(80, 90, 75))
print(average(80, 90, 75, precision=0))
print(average())
Output:
81.67
82.0
None
Summary
- Define functions with
def, document them with docstrings, and return results withreturn. - Use defaults and keyword arguments for clear calls;
*args/**kwargsaccept a flexible number of arguments. - Names are resolved with LEGB; closures remember enclosing variables.
- Lambdas are short anonymous functions, ideal as
keyarguments.
That wraps up the beginner block. In Part 5 we dive into Python's powerful collections: lists, tuples, dictionaries, sets, and comprehensions.
Related articles
PostgreSQL Zero to Advanced #1: Introduction & Setup
Start the series by understanding what a relational database is, installing PostgreSQL locally, and getting comfortable with psql commands like \l, \c and \dt.
PHP Zero to Advanced #1: Introduction & Setup
Start your PHP journey: what PHP is, how to install PHP 8.2+, run the built-in development server, and write your very first script with echo.
Ruby on Rails Zero to Advanced #1: Ruby Basics
Start the Rails journey with the language underneath it: irb, variables, strings, puts vs p, and symbols explained with runnable examples.