Gyaan

Functions and Arguments

beginner functions args kwargs parameters

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:

  1. Regular positional parameters
  2. *args
  3. Keyword-only parameters (anything after *args)
  4. **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.