
Part 24 gave you the foundation: def, parameters, return, scope (LEGB), docstrings, and the idea that functions are first-class objects. You can now write a function, call it, and get a result back.
But real-world functions need more flexibility. Think about the functions you already use — print() accepts any number of arguments: print("a"), print("a", "b", "c"). .get() works with or without a default: d.get("key") or d.get("key", 0). How do they do that? How does one function handle different numbers of inputs?
This part answers that question. You will learn default parameters, ***args, ****kwargs, and keyword-only parameters — the tools that make functions truly flexible. These are the same tools that power every Python framework you will use.
A parameter can have a default value. If the caller does not provide an argument, the default is used:
def greet(name="Guest"):
return f"Hello, {name}!"
print(greet("Shyam")) # Hello, Shyam!
print(greet()) # Hello, Guest!
Default parameters make functions flexible — the same function handles both cases.
def create_user(name, role="member"): # Correct
return {"name": name, "role": role}
def create_user(role="member", name): # SyntaxError
pass
This is one of the most important lessons in this entire series.
def add_item(item, items=[]):
items.append(item)
return items
print(add_item("apple")) # ['apple']
print(add_item("banana")) # ['apple', 'banana'] — Wait, what?
The second call shows both items. The list was not reset.
Default arguments are evaluated once — when the function is defined, not each time it is called. The empty list [] is created once and shared across all calls. Every call appends to the same list object.
We can prove this. In Part 24, you learned that functions are objects. Like any object, they have attributes. One of them is __defaults__ — a tuple where Python stores the default values for your parameters:
print(type(add_item)) # <class 'function'>
print(add_item.__defaults__) # (['apple', 'banana'],)
That ['apple', 'banana'] is your "empty" list default — it was mutated by the previous calls. This proves there is only one list object shared across all calls. You will see more function attributes like __name__ and __doc__ in Part 45 when we learn decorators.
Use None as the default and create a new list inside the function:
def add_item(item, items=None):
if items is None:
items = []
items.append(item)
return items
print(add_item("apple")) # ['apple']
print(add_item("banana")) # ['banana'] — Correct!
Now each call gets its own fresh list.
This pattern applies to any mutable default — lists, dictionaries, sets. Never use a mutable object as a default parameter value.
Sometimes you do not know how many arguments a function will receive:
def total(*numbers):
return sum(numbers)
print(total(1, 2, 3)) # 6
print(total(10, 20, 30, 40)) # 100
print(total(5)) # 5
*numbers collects all positional arguments into a tuple.
def show_args(*args):
print(type(args)) # <class 'tuple'>
for arg in args:
print(arg)
show_args("a", "b", "c")
def log_message(level, *messages):
for msg in messages:
print(f"[{level}] {msg}")
log_message("INFO", "Server started", "Listening on port 8000")
Output:
[INFO] Server started
[INFO] Listening on port 8000
The first argument goes to level. Everything else is collected by *messages.
**kwargs collects all keyword arguments into a dictionary:
def create_profile(**details):
print(type(details)) # <class 'dict'>
for key, value in details.items():
print(f"{key}: {value}")
create_profile(name="Shyam", age=28, city="Bangalore")
Output:
<class 'dict'>
name: Shyam
age: 28
city: Bangalore
Notice something? ***args gives you a tuple (Part 19). **kwargs gives you a dictionary (Part 21).** Python chose the two most useful data structures to collect your arguments — a tuple for unnamed values (ordered, immutable), a dictionary for named values (key-value pairs). This is why we learned data structures first. Everything connects.
That is also why you can call .items() on details above — it is literally a dict, with all the methods you already know from Parts 21–22.
def register_user(username, **extras):
print(f"User: {username}")
for key, value in extras.items():
print(f" {key}: {value}")
register_user("shyam_dev", role="admin", team="backend")
Output:
User: shyam_dev
role: admin
team: backend
Adding * in the parameter list forces all following parameters to be passed by name:
def connect(host, port, *, timeout=30, retries=3):
print(f"Connecting to {host}:{port}")
print(f"Timeout: {timeout}, Retries: {retries}")
connect("localhost", 8080, timeout=10, retries=5) # Works
connect("localhost", 8080, 10, 5) # TypeError
The second call fails because timeout and retries must be passed as keyword arguments.
Keyword-only parameters prevent ambiguity. When a function has many options, forcing keyword arguments makes calls self-documenting:
# Unclear — what do 30 and 5 mean?
connect("localhost", 8080, 30, 5)
# Clear — every argument is labeled
connect("localhost", 8080, timeout=30, retries=5)
When combining all parameter types, they must follow this order:
def func(positional, default="value", *args, keyword_only, **kwargs):
pass
*args* or *args)**kwargsdef api_call(endpoint, method="GET", *path_params, timeout=30, **headers):
print(f"{method} {endpoint}")
if path_params:
print(f" Path: {path_params}")
print(f" Timeout: {timeout}")
for key, value in headers.items():
print(f" {key}: {value}")
api_call("/users", "POST", "v2", timeout=10, Authorization="Bearer abc123")
Output:
POST /users
Path: ('v2',)
Timeout: 10
Authorization: Bearer abc123
Python allows you to annotate parameter types and return types:
def greet(name: str) -> str:
return f"Hello, {name}!"
def add(a: int, b: int) -> int:
return a + b
def divide(a: float, b: float) -> float | None:
if b == 0:
return None
return a / b
Type hints do not enforce types at runtime — Python will not raise an error if you pass the wrong type. They serve as documentation and enable tools like mypy to catch type errors before running the code.
We will cover type hints in depth later. For now, know they exist and start reading them in other people's code.
requests.get(url, timeout=30) — the timeout has a sensible default but can be overridden.**kwargs for flexibility.Build a profile card generator:
create_profile(name, age, **extras):**extrasprint_separator(char="-", length=40):char repeated length timesdisplay_profiles(*profiles):create_profile for at least 3 people with different extra fields (city, job, hobby, etc.)display_profilesExample output:
----------------------------------------
Name: Shyam
Age: 28
city: Bangalore
job: AI Developer
----------------------------------------
Name: Alice
Age: 25
hobby: Photography
language: Kannada
----------------------------------------
Save as src/profile_cards.py.
Next: Part 26 — Recursion Part 1. A function calling itself. This is where you start thinking like an algorithm designer — breaking big problems into smaller identical problems.
INTERVIEWER ಕೇಳಿದ್ರೆ SILENT ಆಗ್ತೀಯ! | Lambda, map, filter, reduce | Python in Kannada | Part-28
Part 28
INTERVIEWER ಕೇಳಿದ್ರೆ SILENT ಆಗ್ತೀಯ! | Lambda, map, filter, reduce | Python in Kannada | Part-28
Part 28