Functions are the building blocks of any Python program. We define them with def, and they can accept a flexible variety of arguments.
Basic Syntax
def greet(name):
"""Return a greeting message.""" # docstring — always a good habit
return f"Hello, {name}!"
result = greet("Manish") # "Hello, Manish!"
If we don’t explicitly return something, the function returns None. And yes, return and print are very different things — return sends a value back to the caller, print just displays text on screen.
Positional vs Keyword Arguments
def create_user(name, age, city):
return {"name": name, "age": age, "city": city}
# Positional — order matters
create_user("Manish", 25, "Delhi")
# Keyword — order doesn't matter
create_user(city="Delhi", name="Manish", age=25)
# Mix of both — positional args must come first
create_user("Manish", city="Delhi", age=25)
Default Argument Values
We can give parameters default values. Parameters with defaults must come after those without.
def connect(host, port=5432, timeout=30):
print(f"Connecting to {host}:{port} (timeout: {timeout}s)")
connect("localhost") # uses defaults: port=5432, timeout=30
connect("localhost", port=3306) # overrides port only
The Mutable Default Argument Trap
This is a classic interview question. Default arguments are evaluated once when the function is defined, not each time it’s called.
# BAD — the same list is shared across all calls
def add_item(item, items=[]):
items.append(item)
return items
print(add_item("a")) # ['a']
print(add_item("b")) # ['a', 'b'] — surprise! It remembered the old list
# GOOD — use None as default and create a new list inside
def add_item(item, items=None):
if items is None:
items = []
items.append(item)
return items
*args and **kwargs
*args collects extra positional arguments into a tuple. **kwargs collects extra keyword arguments into a dict.
def log(message, *args, **kwargs):
print(f"MSG: {message}")
print(f"Extra args: {args}") # tuple
print(f"Extra kwargs: {kwargs}") # dict
log("hello", 1, 2, 3, level="info", source="api")
# MSG: hello
# Extra args: (1, 2, 3)
# Extra kwargs: {'level': 'info', 'source': 'api'}
The names args and kwargs are just convention. We could call them *stuff and **options — the * and ** are what matter.
Argument Unpacking
The * and ** operators can also be used when calling functions to unpack sequences and dicts.
def add(a, b, c):
return a + b + c
nums = [1, 2, 3]
add(*nums) # same as add(1, 2, 3)
config = {"a": 10, "b": 20, "c": 30}
add(**config) # same as add(a=10, b=20, c=30)
Parameter Order Rule
When mixing all types of parameters, the order must be:
- Regular positional parameters
*args- Keyword-only parameters (anything after
*args) **kwargs
def example(a, b, *args, option=True, **kwargs):
pass
In simple language, *args gives us a tuple of “all the extras”, and **kwargs gives us a dict of “all the named extras”. Together, they make our functions incredibly flexible.