What interviewers actually check in a Python interview
Python interviews are not a memory test on syntax. Interviewers are checking whether you understand how Python works under the hood — memory model, iterators, generators, the GIL — and whether you can write clean, Pythonic code on a whiteboard or shared editor. The six areas below come up again and again.
Core language fundamentals
Interpreted vs compiled, dynamic typing, mutable vs immutable, list vs tuple vs set vs dict, is vs ==, pass/continue/break, f-strings and type hints.
Functions and functional tools
*args and **kwargs, lambda, map/filter/reduce, list and dict comprehensions, closures, default argument pitfalls, and scope rules (LEGB).
OOPs and design
Classes, inheritance, MRO, polymorphism, encapsulation, dunder methods, @staticmethod vs @classmethod, abstract classes, and composition over inheritance.
Iterators, generators and decorators
__iter__ and __next__, yield, generator expressions, lazy evaluation, decorators with and without arguments, functools.wraps, and context managers with the with statement.
Exceptions, memory and the GIL
try/except/else/finally, custom exceptions, reference counting, cyclic garbage collection, the Global Interpreter Lock, and when to use threads vs processes vs asyncio.
Data analysis and the coding round
pandas read_csv, groupby, merge, pivot_table, missing values, apply, datetime handling, and the classic coding problems — reverse a string, palindrome, Fibonacci, two-sum, and more.
Python interview questions for freshers
These are the questions that come up first in Python fresher interviews. They test whether your fundamentals are solid — answer with a short definition, then one line of code or an example.
Q01 What are the key features of Python? +
- Interpreted — code runs line by line through the interpreter, no separate compile step.
- Dynamically typed — you do not declare types; the type is attached to the object.
- High-level and readable — indentation defines blocks, syntax is close to English.
- Object-oriented and functional — supports classes, inheritance, plus first-class functions and closures.
- Batteries included — a large standard library (os, json, re, collections, itertools).
- Cross-platform — the same code runs on Windows, macOS, Linux and most embedded systems.
- Extensible — call C/C++ libraries through ctypes, cffi or Cython for performance.
Q02 What is the difference between a list and a tuple? +
| Aspect | List | Tuple |
|---|---|---|
| Mutability | Mutable — can add, remove, change items | Immutable — cannot be changed after creation |
| Syntax | [1, 2, 3] |
(1, 2, 3) |
| Performance | Slightly slower | Faster and uses less memory |
| Use as dict key | No — unhashable | Yes — if all elements are hashable |
| Typical use | Collection that changes — a shopping cart | Fixed record — coordinates, RGB values |
my_list = [1, 2, 3]
my_list.append(4) # ok
my_tuple = (1, 2, 3)
my_tuple[0] = 9 # TypeError
Q03 What is the difference between a list and a set? +
A list is an ordered, indexable collection that allows duplicates. A set is an unordered collection of unique elements with no indexing.
nums = [1, 2, 2, 3]
print(nums) # [1, 2, 2, 3]
unique = set(nums)
print(unique) # {1, 2, 3}
print(2 in nums) # True — O(n) scan
print(2 in unique) # True — O(1) hash lookup
Use a set when you need fast membership checks or uniqueness. Use a list when order and indexing matter.
Q04 What is the difference between a dict and a set? +
Both are hash-based and unordered (though dicts preserve insertion order since Python 3.7). A dict stores key-value pairs. A set stores only keys.
d = {"name": "Ravi", "age": 30}
s = {"name", "age"}
print(d["name"]) # Ravi
print("name" in s) # True
Use a dict when you need to map a key to a value. Use a set when you only need to know whether something exists in a collection.
Q05 What is the difference between is and ==? +
== compares values.
is compares identity —
whether two names refer to the same object in
memory.
a = [1, 2, 3]
b = [1, 2, 3]
print(a == b) # True — same value
print(a is b) # False — different objects
c = a
print(a is c) # True — same object
Use is only for None,
True, False and
sentinel objects — like
if x is None. For everything
else, use ==.
Q06 What are mutable and immutable types in Python? +
Mutable objects can be changed after creation. Immutable objects cannot.
| Mutable | Immutable |
|---|---|
| list | tuple |
| dict | str |
| set | int, float, bool |
| bytearray | frozenset, bytes |
| Custom classes (usually) | Named tuples |
The practical difference: mutable objects can be changed in place, which can cause surprising bugs when two names point to the same object. Immutable objects are safe to share and can be dictionary keys.
Q07 What is a Python dictionary comprehension? +
A dict comprehension builds a dictionary in a
single expression — often clearer and faster
than a loop with dict[key] = value.
# squares of numbers 1..5
squares = {n: n*n for n in range(1, 6)}
print(squares)
# {1: 1, 2: 4, 3: 9, 4: 16, 5: 25}
# with a condition
evens = {n: n*n for n in range(10) if n % 2 == 0}
print(evens)
# {0: 0, 2: 4, 4: 16, 6: 36, 8: 64}
Q08 What is a list comprehension, and how does it differ from a loop? +
A list comprehension is a compact way to build a list from an iterable, often with a filter. It is usually faster than an equivalent for-loop because the loop is run inside the interpreter.
# loop version
squares = []
for n in range(10):
squares.append(n * n)
# comprehension version
squares = [n * n for n in range(10)]
# with a filter
evens = [n for n in range(10) if n % 2 == 0]
print(evens) # [0, 2, 4, 6, 8]
Use comprehensions when the logic fits on one line. If it needs nested loops or side effects, use a normal loop — readability wins.
Q09 What is a lambda function? +
A lambda is an anonymous function written in one line. It takes any number of arguments but has exactly one expression, and the result of that expression is returned.
# named function
def square(x):
return x * x
# equivalent lambda
square = lambda x: x * x
print(square(5)) # 25
# common use — sorting with a key
people = [("Ravi", 30), ("Anita", 25)]
people.sort(key=lambda p: p[1])
print(people)
# [('Anita', 25), ('Ravi', 30)]
Use lambdas for short, throwaway functions —
as a key argument or inside
map and filter. For
anything more complex, use a named function.
Q10 What is the difference between append() and extend() on a list? +
append() adds a single item as
one element — even if that item is itself a
list. extend() iterates over the
argument and adds each element individually.
a = [1, 2]
a.append([3, 4])
print(a) # [1, 2, [3, 4]]
b = [1, 2]
b.extend([3, 4])
print(b) # [1, 2, 3, 4]
Q11 What is the difference between remove() and pop() on a list? +
remove(x) removes the first
occurrence of the value x and returns
None. pop(i)
removes and returns the element at
index i.
nums = [10, 20, 30, 20]
nums.remove(20)
print(nums) # [10, 30, 20]
last = nums.pop()
print(last) # 20
print(nums) # [10, 30]
Note: remove raises
ValueError if the value is not in the list.
pop raises IndexError if the list is
empty.
Q12 What are f-strings in Python? +
f-strings (formatted string literals)
let you embed expressions inside a string
using {}. They are the cleanest
and fastest way to format strings.
name = "Ravi"
age = 30
print(f"Hi {name}, you are {age} years old.")
# Hi Ravi, you are 30 years old.
# expressions inside {}
print(f"Next year you will be {age + 1}.")
# formatting — 2 decimal places
pi = 3.14159
print(f"Pi is {pi:.2f}") # Pi is 3.14
Q13 What is the difference between pass, continue and break? +
pass — a placeholder that does nothing. Used where syntax requires a block but you have nothing to do yet.
continue — skip the rest of the current loop iteration and move to the next one.
break — exit the loop entirely.
for i in range(10):
if i == 3:
continue # skip 3
if i == 7:
break # stop at 7
print(i, end=" ")
# 0 1 2 4 5 6
Q14 What is the use of the range() function? +
range() returns an iterable
sequence of integers without building a list
in memory. Three forms:
range(stop)— 0 to stop-1range(start, stop)— start to stop-1range(start, stop, step)— with a custom step
print(list(range(5))) # [0, 1, 2, 3, 4]
print(list(range(2, 8))) # [2, 3, 4, 5, 6, 7]
print(list(range(0, 10, 2))) # [0, 2, 4, 6, 8]
print(list(range(10, 0, -2))) # [10, 8, 6, 4, 2]
In Python 3, range() is lazy — it
produces numbers on demand, so
range(10_000_000) does not use
gigabytes of memory.
Q15 What is the difference between type() and isinstance()? +
type(obj) returns the exact class
of the object. isinstance(obj, cls)
returns True if the object is an
instance of that class or any subclass.
class Animal: pass
class Dog(Animal): pass
d = Dog()
print(type(d) == Dog) # True
print(type(d) == Animal) # False
print(isinstance(d, Animal)) # True
print(isinstance(d, Dog)) # True
For type checks in application code, prefer
isinstance — it respects
inheritance. Use type() when you
need exact type equality.
Q16 What is PEP 8? +
PEP 8 is the official style guide for Python code. It is not enforced by the interpreter, but the community follows it so that code looks consistent.
Key rules:
- Four spaces per indent level — never mix tabs and spaces.
- Snake_case for functions and variables, PascalCase for classes, UPPER_CASE for constants.
- Two blank lines between top-level functions and classes, one blank line between methods.
- Maximum line length of 79 characters (many teams relax this to 88 or 100).
- Imports on separate lines, grouped: standard library, third-party, local.
- Spaces around operators —
x = a + b, notx=a+b. - Docstrings for every public module, class and function.
Tools like black, flake8
and ruff enforce PEP 8
automatically.
Q17 What are *args and **kwargs? +
*args collects extra positional
arguments into a tuple.
**kwargs collects extra
keyword arguments into a dict. They let a
function accept any number of arguments.
def greet(greeting, *args, **kwargs):
print(greeting)
print("Positional:", args)
print("Keyword:", kwargs)
greet("Hello", "Ravi", "Anita", age=30, city="Noida")
# Hello
# Positional: ('Ravi', 'Anita')
# Keyword: {'age': 30, 'city': 'Noida'}
Order matters in the function signature:
positional, then *args, then
keyword-only arguments, then
**kwargs.
Q18 What is the difference between a shallow copy and a deep copy? +
A shallow copy creates a new container but the elements inside are still shared with the original. A deep copy recursively copies every nested object.
import copy
a = [[1, 2], [3, 4]]
b = copy.copy(a) # shallow
c = copy.deepcopy(a) # deep
a[0][0] = 99
print(b) # [[99, 2], [3, 4]] — inner list shared
print(c) # [[1, 2], [3, 4]] — fully independent
Use copy.copy() for flat
containers and copy.deepcopy()
when nested objects must be independent.
Q19 What is a docstring, and how is it different from a comment? +
A docstring is a string literal as the
first statement of a module, class or
function. It is stored at runtime and
accessible as obj.__doc__, so
tools and help() can read it.
A comment starts with # and
is completely ignored by the interpreter — it
exists only for human readers.
def add(a, b):
"""Return the sum of a and b."""
return a + b
print(add.__doc__) # Return the sum of a and b.
help(add) # shows the docstring
Docstrings are the standard way to document public APIs. Comments are for explaining non-obvious internal logic.
Q20 What is the difference between a module and a package? +
A module is a single .py
file containing Python code. A package
is a directory of modules with an
__init__.py file, which tells
Python that the directory is a package.
# module: math_utils.py
def add(a, b):
return a + b
# package structure:
# mypackage/
# __init__.py
# math_utils.py
# string_utils.py
# import
from mypackage import math_utils
print(math_utils.add(2, 3)) # 5
Packages group related modules — this is how NumPy, pandas and Django are structured.
Intermediate Python interview questions
These questions come up for candidates with 2–4 years of experience. They test whether you understand how Python works under the hood and can write code that is Pythonic.
Q21 What is a generator in Python, and how does yield work? +
A generator is a function that produces
values lazily using yield. Each
call to yield pauses the function
and returns one value; the next call resumes
from where it stopped. This means you can
iterate over a very large sequence without
building it in memory.
def count_up_to(n):
i = 1
while i <= n:
yield i
i += 1
for x in count_up_to(5):
print(x, end=" ")
# 1 2 3 4 5
Generators are used for reading large files, streaming data, and infinite sequences. They are memory-efficient because they produce values one at a time.
Q22 What is the difference between an iterator and an iterable? +
An iterable is any object that can be
looped over — it implements
__iter__ and returns an
iterator. A list, dict,
string and set are iterables.
An iterator is the object that
actually produces values. It implements
__iter__ (returns itself) and
__next__ (returns the next value
or raises StopIteration).
nums = [1, 2, 3] # iterable
it = iter(nums) # iterator
print(next(it)) # 1
print(next(it)) # 2
print(next(it)) # 3
# next(it) raises StopIteration
Every iterator is iterable, but not every
iterable is an iterator. A list is iterable
but not an iterator — you need iter()
to get its iterator.
Q23 What is a decorator in Python? +
A decorator is a function that takes another function as input and returns a new function with extra behaviour. It lets you wrap functions without changing their code — for logging, caching, timing, authentication and similar cross-cutting concerns.
import time
def timer(func):
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 slow():
time.sleep(0.5)
slow()
# slow took 0.5003s
Always use functools.wraps(func)
inside the wrapper to preserve the original
function's name and docstring.
Q24 How do you write a decorator that takes arguments? +
You need an extra layer of nesting — one function to accept the decorator arguments, one to accept the function being decorated, and one to wrap it.
def repeat(times):
def decorator(func):
def wrapper(*args, **kwargs):
for _ in range(times):
func(*args, **kwargs)
return wrapper
return decorator
@repeat(3)
def hi():
print("Hello")
hi()
# Hello
# Hello
# Hello
Q25 What is a context manager, and how does the with statement work? +
A context manager is an object that
defines setup and cleanup logic that runs
automatically around a block of code. The
with statement guarantees the
cleanup runs even if an exception is raised.
Two ways to create one: a class with
__enter__ and
__exit__, or a generator
decorated with
@contextlib.contextmanager.
class Timer:
def __enter__(self):
import time
self.start = time.perf_counter()
return self
def __exit__(self, exc_type, exc, tb):
import time
print(f"Elapsed: {time.perf_counter() - self.start:.4f}s")
with Timer():
sum(range(1_000_000))
# Elapsed: 0.0123s
Note: The most common use is opening files —
with open("data.txt") as f: automatically closes the
file when the block exits.
Q26 What is the difference between @staticmethod and @classmethod? +
| Aspect | @staticmethod | @classmethod |
|---|---|---|
| First argument | None | cls (the class itself) |
| Access to class state | No | Yes — can read class attributes |
| Access to instance state | No | No |
| Typical use | Utility function that belongs conceptually to the class | Alternative constructor, factory method |
class Date:
def __init__(self, year, month, day):
self.year, self.month, self.day = year, month, day
@classmethod
def from_string(cls, s):
y, m, d = map(int, s.split("-"))
return cls(y, m, d)
@staticmethod
def is_leap(year):
return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)
d = Date.from_string("2026-10-14")
print(d.year, Date.is_leap(2024)) # 2026 True
Q27 Explain OOP concepts in Python — encapsulation, inheritance and polymorphism. +
Encapsulation — bundling data and methods, and controlling access. Python uses
naming conventions: _name for protected, __name for
private (name-mangled).
Inheritance — a child class reuses and extends a parent class. Python supports single, multiple and multilevel inheritance.
Polymorphism — the same method name behaves differently for different classes. Python supports it through method overriding and duck typing.
class Animal:
def __init__(self, name):
self._name = name # protected
def speak(self):
raise NotImplementedError
class Dog(Animal):
def speak(self): # polymorphism
return f"{self._name} says Woof"
class Cat(Animal):
def speak(self):
return f"{self._name} says Meow"
for a in [Dog("Rex"), Cat("Milo")]:
print(a.speak())
# Rex says Woof
# Milo says Meow
Q28 What is Method Resolution Order (MRO) in Python? +
MRO is the order in which Python
searches base classes when looking for a
method or attribute. Python uses the C3
linearisation algorithm to compute it. You
can inspect it with
ClassName.__mro__ or
ClassName.mro().
class A:
def who(self): return "A"
class B(A):
def who(self): return "B"
class C(A):
def who(self): return "C"
class D(B, C):
pass
print([c.__name__ for c in D.mro()])
# ['D', 'B', 'C', 'A', 'object']
print(D().who()) # B
MRO matters most with multiple inheritance. It prevents the "diamond problem" by defining a single, deterministic lookup order.
Q29 What are dunder (magic) methods? Give examples. +
Dunder methods are special methods with double underscores that Python calls automatically — for printing, comparing, adding, indexing and more.
| Method | Called by |
|---|---|
__init__ |
Constructor |
__str__ |
print(obj) |
__repr__ |
Debugger / repr(obj) |
__len__ |
len(obj) |
__eq__ |
obj1 == obj2 |
__lt__ |
obj1 < obj2 |
__add__ |
obj1 + obj2 |
__getitem__ |
obj[key] |
__iter__ |
for x in obj |
__next__ |
next(obj) |
class Money:
def __init__(self, amount):
self.amount = amount
def __str__(self):
return f"₹{self.amount}"
def __add__(self, other):
return Money(self.amount + other.amount)
m1 = Money(100)
m2 = Money(50)
print(m1 + m2) # ₹150
Q30 How does exception handling work in Python? +
Python uses try,
except, else and
finally. The
else block runs if no exception
was raised; finally always runs.
try:
n = int(input("Enter a number: "))
result = 10 / n
except ValueError:
print("Not a valid number")
except ZeroDivisionError:
print("Cannot divide by zero")
else:
print(f"Result: {result}")
finally:
print("Done")
Best practices: catch specific exceptions,
not bare except:. Use
raise to re-raise. Create custom
exception classes by subclassing
Exception.
Q31 What is a custom exception, and how do you create one? +
A custom exception is a class you
define by subclassing
Exception. You create them when
built-in exceptions do not describe the error
precisely enough.
class InsufficientBalance(Exception):
def __init__(self, balance, amount):
self.balance = balance
self.amount = amount
super().__init__(
f"Cannot withdraw ₹{amount}; balance is ₹{balance}")
def withdraw(balance, amount):
if amount > balance:
raise InsufficientBalance(balance, amount)
return balance - amount
try:
withdraw(500, 1000)
except InsufficientBalance as e:
print(e)
# Cannot withdraw ₹1000; balance is ₹500
Q32 What are modules and packages? How does import work? +
A module is a .py file. A
package is a folder of modules with
an __init__.py. The
import statement makes a module's
objects available in the current namespace.
# import the whole module
import math
print(math.sqrt(16)) # 4.0
# import specific names
from math import sqrt, pi
print(sqrt(25), pi) # 5.0 3.141592653589793
# import with alias
import pandas as pd
# import from a package
from mypackage import utils
Python's import machinery searches
sys.path — the current
directory, then standard library paths, then
site-packages where third-party libraries
are installed.
Q33 What is a virtual environment, and why is it used? +
A virtual environment is an isolated
Python installation with its own
site-packages folder. It lets
each project use its own versions of
libraries without conflicts.
# create
python -m venv venv
# activate — Linux / macOS
source venv/bin/activate
# activate — Windows
venv\Scripts\activate
# install packages
pip install pandas
# freeze for reproducibility
pip freeze > requirements.txt
# deactivate
deactivate
Without a virtual environment, installing a library for one project can break another. Every serious Python project uses one.
Q34 What are map, filter and reduce in Python? +
These are functional programming tools.
map applies a function to every item
in an iterable. filter keeps only the
items where a function returns
True. reduce combines
all items into a single value using a
two-argument function.
from functools import reduce
nums = [1, 2, 3, 4, 5]
squares = list(map(lambda x: x * x, nums))
print(squares) # [1, 4, 9, 16, 25]
evens = list(filter(lambda x: x % 2 == 0, nums))
print(evens) # [2, 4]
total = reduce(lambda a, b: a + b, nums)
print(total) # 15
In modern Python, list comprehensions are
usually clearer than map and
filter for simple cases.
reduce is best reserved for
genuine accumulations — sums, products,
merging.
Q35 What are closures in Python? +
A closure is a nested function that remembers variables from its enclosing function even after the outer function has returned.
def multiplier(n):
def mul(x):
return x * n # n is captured
return mul
double = multiplier(2)
triple = multiplier(3)
print(double(10)) # 20
print(triple(10)) # 30
Closures are the basis for decorators. If
you need to modify the captured variable,
use nonlocal inside the inner
function.
Q36 What is the difference between range and xrange in Python? +
In Python 2, range() built a
list in memory and xrange()
returned a lazy iterator. In Python 3,
xrange() was removed and
range() now behaves like the
old xrange() — it is lazy and
memory-efficient.
r = range(1_000_000)
print(type(r)) # <class 'range'>
print(len(r)) # 1000000
print(r[500]) # 500 — O(1) indexing
If you are asked this in an interview, the
answer is short: in Python 3, there is no
xrange — range is
lazy.
Q37 What are type hints, and why are they used? +
Type hints annotate the expected types of arguments and return values. They are not enforced at runtime — they are documentation for humans and tools like mypy, Pyright and IDE autocompletion.
from typing import Optional
def greet(name: str, age: int) -> str:
return f"Hi {name}, age {age}"
def find(items: list[int], target: int) -> Optional[int]:
for i, x in enumerate(items):
if x == target:
return i
return None
Type hints catch bugs before runtime when checked by a type checker. They also make large codebases easier to read.
Q38 What is the difference between deepcopy and copy in the copy module? +
copy.copy(x) creates a shallow
copy — a new top-level object, but nested
objects are shared with the original.
copy.deepcopy(x) recursively
copies every nested object, so the copy is
fully independent.
import copy
original = {"user": {"name": "Ravi", "tags": ["a", "b"]}}
shallow = copy.copy(original)
deep = copy.deepcopy(original)
original["user"]["tags"].append("c")
print(shallow["user"]["tags"]) # ['a', 'b', 'c']
print(deep["user"]["tags"]) # ['a', 'b']
Use copy for flat structures,
deepcopy for nested ones.
deepcopy is slower, so use it
only when you actually need independence.
Q39 What is the purpose of __init__.py in a package? +
__init__.py tells Python that a
directory should be treated as a package.
It can be empty, or it can initialise the
package — importing common modules,
defining __all__, or setting
package-level variables.
# mypackage/__init__.py
from .math_utils import add
from .string_utils import clean
__all__ = ["add", "clean"]
With this, users can write
from mypackage import add
instead of importing the submodule
directly. Namespace packages (folders
without __init__.py) exist in
Python 3, but most projects still use the
file.
Q40 What is the purpose of __name__ == "__main__"? +
When a Python file is run directly,
Python sets its __name__ to
"__main__". When the file is
imported as a module,
__name__ is set to the
module's name instead.
The guard
if __name__ == "__main__":
lets you write code that only runs when the
file is executed directly — not when it is
imported.
def greet(name):
return f"Hello, {name}"
if __name__ == "__main__":
print(greet("Ravi")) # runs only when executed directly
Importing mymodule from another
file will not print anything, but you can
still call mymodule.greet("Ravi").
Python interview questions for experienced professionals
These questions come up for candidates with 5+ years of experience or for anyone interviewing at a product company. They expect you to reason about performance, concurrency and design trade-offs.
Q41 What is the GIL in Python, and why does it matter? +
The Global Interpreter Lock (GIL) is a mutex in CPython that allows only one thread to execute Python bytecode at a time. Even on a multi-core CPU, only one thread runs Python code at any moment.
Implications:
- Threads do not speed up CPU-bound code — but they are fine for I/O-bound work (network, disk) because the GIL is released during I/O.
- Multiprocessing avoids the GIL by using separate processes, each with its own interpreter.
- asyncio avoids the GIL issue for I/O concurrency by using a single-threaded event loop and cooperative multitasking.
- C extensions can release the GIL, which is why NumPy operations are fast despite the GIL.
Note: CPython has an experimental "free-threaded" mode (PEP 703) that removes the GIL, but as of 2026 it is not the default. Most production code still uses the GIL model.
Q42 When would you use multithreading vs multiprocessing? +
| Scenario | Use | Why |
|---|---|---|
| Reading many files or URLs | Threads or asyncio | I/O-bound — the GIL is released during I/O |
| Heavy number crunching | Processes | CPU-bound — each process has its own GIL |
| Simple script parallel to a network task | Threads | Low overhead, simple to write |
| Thousands of concurrent connections | asyncio | Single thread, no context switch overhead |
| Data processing using pandas/NumPy | Processes | C extensions release the GIL but not always enough |
A simple rule: if the task spends most of its time waiting, use threads or asyncio. If it spends most of its time computing, use processes.
Q43 How does memory management and garbage collection work in Python? +
Python uses two mechanisms:
- Reference counting — every object keeps a count of how many references point to it. When the count reaches zero, the object is deallocated immediately.
- Garbage collection — a cyclic garbage collector runs periodically to detect reference cycles that reference counting cannot break.
You can interact with the GC through the gc module:
import gc, sys
x = [1, 2, 3]
print(sys.getrefcount(x)) # number of references
print(gc.get_threshold()) # (700, 10, 10)
gc.collect() # force a collection
gc.disable() # turn off automatic GC
Memory leaks in Python usually come from reference cycles, long-lived caches or global state — not from forgetting to free memory.
Q44 What is asyncio, and how does it differ from threading? +
asyncio is a single-threaded event loop
for cooperative multitasking. Coroutines
yield control at await points,
letting the loop run other tasks while one
waits for I/O.
Threading uses OS threads with preemptive scheduling. The GIL means only one thread runs Python code at a time, but threads are still useful for I/O because the GIL is released during blocking calls.
| Aspect | asyncio | Threading |
|---|---|---|
| Scheduling | Cooperative (await) | Preemptive (OS) |
| Threads | One | Many |
| Overhead per task | Very low | Higher — thread stack |
| Shared state safety | Safe — no locks needed | Requires locks |
| Best for | Many concurrent network calls | Blocking libraries, moderate concurrency |
import asyncio
async def fetch(url):
print(f"Fetching {url}")
await asyncio.sleep(1)
return f"Done {url}"
async def main():
results = await asyncio.gather(
fetch("a"), fetch("b"), fetch("c"))
print(results)
asyncio.run(main())
# Fetching a
# Fetching b
# Fetching c
# ['Done a', 'Done b', 'Done c'] — runs in ~1 second total
Q45 How would you profile and optimise a slow Python program? +
Measure first, then optimise. The usual workflow:
- Time it —
time.perf_counter()around key sections. - Profile —
cProfilefor function-level hotspots,line_profilerfor line-level. - Find the hotspot — often 80% of the time is in 20% of the code. Do not optimise code that is not hot.
- Common fixes:
- Replace loops with vectorised NumPy operations.
- Use built-in functions —
sum,min,any— instead of manual loops. - Use a
setordictfor membership tests instead of a list. - Precompute values outside loops.
- Use
functools.lru_cachefor pure functions that repeat. - Move data into a proper structure — pandas DataFrame, NumPy array.
- Re-measure — confirm the change actually helped.
import cProfile
def slow():
return sum(i * i for i in range(1_000_000))
cProfile.run("slow()")
# shows the number of calls and cumulative time
Q46 What are metaclasses in Python? +
A metaclass is the class of a class.
Just as an object is an instance of a class,
a class is an instance of a metaclass. The
default metaclass is type.
Metaclasses let you control how classes are created — validating attributes, registering classes, or injecting methods automatically. They are used in frameworks like Django ORM and SQLAlchemy.
class Meta(type):
def __new__(mcls, name, bases, ns):
print(f"Creating class {name}")
return super().__new__(mcls, name, bases, ns)
class MyClass(metaclass=Meta):
pass
# Creating class MyClass
Rule of thumb: 99% of the time you do not
need a metaclass. Use class decorators or
__init_subclass__ first —
they are simpler and cover most cases.
Q47 What is monkey patching, and when is it acceptable? +
Monkey patching means modifying a class or module at runtime — adding, replacing or removing attributes and methods from outside the original definition.
import math
# replace an existing function
original_sqrt = math.sqrt
math.sqrt = lambda x: original_sqrt(x) * 2
print(math.sqrt(4)) # 4.0 instead of 2.0
# restore later
math.sqrt = original_sqrt
Acceptable uses:
- Fixing a bug in a third-party library before the fix is released.
- Mocking or stubbing during testing.
- Adding compatibility shims for different Python versions.
Avoid it in application code — it makes behaviour hard to trace. Prefer subclassing, composition or wrappers.
Q48 What is the difference between __str__ and __repr__? +
__str__ is the human-readable
representation used by print()
and str().
__repr__ is the developer-facing
representation used by the debugger and
repr() — it should ideally be
valid Python that could recreate the object.
class User:
def __init__(self, name, age):
self.name, self.age = name, age
def __str__(self):
return f"{self.name} ({self.age})"
def __repr__(self):
return f"User(name={self.name!r}, age={self.age})"
u = User("Ravi", 30)
print(u) # Ravi (30)
print(repr(u)) # User(name='Ravi', age=30)
Best practice: always implement
__repr__.
__str__ is optional — if it is
missing, Python falls back to
__repr__.
Q49 How do you handle large files in Python without running out of memory? +
Do not read the whole file into memory. Process it line by line or in chunks.
with open("huge.txt") as f:
for line in f: # memory-efficient
process(line)
def read_in_chunks(path, size=8192):
with open(path, "rb") as f:
while chunk := f.read(size):
yield chunk
import pandas as pd
for chunk in pd.read_csv("big.csv", chunksize=100_000):
process(chunk)
Other options: memory-mapped files with
mmap, generators to stream
processed rows, and dtype
specification in pandas to reduce memory
usage.
Q50 How do you write thread-safe code in Python? +
Techniques, from simplest to most complex:
- Avoid shared state — the easiest fix. Use function-local variables, pass data explicitly, or use immutable objects.
- Use
threading.Lockto guard any read-modify-write on shared state. - Use
queue.Queuefor producer-consumer patterns — it is thread-safe by design. - Use
threading.local()for per-thread data that looks global. - Use
concurrent.futures— the ThreadPoolExecutor and ProcessPoolExecutor handle the plumbing.
import threading
counter = 0
lock = threading.Lock()
def increment():
global counter
with lock:
counter += 1
threads = [threading.Thread(target=increment) for _ in range(1000)]
for t in threads: t.start()
for t in threads: t.join()
print(counter) # 1000
Q51 How do you design a caching layer in Python? +
Start simple, scale only when needed:
- In-process LRU cache —
functools.lru_cachefor pure functions with repeated inputs. - Dictionary-based cache — for small, explicit caching with custom eviction.
- External cache — Redis or Memcached when the cache must be shared across processes or survive restarts.
- HTTP cache headers — for web APIs, use
Cache-Controland ETag headers.
from functools import lru_cache
@lru_cache(maxsize=128)
def fib(n):
if n < 2:
return n
return fib(n - 1) + fib(n - 2)
print(fib(100)) # 354224848179261915075
print(fib.cache_info()) # CacheInfo(hits=98, misses=101, ...)
Watch out for memory growth with unbounded caches, staleness with cached data that changes, and thread safety if the cache is shared between threads.
Q52 What are the differences between shallow copy and assignment? +
Assignment (a = b) just
binds another name to the same object. Both
names point to the same data. Shallow
copy creates a new top-level container,
but the elements inside are still shared.
import copy
a = [[1, 2], [3, 4]]
b = a # assignment — same object
c = copy.copy(a) # shallow copy
a.append([5, 6])
print(b) # [[1, 2], [3, 4], [5, 6]] — same object
print(c) # [[1, 2], [3, 4]] — independent top level
print(a is b, a is c) # True False
Use assignment when you want to share, shallow copy when you want a new top-level container, and deepcopy when you want full independence.
Q53 What is the difference between list.sort() and sorted()? +
list.sort() sorts the list
in-place and returns None. It
only works on lists.
sorted(iterable) returns a new
sorted list and leaves the original
unchanged. It works on any iterable — lists,
tuples, sets, dicts.
nums = [3, 1, 4, 2]
nums.sort()
print(nums) # [1, 2, 3, 4]
nums = [3, 1, 4, 2]
sorted_nums = sorted(nums)
print(nums) # [3, 1, 4, 2] — unchanged
print(sorted_nums) # [1, 2, 3, 4]
# both accept key and reverse
words = ["banana", "apple", "cherry"]
print(sorted(words, key=len, reverse=True))
# ['banana', 'cherry', 'apple']
Q54 What is the difference between multiprocessing and subprocess? +
multiprocessing spawns Python
processes that run Python code, with
built-in IPC — Queue,
Pipe, Manager.
Designed for parallel Python computation.
subprocess runs external programs (shell commands, other executables) and captures their output. It is for running non-Python commands, not parallel Python code.
import subprocess
result = subprocess.run(["ls", "-la"],
capture_output=True, text=True)
print(result.stdout)
from multiprocessing import Pool
def square(n):
return n * n
with Pool(4) as pool:
print(pool.map(square, range(10)))
# [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
Q55 How do you write a good test in Python? +
Tests should be isolated, fast, and test one
behaviour each. Use pytest — it
is the standard in the Python ecosystem.
Principles:
- Arrange-Act-Assert — set up, execute, verify. Keep each test focused.
- One assertion per concept — do not bundle unrelated checks.
- Use fixtures for shared setup — database connections, test data, mock objects.
- Test edge cases — empty inputs, boundaries, invalid types.
- Test the public API — not private methods. Refactoring should not break tests.
- Mock external dependencies — APIs, databases, file systems — so tests run fast and deterministically.
def add(a, b):
return a + b
def test_add_positive():
assert add(2, 3) == 5
def test_add_negative():
assert add(-1, -1) == -2
# run: pytest test_file.py
Scenario-based Python interview questions
These are the questions an interviewer asks when they want to see how you think under a realistic problem. Answer with a step-by-step approach.
S01 A Python script works on your machine but fails on the server. How do you debug it? +
Step 1 — Read the full traceback. It shows the exact file, line and error type. Do not guess.
Step 2 — Check the Python version — python --version on both
machines. Syntax and behaviour change between 3.9 and 3.13.
Step 3 — Check installed packages — pip freeze on both. Version
mismatches are the most common cause of "works on my machine".
Step 4 — Check environment variables and config files. A missing API key or database URL will fail silently or with a generic error.
Step 5 — Check file paths. Absolute paths and Windows-style backslashes break on Linux.
Step 6 — Check file permissions and file existence — a script may not have access to read a log directory on the server.
Step 7 — Reproduce locally with the same Python version and packages, ideally in a container.
S02 A function returns the wrong result for some inputs. How do you debug it? +
Step 1 — Write down the input that produces the wrong output. Be exact — the value, not a description.
Step 2 — Add print() statements or use pdb to step
through the function and inspect intermediate values.
Step 3 — Check the boundaries. Are the wrong results only at the edges — zero, negative, empty, maximum?
Step 4 — Check for off-by-one errors. Loops with range(n) vs
range(1, n+1).
Step 5 — Check for type issues — an integer passed where a float was expected, a string that looks like a number.
Step 6 — Write a small test that covers the failing input. Fix the bug. The test stays, so the bug cannot come back.
S03 You are asked to improve the performance of a slow data processing script. What do you do? +
Step 1 — Measure. Time the current script with
time.perf_counter(). Profile with cProfile to find the
hotspot.
Step 2 — Fix the algorithm first. A better algorithm beats a faster
implementation. O(n²) to O(n) is a much bigger win than
micro-optimisation.
Step 3 — Replace loops with pandas or NumPy vectorised operations. This is usually the single biggest win for data work.
Step 4 — Use the right data structure — set or dict
for membership, deque for queues, heapq for priority.
Step 5 — Avoid repeated work — cache with functools.lru_cache,
precompute outside loops, batch database queries.
Step 6 — If it is still slow, parallelise. Processes for CPU-bound, threads or asyncio for I/O-bound.
Step 7 — Re-measure. Confirm the change actually helped.
S04 How would you process a 10 GB CSV file without loading it into memory? +
Step 1 — Read the file in chunks with
pandas.read_csv(..., chunksize=100_000) and process each chunk.
Step 2 — If you only need aggregated results, accumulate them across chunks — sum, count, min, max — instead of storing all rows.
Step 3 — For non-pandas processing, use csv.reader with
for row in reader: — it streams lazily.
Step 4 — If you must do multiple passes, write intermediate results to disk (Parquet, SQLite) rather than holding them in memory.
Step 5 — Consider a streaming framework — PySpark, Dask, Polars lazy mode — for genuinely large data.
Step 6 — Use dtype specification in pandas to reduce per-row
memory — int32 instead of int64, category for
repeated strings.
S05 A script crashes with a MemoryError after running for hours. How do you fix it? +
Step 1 — Reproduce the leak on a smaller scale. A leak that takes hours on production can often be triggered in minutes with a controlled loop.
Step 2 — Use tracemalloc or memory_profiler to see
where memory is growing.
Step 3 — Check for reference cycles. Objects that refer to each other cannot be freed by reference counting alone; the cyclic GC handles them but only periodically.
Step 4 — Check for growing collections — a list or dict that is appended to on every loop iteration and never cleared.
Step 5 — Check for cached data — an unbounded lru_cache or a
global dict used as a cache.
Step 6 — Check whether file handles, database connections or threads are being
opened and never closed. Use with blocks.
Step 7 — If the leak is in a C library, upgrade or replace the library. Python's GC cannot clean up memory allocated outside its control.
S06 You need to call an API 10,000 times. How do you do it efficiently? +
Step 1 — Check the API's rate limits and terms. Do not hammer a service — you will get blocked.
Step 2 — Use asyncio with aiohttp or
httpx for concurrent requests. Sequential requests at 200 ms each would
take over 30 minutes; concurrency brings it down to seconds.
Step 3 — Use a semaphore to cap concurrency. 100 concurrent requests is usually enough; more can cause timeouts and rate limit errors.
Step 4 — Add retries with exponential backoff for transient errors — 429, 500, 502, 503.
Step 5 — Cache responses where possible. If the same request appears twice, do not call the API twice.
Step 6 — Log failures with the request details so you can re-run only the failed calls.
import asyncio, aiohttp
async def fetch(session, url, sem):
async with sem:
async with session.get(url) as resp:
return await resp.json()
async def main(urls):
sem = asyncio.Semaphore(100)
async with aiohttp.ClientSession() as session:
tasks = [fetch(session, u, sem) for u in urls]
return await asyncio.gather(*tasks)
# asyncio.run(main(url_list))
S07 You inherit a legacy Python codebase with no tests. How do you approach adding tests? +
Step 1 — Do not try to test everything at once. Pick the most critical code path — the one that would cause the most damage if it broke.
Step 2 — Add characterisation tests. Run the current code with known inputs, record the outputs, and write tests that assert those outputs. This locks in current behaviour, bugs and all.
Step 3 — Refactor only after you have tests. The tests give you confidence to change things.
Step 4 — Add tests as you fix bugs. Every bug becomes a regression test.
Step 5 — Add tests for new code as you write it. Do not add new untested code on top of old untested code.
Step 6 — Run tests in CI. A test suite nobody runs is worse than no test suite — it gives false confidence.
Step 7 — Track coverage. Aim for meaningful coverage of critical paths, not 100% for its own sake.
S08 You need to build a data pipeline that runs daily. How do you structure it? +
Step 1 — Break the pipeline into stages: extract, transform, load (ETL). Each stage should be a separate function or module.
Step 2 — Make each stage idempotent. Running the pipeline twice should produce the same result, not duplicate rows.
Step 3 — Use a scheduler — cron for simple jobs, Apache Airflow, Prefect or Dagster for anything with dependencies and retries.
Step 4 — Log everything. When it fails at 3 AM, the logs are the only thing you have.
Step 5 — Add alerting. Do not wait for someone to notice the dashboard is empty — send an email or Slack message on failure.
Step 6 — Store data in a format that supports what you will do with it. Parquet for analytics, SQLite for small structured data, a real database for transactional workloads.
Step 7 — Add data validation. Check row counts, null rates and value ranges against the previous run. Catch silent failures.
Step 8 — Version your code and configuration. Use Git for code, environment variables or a config service for secrets.
Python coding round — 15 programs with code and output
These are the fifteen programs that come up most often in Python coding rounds. Write each one by hand at least once before your interview — reading Python is not the same as writing it.
C01 Reverse a string +
def reverse_string(s):
return s[::-1]
print(reverse_string("hello")) # olleh
print(reverse_string("Python 3")) # 3 nohtyP
# without slicing
def reverse_loop(s):
result = ""
for ch in s:
result = ch + result
return result
print(reverse_loop("hello")) # olleh
Slicing with [::-1] is the
idiomatic answer. Mention the loop version
to show you understand what slicing does
internally.
C02 Check if a string is a palindrome +
def is_palindrome(s):
s = s.lower().replace(" ", "")
return s == s[::-1]
print(is_palindrome("racecar")) # True
print(is_palindrome("A man a plan a canal Panama")) # True
print(is_palindrome("hello")) # False
The key step is normalising the input — lowercase and remove spaces — before comparing.
C03 Fibonacci series — iterative and generator +
def fib_iter(n):
a, b = 0, 1
result = []
for _ in range(n):
result.append(a)
a, b = b, a + b
return result
print(fib_iter(10))
# [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
def fib_gen(n):
a, b = 0, 1
for _ in range(n):
yield a
a, b = b, a + b
print(list(fib_gen(10)))
# [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
The generator version is memory-efficient —
it does not build the whole list in memory.
For n = 1_000_000, the list
version would fail; the generator version
works fine.
C04 Check if a number is prime +
def is_prime(n):
if n < 2:
return False
for i in range(2, int(n**0.5) + 1):
if n % i == 0:
return False
return True
print(is_prime(7)) # True
print(is_prime(10)) # False
print(is_prime(97)) # True
print(is_prime(1)) # False
The key optimisation is only checking
divisors up to sqrt(n). If
n has a factor larger than
sqrt(n), it also has one
smaller.
C05 Factorial of a number +
def factorial_iter(n):
result = 1
for i in range(2, n + 1):
result *= i
return result
def factorial_rec(n):
if n <= 1:
return 1
return n * factorial_rec(n - 1)
print(factorial_iter(5)) # 120
print(factorial_rec(5)) # 120
# built-in option
import math
print(math.factorial(5)) # 120
Note: Python has no tail-call optimisation. For large
n, the recursive version hits
RecursionError. Use the iterative version or
math.factorial.
C06 Check if two strings are anagrams +
def is_anagram(s1, s2):
s1 = s1.lower().replace(" ", "")
s2 = s2.lower().replace(" ", "")
return sorted(s1) == sorted(s2)
print(is_anagram("listen", "silent")) # True
print(is_anagram("hello", "world")) # False
from collections import Counter
def is_anagram2(s1, s2):
return Counter(s1.lower()) == Counter(s2.lower())
print(is_anagram2("listen", "silent")) # True
The Counter version is O(n) — better than sorting, which is O(n log n). Both are accepted in interviews.
C07 Count character frequency in a string +
from collections import Counter
s = "programming"
counts = Counter(s)
print(counts)
# Counter({'r': 2, 'g': 2, 'm': 2, 'p': 1, 'o': 1, 'a': 1, 'i': 1, 'n': 1})
# most common 2
print(counts.most_common(2))
# [('r', 2), ('g', 2)]
s = "programming"
counts = {}
for ch in s:
counts[ch] = counts.get(ch, 0) + 1
print(counts)
# {'p': 1, 'r': 2, 'o': 1, 'g': 2, 'a': 1, 'm': 2, 'i': 1, 'n': 1}
Counter is the clean answer.
The dict.get(ch, 0) idiom is
what you write when you do not want the
import.
C08 Find the second largest number in a list +
def second_largest(nums):
unique = sorted(set(nums), reverse=True)
if len(unique) < 2:
return None
return unique[1]
print(second_largest([10, 5, 20, 20, 8])) # 10
print(second_largest([5, 5])) # None
def second_largest_fast(nums):
first = second = float("-inf")
for n in nums:
if n > first:
first, second = n, first
elif second < n < first:
second = n
return second if second != float("-inf") else None
print(second_largest_fast([10, 5, 20, 20, 8])) # 10
The set + sorted
version handles duplicates cleanly. The
one-pass version is O(n) and does not sort.
Mention both.
C09 Remove duplicates from a list while keeping order +
def dedupe(nums):
return list(dict.fromkeys(nums))
print(dedupe([1, 2, 3, 2, 1, 4]))
# [1, 2, 3, 4]
def dedupe2(nums):
seen = set()
result = []
for n in nums:
if n not in seen:
seen.add(n)
result.append(n)
return result
print(dedupe2([1, 2, 3, 2, 1, 4]))
# [1, 2, 3, 4]
Both preserve insertion order. Using
set(nums) directly would lose
the original order in older Python
versions, so it is not the right answer
here.
C10 Flatten a nested list +
def flatten(nested):
result = []
for item in nested:
if isinstance(item, list):
result.extend(flatten(item))
else:
result.append(item)
return result
nested = [1, [2, 3], [4, [5, 6]], 7]
print(flatten(nested))
# [1, 2, 3, 4, 5, 6, 7]
def flatten_gen(nested):
for item in nested:
if isinstance(item, list):
yield from flatten_gen(item)
else:
yield item
print(list(flatten_gen(nested)))
# [1, 2, 3, 4, 5, 6, 7]
The generator version uses
yield from — cleaner and
memory-efficient for deeply nested
structures.
C11 Merge two dictionaries +
d1 = {"a": 1, "b": 2}
d2 = {"b": 99, "c": 3}
merged = d1 | d2
print(merged)
# {'a': 1, 'b': 99, 'c': 3} — d2 wins for common keys
merged = {**d1, **d2}
print(merged)
# {'a': 1, 'b': 99, 'c': 3}
d1.update(d2)
print(d1)
# {'a': 1, 'b': 99, 'c': 3}
The | operator (Python 3.9+)
is the cleanest. {**d1, **d2}
works in all Python 3 versions.
update modifies in place.
C12 Word count from text +
from collections import Counter
text = "the quick brown fox jumps over the lazy dog the fox"
words = text.lower().split()
counts = Counter(words)
print(counts)
# Counter({'the': 3, 'fox': 2, 'quick': 1, 'brown': 1, ...})
print(counts.most_common(3))
# [('the', 3), ('fox', 2), ('quick', 1)]
with open("book.txt", encoding="utf-8") as f:
words = f.read().lower().split()
counts = Counter(words)
print(counts.most_common(10))
For real text, add punctuation stripping
with re.findall(r"\w+", text.lower()).
C13 Find the missing number in a list of 1 to n +
def missing_number(nums, n):
expected = n * (n + 1) // 2
actual = sum(nums)
return expected - actual
print(missing_number([1, 2, 4, 5, 6], 6)) # 3
print(missing_number([1, 2, 3, 4, 5], 6)) # 6
def missing_xor(nums, n):
xor = 0
for i in range(1, n + 1):
xor ^= i
for x in nums:
xor ^= x
return xor
print(missing_xor([1, 2, 4, 5, 6], 6)) # 3
The sum formula is O(n) and simplest. The XOR version also O(n) but avoids potential overflow in languages with fixed-width integers — in Python it is a nice alternative to mention.
C14 Two-sum — find two numbers that add to a target +
def two_sum(nums, target):
seen = {}
for i, n in enumerate(nums):
complement = target - n
if complement in seen:
return [seen[complement], i]
seen[n] = i
return []
print(two_sum([2, 7, 11, 15], 9)) # [0, 1]
print(two_sum([3, 2, 4], 6)) # [1, 2]
print(two_sum([3, 3], 6)) # [0, 1]
The hash map turns an O(n²) brute-force solution into O(n). This is the classic "use a dictionary" interview question.
def two_sum_brute(nums, target):
for i in range(len(nums)):
for j in range(i + 1, len(nums)):
if nums[i] + nums[j] == target:
return [i, j]
return []
C15 Write a simple class with __str__ and __repr__ +
class BankAccount:
def __init__(self, owner, balance=0):
self.owner = owner
self.balance = balance
def deposit(self, amount):
if amount <= 0:
raise ValueError("Deposit must be positive")
self.balance += amount
return self.balance
def withdraw(self, amount):
if amount > self.balance:
raise ValueError("Insufficient funds")
self.balance -= amount
return self.balance
def __str__(self):
return f"{self.owner}'s account: ₹{self.balance}"
def __repr__(self):
return f"BankAccount(owner={self.owner!r}, balance={self.balance})"
acc = BankAccount("Ravi", 5000)
acc.deposit(2000)
acc.withdraw(500)
print(acc) # Ravi's account: ₹6500
print(repr(acc)) # BankAccount(owner='Ravi', balance=6500)
Notice the pattern: __str__
is for users, __repr__ is for
developers. Both are worth showing.
Python for data analysis — pandas and NumPy interview questions
These questions come up in Python interviews for data analyst and data science roles. Every answer includes runnable pandas or NumPy code and its output.
D01 How do you read a CSV file into a pandas DataFrame? +
import pandas as pd
df = pd.read_csv("sales.csv")
print(df.head())
print(df.shape) # (rows, cols)
print(df.info()) # dtypes, non-null counts
print(df.describe()) # summary stats
Useful parameters:
usecols=["date", "amount"]— load only the columns you need.dtype={"id": "int32"}— reduce memory.parse_dates=["order_date"]— parse date columns on load.chunksize=100_000— read large files in chunks.na_values=["", "NA", "N/A"]— treat these as missing.
D02 How do you use groupby in pandas? +
groupby splits the data into
groups by one or more keys, applies an
aggregation, and combines the results.
This is the pandas equivalent of SQL's
GROUP BY.
import pandas as pd
df = pd.DataFrame({
"city": ["Delhi", "Noida", "Delhi", "Noida", "Delhi"],
"sales": [100, 200, 150, 250, 300],
})
result = df.groupby("city")["sales"].sum()
print(result)
city Delhi 550 Noida 450 Name: sales, dtype: int64
result = df.groupby("city")["sales"].agg(["sum", "mean", "count"])
print(result)
sum mean count city Delhi 550 183.333333 3 Noida 450 225.000000 2
D03 How do you merge two DataFrames in pandas? +
merge() combines two DataFrames
on a common key, like a SQL JOIN. The
how parameter controls the join
type.
| how= | Equivalent SQL | Keeps |
|---|---|---|
| "inner" (default) | INNER JOIN | Keys in both |
| "left" | LEFT JOIN | All keys from left |
| "right" | RIGHT JOIN | All keys from right |
| "outer" | FULL OUTER JOIN | All keys from both |
import pandas as pd
orders = pd.DataFrame({
"order_id": [1, 2, 3],
"customer_id": [10, 11, 12],
"amount": [500, 1000, 750],
})
customers = pd.DataFrame({
"customer_id": [10, 11, 13],
"name": ["Ravi", "Anita", "Sam"],
})
merged = pd.merge(orders, customers, on="customer_id", how="inner")
print(merged)
order_id customer_id amount name 0 1 10 500 Ravi 1 2 11 1000 Anita
Note: Use on= when the key column has the same
name in both DataFrames. Use left_on= and
right_on= when the names differ.
D04 How do you create a pivot table in pandas? +
pivot_table() reshapes data
into a grid — one key down the rows, another
across the columns, and an aggregate value
in each cell.
import pandas as pd
df = pd.DataFrame({
"region": ["North", "North", "South", "South", "North"],
"product": ["A", "B", "A", "B", "A"],
"sales": [100, 150, 200, 250, 300],
})
pivot = df.pivot_table(
values="sales",
index="region",
columns="product",
aggfunc="sum",
fill_value=0,
)
print(pivot)
product A B region North 400 150 South 200 250
fill_value=0 replaces missing
combinations with zero instead of
NaN. This is what you usually
want for sales reports.
D05 How do you handle missing values in pandas? +
import pandas as pd
import numpy as np
df = pd.DataFrame({
"name": ["Ravi", "Anita", None, "Sam"],
"salary": [50000, None, 70000, 60000],
})
print(df.isnull().sum())
# name 1
# salary 1
# drop rows with any missing value
df_dropped = df.dropna()
# fill with a fixed value
df_filled = df.fillna(0)
# fill salary with the mean
df["salary"] = df["salary"].fillna(df["salary"].mean())
# forward-fill (useful for time series)
df_filled = df.ffill()
The right strategy depends on why the data is missing. For numeric columns, mean or median imputation is common. For categorical columns, a "Unknown" category works. For time series, forward-fill or interpolation is common. Dropping should be a last resort — you lose information.
D06 How does apply() work in pandas, and when should you use it? +
apply() runs a Python function
on every element, row or column of a
DataFrame. It is flexible but slower than
vectorised operations.
import pandas as pd
df = pd.DataFrame({"name": ["ravi", "anita", "sam"]})
df["name_upper"] = df["name"].apply(lambda x: x.upper())
print(df)
name name_upper 0 ravi RAVI 1 anita ANITA 2 sam SAM
df = pd.DataFrame({"price": [100, 200], "qty": [2, 3]})
df["total"] = df.apply(lambda row: row["price"] * row["qty"], axis=1)
print(df)
price qty total 0 100 2 200 1 200 3 600
For simple arithmetic like this, the
vectorised version — df["price"] * df["qty"] —
is much faster. Use apply only
when the logic cannot be expressed
vectorised.
D07 How do you work with dates and times in pandas? +
import pandas as pd
df = pd.DataFrame({
"order_date": ["2026-01-15", "2026-02-20", "2026-03-25"],
"amount": [100, 200, 150],
})
# convert to datetime
df["order_date"] = pd.to_datetime(df["order_date"])
# extract parts
df["year"] = df["order_date"].dt.year
df["month"] = df["order_date"].dt.month
df["day"] = df["order_date"].dt.day
print(df)
order_date amount year month day 0 2026-01-15 100 2026 1 15 1 2026-02-20 200 2026 2 20 2 2026-03-25 150 2026 3 25
monthly = df.groupby(df["order_date"].dt.to_period("M"))["amount"].sum()
print(monthly)
order_date 2026-01 100 2026-02 200 2026-03 150 Freq: M, Name: amount, dtype: int64
Use .dt to access datetime
properties, and pd.to_period()
or pd.Grouper() for time-based
grouping.
D08 Why is vectorisation faster than loops in pandas and NumPy? +
NumPy stores data in contiguous C arrays and runs operations on them in compiled C code. A Python loop runs one iteration at a time in the interpreter, which is much slower — often 50–100 times slower for simple arithmetic.
import numpy as np
import time
arr = np.arange(1_000_000)
# Python loop
start = time.perf_counter()
result_loop = [x * 2 for x in arr]
loop_time = time.perf_counter() - start
# vectorised
start = time.perf_counter()
result_vec = arr * 2
vec_time = time.perf_counter() - start
print(f"Loop: {loop_time:.4f}s")
print(f"Vectorised: {vec_time:.4f}s")
Loop: 0.0800s Vectorised: 0.0020s
The rule: use vectorised operations
whenever possible — df["a"] + df["b"]
instead of a row-by-row loop. Use
apply as a fallback, and a
Python loop only as a last resort.
Quick-revision cheat table
Fifteen one-line definitions and syntax reminders to skim on the morning of your interview.
| Topic | One-line reminder |
|---|---|
| Interpreted vs compiled | Python compiles source to bytecode, then interprets that bytecode in the CPython VM. |
| Mutable vs immutable | Mutable — list, dict, set. Immutable — tuple, str, int, frozenset. |
| is vs == | is compares identity, == compares value. Use
is only for None/True/False. |
| *args and **kwargs | *args collects positional args into a tuple, **kwargs
collects keyword args into a dict. |
| Lambda | Anonymous one-line function — lambda x: x * 2. No statements, only
an expression. |
| Comprehension | Compact way to build a list/dict/set —
[n*n for n in range(5) if n % 2 == 0]. |
| Generator | Lazy function using yield. Produces values on demand —
memory-efficient. |
| Iterator vs iterable | Iterable has __iter__. Iterator has __iter__ and
__next__. |
| Decorator | A function that wraps another function. Use functools.wraps to
preserve metadata. |
| Context manager | Object with __enter__ and __exit__, used with
with. Auto cleanup. |
| OOPs | Encapsulation, inheritance, polymorphism, abstraction. Python uses
_ and __ for access. |
| MRO | Method Resolution Order — the sequence Python searches for a method. Uses C3 linearisation. |
| Dunder methods | Special methods called by Python syntax — __init__,
__str__, __eq__, __add__. |
| GIL | Global Interpreter Lock — one thread runs Python bytecode at a time. Blocks CPU parallelism with threads. |
| Shallow vs deep copy | Shallow shares nested objects. Deep recursively copies everything. |
| Multithreading vs multiprocessing | Threads for I/O-bound, processes for CPU-bound (bypasses the GIL). |
| asyncio | Single-threaded event loop with coroutines — good for many concurrent I/O operations. |
| Virtual environment | Isolated Python installation per project — python -m venv venv.
|
| Type hints | Annotations not enforced at runtime — checked by mypy or Pyright. |
| pandas groupby | df.groupby("city")["sales"].sum() — like SQL GROUP BY. |
How to prepare for a Python interview in 7 days
If your interview is next week, follow this plan. Each day has a single focus and a practice task — do not just read, write code.
| Day | Focus | Practice task |
|---|---|---|
| Day 1 | Core fundamentals | Answer Q1–Q6 out loud. Write a script that shows mutable vs immutable,
is vs ==, and shallow vs deep copy. Run it. |
| Day 2 | Functions and comprehensions | Answer Q9, Q14, Q17, Q34. Write a function using *args and
**kwargs, plus three comprehensions (list, dict, set). |
| Day 3 | OOPs and dunder methods | Answer Q27–Q29, Q48. Write a class with __init__,
__str__, __repr__, __eq__ and
__add__. |
| Day 4 | Generators, decorators, context managers | Answer Q21–Q25. Write a generator for Fibonacci, a timing decorator, and a
context manager using __enter__ and __exit__. |
| Day 5 | Exceptions and memory | Answer Q30–Q33, Q41, Q43. Write a custom exception and a try/except/else/finally block. Explain the GIL out loud. |
| Day 6 | Coding round | Write all 15 programs from section 06 by hand. Run each one. Rewrite the two you struggled with. |
| Day 7 | pandas and mock interview | Answer the 8 pandas questions with code. Run each snippet. Then answer one scenario question out loud per level. |
Common mistakes candidates make
Using mutable default arguments
def f(items=[]): is a classic
bug — the same list is reused across
calls. Use def f(items=None):
and create the list inside the function.
Confusing is and ==
is compares identity,
== compares value. Using
is for numbers or strings
works by accident for small values and
fails silently for large ones.
Bare except clauses
except: catches everything —
including KeyboardInterrupt
and SystemExit. Catch specific
exceptions, or at minimum
except Exception:.
Writing loops where vectorisation works
In pandas and NumPy, an explicit loop is 50–100 times slower than the vectorised version. Learn the vectorised form first.
Ignoring the GIL in concurrency questions
Saying "use threads for parallelism" in a Python interview is a red flag. Know that threads help with I/O, and processes are needed for CPU-bound work.
Reading code instead of writing it
You will not remember how to write a decorator, a generator or a two-sum by reading them three times. Write each one by hand, then again without looking.
Practise these topics with Uncodemy
Python, pandas, NumPy, SQL and the full data analysis workflow are taught inside Uncodemy's Data Analytics and Data Science programs. Python fundamentals are also taught as a standalone module inside both tracks.
| Program | What it covers | Duration / fee |
|---|---|---|
| Data Analytics | Python (20 h), SQL, Excel, Power BI, pandas, NumPy, statistics. This is the direct track for the Python and pandas topics on this page. | ₹17,500 + GST · 6–7 months |
| Data Science | Python, statistics, machine learning, deep learning, NLP, 7 projects. Includes the Python for Data Science module. | ₹17,500 + GST · 8–9 months |
| Business Analyst | SQL, Excel, BRD/FRD, process modelling, Agile/Scrum, JIRA, stakeholder management. Includes Python for analysis. | ₹17,500 + GST · 6–7 months |
Placement assistance (as published by Uncodemy): resume building, LinkedIn optimisation, portfolio review, interview preparation, mock interviews and opportunity sharing through a network of 850+ companies. Uncodemy does not guarantee employment, as hiring decisions rest with employers. See placement support.
Also useful: AI career guidance for picking a path, and the 2-class free trial to see how a live batch runs before you commit.