Python Functions


Python Functions Interview with follow-up questions

1. Can you explain what a function is in Python and why it is used?

A function is a named, reusable block of code that performs a specific task, defined with def (or anonymously with lambda). You call it by name, optionally passing arguments, and it can return a value with return (or None implicitly).

def greet(name: str) -> str:
    return f"Hello, {name}"

Why functions are used:

  • Reuse and DRY: write the logic once, call it many times instead of duplicating code.
  • Abstraction: a well-named function lets callers use what it does without caring how.
  • Organization and testability: small, single-purpose functions are easier to read, test, and reason about.
  • Composition: functions are first-class objects in Python — you can pass them as arguments, return them, and store them — which underpins callbacks, map/filter/sorted(key=...), closures, and decorators.

Follow-ups an interviewer may chain into: the mutable-default-argument trap (def f(x=[]) shares one list — use None); the difference between parameters and arguments; *args/**kwargs for variadic signatures; and that annotating signatures (name: str -> str) is expected in professional 2026 code and checked by mypy/pyright.

↑ Back to top

Follow-up 1

How do you define a function in Python?

In Python, a function is defined using the def keyword followed by the function name, parentheses, and a colon. The function body is indented and contains the code that will be executed when the function is called.

Here's an example of a function definition in Python:

# Function definition

def greet():
    print('Hello, World!')

Follow-up 2

What is the difference between a function and a method in Python?

In Python, a function is a block of code that is defined outside of any class, whereas a method is a function that is defined inside a class. Functions can be called independently, whereas methods are called on an instance of a class or the class itself.

Here's an example to illustrate the difference:

# Function definition

def greet():
    print('Hello, World!')

# Method definition

class Greeting:
    def greet(self):
        print('Hello, World!')

# Function call

greet()

# Method call

obj = Greeting()
obj.greet()

Follow-up 3

Can you give an example of a function in Python?

Sure! Here's an example of a function that calculates the sum of two numbers:

# Function definition

def add_numbers(a, b):
    return a + b

# Function call

result = add_numbers(5, 3)
print(result)  # Output: 8

Follow-up 4

What is the return statement in Python functions?

The return statement is used in Python functions to specify the value that the function should return when it is called. It is optional and can be used to return a single value or multiple values.

Here's an example that demonstrates the use of the return statement:

# Function definition

def add_numbers(a, b):
    return a + b

# Function call

result = add_numbers(5, 3)
print(result)  # Output: 8

Follow-up 5

Can you explain the concept of function arguments in Python?

In Python, function arguments are the values that are passed to a function when it is called. There are three types of function arguments:

  1. Positional arguments: These are arguments that are passed based on their position in the function call.

  2. Keyword arguments: These are arguments that are passed with a keyword and an equal sign, allowing you to specify the argument name explicitly.

  3. Default arguments: These are arguments that have a default value assigned to them, which is used if the argument is not provided in the function call.

Here's an example that demonstrates the use of different types of function arguments:

# Function definition

def greet(name, message='Hello', age=None):
    print(f'{message}, {name}!')
    if age is not None:
        print(f'You are {age} years old.')

# Function calls

greet('Alice')
# Output: Hello, Alice!

greet('Bob', message='Hi')
# Output: Hi, Bob!

greet('Charlie', age=25)
# Output: Hello, Charlie!
#         You are 25 years old.

2. What is a lambda function in Python?

A lambda is a small anonymous function defined inline with the lambda keyword. It takes any number of arguments but its body is a single expression, whose value is returned automatically (no return):

square = lambda x: x * x
add = lambda a, b: a + b

In practice you almost never name a lambda (the example above is exactly what def is for). The idiomatic use is as a short throwaway callable passed to another function:

points = [(1, 5), (3, 2), (2, 8)]
points.sort(key=lambda p: p[1])          # sort by second element
evens = list(filter(lambda n: n % 2 == 0, range(10)))

Follow-ups interviewers raise:

  • lambda vs def: a lambda is just one expression with no name, docstring, statements, or annotations. If it needs a name, a docstring, or more than one line, use def — PEP 8 explicitly says don't assign a lambda to a variable.
  • Often a comprehension is clearer than map/filter + lambda: [n for n in range(10) if n % 2 == 0].
  • Late-binding gotcha: lambdas in a loop capture the variable, not its value — [lambda: i for i in range(3)] all return 2. Bind with a default arg: lambda i=i: i.
↑ Back to top

Follow-up 1

Can you give an example of a lambda function?

Sure! Here's an example of a lambda function that adds two numbers:

add = lambda x, y: x + y
print(add(3, 5))  # Output: 8

Follow-up 2

What are the advantages of using lambda functions?

There are several advantages of using lambda functions in Python:

  1. Concise syntax: Lambda functions allow you to define small functions in a single line of code, making the code more readable and compact.

  2. No need for a function name: Since lambda functions are anonymous, you don't need to come up with a function name, which can be useful when you only need the function for a short period of time.

  3. Easy to use with higher-order functions: Lambda functions are often used with higher-order functions like map(), filter(), and reduce(), where a function is passed as an argument.

  4. Encapsulation: Lambda functions encapsulate functionality within a single expression, making it easier to understand and maintain the code.

Follow-up 3

Can lambda functions have multiple inputs?

Yes, lambda functions can have multiple inputs. The arguments are specified after the lambda keyword, separated by commas. Here's an example of a lambda function with multiple inputs:

multiply = lambda x, y: x * y
print(multiply(3, 5))  # Output: 15

Follow-up 4

In what scenarios would you use a lambda function over a regular function?

Lambda functions are often used in scenarios where a small and simple function is needed for a short period of time. Some common scenarios where lambda functions are used include:

  1. Sorting: Lambda functions can be used as the key parameter in sorting functions like sorted() to define custom sorting criteria.

  2. Filtering: Lambda functions can be used with the filter() function to filter elements from a list based on a condition.

  3. Mapping: Lambda functions can be used with the map() function to apply a function to each element of a list.

  4. Reducing: Lambda functions can be used with the reduce() function to perform a cumulative computation on a list of values.

  5. Callback functions: Lambda functions can be used as callback functions in event-driven programming or asynchronous programming.

3. Can you explain what a generator is in Python?

A generator produces a sequence of values lazily, one at a time, instead of building the whole sequence in memory. The common form is a function that uses yield: calling it returns a generator object, and each next() (or loop iteration) runs the body up to the next yield, suspending its state in between.

def countdown(n: int):
    while n > 0:
        yield n
        n -= 1

for x in countdown(3):
    print(x)            # 3, 2, 1

There's also generator-expression syntax: (x * x for x in range(1_000_000)) — like a list comprehension but lazy.

Why interviewers ask, and the follow-ups:

  • Memory: a generator holds one item at a time, so it scales to huge or infinite streams (log files, paginated APIs) where a list would exhaust RAM.
  • Lazy/one-shot: it's evaluated on demand and exhausted after one pass — you can't reuse or index it, and len() doesn't work. Re-create it to iterate again.
  • yield vs return: yield suspends and resumes, preserving local state; return ends the generator (raising StopIteration).
  • Advanced: yield from delegates to a sub-generator, and send()/throw() allow two-way communication (the basis of older coroutine patterns, now largely superseded by async/await).
↑ Back to top

Follow-up 1

How is a generator different from a normal function?

Generators are different from normal functions in several ways:

  1. Generators use the yield keyword to return values one at a time, whereas normal functions use the return keyword to return a single value.
  2. Generators can be paused and resumed, allowing them to generate values on-the-fly, whereas normal functions execute from start to finish and return a value.
  3. Generators are memory-efficient, as they generate values on-the-fly instead of storing them in memory, whereas normal functions store all values in memory at once.

Follow-up 2

Can you give an example of a generator?

Sure! Here's an example of a generator function that generates the Fibonacci sequence:

def fibonacci():
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b

# Usage:
for num in fibonacci():
    if num > 100:
        break
    print(num)

This generator function generates the Fibonacci sequence indefinitely, but we can break out of the loop when a certain condition is met.

Follow-up 3

What is the yield statement in Python?

The yield statement is used in generator functions to specify the value to be returned. When a generator function is called, it returns a generator object. The yield statement can be used multiple times in a generator function, allowing it to generate a sequence of values on-the-fly. Each time the yield statement is encountered, the generator function is paused, and the value is returned. When the generator is resumed, it continues execution from where it left off.

Follow-up 4

What are the benefits of using generators in Python?

There are several benefits of using generators in Python:

  1. Memory efficiency: Generators generate values on-the-fly, which means they don't store all values in memory at once. This makes them memory-efficient, especially when dealing with large datasets or infinite sequences.
  2. Lazy evaluation: Generators use lazy evaluation, which means they only generate values as they are needed. This can improve performance and reduce unnecessary computation.
  3. Simplified code: Generators allow you to write code that is more concise and readable. They eliminate the need for manual iteration and state management, as they handle these aspects internally.
  4. Infinite sequences: Generators can be used to generate infinite sequences, such as the Fibonacci sequence or prime numbers, without consuming excessive memory.
  5. Pipelining: Generators can be chained together using the yield from statement, allowing for efficient data processing pipelines.

4. What is a decorator in Python?

A decorator is a callable that takes a function (or class) and returns a modified version, letting you add behavior — logging, timing, caching, access control — without editing the wrapped function's body. The @decorator syntax above a def is just sugar for func = decorator(func).

import functools

def timed(func):
    @functools.wraps(func)            # preserve name/docstring/signature
    def wrapper(*args, **kwargs):
        # ... before
        result = func(*args, **kwargs)
        # ... after
        return result
    return wrapper

@timed
def work(n: int) -> int:
    return sum(range(n))

Follow-ups interviewers expect:

  • Why functools.wraps? Without it the wrapper masks the original's __name__, __doc__, and signature, breaking introspection and debugging.
  • *args, **kwargs in the wrapper so it works for any signature.
  • Decorators with arguments need an extra layer (a function returning a decorator), e.g. @retry(times=3).
  • Know the built-ins: @staticmethod, @classmethod, @property, @functools.cache/lru_cache, and @dataclass.
  • Stacking order: decorators apply bottom-up — the one nearest the def wraps first.
↑ Back to top

Follow-up 1

Can you give an example of a decorator?

Sure! Here's an example of a decorator that logs the execution time of a function:

import time

def timer_decorator(func):
    def wrapper(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        end_time = time.time()
        execution_time = end_time - start_time
        print(f'The function {func.__name__} took {execution_time} seconds to execute.')
        return result
    return wrapper

@timer_decorator
def my_function():
    # Function code goes here
    pass

Follow-up 2

What are the advantages of using decorators?

There are several advantages of using decorators in Python:

  1. Code reusability: Decorators allow us to add common functionality to multiple functions or classes without duplicating code.
  2. Separation of concerns: Decorators separate the core logic of a function or class from additional functionality, making the code more modular and easier to maintain.
  3. Code readability: Decorators provide a clean and concise way to modify the behavior of a function or class, making the code more readable and understandable.
  4. Easy to apply and remove: Decorators can be easily applied or removed from a function or class by simply adding or removing the @ symbol.

Follow-up 3

Can you explain how decorators can modify the behavior of a function?

Yes, decorators can modify the behavior of a function by wrapping the original function with additional code. This additional code can perform actions before or after the execution of the original function, modify the arguments passed to the function, or modify the return value of the function.

For example, a decorator can add logging functionality to a function by printing the arguments and return value. It can also add input validation by checking the arguments before calling the original function.

Here's an example of a decorator that adds logging functionality:

def logger_decorator(func):
    def wrapper(*args, **kwargs):
        print(f'Calling function {func.__name__} with arguments: {args}, {kwargs}')
        result = func(*args, **kwargs)
        print(f'Function {func.__name__} returned: {result}')
        return result
    return wrapper

Follow-up 4

Can you use multiple decorators for a single function?

Yes, it is possible to use multiple decorators for a single function. When multiple decorators are applied to a function, they are executed in the order they are defined.

Here's an example of using multiple decorators:

def decorator1(func):
    def wrapper(*args, **kwargs):
        print('Decorator 1')
        return func(*args, **kwargs)
    return wrapper


def decorator2(func):
    def wrapper(*args, **kwargs):
        print('Decorator 2')
        return func(*args, **kwargs)
    return wrapper


@decorator1
@decorator2
def my_function():
    # Function code goes here
    pass

5. Can you explain the concept of recursion in Python?

Recursion is when a function calls itself to solve a problem by reducing it to smaller instances of the same problem. Every correct recursive function needs two parts: a base case that stops the recursion, and a recursive case that moves toward that base case.

def factorial(n: int) -> int:
    if n <= 1:            # base case
        return 1
    return n * factorial(n - 1)   # recursive case

Follow-ups interviewers reliably ask:

  • Recursion limit: CPython caps the call stack (default ~1000) and raises RecursionError. Deep recursion blows the stack — you can raise the limit with sys.setrecursionlimit, but that's a smell.
  • No tail-call optimization: unlike some functional languages, CPython does not optimize tail recursion, so a deep recursive solution is often better written iteratively (with a loop or an explicit stack). This is the answer they're fishing for.
  • Overlapping subproblems: naive recursion (e.g. Fibonacci) recomputes the same values exponentially. Use memoization@functools.cache — or convert to dynamic programming.
  • When recursion is the natural fit: tree/graph traversal, divide-and-conquer (merge sort, quicksort), and parsing nested structures, where the data itself is recursive.
↑ Back to top

Follow-up 1

Can you give an example of a recursive function?

Sure! Here's an example of a recursive function in Python that calculates the factorial of a number:

def factorial(n):
    if n == 0:
        return 1
    else:
        return n * factorial(n-1)

print(factorial(5))  # Output: 120

Follow-up 2

What are the advantages and disadvantages of using recursion?

Advantages of using recursion:

  • It allows for a more concise and elegant solution to certain problems.
  • It can simplify the code by breaking down complex problems into smaller, more manageable subproblems.

Disadvantages of using recursion:

  • Recursive functions can be less efficient in terms of time and space complexity compared to iterative solutions.
  • Recursive functions may lead to stack overflow errors if the recursion depth is too large.
  • Recursive functions can be harder to debug and understand compared to iterative solutions.

Follow-up 3

How does Python handle recursive function calls?

Python handles recursive function calls by creating a new stack frame for each recursive call. Each stack frame contains the local variables and the return address of the function call. When a recursive function is called, the current state of the function is saved on the stack, and a new instance of the function is executed with its own set of local variables. This process continues until the base case is reached, at which point the function calls start returning and the stack frames are popped off the stack.

Follow-up 4

What is the maximum recursion depth in Python?

The maximum recursion depth in Python is determined by the system's maximum stack size. By default, Python has a recursion depth limit of 1000. This means that if a recursive function exceeds this limit, a 'RecursionError: maximum recursion depth exceeded' exception will be raised. However, you can modify the recursion depth limit using the sys.setrecursionlimit() function, although it is generally not recommended to increase it too much as it can lead to stack overflow errors.

6. Why are mutable default arguments dangerous in Python?

Extremely common "do you really understand Python" filter. A function's default arguments are evaluated once, when the function is defined — not on each call. A mutable default (list/dict/set) is therefore shared across all calls:

def add_item(item, items=[]):      # BUG: one shared list
    items.append(item)
    return items

add_item(1)   # [1]
add_item(2)   # [1, 2]  — not [2]!

The idiomatic fix is the sentinel pattern:

def add_item(item, items=None):
    if items is None:
        items = []
    items.append(item)
    return items

Follow-ups: the same "evaluated once" rule is why you don't put datetime.now() as a default; and why this trips people up is that the default object persists on the function object itself (func.__defaults__). The bug is occasionally used deliberately for memoization, but that's better expressed with functools.cache.

↑ Back to top

Live mock interview

Mock interview: Python Functions

Intermediate ~5 min Your own free AI key

Your voice and your AI key never touch our servers; the key stays in this browser and is sent only to Google. Only your round scores are saved to track progress.