Python interview questions, answered the way you’d say them.

49 questions for Data Analyst, Data Scientist and AI/ML Engineer roles, Easy to Hard, 23 of them reported by candidates at companies. Say your answer out loud, then open ours. Free, no sign-up.

Practise 10 questionsSee all 49

MediumPython

Group and aggregate: revenue by city

Say your answer out loud first. Then check ours.

Show our answerAnother question

Reported in interviews at

Every question, with our answer.

Open a question to see the answer to say. Each links to its sources, and to its full explanation where we have one.

  1. What is the difference between mutable and immutable types?

    Easy

    A mutable object can be changed in place, like a list, dict or set, while an immutable one can’t, like numbers, strings, tuples and frozensets; “changing” a string actually builds a new one. The catch: if two names refer to the same mutable object, a change through either name shows up in both. Dict keys and set members must be hashable, which numbers, strings and tuples of hashable values are and lists aren’t.

    The full answer, in the guidePractise it in the Python LabPractise it with Prism

  2. List, tuple, set or dict: when do you use which?

    Easy

    Use a list for an ordered collection you’ll modify and a tuple for a record that stays fixed. A set keeps only unique values and is quick to answer “is this in here?”, and a dict maps keys to values. Checking membership in a set or dict takes about the same time at any size, while on a list it slows down as the list grows.

    The full answer, in the guidePractise it in the Python LabPractise it with Prism

  3. What is the difference between is and ==?

    Easy

    == checks whether two values are equal, while is checks whether two names refer to the very same object. Two separate lists holding [1, 2] are equal with ==, but is says False. I use is only for None, as in x is None, and for True or False checks I really mean.

    The full answer, in the guidePractise it in the Python LabPractise it with Prism

  4. Write list, dict and set comprehensions

    EasyCode

    A comprehension builds a list, dict or set in a single expression from a loop, optionally filtered with if: square brackets make a list, braces with key: value make a dict, and braces alone make a set. Swap in round brackets and it becomes a generator expression, which yields values one by one rather than building the whole list.

    prices = {"kurta": 1299, "shoes": 2499, "bottle": 499}
    
    [p for p in prices.values() if p > 1000]        # list: [1299, 2499]
    {name: round(p * 0.9) for name, p in prices.items()}   # dict: 10% off
    {len(name) for name in prices}                  # set: {5, 6}
    sum(p for p in prices.values())                 # generator expression: 4297

    The full answer, in the guidePractise it in the Python LabPractise it with Prism

  5. What do *args and **kwargs do?

    Easy

    *args gathers any number of extra positional arguments into a tuple, and **kwargs gathers extra keyword arguments into a dict. The names are only a convention; the stars do the work. Worth adding: any parameter that comes after *args can only be passed by keyword.

    The full answer, in the guidePractise it in the Python LabPractise it with Prism

  6. What is a lambda, and when would you use one?

    Easy

    A lambda is a small function with no name, written as a single expression. I use one where a function is needed only briefly, such as the key for sorted, for example sorting products by price with key=lambda p: p[1]. Anything longer, or anything I’ll reuse, gets a normal def.

    The full answer, in the guidePractise it in the Python LabPractise it with Prism

  7. How do you handle a missing dict key?

    Easy

    d[key] raises a KeyError when the key is missing, while d.get(key, default) returns the default instead. For grouping, collections.defaultdict creates the missing value for you, so you can append without checking the key first, and Counter does the counting for you.

    Python docs: collections: Container datatypes

    The full answer, in the guidePractise it in the Python LabPractise it with Prism

  8. What is the difference between a Series and a DataFrame?

    Easy

    A Series is a single labelled column of values, and a DataFrame is a table made of several Series that share one index. Selecting with single brackets and one name, orders["amount"], gives a Series; double brackets, orders[["amount"]], give a DataFrame with one column.

    The full answer, in the guidePractise it in the Python LabPractise it with Prism

  9. loc or iloc?

    Easy

    loc selects by label and includes the end of a slice, so with order ids as the index, loc[1001:1003] returns three orders. iloc selects by position and excludes the end, just like Python lists. loc also accepts a boolean mask, which lets you filter rows and pick columns in one step.

    pandas 3.0 docs: Indexing and selecting data

    The full answer, in the guidePractise it in the Python LabPractise it with Prism

  10. Filter rows on several conditions

    EasyCode

    Join conditions with & for and, | for or and ~ for not, and put each condition in brackets, because Python’s and and or can’t work on whole columns. isin, between (inclusive at both ends) and query make long filters easier to read. A NaN compares as False, so an order with a missing amount never passes an amount filter.

    mask = (orders["status"] == "delivered") & (orders["amount"] > 2000)
    orders[mask]                                         # 1002, 1004, 1007
    
    orders[orders["city"].isin(["Pune", "Delhi"])]
    orders[orders["amount"].between(1000, 2000)]
    orders.query("status == 'delivered' and amount > 2000")

    The full answer, in the guidePractise it in the Python LabPractise it with Prism

  11. Count values and find duplicates

    EasyCode

    value_counts counts each value, largest first, and nunique counts the distinct values. duplicated marks the repeats, leaving the first occurrence unmarked, and drop_duplicates removes them. Pass subset to choose which columns define a duplicate: on the example orders, subset=["customer"] finds 3 repeat orders from 5 customers.

    orders["city"].value_counts()
    orders["customer"].nunique()                         # 5
    orders.duplicated(subset=["customer"]).sum()         # 3 repeat orders
    orders.drop_duplicates(subset=["customer"], keep="first")   # each customer's first order

    The full answer, in the guidePractise it in the Python LabPractise it with Prism

  12. Why is a NumPy array faster than a Python list?

    Easy

    A NumPy array keeps one data type in a single block of memory, so its operations can loop in compiled code. A list stores separate Python objects, and looping over them happens in Python, item by item. Replacing loops with whole-array operations, such as prices * 0.9, is called vectorisation.

    The full answer, in the guidePractise it in the Python LabPractise it with Prism

  13. Filter an array with a condition

    EasyCode

    A comparison on an array, such as amounts > 2000, returns a boolean mask of True and False values, and indexing with it keeps only the matching values. np.where chooses between two values element by element. The mean of a mask is the share that’s True: 0.4 when 2 of 5 values pass.

    amounts = np.array([1798, 2499, 1999, 3398, 1299])
    amounts[amounts > 2000]                      # array([2499, 3398])
    np.where(amounts > 2000, "High", "Normal")
    (amounts > 2000).mean()                      # 0.4: 2 of 5

    The full answer, in the guidePractise it in the Python LabPractise it with Prism

  14. Remove duplicates from a list, keeping the order

    EasyCode

    set() removes duplicates but loses the order. Since Python 3.7, dicts keep insertion order as a language rule, so list(dict.fromkeys(items)) does it in one line. I’d also show the loop version, a seen set plus an output list, to prove I understand why it works.

    def dedupe(items):
        return list(dict.fromkeys(items))
    
    def dedupe_loop(items):
        seen, out = set(), []
        for x in items:
            if x not in seen:
                seen.add(x)
                out.append(x)
        return out

    Python docs: Built-in Types

    The full answer, in the guidePractise it in the Python LabPractise it with Prism

  15. Count the words in a text and show the top 3

    EasyCode

    Lower-case the text, extract the words with a regular expression so punctuation doesn’t stick to them, then count them with Counter and call most_common(3). When counts tie, most_common keeps the order in which the words first appeared, which is worth saying out loud.

    import re
    from collections import Counter
    
    text = "UPI is fast. UPI is free. Cards are not free."
    words = re.findall(r"[a-z']+", text.lower())
    Counter(words).most_common(3)     # [('upi', 2), ('is', 2), ('free', 2)]

    Python docs: collections: Container datatypes

    The full answer, in the guidePractise it in the Python LabPractise it with Prism

  16. What is a variable in Python?

    Easy

    A variable is a name that refers to an object. Assignment binds the name and never copies, so after b = a both names point to the same object, and if it’s mutable, a change made through b shows up through a. The type belongs to the object, not the name, so the same name can later refer to a string. Names are case-sensitive, can’t start with a digit and can’t be keywords.

    a = [1, 2]
    b = a              # b names the same list: no copy
    b.append(3)
    print(a)           # [1, 2, 3]
    print(a is b)      # True
    a = "text"         # a now names a string; the list is untouched
    print(b)           # [1, 2, 3]

    Tie it to mutability: rebinding a name never changes the object, but changing a mutable object shows through every name that refers to it.

    Python docs: Execution model: naming and bindingPython docs: Programming FAQ: why did changing list y also change list x?Python docs: Lexical analysis: names (identifiers and keywords)

    Practise it with Prism

  17. Without pandas or the csv module, read a comma-separated file and print the sum of each row.

    EasyCode

    Open the file with with, so it’s closed even if an error occurs, and loop over it line by line, which never holds the whole file in memory. For each line, strip the newline, skip blank lines, split on commas, convert each field with float and sum them. Catch ValueError for a header or a bad value, and say the limit out loud: plain splitting breaks on quoted fields that contain commas, which the csv module handles.

    with open("numbers.csv") as f:
        for line_no, line in enumerate(f, start=1):
            line = line.strip()
            if not line:
                continue
            try:
                print(line_no, sum(float(x) for x in line.split(",")))
            except ValueError:
                print(line_no, "skipped: not all numbers")

    If the standard library is then allowed, csv.reader handles quotes and embedded commas; open the file with newline='', as its docs advise.

    Python docs: Tutorial: reading and writing filesPython docs: csv: CSV file reading and writing

    Practise it with Prism

  18. How do you find the 25th percentile of a column in pandas?

    EasyCode

    df["amount"].quantile(0.25) returns it, and describe() shows it as the 25% row, beside the median and the 75th percentile. Pass a list, such as [0.25, 0.5, 0.75], for several at once, or use it after groupby for one value per group. By default it interpolates linearly between the two nearest values, so the result may not be a value in the data; set interpolation="lower" or "nearest" if you need one that is.

    import pandas as pd
    amounts = pd.Series([499, 1299, 1899, 2499, 3398])
    amounts.quantile(0.25)                                   # 1299.0
    amounts.quantile([0.25, 0.5, 0.75])                      # 1299.0, 1899.0, 2499.0
    pd.Series([100, 200, 300, 400]).quantile(0.25)           # 175.0, between 100 and 200
    pd.Series([100, 200, 300, 400]).quantile(0.25, interpolation="lower")   # 100
    df.groupby("city")["amount"].quantile(0.25)              # one value per city

    NumPy’s np.percentile takes 25 where np.quantile and pandas take 0.25: mixing the two scales is a classic slip.

    pandas docs: Series.quantileNumPy docs: numpy.percentile

    Practise it with Prism

  19. How do indexing and slicing work in Python?

    Easy

    Sequences such as strings, lists and tuples are indexed from 0, and negative indexes count from the end, so s[-1] is the last item. A slice s[start:stop:step] includes start and excludes stop, and every part is optional: s[:3] is the first three, s[::-1] a reversed copy. An index out of range raises IndexError, but an out-of-range slice just returns what exists, and slicing a list gives a new, shallow copy.

    s = "PANDAS"
    s[0], s[-1]        # ('P', 'S')
    s[1:4]             # 'AND': positions 1, 2 and 3
    s[::2]             # 'PNA': every second character
    s[::-1]            # 'SADNAP'
    s[20:]             # '': an out-of-range slice is just empty
    s[20]              # IndexError: string index out of range

    pandas splits the idea in two: iloc slices by position like Python, while loc slices by label and includes the end.

    Python docs: Tutorial: an informal introduction (text and lists)

    Practise it with Prism

  20. Write a function that computes intersection over union (IoU) for sets of bounding boxes.

    EasyCode

    For boxes as (x1, y1, x2, y2), the overlap’s corners are the larger of the two top-left corners and the smaller of the two bottom-right ones; clip its width and height at zero, so boxes that don’t touch score 0. IoU is the overlap’s area divided by the union: area A + area B − overlap. For two sets of boxes, broadcast the same arithmetic into an N × M matrix, which is what torchvision.ops.box_iou returns.

    import numpy as np
    def box_iou(a, b):
        """a: (N, 4), b: (M, 4) boxes as x1, y1, x2, y2. Returns an (N, M) matrix."""
        tl = np.maximum(a[:, None, :2], b[None, :, :2])        # top-left corner of each overlap
        br = np.minimum(a[:, None, 2:], b[None, :, 2:])        # bottom-right corner
        inter = np.clip(br - tl, 0, None).prod(axis=2)         # 0 when boxes don't overlap
        area = lambda x: (x[:, 2] - x[:, 0]) * (x[:, 3] - x[:, 1])
        return inter / (area(a)[:, None] + area(b)[None, :] - inter)
    print(box_iou(np.array([[0, 0, 2, 2]]), np.array([[1, 1, 3, 3], [5, 5, 6, 6]])))  # [[0.143 0.]]

    Guard against zero-area boxes, where the union can be 0 and the division fails.

    torchvision docs: torchvision.ops.box_iouNumPy docs: Broadcasting

    Practise it with Prism

  21. Shallow copy or deep copy?

    Medium

    A shallow copy creates a new outer container but shares the objects inside it, while a deep copy copies everything, all the way down. The difference shows up as soon as you have nested data, like a list of lists: change an inner list through a shallow copy and the original changes too. copy.deepcopy avoids that.

    Python docs: copy: Shallow and deep copy operations

    The full answer, in the guidePractise it in the Python LabPractise it with Prism

  22. Why is a list as a default argument a bug?

    Medium

    The default is evaluated just once, when Python defines the function, not on every call. So a default list is shared across calls and keeps growing: the second call sees what the first one appended. The fix is to default to None and create a fresh list inside the function.

    The Python Tutorial: More Control Flow Tools

    The full answer, in the guidePractise it in the Python LabPractise it with Prism

  23. What is a generator, and why use one?

    Medium

    A generator is a function that returns values one at a time with yield, pausing after each. Since it never keeps the full sequence in memory, it’s a good fit for big files and streams; nothing is read until the next value is requested. The trade-off is that you can only go through it once.

    The full answer, in the guidePractise it in the Python LabPractise it with Prism

  24. How do try, except, else and finally work?

    Medium

    try holds the code that might fail, and except handles a named error. else runs only when nothing went wrong, and finally runs every time, error or not. Catch specific exceptions, such as ValueError when text won’t convert to a number: a bare except: hides real bugs.

    The full answer, in the guidePractise it in the Python LabPractise it with Prism

  25. Explain classes, self, and class versus instance attributes

    Medium

    A class is a blueprint for objects. __init__ prepares each new object, and self refers to that object. A class attribute is common to all instances, while an instance attribute belongs to just one, and a subclass inherits the parent’s methods and can extend them, reaching the parent’s version through super().

    The full answer, in the guidePractise it in the Python LabPractise it with Prism

  26. Group and aggregate: revenue by city

    MediumCode

    Filter to delivered orders, group by city and use named aggregation in agg, so each output column gets a clear name: orders, revenue and average order. Then flag what the totals hide: sum and mean quietly ignore NaN, so Delhi counts 2 orders yet has only one amount. Always check for missing values before you rely on a total.

    summary = (orders[orders["status"] == "delivered"]
               .groupby("city", as_index=False)
               .agg(orders=("order_id", "count"),
                    revenue=("amount", "sum"),
                    avg_order=("amount", "mean")))
    # Delhi: orders 2, revenue 3398.0, avg_order 3398.0, because order 1008 has no amount

    pandas 3.0 docs: Group by: split-apply-combine

    The full answer, in the guidePractise it in the Python LabPractise it with Prism

  27. agg or transform?

    Medium

    agg gives one row per group, while transform gives one value for every original row, aligned to the index. So transform is how you attach a group-level number as a new column, like the share each order makes of its city’s revenue. It plays the role a window function plays in SQL.

    The full answer, in the guidePractise it in the Python LabPractise it with Prism

  28. Explain merge types, and check a merge didn’t go wrong

    Medium

    merge joins DataFrames on columns, and how can be inner, left, right, outer or cross, the same as SQL joins. Two arguments guard against mistakes: validate fails loudly when a key you expected to be unique isn’t, and indicator=True adds a _merge column saying where each row came from. For completeness, join merges on the index and concat stacks frames.

    On the example data, a left merge with validate="many_to_one" shows 6 rows in both and 2 left_only, because Meera and Rohan have no segment.

    pandas 3.0 docs: Merge, join, concatenate and compare

    The full answer, in the guidePractise it in the Python LabPractise it with Prism

  29. pivot or pivot_table?

    Medium

    pivot only reshapes, so it raises an error if any index and column pair appears twice. pivot_table aggregates those duplicates, using the mean by default, and can fill the gaps with fill_value. On real data I reach for pivot_table; in the example orders, Delhi has two Home orders, so pivot fails there.

    The full answer, in the guidePractise it in the Python LabPractise it with Prism

  30. apply or a vectorised operation?

    Medium

    apply with a Python function runs once per row, which gets slow on big data, while a vectorised operation works on the whole column at once in compiled code. For conditions I use np.where, np.select or pd.cut, and for lookups map. Watch the NaN: pd.cut leaves it missing, but np.where quietly drops it into the “else” bucket.

    The full answer, in the guidePractise it in the Python LabPractise it with Prism

  31. How do you find and handle missing values in pandas?

    Medium

    Start by counting them with isna().sum() and asking why they’re missing, since a blank amount could mean a failed payment rather than zero. Then decide: drop those rows, fill them with a median or a group’s median, or keep them and add a flag column. Never check for NaN with ==, because NaN doesn’t equal itself; use isna().

    pandas 3.0 docs: Working with missing data

    The full answer, in the guidePractise it in the Python LabPractise it with Prism

  32. Parse dates and summarise by month

    MediumCode

    Convert text with pd.to_datetime and pass the format, because Indian files often put the day first and a guess can misread 05/09/2026, which is 5 September. errors="coerce" turns bad dates into NaT. Then the .dt accessor pulls out parts such as the month or weekday, and resample groups a date index by period.

    raw = pd.Series(["05/09/2026", "20/09/2026", "not a date"])
    pd.to_datetime(raw, format="%d/%m/%Y", errors="coerce")   # 2026-09-05, 2026-09-20, NaT
    
    orders["month"]   = orders["order_date"].dt.to_period("M")
    orders["weekday"] = orders["order_date"].dt.day_name()
    
    monthly = orders.set_index("order_date").resample("MS")["amount"].sum()

    pandas 3.0 docs: Time series / date functionality

    The full answer, in the guidePractise it in the Python LabPractise it with Prism

  33. What is broadcasting, and what does axis mean?

    Medium

    axis=0 runs down the rows and returns one result per column; axis=1 runs across the columns and returns one result per row. Broadcasting is how arrays of different shapes still combine: line the shapes up from the right, and each pair of sizes has to be equal or contain a 1. That’s how a (2, 3) array of sales divides by its (2, 1) store totals.

    NumPy docs: Broadcasting

    The full answer, in the guidePractise it in the Python LabPractise it with Prism

  34. Find two products whose prices add up to a gift card’s value

    MediumCode

    It’s the classic two-sum problem. Testing every pair costs time that grows with the square of the list’s length, so I’d do one pass with a dict of prices already seen: for each price, look up the partner it needs, and return both positions if that partner is there. With prices 499, 1299, 1899 and 2499 and a target of 3798, it returns positions 1 and 3.

    def pair_with_sum(prices, target):
        seen = {}                          # price -> position
        for i, p in enumerate(prices):
            if target - p in seen:
                return seen[target - p], i
            seen[p] = i
        return None
    
    pair_with_sum([499, 1299, 1899, 2499], 3798)    # (1, 3): 1299 + 2499

    The full answer, in the guidePractise it in the Python LabPractise it with Prism

  35. Find the top 2 orders by amount in each city

    MediumCode

    Either sort by amount and take the first rows of each group with groupby("city").head(2), or rank inside each group with rank(method="dense", ascending=False) and keep ranks of 2 or less. The ranking route mirrors SQL’s DENSE_RANK and keeps ties, while head(2) returns at most two rows per city, so say which one you chose.

    top2 = (orders.sort_values("amount", ascending=False)
                  .groupby("city")
                  .head(2))
    
    orders["rank_in_city"] = (orders.groupby("city")["amount"]
                                    .rank(method="dense", ascending=False))
    orders[orders["rank_in_city"] <= 2]

    The full answer, in the guidePractise it in the Python LabPractise it with Prism

  36. How does object-oriented programming work in Python?

    Medium

    A class bundles data with the methods that use it, and the usual four ideas map onto Python like this. Encapsulation is by convention: a leading underscore marks a name as internal, and a double underscore triggers name mangling, but nothing is truly private. Inheritance lets a subclass reuse and extend its parent’s methods, reaching them through super(); polymorphism comes from overriding and duck typing; and abstraction uses abc.ABC with @abstractmethod, which blocks instantiation until every abstract method is implemented.

    from abc import ABC, abstractmethod
    class Payment(ABC):                                    # abstraction: can't be created directly
        def __init__(self, amount): self._amount = amount  # _ marks it internal
        @abstractmethod
        def fee(self): ...
    class UPI(Payment):                                    # inheritance
        def fee(self): return 0
    class Card(Payment):
        def fee(self): return self._amount * 0.02          # an assumed 2% fee
    for p in (UPI(1000), Card(1000)): print(p.fee())       # polymorphism: 0, then 20.0

    With multiple inheritance, show the method resolution order: Card.__mro__ lists the classes in the order Python searches them.

    Python docs: Tutorial: classesPython docs: abc: abstract base classesPython docs: Glossary (duck typing)

    Practise it with Prism

  37. Translate a SQL query with JOIN, WHERE, CASE and GROUP BY into pandas.

    MediumCode

    Map each clause to a pandas step. JOIN becomes merge on the key, with how for the join type; WHERE becomes a boolean mask in loc, with isin for an IN list; CASE becomes pd.cut for numeric bands or np.select for other conditions; and GROUP BY becomes groupby with named aggregation. One trap differs from SQL: merge matches null keys to each other, so drop or fill them first.

    df = orders.merge(customers, on="customer_id", how="inner")                  # JOIN
    df = df.loc[df["city"].isin(["Delhi", "Mumbai", "Pune", "Jaipur"])
                & (df["order_date"] >= "2024-04-01") & (df["order_date"] < "2025-04-01")]   # WHERE
    df["size"] = pd.cut(df["amount"], bins=[0, 500, 1500, float("inf")],
                        labels=["Small", "Medium", "Large"], right=False)        # CASE
    summary = (df.groupby(["city", "size"], observed=True)                       # GROUP BY
                 .agg(orders=("order_id", "count"), revenue=("amount", "sum")))

    Pass validate="many_to_one" to merge, so a customer listed twice raises an error instead of quietly doubling orders.

    pandas docs: Comparison with SQLpandas docs: pandas.cutpandas docs: DataFrame.merge

    Practise it with Prism

  38. How does multithreading work in Python, and what is the GIL?

    Medium

    In CPython’s standard build, the global interpreter lock lets only one thread run Python bytecode at a time. Threads still help with I/O-bound work, such as calling APIs or reading files, because the lock is released while a thread waits on I/O. For CPU-bound work, use multiprocessing or ProcessPoolExecutor, which run separate processes, or libraries such as NumPy that release the lock in their compiled code. Since 3.13 there’s also an optional free-threaded build without the GIL, though it isn’t the default.

    from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
    
    def fetch(url): ...      # waits on the network: threads help
    def score(chunk): ...    # pure-Python number crunching: processes help
    
    if __name__ == "__main__":
        with ThreadPoolExecutor(max_workers=8) as ex:
            pages = list(ex.map(fetch, urls))
        with ProcessPoolExecutor() as ex:
            results = list(ex.map(score, chunks))

    ProcessPoolExecutor won’t work in the interactive interpreter: worker processes must be able to import your main module, which is why the pool sits under the __main__ guard.

    Python docs: Glossary: global interpreter lockPython docs: threadingPython docs: concurrent.futuresPython docs: Python support for free threadingNumPy docs: Thread safety

    Practise it with Prism

  39. Build a text-classification pipeline in scikit-learn on one of its built-in datasets.

    MediumCode

    Load the data, for example fetch_20newsgroups, which comes already split into train and test sets. Chain a TfidfVectorizer, which turns each document into TF-IDF features, with a linear classifier such as LogisticRegression in one pipeline, so the vocabulary is learnt from training texts only. Fit it, predict the test set and report per-class precision and recall with classification_report; then tune settings such as the n-gram range with cross-validation on the whole pipeline.

    from sklearn.datasets import fetch_20newsgroups
    from sklearn.pipeline import make_pipeline
    from sklearn.feature_extraction.text import TfidfVectorizer
    from sklearn.linear_model import LogisticRegression
    from sklearn.metrics import classification_report
    cats, strip = ["sci.space", "rec.autos"], ("headers", "footers", "quotes")
    train = fetch_20newsgroups(subset="train", categories=cats, remove=strip)
    test = fetch_20newsgroups(subset="test", categories=cats, remove=strip)
    model = make_pipeline(TfidfVectorizer(sublinear_tf=True), LogisticRegression(max_iter=1000)).fit(train.data, train.target)
    print(classification_report(test.target, model.predict(test.data), target_names=test.target_names))

    Strip headers, footers and quotes, as here: scikit-learn’s own docs show that classifiers otherwise learn from metadata rather than the topic.

    scikit-learn: The 20 newsgroups text datasetscikit-learn: TfidfVectorizerscikit-learn: Pipelines and composite estimators

    Practise it with Prism

  40. Implement k-means from scratch in NumPy, with the distance step vectorised.

    MediumCode

    Start from k centroids, here k random points, then repeat two steps until they stop moving. Assign: compute every point-to-centroid squared distance at once by broadcasting, shapes (n, 1, d) minus (1, k, d) give (n, k, d), sum the last axis and take argmin. Update: move each centroid to the mean of its points, leaving it in place if none were assigned. Mention that better starting points, such as scikit-learn’s default k-means++, speed up convergence.

    import numpy as np
    def kmeans(X, k, iters=100, seed=0):
        rng = np.random.default_rng(seed)
        C = X[rng.choice(len(X), k, replace=False)]                     # k random points to start
        for _ in range(iters):
            labels = ((X[:, None, :] - C[None, :, :]) ** 2).sum(axis=2).argmin(axis=1)
            new = np.array([X[labels == j].mean(axis=0) if (labels == j).any() else C[j] for j in range(k)])
            if np.allclose(new, C): break
            C = new
        return C, labels

    For large data the (n, k, d) array gets big: expand the distance as ‖x‖² − 2x·c + ‖c‖², which needs only an (n, k) matrix.

    NumPy docs: Broadcastingscikit-learn: KMeans

    Practise it with Prism

  41. Implement gradient descent from scratch for linear regression.

    MediumCode

    Start the weights at zero, then repeat: predict, measure the error, take the gradient of the mean squared error, (2/n) Xᵀ(Xw − y), and step against it, scaled by the learning rate. A column of ones lets the bias be learnt as one more weight. Watch the loss: if it rises or bounces, the learning rate is too big; if it barely falls, it’s too small or the features need scaling. Check the result against the least-squares solution.

    import numpy as np
    def gradient_descent(X, y, lr=0.1, epochs=1000):
        X = np.c_[np.ones(len(X)), X]                  # a column of ones learns the bias
        w = np.zeros(X.shape[1])
        for _ in range(epochs):
            grad = 2 / len(X) * X.T @ (X @ w - y)      # gradient of the mean squared error
            w -= lr * grad
        return w
    X = np.random.default_rng(0).normal(size=(200, 2)); y = 3 + X @ np.array([2.0, -1.0])
    print(gradient_descent(X, y).round(3))             # [ 3.  2. -1.]

    Be ready to switch to mini-batches: the same update on a random slice of rows each step is stochastic gradient descent.

    Google Machine Learning Crash Course: Gradient descentGoogle Machine Learning Crash Course: HyperparametersNumPy docs: numpy.linalg.lstsq

    Practise it with Prism

  42. Find the k nearest neighbours of a point using only plain Python.

    MediumCode

    Compute the distance from the query point to every point and keep the k smallest. math.dist gives the Euclidean distance, and heapq.nsmallest(k, points, key=…) keeps only k candidates as it scans: O(n log k) time and O(k) extra memory, against O(n log n) for sorting everything. To classify, take a majority vote of those k labels with Counter; moving the query point changes nothing in the code.

    import heapq, math
    from collections import Counter
    
    def k_nearest(points, query, k):
        return heapq.nsmallest(k, points, key=lambda p: math.dist(p, query))
    def knn_classify(train, query, k=3):                 # train: a list of (point, label)
        near = heapq.nsmallest(k, train, key=lambda t: math.dist(t[0], query))
        return Counter(label for _, label in near).most_common(1)[0][0]
    
    print(k_nearest([(1, 2), (3, 4), (0, 1), (5, 5)], (0, 0), 2))   # [(0, 1), (1, 2)]

    For a vote between two classes, pick an odd k so it can’t tie.

    Python docs: heapq: heap queue algorithmPython docs: math.dist

    Practise it with Prism

  43. What is a decorator? Write one

    HardCode

    A decorator takes a function and returns a new one that wraps extra behaviour around it, such as timing, logging or retrying. Writing @timed above load_orders is shorthand for load_orders = timed(load_orders). Inside it I’d use functools.wraps, so the wrapped function keeps its original name and docstring.

    import time
    from functools import wraps
    def timed(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            start = time.perf_counter()
            result = func(*args, **kwargs)
            print(f"{func.__name__} took {time.perf_counter() - start:.3f}s")
            return result
        return wrapper

    The full answer, in the guidePractise it in the Python LabPractise it with Prism

  44. Calculate running totals, moving averages and growth

    HardCode

    Each has a pandas method that mirrors a SQL window function: cumsum gives a running total, rolling a moving average, shift the previous row, like SQL’s LAG, and pct_change the growth. Watch the window: rolling(7) counts rows, but on a date index rolling("7D") counts calendar days, so missing dates don’t stretch it.

    daily = orders.groupby("order_date")["amount"].sum()
    
    daily.cumsum()                           # running total
    daily.rolling(7, min_periods=1).mean()   # last 7 rows
    daily.rolling("7D").mean()               # last 7 calendar days
    daily.shift(1)                           # previous row's value
    daily.pct_change()                       # change on the previous row

    The full answer, in the guidePractise it in the Python LabPractise it with Prism

  45. Why didn’t my assignment change the DataFrame?

    Hard

    It’s chained assignment. Since pandas 3.0, copy-on-write is the only mode, so every indexing step behaves like a copy, and df["col"][mask] = value updates a temporary copy, never df; pandas warns with ChainedAssignmentError, and the old SettingWithCopyWarning is gone. The fix is to do the selection and the assignment in one .loc call.

    pandas 3.0 docs: Copy-on-Write (CoW)pandas docs: What’s new in 3.0.0

    The full answer, in the guidePractise it in the Python LabPractise it with Prism

  46. A DataFrame is too big for memory. What do you do?

    Hard

    Start by measuring with memory_usage(deep=True). Then read less: just the columns you need via usecols, lighter types such as category for repeated text like city names and downcast numbers, or the file in pieces with chunksize, aggregating each piece as you go. If it still won’t fit, push the work into a database and use SQL.

    pandas 3.0 docs: Scaling to large datasets

    The full answer, in the guidePractise it in the Python LabPractise it with Prism

  47. Read a messy CSV and clean it

    HardCode

    Work in a fixed order: tidy the column names, parse the dates with their format, clean the text, convert amounts like “₹1,798” to numbers, and only then drop duplicates, because rows that only differ by spaces or capital letters become identical once cleaned. Finish with info() to confirm every column’s type. It’s the most common take-home task, so expect it.

    df = pd.read_csv("orders.csv")
    df.columns = df.columns.str.strip().str.lower().str.replace(" ", "_")
    df["order_date"] = pd.to_datetime(df["order_date"], format="%d/%m/%Y")
    df["city"] = df["city"].str.strip().str.title()
    df["amount"] = pd.to_numeric(df["amount"].str.replace(r"[₹,]", "", regex=True),
                                 errors="coerce")
    df = df.drop_duplicates()
    df.info()

    The full answer, in the guidePractise it in the Python LabPractise it with Prism

  48. Write end-to-end fraud-detection code in Python, from loading payments data to scoring new payments.

    HardCode

    Split first, by time if the payments have dates: train on earlier months and test on later ones, the way the model will be used; otherwise stratify, so the rare frauds appear in both parts. Fit any preprocessing inside a Pipeline on the training part only. Judge the model on average precision, not accuracy, because frauds are rare, choose the threshold from the cost of a missed fraud against a false alarm, then save the fitted model and score new payments with it.

    import pandas as pd, joblib
    from sklearn.ensemble import HistGradientBoostingClassifier
    from sklearn.metrics import average_precision_score
    df = pd.read_csv("payments.csv", parse_dates=["paid_at"]).sort_values("paid_at")
    X = df.drop(columns=["is_fraud", "paid_at"]).astype({"merchant_type": "category"})
    y = df["is_fraud"]
    cut = int(len(df) * 0.8)                       # train on the earliest 80%, test on the latest 20%
    model = HistGradientBoostingClassifier().fit(X[:cut], y[:cut])   # handles NaN and category columns
    print(average_precision_score(y[cut:], model.predict_proba(X[cut:])[:, 1]))
    joblib.dump(model, "fraud.joblib")             # later: joblib.load(...).predict_proba(new)[:, 1]

    Load a saved model only from a trusted source and with the same scikit-learn version: joblib files can run code, and scikit-learn doesn’t support loading across versions.

    scikit-learn: HistGradientBoostingClassifierscikit-learn: average_precision_scorescikit-learn: Pipelines and composite estimatorsscikit-learn: Model persistence

    Practise it with Prism

  49. How do you compute mean average precision (mAP) for an object detector?

    HardCode

    For each class, sort detections by confidence and match each to an unmatched ground-truth box with IoU at or above a threshold, such as 0.5; a match is a true positive, and anything else, including a second detection of the same object, is a false positive. Walking down that list traces a precision–recall curve, and average precision is the area under it, with each precision raised to the best precision at any higher recall. mAP averages that over classes; COCO also averages over IoU thresholds from 0.50 to 0.95.

    import numpy as np
    def average_precision(scores, is_tp, n_truth):
        order = np.argsort(-np.asarray(scores))
        hits = np.asarray(is_tp, dtype=bool)[order]
        tp, fp = np.cumsum(hits), np.cumsum(~hits)
        recall, precision = tp / n_truth, tp / (tp + fp)
        precision = np.maximum.accumulate(precision[::-1])[::-1]   # best precision at this recall or beyond
        steps = np.diff(np.concatenate([[0], recall]))             # recall gained at each detection
        return float(np.sum(steps * precision))                    # mAP: the mean of this over classes
    print(average_precision([0.9, 0.8, 0.7, 0.6], [1, 0, 1, 1], n_truth=4))   # 0.625

    Don’t swap in scikit-learn’s average_precision_score: it doesn’t interpolate, and it scores only the detections you pass it, so objects the detector missed never lower its recall.

    COCO: Detection evaluationCOCO API: cocoeval.py (GitHub)scikit-learn: average_precision_score

    Practise it with Prism

How to practise with the bank.

  • Say it before you look

    Answer out loud first, as you would in the room, then open ours. Recalling an answer fixes it far better than rereading it.

  • Missed ones come back

    Mark each answer Got it or Review again. The ones you miss return after 1, 3, 7 and 21 days, kept in this browser.

  • Reported at companies

    A logo marks a question candidates reported in a public interview write-up. We keep the source of every one.

Follow your role’s path, step by step

Then try a mock interview with Prism. 5 questions for your role, answered in your own words, with feedback on what you covered and what to add. Free.Start a mock interview