InterviewQAs

Python Functions and Arguments

Download as PDF
All questions in this page are included
Preparing…
Download PDF
PFA
Python Functions and Arguments

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.

Question 01

Explain the difference between positional and keyword arguments in Python with an example.

EASY

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.

Question 02

Which of the following are valid ways to define default arguments in Python?

MEDIUM
  • A def func(a=10, b=20): pass
  • B def func(a, b=20, c=30): pass
  • C def func(a=10, b): pass
  • D def func(a, b, c=5): pass

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.

Question 03

Write a Python function that calculates the total cost including tax, where tax has a default value of 5%.

EASY

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))
Question 04

What are *args and **kwargs in Python and when would you use them?

MEDIUM

`*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.

Question 05

Consider the following function call: `func(1, 2, 3, x=10, y=20)`. Which function signatures are compatible?

HARD
  • A def func(a, b, c, x, y): pass
  • B def func(a, b, c, **kwargs): pass
  • C def func(a, b, c, x=0, y=0): pass
  • D def func(*args, **kwargs): pass

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.

Question 06

Write a Python function that swaps the values of two variables and returns them.

MEDIUM

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)
Question 07

Explain the concept of argument mutability and its effect on default argument values.

MEDIUM

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.

Question 08

Implement a Python function that logs function calls including their arguments and return value using a decorator.

HARD

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)
Question 09

Which statements are true about Python keyword-only arguments?

MEDIUM
  • A They must come after a * or *args in the function signature
  • B They can be provided positionally or as keyword arguments
  • C They enforce clarity by requiring explicit naming in calls
  • D They allow variable number of keyword arguments via **kwargs

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.

Question 10

Write a Python function that merges an arbitrary number of dictionaries into one.

HARD

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))
Question 11

What is the difference between a function parameter and a function argument in Python?

EASY

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.

Question 12

Which statements are correct about Python function return values?

MEDIUM
  • A Functions can return multiple values as a tuple
  • B A function always returns None if there is no return statement
  • C Functions can only return a single object and cannot use tuples
  • D Returning a mutable object can affect future function calls if used as a default parameter

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.

Question 13

Write a function that takes any number of integers and returns their sum and average.

MEDIUM

*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))
Question 14

Explain how argument evaluation works for default parameter values in Python.

HARD

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.

Question 15

Which of the following are correct ways to unpack arguments in Python function calls?

MEDIUM
  • A func(*[1,2,3])
  • B func(**{'x':10, 'y':20})
  • C func(*{'x':10, 'y':20})
  • D func(*args, **kwargs)

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.

Question 16

Create a decorator that measures execution time of any function it wraps.

HARD

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)
Question 17

How can you enforce keyword-only arguments in Python functions and why is it useful?

MEDIUM

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.

Question 18

Write a function that takes any number of keyword arguments and returns a new dictionary with keys uppercased.

MEDIUM

**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'))
Question 19

Which statements are true regarding closures in Python?

HARD
  • A A closure occurs when a nested function captures variables from the enclosing scope
  • B Closures allow the nested function to retain access to the captured variables even after the outer function has finished executing
  • C Closures require the use of global variables to persist state
  • D Closures are commonly used for function factories or decorators

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.

Question 20

Write a Python function that accepts another function and a list, and applies the function to each element of the list, returning a new list.

HARD

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]))
Question 21

Explain the difference between a function returning a value and a function modifying a mutable argument.

EASY

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.

Question 22

Which of the following statements are true about Python recursive functions?

MEDIUM
  • A They must have a base case to prevent infinite recursion
  • B They always return a tuple
  • C They can call themselves with modified arguments
  • D Recursion depth is unlimited in Python

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.

Question 23

Write a Python function using recursion to calculate the factorial of a number.

MEDIUM

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))
Question 24

Discuss the potential pitfalls of using mutable default arguments and strategies to avoid them.

HARD

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.

Question 25

Which statements about Python function annotations are correct?

MEDIUM
  • A Function annotations enforce type checking at runtime
  • B They provide metadata about parameters and return values
  • C Annotations are optional and do not affect function execution
  • D They can be accessed via the `__annotations__` attribute

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.

Question 26

Write a decorator that ensures a function's arguments are integers and raises TypeError otherwise.

HARD

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))
Question 27

How does Python handle argument passing—by value or by reference?

MEDIUM

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.

Question 28

Write a Python function that accepts a variable number of keyword arguments representing product prices and returns their total.

MEDIUM

**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))
Question 29

Which statements about Python closures are true?

HARD
  • A Closures allow a function to access variables from its enclosing scope even after the outer function has returned
  • B Closures are created when a nested function references variables from the enclosing function
  • C Closures cannot modify the enclosed variables unless nonlocal or global is used
  • D Closures automatically prevent memory leaks

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.

Question 30

Create a function that returns another function which multiplies its input by a fixed factor.

HARD

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))