Python functions are the cornerstone of modular and maintainable code. Understanding how to define and call functions with various types of arguments is critical for building scalable applications.
Arguments in Python can be positional, keyword, default, or variable-length, offering flexibility in function design. Choosing the right type of argument can simplify code, reduce errors, and improve readability.
Advanced techniques like argument unpacking with *args and **kwargs allow functions to handle dynamic input, which is particularly useful when integrating APIs, handling user input, or building decorators.
Practical use cases often involve mixing positional and keyword arguments. Recognizing how Python binds values during function calls helps prevent subtle bugs, especially when default values or mutable types are involved.
Understanding Python function arguments also includes grasping scope, closures, and parameter evaluation. This knowledge is vital for writing efficient and predictable code in production environments.
Positional arguments are values passed to a function in the order the parameters are defined. The function assigns these values based strictly on their position. For example, in a function `def greet(name, age)`, calling `greet('Alice', 25)` assigns 'Alice' to `name` and 25 to `age`.
Keyword arguments, on the other hand, explicitly specify the parameter name during the function call. This allows you to pass arguments in any order. Using the previous example, `greet(age=25, name='Alice')` achieves the same result but with improved readability and flexibility.
Combining both types is also possible, but positional arguments must always come before keyword arguments. Understanding this distinction prevents common bugs and improves code clarity when functions have multiple parameters.
Default arguments must follow non-default arguments. Option C is invalid because `b` has no default value but comes after `a` which has a default.
Options A, B, and D correctly position default values and comply with Python's parameter rules.
This function uses a default argument `tax` set to 0.05. If the caller does not provide a tax value, the function automatically applies 5%.
Using default arguments makes functions flexible and avoids repetition in code where common values are frequently used.
// Python
def total_cost(price, quantity, tax=0.05):
return price * quantity * (1 + tax)
# Example call
print(total_cost(100, 2))
`*args` allows a function to accept a variable number of positional arguments. Inside the function, `args` is a tuple containing all additional arguments passed by the caller.
`**kwargs` allows a function to accept a variable number of keyword arguments. Inside the function, `kwargs` is a dictionary containing the key-value pairs provided during the call.
These are particularly useful for creating flexible APIs, wrapper functions, or decorators where the exact number or names of arguments may not be known in advance. They help prevent code duplication and support dynamic behavior.
Option B works because **kwargs can capture keyword arguments `x` and `y`.
Option C works because `x` and `y` have default values and can be overridden by the call.
Option D works because *args captures positional arguments and **kwargs captures the keyword arguments.
Option A is invalid because Python does not allow mixing keyword arguments without default values in this manner.
Python supports multiple return values as tuples. The function returns a tuple `(b, a)` and unpacks it directly when assigned to `x` and `y`.
This approach avoids temporary variables and makes swaps concise and readable, commonly used in algorithms and data processing tasks.
// Python
def swap(a, b):
return b, a
# Example call
x, y = swap(10, 20)
print(x, y)
In Python, default argument values are evaluated once at function definition time. If a mutable object (like a list or dictionary) is used as a default, modifications persist across function calls.
For example, using `def append_item(item, lst=[])` and calling `append_item(1)` multiple times will accumulate items in the same list.
To prevent this, immutable types should be used for defaults, or `None` should be used with explicit initialization inside the function. This prevents unintended side effects in real-world applications.
This decorator function takes any function `func` and wraps it in another function `wrapper` that logs the call details and result.
Using *args and **kwargs ensures compatibility with functions of any signature, making it practical for debugging and monitoring in production systems.
// Python
def log_calls(func):
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__} with args={args}, kwargs={kwargs}")
result = func(*args, **kwargs)
print(f"{func.__name__} returned {result}")
return result
return wrapper
@log_calls
def multiply(a, b):
return a * b
multiply(3, 5)
Keyword-only arguments are defined after a `*` in the signature, ensuring they cannot be passed positionally.
This improves code clarity and reduces ambiguity in function calls. **kwargs is a separate mechanism to capture arbitrary keyword arguments, not a keyword-only argument itself.
This function uses *args to accept any number of dictionaries. `dict.update` merges each dictionary into the result.
It handles overlapping keys by overwriting with the later dictionary's value, which is a common requirement in data processing and configuration merging scenarios.
// Python
def merge_dicts(*dict_list):
result = {}
for d in dict_list:
result.update(d)
return result
# Example call
d1 = {'a': 1}
d2 = {'b': 2}
d3 = {'a': 3, 'c': 4}
print(merge_dicts(d1, d2, d3))
A function parameter is a variable defined in the function signature that specifies what kind of input the function expects. For example, in `def add(x, y)`, `x` and `y` are parameters.
A function argument is the actual value passed to the function when calling it. For example, `add(2, 3)` passes `2` and `3` as arguments.
Understanding this distinction is important because parameters define the interface of a function, while arguments are the runtime data that flows through that interface.
Python functions can return multiple values by packing them into a tuple. If no return statement is provided, the function implicitly returns None.
Returning mutable objects or using them as default parameters can lead to side effects in subsequent calls, which is a common real-world pitfall.
*args allows the function to accept an arbitrary number of positional arguments. Summing them and calculating the average demonstrates practical handling of variable input sizes.
This pattern is useful in scenarios such as processing numeric input from user forms or aggregating statistics from datasets.
// Python
def sum_and_average(*numbers):
total = sum(numbers)
avg = total / len(numbers) if numbers else 0
return total, avg
# Example call
print(sum_and_average(10, 20, 30, 40))
Default parameter values are evaluated only once at function definition time, not each time the function is called. This can lead to unexpected behavior if a mutable object is used as a default.
For example, using `def append_item(value, lst=[])` will result in the same list being shared across multiple calls, causing accumulated values.
To avoid this, it is common to use `None` as the default and initialize the mutable object inside the function: `lst = lst or []`. This ensures a fresh object on each call and avoids side effects.
The * operator unpacks sequences into positional arguments, and ** unpacks dictionaries into keyword arguments.
Option C is invalid because unpacking a dictionary with * only gives keys as positional arguments, not values, which usually leads to errors.
The decorator wraps the original function, records the start and end time, and prints the execution duration.
Using *args and **kwargs ensures compatibility with functions of any signature. This is commonly applied in performance monitoring or debugging in production code.
// Python
import time
def time_it(func):
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f"{func.__name__} executed in {end - start:.6f} seconds")
return result
return wrapper
@time_it
def compute_sum(n):
return sum(range(n))
compute_sum(1000000)
Keyword-only arguments are defined after a * in the function signature. This ensures that callers must provide values using the parameter names, e.g., `def greet(*, name, age):`.
Enforcing keyword-only arguments improves readability and prevents positional errors, especially in functions with many parameters.
It is particularly useful in APIs or configuration-heavy functions, ensuring that important options are always named explicitly in the call.
**kwargs captures all keyword arguments as a dictionary. Dictionary comprehension is then used to create a new dictionary with transformed keys.
This technique is practical in data normalization, API request preprocessing, or configuration handling where keys need standardization.
// Python
def uppercase_keys(**kwargs):
return {k.upper(): v for k, v in kwargs.items()}
# Example call
print(uppercase_keys(a=1, b=2, name='Vijay'))
Closures are nested functions that capture variables from their outer scope. They retain access to those variables even after the outer function exits.
Closures do not require global variables and are widely used for maintaining state in decorators, creating function factories, and encapsulating behavior.
This function demonstrates higher-order function usage, where functions are treated as first-class objects and passed as arguments.
It applies `func` to each element using a list comprehension. This pattern is widely used in functional programming and data transformation tasks.
// Python
def apply_function(func, items):
return [func(item) for item in items]
# Example call
print(apply_function(lambda x: x**2, [1, 2, 3, 4]))
A function returning a value explicitly passes information back to the caller through the return statement. For example, `def add(x, y): return x + y` returns the sum of two numbers.
A function modifying a mutable argument, such as a list or dictionary, changes the object in-place. For example, `def append_item(lst, item): lst.append(item)` modifies the original list passed by the caller.
Understanding this difference is crucial because modifications to mutable objects persist across function calls, while return values need to be captured explicitly.
Recursive functions can call themselves with new arguments to approach a base case. Without a base case, they will raise a RecursionError.
Python has a recursion depth limit by default (usually 1000), so recursion is not unlimited.
This function uses a base case to stop recursion when `n` is 0 or 1. Each recursive call multiplies `n` by the factorial of `n-1`.
Recursive factorials are commonly used to demonstrate recursion, though iterative solutions may be preferred in performance-critical scenarios.
// Python
def factorial(n):
if n == 0 or n == 1:
return 1
return n * factorial(n - 1)
# Example call
print(factorial(5))
Mutable default arguments are evaluated once when the function is defined. If the object is modified, those changes persist across calls, which can lead to unexpected behavior.
For example, using `def append_item(value, lst=[]): lst.append(value)` will accumulate values from multiple calls in the same list.
A common strategy to avoid this is using `None` as the default and initializing inside the function: `lst = lst or []`. This ensures a fresh mutable object on each call, preventing side effects.
Function annotations allow developers to document expected types or other metadata, but they are not enforced by Python unless used with external type checkers.
The `__annotations__` attribute provides access to this metadata programmatically.
The decorator checks all positional and keyword arguments for integer types before calling the function.
This pattern is practical in scenarios where functions must enforce strict type contracts to prevent runtime errors.
// Python
def enforce_ints(func):
def wrapper(*args, **kwargs):
for arg in args:
if not isinstance(arg, int):
raise TypeError(f"Argument {arg} is not an int")
for k, v in kwargs.items():
if not isinstance(v, int):
raise TypeError(f"Argument {k}={v} is not an int")
return func(*args, **kwargs)
return wrapper
@enforce_ints
def add(a, b):
return a + b
print(add(3, 4))
Python uses a strategy often called 'call by object reference' or 'call by assignment'. Arguments are passed as references to objects, not copies.
Immutable objects (like integers, strings, tuples) behave like pass-by-value because modifications create new objects.
Mutable objects (like lists, dictionaries) behave like pass-by-reference because modifications affect the original object. Understanding this helps avoid unintended side effects.
**kwargs captures all keyword arguments as a dictionary, allowing dynamic handling of input values.
Using `sum(prices.values())` efficiently aggregates all provided prices, which is useful in real-world billing or inventory computations.
// Python
def total_price(**prices):
return sum(prices.values())
# Example call
print(total_price(apple=50, banana=30, orange=20))
Closures capture variables from their outer scope and maintain access even after the outer function finishes execution.
To modify a captured variable, the `nonlocal` keyword must be used. Closures do not automatically manage memory and careful references are required to avoid leaks.
This function demonstrates a closure where the inner function `multiply` retains access to `factor` from the outer function.
This pattern is useful for creating configurable behavior, such as scaling factors, function factories, or customized callbacks.
// Python
def multiplier(factor):
def multiply(n):
return n * factor
return multiply
# Example call
double = multiplier(2)
triple = multiplier(3)
print(double(5))
print(triple(5))