← All courses

Interview questionsSoftware EngineeringBeginner

Top 10 Python interview questions

Mutable vs immutable types, lists vs tuples, decorators, generators, the GIL, comprehensions, *args and **kwargs and more: the Python questions asked across developer, data and automation roles.

10 min · 10 questions

1What is the difference between a list, a tuple, a set and a dictionary?

listtuplesetdict
Syntax[1, 2](1, 2){1, 2}{"a": 1}
Orderedyesyesnoyes (insertion order, 3.7+)
Mutableyesnoyesyes
Duplicatesallowedallowednounique keys
Lookup x inO(n)O(n)O(1) averageO(1) average by key

Use a list for an ordered collection that changes, a tuple for fixed records (coordinates, function returns) and as dictionary keys (they're hashable), a set for uniqueness and fast membership tests, and a dict for key-value lookups.

2What are mutable and immutable types? Why does it matter?

Immutable objects can't be changed after creation: int, float, str, tuple, frozenset, bool. Mutable ones can: list, dict, set, most custom objects.

It matters because variables are references:

python
a = [1, 2]
b = a
b.append(3)
print(a)          # [1, 2, 3]: same list

def add_item(item, bucket=[]):   # classic bug: the default is created once
    bucket.append(item)
    return bucket
add_item(1); add_item(2)         # [1, 2]: shared between calls

def add_item(item, bucket=None): # fix
    bucket = [] if bucket is None else bucket
    bucket.append(item)
    return bucket

Only immutable (hashable) objects can be dictionary keys or set members. Use copy.copy for a shallow copy and copy.deepcopy for nested structures.

3What are list comprehensions and generator expressions?

A list comprehension builds a list in one readable expression:

python
squares = [x * x for x in range(10) if x % 2 == 0]
names = {s["id"]: s["name"] for s in students}      # dict comprehension

A generator expression uses parentheses and produces items lazily, one at a time, without building the whole list in memory:

python
total = sum(x * x for x in range(10_000_000))       # constant memory

Use comprehensions for small-to-medium results you need as a list; generators for large or streaming data. Don't nest comprehensions so deeply that they become unreadable.

4What are generators and the yield keyword?

A generator function uses yield to produce a sequence of values lazily. Each call to next() resumes the function where it left off, keeping its local state.

python
def read_large_file(path):
    with open(path) as f:
        for line in f:
            yield line.rstrip("\n")

for row in read_large_file("logs.txt"):   # one line in memory at a time
    if "ERROR" in row:
        print(row)

Benefits: memory efficiency for large data, infinite sequences, and pipeline-style processing. A generator can be iterated only once. yield from delegates to a sub-generator.

5What are decorators? Write one.

A decorator is a function that takes a function and returns a new function that adds behaviour (logging, timing, caching, authentication) without changing the original code.

python
import functools, time

def timed(fn):
    @functools.wraps(fn)                # keeps the name and docstring
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        try:
            return fn(*args, **kwargs)
        finally:
            print(f"{fn.__name__} took {time.perf_counter() - start:.3f}s")
    return wrapper

@timed
def build_report(month):
    ...

@timed is shorthand for build_report = timed(build_report). Real examples: @app.route in Flask, @login_required in Django, @functools.lru_cache for memoisation, @property, @staticmethod and @classmethod.

6Explain *args and **kwargs.

  • *args collects extra positional arguments into a tuple.
  • **kwargs collects extra keyword arguments into a dict.
python
def log(level, *messages, **context):
    print(level, " ".join(messages), context)

log("INFO", "user", "logged in", user_id=42, ip="10.0.0.1")
# INFO user logged in {'user_id': 42, 'ip': '10.0.0.1'}

They're also used to unpack when calling: f(*my_list, **my_dict). Common in decorators and wrappers that pass arguments through unchanged. The order in a signature is: positional, *args, keyword-only, **kwargs.

7What is the GIL? How do you do parallel work in Python?

The Global Interpreter Lock in CPython allows only one thread to execute Python bytecode at a time. So:

  • Threads don't speed up CPU-bound pure-Python code, but work well for I/O-bound tasks (network calls, file I/O), because the GIL is released while waiting.
  • For CPU-bound work, use multiprocessing / ProcessPoolExecutor (separate processes, each with its own GIL), or libraries like NumPy that run in C and release the GIL.
  • asyncio handles thousands of concurrent I/O operations in a single thread with async/await.
python
from concurrent.futures import ThreadPoolExecutor
with ThreadPoolExecutor(max_workers=10) as pool:
    pages = list(pool.map(fetch_url, urls))   # I/O-bound: threads help

Mention that Python 3.13 introduced an experimental free-threaded build without the GIL.

8What is the difference between is and ==?

  • == checks equality of value (calls __eq__).
  • is checks identity: whether both names point to the same object in memory.
python
a = [1, 2]; b = [1, 2]
a == b    # True
a is b    # False
x = None
x is None # the correct way to check for None

Use is only for singletons (None, True, False). Small integers and some strings are cached by CPython, so is may appear to work for them by accident; never rely on it.

9How does exception handling work in Python?

python
try:
    amount = int(request["amount"])
    result = transfer(amount)
except (KeyError, ValueError) as e:
    log.warning("Bad input: %s", e)
    raise BadRequest("Amount must be a number") from e
except TransferError:
    retry_later()
else:
    notify_success(result)        # runs only if no exception occurred
finally:
    db.close()                    # always runs

Best practices: catch specific exceptions (never a bare except:), keep the try block small, use raise ... from e to keep the original cause, define custom exception classes for your domain, and use context managers (with) so resources close automatically.

10Explain OOP in Python: classes, inheritance, dunder methods and @property.

python
class Account:
    interest_rate = 0.04                    # class attribute, shared

    def __init__(self, owner, balance=0):   # constructor
        self.owner = owner                  # instance attribute
        self._balance = balance             # "protected" by convention

    @property
    def balance(self):                      # read like an attribute, no setter
        return self._balance

    def deposit(self, amount):
        if amount <= 0:
            raise ValueError("Amount must be positive")
        self._balance += amount

    def __repr__(self):                     # developer-friendly string
        return f"Account({self.owner!r}, {self._balance})"

class Savings(Account):
    def add_interest(self):
        self.deposit(self.balance * self.interest_rate)

Points to mention: self is the instance; super().__init__() calls the parent; Python supports multiple inheritance with the MRO (method resolution order); dunder methods (__len__, __eq__, __iter__, __str__) integrate classes with built-in operations; @classmethod gets the class (alternative constructors) and @staticmethod gets neither; @dataclass generates __init__, __repr__ and __eq__ for data classes.

Keep going