Python interviews for data science roles test both general Python knowledge and data-specific libraries. Unlike software engineering interviews that focus heavily on algorithms and data structures, data science Python interviews emphasise pandas, NumPy, functional programming, and writing efficient data processing code. This guide covers the 50 most frequently asked Python interview questions with detailed answers for 2026.
Python Fundamentals
Q1. What is the difference between a list, tuple, set, and dictionary?
A list is an ordered, mutable sequence that allows duplicate elements: [1, 2, 2, 3]. Accessed by index. Use when you need an ordered collection you will modify. A tuple is an ordered, immutable sequence: (1, 2, 3). Faster than lists, hashable (can be used as dictionary keys), signals to readers that the data should not change. A set is an unordered collection of unique elements: {1, 2, 3}. O(1) membership testing, fast union/intersection/difference operations. Use for deduplication or membership checks. A dictionary is an unordered (insertion-ordered since Python 3.7) collection of key-value pairs: {‘a’: 1, ‘b’: 2}. O(1) lookup by key. Use for mapping relationships.
Q2. Explain mutable vs immutable objects in Python with examples.
Mutable objects can be changed after creation: lists, dictionaries, sets, and most custom objects. Immutable objects cannot be changed: integers, floats, strings, tuples, frozensets. This distinction matters for function arguments (passing a mutable object means the function can modify the caller’s object) and for dictionary keys (only hashable, immutable objects can be keys). When you “modify” an immutable object like a string, Python creates a new object. This is why string concatenation in a loop is O(n²) — use ”.join() instead, which is O(n).
Q3. What is the difference between == and is?
== compares values — are the two objects equal? is compares identity — are the two variables pointing to the exact same object in memory? For small integers (-5 to 256) and interned strings, Python caches objects so is may return True even for separate assignments — this is an implementation detail, not a guarantee. The practical rule: use == to compare values, use is only to compare with None (if x is None), True, or False. Never use is to compare integers, strings, or any custom objects for equality.
Q4. What are *args and **kwargs?
*args allows a function to accept any number of positional arguments, which are collected into a tuple. **kwargs allows any number of keyword arguments, collected into a dictionary. They are named by convention, not syntax — the * and ** are what matter. They can be combined: def func(*args, **kwargs). Use *args when the number of positional arguments is unknown (like print(), sum()). Use **kwargs for optional configuration parameters. In function calls, * unpacks an iterable into positional arguments and ** unpacks a dictionary into keyword arguments.
Q5. What is the difference between a shallow copy and a deep copy?
A shallow copy creates a new container object but fills it with references to the same objects as the original. Changes to mutable nested objects affect both the original and the copy. import copy; new = copy.copy(original). A deep copy creates a completely independent copy — all nested objects are recursively copied. Changes to the copy never affect the original. import copy; new = copy.deepcopy(original). For pandas DataFrames, df.copy(deep=True) creates a deep copy. df.copy(deep=False) or just df[cols] creates a shallow copy, which is why you sometimes see the SettingWithCopyWarning.
Q6. Explain Python’s GIL (Global Interpreter Lock).
The GIL is a mutex in CPython (the standard Python implementation) that allows only one thread to execute Python bytecode at a time. It simplifies memory management (no race conditions on Python objects) but prevents true multi-threaded parallelism for CPU-bound tasks. For I/O-bound tasks (network requests, file I/O), threads are still effective because threads release the GIL while waiting for I/O. For CPU-bound parallelism, use multiprocessing (each process has its own GIL and Python interpreter) or libraries like NumPy that release the GIL during C-level computations. Python 3.13 introduced per-interpreter GIL as an experimental feature, and full GIL removal is being worked on.
Q7. What is a decorator and write one from scratch?
A decorator is a function that takes another function and extends its behaviour without modifying it — a wrapper. Syntactic sugar for func = decorator(func).
import time
import functools
def timer(func):
@functools.wraps(func) # preserves func's name/docstring
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
end = time.perf_counter()
print(f'{func.__name__} took {end - start:.4f}s')
return result
return wrapper
@timer
def train_model(X, y):
# ... training code ...
return model
# Equivalent to: train_model = timer(train_model)
Q8. What is a generator and when would you use one?
A generator is a function that yields values one at a time using the yield keyword, rather than returning a full list. It is lazy — it computes values on demand, making it memory-efficient for large or infinite sequences. Once a value is yielded, the function’s state is paused and resumed on the next next() call. Use generators for: processing large files line-by-line without loading into RAM; streaming data pipelines; infinite sequences (like a counter); and anywhere you only need to iterate through data once. A generator expression (x*2 for x in range(1000000)) uses much less memory than a list comprehension [x*2 for x in range(1000000)].
Q9. Explain list comprehensions, dict comprehensions, and set comprehensions.
List comprehension: [expression for item in iterable if condition]. Equivalent to a for loop that appends to a list, but more readable and often faster. Dict comprehension: {key: value for item in iterable if condition}. Set comprehension: {expression for item in iterable}. Use comprehensions for simple transformations and filters. Use regular for loops when: the logic is complex (multiple conditions, nested logic); you are debugging; or you need to break out of the loop early. Never sacrifice readability for comprehension syntax — a clear three-line loop beats an unreadable one-liner.
Q10. What is the difference between range() and xrange() in Python 3?
xrange() does not exist in Python 3. In Python 2, range() returned a list and xrange() returned an iterator. In Python 3, range() behaves like Python 2’s xrange() — it returns a lazy range object that does not generate all values in memory. range(1000000) in Python 3 uses constant memory regardless of the range size. If you need a list, explicitly convert: list(range(10)).
Pandas and NumPy Interview Questions
Q11. What is the difference between loc and iloc in pandas?
loc is label-based: it uses the actual index labels and column names. df.loc[2, ‘salary’] — row with index label 2, column named ‘salary’. Both endpoints are inclusive in slices: df.loc[1:3] includes rows 1, 2, and 3. iloc is integer position-based: it always uses 0-based integer positions regardless of the actual index labels. df.iloc[2, 3] — third row (0-indexed), fourth column. Slices in iloc follow standard Python convention (exclusive end): df.iloc[1:3] returns rows at positions 1 and 2. When the index is the default integer range, loc and iloc give the same result — but they diverge when you reset, filter, or set a custom index.
Q12. What is vectorisation in pandas/NumPy and why does it matter?
Vectorisation means applying an operation to an entire array at once using compiled C/Fortran code, rather than iterating over elements in Python. NumPy and pandas are wrappers around highly optimised C libraries. A vectorised operation like df[‘salary’] * 1.1 applies the multiplication to all elements simultaneously in C, which is 10-100x faster than a Python for loop. This is why you should never use iterrows() or apply() when a vectorised alternative exists — np.where(), string operations via .str accessor, and arithmetic on whole columns are all vectorised. The performance difference is not an optimisation — it is the difference between seconds and hours on large datasets.
Q13. What is the difference between merge, join, and concat in pandas?
pd.concat() stacks DataFrames vertically (appending rows) or horizontally (appending columns) — no key matching, just positional alignment. pd.merge() is the most powerful — it joins on one or more key columns (like SQL JOIN), supporting inner/left/right/outer joins. df.join() is a convenience wrapper around merge that joins on the DataFrame’s index by default — faster for index-based joins. Use concat for combining similarly structured DataFrames (loading multiple CSVs). Use merge for joining on meaningful keys (customer_id, product_id). Use join for index-based lookups.
Q14. How do you handle missing values in pandas? What are the different strategies?
Detect: df.isnull().sum() and df.isnull().mean()*100 for percentages. Drop: df.dropna() (rows), df.dropna(axis=1) (columns), df.dropna(subset=[‘col’]) (only if specific column is null). Fill: df.fillna(0), df.fillna(df.mean()), df.fillna(method=’ffill’) (forward fill for time series). Impute: SimpleImputer for mean/median/mode, KNNImputer for distance-weighted imputation. The strategy should match the reason data is missing: MCAR (missing completely at random) — any strategy works; MAR (missing at random, conditional on observed data) — use model-based imputation; MNAR (missing not at random, missing value relates to its value) — most dangerous, may need domain knowledge.
Q15. Explain broadcasting in NumPy.
Broadcasting is NumPy’s mechanism for performing operations on arrays of different shapes without explicitly copying data. When two arrays have incompatible shapes, NumPy “broadcasts” the smaller array across the larger one by virtually expanding it. Rules: dimensions are compared from the trailing (right) end; dimensions are compatible if they are equal or one of them is 1. Example: a (3,4) array + a (4,) array — the (4,) array is broadcast across 3 rows. This enables concise, memory-efficient code: subtracting the column mean from every row: data – data.mean(axis=0) works without needing to reshape the mean array.
Object-Oriented Python
Q16. What are the four pillars of OOP and how does Python implement each?
Encapsulation: bundling data and methods that operate on that data. Python uses classes. “Private” attributes are by convention (_name = weak private, __name = name mangling). Inheritance: a class inherits attributes and methods from a parent class. Python supports multiple inheritance. Use super() to call the parent’s methods. Polymorphism: different classes can implement the same interface differently. Python achieves this via duck typing — if it has the right methods, it works, regardless of its type. Abstraction: hiding implementation details behind a simple interface. Use abstract base classes (abc module) to define interfaces that subclasses must implement.
Q17. What is the difference between @staticmethod and @classmethod?
A regular method receives the instance (self) as the first argument and can access instance state. A @classmethod receives the class (cls) as the first argument — useful for alternative constructors (class factories) and accessing class-level state. A @staticmethod receives neither self nor cls — it is just a regular function that lives in the class namespace for organisational reasons. Example: DataFrame.from_csv() is a classmethod — an alternative way to create a DataFrame. Use @classmethod when you need class-level logic; @staticmethod when the method does not need class or instance state but belongs logically with the class.
Q18. What is a context manager and how do you create one?
A context manager manages resources — it sets something up on entry and tears it down on exit, even if an exception occurs. The with statement uses context managers. Built-in examples: open() (closes the file), threading.Lock() (releases the lock). Create one with __enter__ and __exit__ methods, or with the @contextmanager decorator from contextlib.
from contextlib import contextmanager
import time
@contextmanager
def timer_ctx(label):
start = time.perf_counter()
try:
yield # code in the with block runs here
finally: # runs even if an exception occurs
elapsed = time.perf_counter() - start
print(f'{label}: {elapsed:.4f}s')
with timer_ctx('model training'):
model.fit(X_train, y_train)
Functional Programming and Performance
Q19. What is the difference between map(), filter(), and reduce()?
map(func, iterable) applies func to every element, returning an iterator of results. filter(func, iterable) returns an iterator of elements where func(element) is True. reduce(func, iterable) applies func cumulatively to reduce the iterable to a single value — it is in functools in Python 3. In modern Python, list comprehensions and generator expressions are generally preferred over map() and filter() for readability. reduce() is still useful for operations like finding the product of a list or combining dictionaries. Lambda functions are often passed to these: map(lambda x: x**2, numbers).
Q20. What is the time complexity of common Python operations?
List: append O(1), insert O(n), delete O(n), index access O(1), search O(n), len O(1). Dict: get/set/delete O(1) average, O(n) worst case (hash collisions, rare). Set: add/discard/membership O(1) average. String concatenation in a loop: O(n²) — use ”.join() which is O(n). Sorting: O(n log n). These complexities matter for data processing at scale. When you have a large list and need repeated membership checks, convert it to a set first. When you need a sorted structure with fast insertion, use heapq or sortedcontainers.
Q21–30 (Rapid fire):
Q21. What is a lambda function? An anonymous single-expression function: lambda x: x*2. Use for simple, short operations passed to higher-order functions. Prefer named functions for anything complex — lambda’s readability advantage disappears with complexity.
Q22. What is the difference between append() and extend() in lists? append() adds its argument as a single element (appending a list adds a nested list). extend() iterates over its argument and adds each element individually. lst.extend([1,2]) is equivalent to lst += [1,2].
Q23. What is __init__ vs __new__? __new__ creates the instance (called first, returns the new object). __init__ initialises it (called second, receives the object, sets attributes). Override __new__ only for immutable types like int and str, or for singleton patterns.
Q24. How does Python manage memory? Reference counting (each object tracks how many references point to it; freed when count reaches 0) plus a cyclic garbage collector that handles reference cycles. The gc module can control the garbage collector. del x decrements the reference count.
Q25. What is the difference between is None and == None? Always use is None. None is a singleton — there is only one None object. is checks identity (same object), == checks equality (which can be overridden). Using == None can give unexpected results if __eq__ is defined.
Q26. What is a defaultdict? from collections import defaultdict. A dictionary subclass that provides a default value for missing keys via a factory function, avoiding KeyError. defaultdict(list) initialises missing keys with []; defaultdict(int) with 0; defaultdict(set) with set().
Q27. What is enumerate() and when do you use it? enumerate(iterable, start=0) returns (index, element) pairs. Use instead of manually maintaining a counter variable: for i, val in enumerate(my_list). Cleaner and more Pythonic than for i in range(len(my_list)).
Q28. What is zip() and how do you unzip? zip(a, b) combines iterables element-wise into tuples: zip([1,2],[3,4]) → [(1,3),(2,4)]. Stops at the shorter iterable (use itertools.zip_longest for equal-length pairing). Unzip: a, b = zip(*zipped).
Q29. What is __repr__ vs __str__? __str__ is for human-readable output (print(), str()). __repr__ is for unambiguous developer representation (repr(), and the default in the REPL). If only __repr__ is defined, it serves as __str__ too. Rule: __repr__ should ideally produce valid Python to reconstruct the object.
Q30. What are Python’s built-in sorting key tricks? sorted(data, key=lambda x: x[‘age’]) sorts by a computed key. sorted(data, key=lambda x: (x[‘city’], x[‘age’])) sorts by multiple keys. sorted(data, key=operator.itemgetter(‘age’)) is faster than lambda for simple attribute access. reverse=True for descending. list.sort() sorts in-place; sorted() returns a new list.
Python for Data Science Patterns
import pandas as pd
import numpy as np
# The most common patterns interviewers test:
# 1. Apply a custom function conditionally
df['tier'] = np.where(df['revenue'] > 10000, 'High',
np.where(df['revenue'] > 5000, 'Medium', 'Low'))
# 2. Group-level feature (without losing rows)
df['revenue_vs_avg'] = df['revenue'] / df.groupby('region')['revenue'].transform('mean')
# 3. Explode a list column
df_exploded = df.assign(tags=df['tags'].str.split(',')).explode('tags')
# 4. Pivot and melt
wide = df.pivot_table(index='user', columns='month', values='spend', aggfunc='sum')
long = wide.reset_index().melt(id_vars='user', var_name='month', value_name='spend')
# 5. Rolling window calculation
df = df.sort_values('date')
df['rolling_7d_mean'] = df.groupby('user_id')['spend'].transform(
lambda x: x.rolling(7, min_periods=1).mean()
)
Conclusion
Python interviews for data science roles test practical problem-solving more than computer science theory. The highest-value topics to master are: pandas (loc/iloc, groupby, merge, apply vs vectorisation), NumPy (broadcasting, array operations), generators and comprehensions, and object-oriented basics. Practice writing code without IDE assistance — many interviews use plain editors or whiteboards. Read PEP 8 and write idiomatic Python. The most impressive interview Python is clean, readable, and efficient — not the cleverest one-liner possible.



