Python interview questions for freshers
Data types, mutability, slicing, functions, scope and exceptions, as asked in campus and junior interviews.
What is Python and what are its key features?
Python is a high-level, general-purpose language: dynamically but strongly typed, garbage collected, with a large standard library. It is used for scripting, web backends, automation and data work. See what Python is.
Follow-up: Is Python compiled or interpreted? Both: CPython compiles source to bytecode, then interprets the bytecode.
What is PEP 8?
PEP 8 is Python's official style guide: 4 spaces per indent, snake_case for functions and variables, CapWords for classes, UPPER_CASE for constants, lines up to 79 characters, imports grouped at the top. Teams enforce it with a linter such as ruff and a formatter such as black.
What is the difference between a list and a tuple in Python?
A list is mutable and a tuple is immutable, so a tuple is hashable when its items are and can be a dict key or a set member; a list cannot.
point = (3, 4)
labels = {point: "A"}
print(labels[(3, 4)])
try:
labels[[3, 4]] = "B"
except TypeError as e:
print("TypeError:", e)The trap: a list inside a tuple can still change, so t = ([1], 2); t[0].append(5) works. See tuples.
What is the difference between == and is in Python?
== compares values (it calls __eq__); is checks whether both names point to the same object.
a = [1, 2, 3]
b = [1, 2, 3]
print(a == b)
print(a is b)
c = a
print(c is a)
x = None
print(x is None)a and b are equal but separate lists. Use is only for singletons such as None, because a class can override __eq__ but not is.
What does this print: += on a string that has a second name?
s = "hello"
t = s
s += " world"
print(t, s)Predict the output
Strings are immutable, so s += " world" builds a new string and rebinds s; t still points to "hello". With a list the result flips, because += extends a list in place and t sees [1, 2]:
s = [1]
t = s
s += [2]
print(t)Is Python dynamically typed or statically typed? Is it strongly typed?
Python is dynamically typed (types belong to values, and a name can be rebound to any type) and strongly typed (it never silently converts between unrelated types).
x = 5
print(type(x).__name__)
x = "five"
print(type(x).__name__)
try:
print("3" + 3)
except TypeError as e:
print("TypeError:", e)In JavaScript, which is weakly typed, "3" + 3 gives "33".
What is a list comprehension in Python?
A list comprehension builds a list in one expression, [expression for item in iterable if condition], shorter than a loop with append and usually a little faster.
nums = [1, 2, 3, 4, 5, 6]
squares_of_evens = [n * n for n in nums if n % 2 == 0]
print(squares_of_evens)
words = ["apple", "kiwi", "banana"]
lengths = {w: len(w) for w in words}
print(lengths)
first_letters = {w[0] for w in words}
print(sorted(first_letters))The same syntax builds dicts and sets; with parentheses you get a lazy generator expression. More in list comprehensions.
What does this print: slices with a step, a reverse and a negative start?
nums = [0, 1, 2, 3, 4, 5]
print(nums[1:5:2], nums[::-1][:2], nums[-2:])Predict the output
A slice is start:stop:step with stop excluded, so nums[1:5:2] takes indexes 1 and 3. nums[::-1] is a reversed copy starting 5, 4, and nums[-2:] is the last two items. Slicing never raises IndexError and always returns a new list.
What are *args and **kwargs in Python?
*args collects extra positional arguments into a tuple and **kwargs collects extra keyword arguments into a dict. The stars do the work; the names are a convention.
def describe(name, *args, **kwargs):
print(name, args, kwargs)
describe("order", 1, 2, size="L", paid=True)
nums = [10, 20]
opts = {"size": "M"}
describe("unpacked", *nums, **opts)At a call site the stars unpack, which is how a decorator forwards everything with func(*args, **kwargs). See args and kwargs.
What is the difference between break, continue and pass?
break exits the loop, continue skips to the next iteration, and pass does nothing (a placeholder where Python needs a statement).
for n in range(6):
if n == 1:
pass # placeholder, the loop carries on
if n == 2:
continue # skip the print for 2
if n == 4:
break # stop the loop at 4
print(n)
for n in [1, 3, 5]:
if n % 2 == 0:
print("found an even number")
break
else:
print("no even number")The second loop shows a loop's else, which runs only when the loop ends without break.
What does this print: assigning to a global's name inside a function?
x = 10
def change():
x = 20
return x
print(change(), x)Predict the output
Assigning to x inside a function creates a local variable, so the global stays 10; rebinding the global needs global x. The trap: x = x + 1 in a function raises UnboundLocalError, because the assignment makes x local for the whole body.
What does this print: pass by value or by reference?
def modify(items, count):
items.append(4)
count += 1
items = [0]
nums = [1, 2, 3]
total = 10
modify(nums, total)
print(nums, total)Predict the output
Python passes object references by value, often called call by sharing: mutating the object is visible to the caller, rebinding the parameter is not. items.append(4) changes nums, while count += 1 and items = [0] only rebind local names, so total stays 10 and nums never becomes [0].
What does this print: range with a negative step?
print(list(range(10, 0, -3)))Predict the output
range(start, stop, step) never includes stop; with a negative step it counts down while the value is above stop: 10, 7, 4, 1. In Python 3 range is lazy, not a list, but it supports len(), indexing and a constant-time in for integers.
How does exception handling work in Python (try, except, else, finally)?
try holds code that may fail, except handles a specific exception, else runs only when nothing was raised, and finally always runs, even after a return.
def parse(text):
try:
value = int(text)
except ValueError:
print("bad input:", text)
return None
else:
print("parsed", value)
return value
finally:
print("finally runs for", text)
parse("42")
parse("4x")Catch the narrowest exception you can; a bare except: also catches KeyboardInterrupt and hides bugs. More in exceptions.
What is a lambda function in Python?
A lambda is an anonymous single-expression function, such as lambda x: x * 2, used mostly as a key or a callback.
people = [("Asha", 31), ("Ravi", 25), ("Meera", 28)]
by_age = sorted(people, key=lambda p: p[1])
print(by_age)
double = lambda x: x * 2
print(list(map(double, [1, 2, 3])))It cannot hold statements. If you give one a name, as with double, PEP 8 says to write a def.
What does if __name__ == "__main__": do?
A file run directly has __name__ == "__main__"; an imported one has its module name. The guard runs code only when the file is executed as a script, so importing it for one function does not also start its server or tests.
def main():
print("running as a script")
print("__name__ is", __name__)
if __name__ == "__main__":
main()See the main function.
What does this print: 0.1 + 0.2 compared with 0.3?
print(0.1 + 0.2 == 0.3, round(0.1 + 0.2, 2) == 0.3)Predict the output
0.1, 0.2 and 0.3 have no exact binary representation, so 0.1 + 0.2 is not exactly 0.3; rounding to two places hides the error.
import math
from decimal import Decimal
print(0.1 + 0.2)
print(math.isclose(0.1 + 0.2, 0.3))
print(Decimal("0.1") + Decimal("0.2"))Compare floats with math.isclose, and handle money with decimal.Decimal or integer cents.
What does this print: // and % with negative numbers?
print(7 // 2, -7 // 2, 7 % -3)Predict the output
// rounds toward negative infinity, not toward zero, so -7 // 2 is -4 (C and Java give -3). % follows it, so its result takes the divisor's sign: 7 % -3 is -2.
What are f-strings and how do you format numbers with them?
An f-string (f"...", Python 3.6+) evaluates the expressions in {} and formats them in place; it is the most readable formatting style.
name = "Priya"
score = 91.456
items = 3
print(f"{name} scored {score:.1f}")
print(f"{items:03d} | {name:>8} | {score:,.2f}")
print(f"{items * 2 = }")After the colon comes the format spec, such as .1f, 03d, >8 or ,. The = form (3.8+) prints the expression and its value.
What is the difference between a module and a package in Python?
A module is one .py file; a package is a directory of modules, usually marked by an __init__.py file.
shop/
__init__.py
cart.py
payments/
__init__.py
upi.py
An import runs a module's top-level code once and caches it in sys.modules. See modules and imports.
OOPs in Python interview questions
Classes, inheritance and the dunder methods that make Python objects behave like built-ins.
What is a class and what is an object in Python?
A class is a blueprint of attributes and methods; an object is one instance built from it, set up by __init__.
class BankAccount:
bank = "Coddy Bank" # class attribute, shared
def __init__(self, owner, balance=0):
self.owner = owner # instance attributes
self.balance = balance
def deposit(self, amount):
self.balance += amount
return self.balance
a = BankAccount("Ravi")
b = BankAccount("Asha", 500)
a.deposit(100)
print(a.owner, a.balance, b.owner, b.balance, a.bank)Each instance has its own owner and balance, while the class attribute bank is shared. See classes.
What is self in Python?
self is the instance a method was called on, passed automatically as the first argument, so a.deposit(100) is BankAccount.deposit(a, 100). It is a convention, not a keyword, and there is no implicit this, so attributes need self.balance.
How do you make a variable private in Python?
Python has no private keyword. _balance marks an attribute as internal by convention; __balance is name-mangled to _ClassName__balance, so subclasses do not overwrite it by accident.
class Account:
def __init__(self):
self._internal = "by convention"
self.__secret = 42
acc = Account()
print(acc._internal)
try:
print(acc.__secret)
except AttributeError as e:
print("AttributeError:", e)
print(acc._Account__secret)Mangling prevents clashes, not access. For controlled access, use a @property.
What is polymorphism in Python, and what is duck typing?
Polymorphism means one interface with many implementations. In Python it mostly comes from duck typing: if an object has the method you call, it works, whatever its class.
class Dog:
def speak(self):
return "Woof"
class Robot:
def speak(self):
return "Beep"
for thing in [Dog(), Robot()]:
print(thing.speak())
print(len("abc"), len([1, 2]), len({"a": 1}))Python has no overloading by signature; a second def with the same name replaces the first, so use default arguments or functools.singledispatch.
What is the difference between __new__ and __init__?
__new__ creates and returns the object; __init__ then initializes it and must return None.
class Point:
def __new__(cls, *args):
print("__new__ called")
return super().__new__(cls)
def __init__(self, x, y):
print("__init__ called")
self.x, self.y = x, y
p = Point(1, 2)
print(p.x, p.y)You override __new__ mainly to subclass immutable types like int or tuple, or for singletons. If it returns something that is not an instance of the class, __init__ is skipped.
What does this print: a list defined in the class body?
class Basket:
items = []
def add(self, x):
self.items.append(x)
a = Basket()
b = Basket()
a.add(1)
b.add(2)
print(a.items)Predict the output
items = [] in the class body is one list shared by every instance, and self.items.append(x) mutates it, so both objects see both values. Create the list in __init__ with self.items = []. An assignment to self.items would create an instance attribute; only in-place mutation leaks.
What is the difference between @staticmethod and @classmethod?
A class method receives the class (cls), so it can build instances and read class state; a static method receives nothing implicit and is a plain function in the class namespace.
class Date:
def __init__(self, year, month, day):
self.year, self.month, self.day = year, month, day
@classmethod
def from_string(cls, text):
y, m, d = map(int, text.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("2024-02-29")
print(d.year, d.month, d.day, Date.is_leap(2024))With cls, SubDate.from_string(...) returns a SubDate; a static method that hardcodes Date(...) could not.
What is the difference between __str__ and __repr__?
__str__ is the readable form used by print() and str(); __repr__ is the unambiguous developer form used by the REPL, repr() and containers.
class User:
def __init__(self, name):
self.name = name
def __repr__(self):
return f"User(name={self.name!r})"
def __str__(self):
return self.name
u = User("Asha")
print(u)
print(repr(u))
print([u])A list prints the repr of its items, as the last line shows. If you implement only one, make it __repr__, because str() falls back to it.
How do you create an abstract class or interface in Python?
Inherit from abc.ABC and mark the required methods with @abstractmethod; a class with unimplemented abstract methods cannot be instantiated.
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
...
class Square(Shape):
def __init__(self, side):
self.side = side
def area(self):
return self.side ** 2
print(Square(3).area())
try:
Shape()
except TypeError as e:
print("TypeError:", e)Python has no interface keyword, so an ABC with only abstract methods plays that role. typing.Protocol gives duck-typed interfaces without inheritance.
What does the @property decorator do?
@property turns a method into an attribute read without parentheses, and its .setter runs on assignment, usually to validate. It replaces get_x() and set_x() methods.
class Temperature:
def __init__(self, celsius):
self.celsius = celsius
@property
def celsius(self):
return self._celsius
@celsius.setter
def celsius(self, value):
if value < -273.15:
raise ValueError("below absolute zero")
self._celsius = value
@property
def fahrenheit(self):
return self._celsius * 9 / 5 + 32
t = Temperature(25)
print(t.fahrenheit)
try:
t.celsius = -300
except ValueError as e:
print("ValueError:", e)__init__ assigns through the setter, so the validation also applies on creation.
What does this print: super() in a diamond hierarchy?
class A:
def hi(self):
return "A"
class B(A):
def hi(self):
return "B" + super().hi()
class C(A):
def hi(self):
return "C" + super().hi()
class D(B, C):
def hi(self):
return "D" + super().hi()
print(D().hi())Predict the output
Python resolves methods along the MRO, computed with C3 linearization; here it is D, B, C, A, object (check D.__mro__). super() means the next class in the MRO of the object's actual type, not the parent, so super() inside B calls C.hi and A runs once. See inheritance.
What are dataclasses in Python and when would you use one?
@dataclass (Python 3.7+) generates __init__, __repr__ and __eq__ from the annotated fields. frozen=True makes instances immutable and hashable, and order=True adds comparisons.
from dataclasses import dataclass, field
@dataclass(frozen=True)
class Point:
x: int
y: int
@dataclass
class Cart:
owner: str
items: list = field(default_factory=list)
p = Point(1, 2)
print(p, p == Point(1, 2))
c = Cart("Ravi")
c.items.append("book")
print(c)Mutable defaults need field(default_factory=list); items: list = [] raises ValueError when the class is defined, which guards against the mutable default argument trap.
Python data structures interview questions
Lists, dicts, sets and the standard library containers, with the time complexity you are expected to know.
How do you iterate over a dictionary in Python?
Looping over a dict gives its keys; .items() gives key and value pairs and .values() the values, in insertion order.
stock = {"apple": 3, "banana": 0, "kiwi": 7}
for key in stock:
print(key, end=" ")
print()
for key, qty in stock.items():
if qty == 0:
print("out of stock:", key)
print(stock.get("mango", 0))
print({k: v for k, v in stock.items() if v > 0})Adding or removing keys while iterating raises RuntimeError, so iterate over list(d) instead. See dictionaries.
What does this print: append vs extend with a list?
a = [1, 2]
b = [1, 2]
a.append([3, 4])
b.extend([3, 4])
print(a, len(a), b, len(b))Predict the output
append adds its argument as one item, even a list; extend adds each item of an iterable, so a has length 3 and b length 4. Both return None, so a = a.append(x) sets a to None.
What is a set in Python and when do you use one?
A set holds unique, hashable items with O(1) average membership tests. Use it to remove duplicates, test membership and do set algebra.
a = {1, 2, 3, 4}
b = {3, 4, 5}
print(a | b, a & b, a - b, a ^ b)
nums = [3, 1, 3, 2, 1]
print(len(nums) != len(set(nums)))
print(4 in a)Two traps: {} is an empty dict (use set()), and a set cannot hold lists.
How do you implement a stack in Python?
Use a list: append pushes and pop() pops, both O(1), and stack[-1] is the top.
def balanced(text):
pairs = {")": "(", "]": "[", "}": "{"}
stack = []
for ch in text:
if ch in "([{":
stack.append(ch)
elif ch in pairs:
if not stack or stack.pop() != pairs[ch]:
return False
return not stack
print(balanced("{[()]}"), balanced("([)]"), balanced("(("))Bracket matching is the classic stack question. Do not use a list as a queue, because pop(0) is O(n). See the stack visualization.
What does this print: changing a shallow copy of nested lists?
import copy
a = [[1, 2], [3]]
b = copy.copy(a)
b[0].append(99)
b.append([4])
print(a)Predict the output
copy.copy makes a new outer list holding the same inner lists, so b[0].append(99) shows up in a while b.append([4]) does not. copy.deepcopy copies recursively. a[:] and a.copy() are shallow too, which is fine for immutable items.
What does this print: a 2D grid built with *?
grid = [[0] * 3] * 2
grid[0][0] = 1
print(grid)Predict the output
[row] * 2 repeats the reference, so both rows are one list and a write through one shows in the other. Build a grid with a comprehension, [[0] * 3 for _ in range(2)]; [0] * 3 alone is safe because integers are immutable.
Which classes from the collections module do you use, and when?
The four you use most are Counter for counting, defaultdict for grouping, deque for queues and sliding windows, and namedtuple for small immutable records.
from collections import Counter, defaultdict, deque, namedtuple
words = "the cat and the hat and the bat".split()
print(Counter(words).most_common(2))
by_letter = defaultdict(list)
for w in words:
by_letter[w[0]].append(w)
print(dict(by_letter))
window = deque([1, 2, 3], maxlen=3)
window.append(4)
print(window)
Point = namedtuple("Point", "x y")
print(Point(2, 5).y)Counter.most_common(k) answers "top k" questions in one line, and deque(maxlen=n) drops the oldest item automatically.
What is the time complexity of common list, dict and set operations?
A list is a dynamic array: indexing, append and pop() are O(1), while in, insert(0, x) and pop(0) are O(n), and sort() is O(n log n). Dict and set are hash tables with O(1) average lookup, insert and delete.
Follow-up: How do you speed up if x in big_list inside a loop? Convert big_list to a set first, which turns O(n * m) into O(n + m).
Why use collections.deque instead of a list for a queue?
list.pop(0) is O(n) because every remaining element shifts left; deque.popleft() is O(1).
from collections import deque
queue = deque()
for job in ["a", "b", "c"]:
queue.append(job)
while queue:
print("processing", queue.popleft())
d = deque([1, 2, 3, 4])
d.rotate(1)
print(d)Indexing into the middle of a deque is O(n), so it is not a drop-in list replacement. Breadth-first search in Python should use a deque; between threads, use queue.Queue.
How do you use a heap or priority queue in Python?
heapq turns a list into a binary min-heap: heappush and heappop are O(log n) and heap[0] is the smallest item. Up to 3.13 there is no max-heap API (3.14 added heappush_max), so push negated values.
import heapq
nums = [5, 1, 8, 3, 9, 2]
heap = []
for n in nums:
heapq.heappush(heap, n)
print(heap[0], heapq.heappop(heap), heap[0])
print(heapq.nlargest(3, nums))
k, top = 3, []
for n in nums:
heapq.heappush(top, n)
if len(top) > k:
heapq.heappop(top)
print(top[0])The last block is the "kth largest" answer: a min-heap of size k, O(n log k). See the heap visualization.
How does a Python dictionary work internally?
A dict is a hash table: the key's hash picks a slot and collisions probe other slots, so lookup, insert and delete are O(1) on average and O(n) when many keys collide. Since CPython 3.6 entries sit in a dense array in insertion order, which is why dicts keep order (guaranteed from 3.7, see the dict docs). See the hash table visualization.
How does sorting work in Python, and what does it mean that it is stable?
sorted() returns a new list; list.sort() sorts in place and returns None. Both use Timsort (with powersort merging since 3.11): O(n log n), near O(n) on partly sorted data, and stable, so equal items keep their input order.
students = [("Ravi", "B", 82), ("Asha", "A", 91), ("Meera", "B", 91), ("Kiran", "A", 75)]
by_score = sorted(students, key=lambda s: s[2], reverse=True)
print([s[0] for s in by_score])
by_section_then_score = sorted(students, key=lambda s: (s[1], -s[2]))
print([s[0] for s in by_section_then_score])
nums = [3, 1, 2]
print(nums.sort(), nums)Asha stays ahead of Meera at 91 because she came first. See the merge sort page.
Python coding interview questions
Short programs from coding rounds, each with the idiomatic Python answer and a judge problem to practice.
How do you reverse a string in Python?
Slice with a step of -1: s[::-1].
s = "coddy"
print(s[::-1])
print("".join(reversed(s)))
def is_palindrome(text):
cleaned = [c.lower() for c in text if c.isalnum()]
return cleaned == cleaned[::-1]
print(is_palindrome("A man, a plan, a canal: Panama"))If slicing is banned, swap characters in a list with two pointers. Avoid building the result with += in a loop, which can be O(n squared).
How do you find two numbers in a list that add up to a target?
Walk the list once, keeping a dict from value to index, and check whether target - n was already seen: O(n) time instead of O(n squared).
def two_sum(nums, target):
seen = {}
for i, n in enumerate(nums):
if target - n in seen:
return [seen[target - n], i]
seen[n] = i
return None
print(two_sum([2, 7, 11, 15], 9))
print(two_sum([3, 2, 4], 6))
print(two_sum([1, 2], 7))Check before inserting, so a number is never paired with itself.
How do you check if two strings are anagrams in Python?
Compare letter counts with collections.Counter, which is O(n); sorted(a) == sorted(b) is shorter but O(n log n).
from collections import Counter
def is_anagram(a, b):
return Counter(a) == Counter(b)
print(is_anagram("listen", "silent"), is_anagram("rat", "car"))
print(sorted("listen") == sorted("silent"))
groups = {}
for w in ["eat", "tea", "tan", "ate", "nat", "bat"]:
groups.setdefault("".join(sorted(w)), []).append(w)
print(list(groups.values()))The usual follow-up, grouping words into anagram groups, is at the end: anagrams share the same sorted letters, so that string works as a dict key.
Write FizzBuzz in Python.
Print 1 to n, with "Fizz" for multiples of 3, "Buzz" for multiples of 5 and "FizzBuzz" for both. Building the string piece by piece avoids a separate i % 15 check.
for i in range(1, 16):
out = ""
if i % 3 == 0:
out += "Fizz"
if i % 5 == 0:
out += "Buzz"
print(out or i, end=" ")
print()out or i prints the number when out is empty, because an empty string is falsy.
How do you generate Fibonacci numbers in Python?
Use a loop with two variables: O(n) time, O(1) space.
from functools import lru_cache
def fib_iter(n):
a, b = 0, 1
for _ in range(n):
a, b = b, a + b
return a
@lru_cache(maxsize=None)
def fib_memo(n):
return n if n < 2 else fib_memo(n - 1) + fib_memo(n - 2)
print([fib_iter(i) for i in range(10)])
print(fib_memo(80))Plain recursion takes exponential time; lru_cache makes it O(n) but still hits CPython's default recursion limit of 1000 for large n.
How do you check whether a number is prime in Python?
Try divisors from 2 while i * i <= n; if none divides n, it is prime. That is O(sqrt n).
def is_prime(n):
if n < 2:
return False
i = 2
while i * i <= n:
if n % i == 0:
return False
i += 1
return True
print([n for n in range(30) if is_prime(n)])Handle 0, 1 and negative numbers first, which is where most bugs are. For all primes up to n, use the Sieve of Eratosthenes.
How do you find the second largest number in a list?
Keep two variables, first and second, in one pass: O(n) time and O(1) space.
def second_largest(nums):
first = second = None
for n in nums:
if first is None or n > first:
first, second = n, first
elif n != first and (second is None or n > second):
second = n
return second
print(second_largest([10, 5, 10, 8]))
print(second_largest([3, 3]))The trap is duplicates: in [10, 5, 10, 8] the answer is 8, and [3, 3] has no second largest. sorted(set(nums))[-2] is O(n log n) and fails on fewer than two distinct values.
How do you remove duplicates from a list while keeping the order?
Use list(dict.fromkeys(items)): dict keys are unique and keep insertion order.
items = [3, 1, 3, 2, 1, 5]
print(list(dict.fromkeys(items)))
print(list(set(items)))
seen, result = set(), []
for x in items:
if x not in seen:
seen.add(x)
result.append(x)
print(result)list(set(items)) also removes duplicates but promises no order; it only looks sorted here because small integers hash to themselves. Write the loop when you need a condition or the items are not hashable.
How do you find the first non-repeating character in a string?
Count every character with Counter, then scan the string again and return the first one whose count is 1. Two passes, O(n) time.
from collections import Counter
def first_unique(s):
counts = Counter(s)
for i, ch in enumerate(s):
if counts[ch] == 1:
return i
return -1
print(first_unique("swiss"), first_unique("aabbcdc"), first_unique("aabb"))The second pass must walk the string, not the counter, because the string's order defines "first".
How do you flatten a nested list in Python?
Recurse: yield each item, and flatten any item that is a list with yield from.
def flatten(items):
for item in items:
if isinstance(item, list):
yield from flatten(item)
else:
yield item
print(list(flatten([1, [2, [3, [4]], 5], [], 6])))For one level, [x for sub in items for x in sub] is enough. Very deep nesting needs an explicit stack to avoid the recursion limit. See the recursion visualization.
How do you find the longest substring without repeating characters?
Use a sliding window and remember each character's last index; when a character repeats inside the window, move the left edge past its previous position. One pass, O(n).
def longest_unique(s):
last_seen = {}
start = best = 0
for i, ch in enumerate(s):
if ch in last_seen and last_seen[ch] >= start:
start = last_seen[ch] + 1
last_seen[ch] = i
best = max(best, i - start + 1)
return best
print(longest_unique("abcabcbb"), longest_unique("bbbbb"), longest_unique("pwwkew"))last_seen[ch] >= start keeps a repeat outside the window from moving start backwards.
How do you implement binary search in Python, and what does bisect do?
Keep lo and hi bounds on a sorted list and halve the range each step, O(log n); bisect does the same search and returns the insertion point.
import bisect
def binary_search(nums, target):
lo, hi = 0, len(nums) - 1
while lo <= hi:
mid = (lo + hi) // 2
if nums[mid] == target:
return mid
if nums[mid] < target:
lo = mid + 1
else:
hi = mid - 1
return -1
nums = [1, 3, 5, 7, 9, 11]
print(binary_search(nums, 7), binary_search(nums, 4))
print(bisect.bisect_left(nums, 4), bisect.bisect_left(nums, 7))
bisect.insort(nums, 4)
print(nums)Python integers never overflow, so (lo + hi) // 2 is safe. See the binary search visualization.
Python interview questions for experienced developers
For 2 to 10 years of experience: the traps behind real bugs, decorators, generators, context managers and memory.
What does this print: a default list argument used twice?
def add(item, bucket=[]):
bucket.append(item)
return bucket
add(1)
print(add(2))Predict the output
Default values are evaluated once, when def runs, so every call shares the same list and the 1 from the first call is still there. Use a None sentinel:
def add(item, bucket=None):
if bucket is None:
bucket = []
bucket.append(item)
return bucket
The bug only appears on the second call, so a quick manual test passes.
What does this print: lambdas created in a loop?
funcs = [lambda: i for i in range(3)]
print([f() for f in funcs])Predict the output
Closures capture variables, not values: every lambda reads the same i when it is called, after the loop left i at 2. Bind the current value at creation with a default argument (lambda i=i: i) or functools.partial. Callbacks created in a loop have the same bug.
What is a decorator in Python and how do you write one?
A decorator takes a function and returns a replacement, usually a wrapper that adds behavior around the call; @log_calls above def add means add = log_calls(add).
import functools
def log_calls(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f"calling {func.__name__}{args}")
result = func(*args, **kwargs)
print(f"{func.__name__} returned {result}")
return result
return wrapper
@log_calls
def add(a, b):
return a + b
add(2, 3)
print(add.__name__)Without functools.wraps, add.__name__ would be wrapper. See decorators.
What is the difference between an iterator and a generator in Python?
An iterator is any object with __iter__ and __next__. A generator is the easy way to write one: a function with yield that pauses there and resumes on the next next().
def countdown(n):
print("start")
while n > 0:
yield n
n -= 1
print("done")
gen = countdown(3)
print(next(gen))
print(list(gen))Calling countdown(3) runs nothing until the first next(). Values come on demand, so a generator can stream a file larger than memory.
What does this print: summing the same generator twice?
gen = (x * x for x in range(3))
print(sum(gen), sum(gen))Predict the output
A generator can be consumed once. The first sum exhausts it; the second gets nothing and returns 0, with no error. Build a list if you need the values twice. map, filter, zip and file objects are one-shot iterators too.
What is a context manager and how does the with statement work?
A context manager guarantees cleanup when a with block ends, even on an exception. It is any object with __enter__ and __exit__, like the file open() returns.
from contextlib import contextmanager
class Resource:
def __enter__(self):
print("open")
return self
def __exit__(self, exc_type, exc, tb):
print("close, error was", exc_type.__name__ if exc_type else None)
return False
with Resource():
print("working")
@contextmanager
def job_block(name):
print("enter", name)
try:
yield
finally:
print("exit", name)
try:
with job_block("job"):
raise ValueError("boom")
except ValueError:
print("error still raised")contextlib.contextmanager turns a generator into one: code before yield is setup, the finally block is cleanup. See context managers.
What is pickling and unpickling in Python?
Pickling turns a Python object into bytes with the pickle module, and unpickling rebuilds it.
import pickle
order = {"id": 7, "items": ["pen", "book"], "paid": True}
data = pickle.dumps(order)
print(type(data).__name__)
restored = pickle.loads(data)
print(restored == order, restored is order)Never unpickle data you do not trust, because unpickling can run arbitrary code. To exchange data with other languages, use JSON.
What is a closure in Python, and what does nonlocal do?
A closure is an inner function that remembers its enclosing function's variables after that function returns; nonlocal lets it rebind them.
def make_counter():
count = 0
def increment():
nonlocal count
count += 1
return count
return increment
c1 = make_counter()
c2 = make_counter()
print(c1(), c1(), c1(), c2())Each make_counter call creates a new count, so c1 and c2 keep separate state. Without nonlocal, count += 1 raises UnboundLocalError.
What does this print: removing items from a list while looping over it?
nums = [1, 2, 3, 4]
for n in nums:
if n < 3:
nums.remove(n)
print(nums)Predict the output
Removing an item shifts the rest left while the loop's index moves on, so the next item is skipped: 2 slides into the removed 1's slot and is never checked. Build a new list with a comprehension, or loop over a copy, nums[:].
How do you create custom exceptions, and what does raise ... from do?
Subclass Exception, usually with one base class per library so callers can catch all your errors at once. raise NewError(...) from e chains the original as __cause__.
class PaymentError(Exception):
pass
class CardDeclined(PaymentError):
def __init__(self, code):
super().__init__(f"card declined with code {code}")
self.code = code
def charge(card):
try:
return card["number"]
except KeyError as e:
raise CardDeclined(51) from e
try:
charge({})
except PaymentError as e:
print(type(e).__name__, "|", e, "|", repr(e.__cause__))Use raise ... from None to hide an irrelevant original, and store extra data, like code, as attributes.
Are type hints enforced at runtime in Python?
No. The interpreter stores annotations in __annotations__ and ignores them; checkers such as mypy read them before the code runs.
def double(x: int) -> int:
return x * 2
print(double("ab"))
print(double.__annotations__)Pydantic reads them on purpose to validate data, which is how FastAPI checks request bodies. See type hints.
What is a virtual environment and why do you need one?
A virtual environment is an isolated directory with its own site-packages, so each project pins its own dependency versions without touching the system Python or other projects.
python -m venv .venv
source .venv/bin/activate # on Windows: .venv\Scripts\activate
pip install requests
pip freeze > requirements.txt
Commit requirements.txt or a lock file from Poetry or uv, never the .venv folder.
How does memory management and garbage collection work in Python?
CPython frees most objects by reference counting, the moment their count reaches zero. A cyclic garbage collector (the gc module) frees groups of objects that only reference each other.
import gc
class Node:
def __init__(self, name):
self.name = name
self.other = None
def __del__(self):
print("freed", self.name)
a = Node("a")
del a
print("after del a")
b, c = Node("b"), Node("c")
b.other, c.other = c, b
del b, c
print("after del b, c")
gc.collect()
print("after collect")a is freed at del a; b and c reference each other, so they survive until gc.collect().
What is __slots__ and when should you use it?
__slots__ fixes the set of attributes, so instances skip the per-instance __dict__ and use less memory, but cannot get new attributes.
class Plain:
def __init__(self, x, y):
self.x, self.y = x, y
class Slotted:
__slots__ = ("x", "y")
def __init__(self, x, y):
self.x, self.y = x, y
p, s = Plain(1, 2), Slotted(1, 2)
print(hasattr(p, "__dict__"), hasattr(s, "__dict__"))
try:
s.z = 3
except AttributeError as e:
print("AttributeError:", e)Use it for very many small objects. Every class in the inheritance chain needs __slots__, or a __dict__ comes back; @dataclass(slots=True) (3.10+) writes it for you.
Advanced Python interview questions
Concurrency, the GIL, asyncio, metaclasses, descriptors and the CPython details senior interviews probe.
What is the GIL in Python?
The Global Interpreter Lock is a mutex in CPython that lets one thread at a time execute Python bytecode, so threads cannot run CPU-bound Python code in parallel. Threads still help with I/O, because a waiting thread releases the GIL; for CPU-bound work use multiprocessing. The GIL does not make code thread-safe: counter += 1 can still lose updates without a lock. Python 3.13 added an experimental free-threaded build (PEP 703), officially supported since 3.14 (PEP 779); the default build keeps the GIL.
When do you use threading, multiprocessing or asyncio in Python?
Use multiprocessing for CPU-bound work, asyncio for many concurrent I/O tasks with async libraries, and threads for I/O-bound work with blocking libraries. On the default build, with the GIL, only processes run Python code on several cores; the free-threaded build lets threads do it too. The OS can switch threads at any moment, so shared state needs locks, while asyncio switches only at await. The risks: race conditions for threads, pickling overhead for processes, and one blocking call stalling every asyncio task.
How do async and await work in Python?
async def defines a coroutine function; calling it returns a coroutine and runs nothing. An event loop runs coroutines on one thread and switches tasks at each await.
import asyncio
async def fetch(name, delay):
print("start", name)
await asyncio.sleep(delay)
print("end", name)
return name.upper()
async def main():
results = await asyncio.gather(fetch("a", 0.2), fetch("b", 0.1))
print(results)
asyncio.run(main())Both tasks start before either ends, and gather keeps argument order. The classic mistake is a blocking call such as time.sleep in a coroutine, which stalls every task; use asyncio.to_thread for blocking work.
What is a metaclass in Python?
A metaclass is the class of a class: it controls how classes are created. The default metaclass is type.
class Registry(type):
plugins = {}
def __new__(mcls, name, bases, namespace):
cls = super().__new__(mcls, name, bases, namespace)
if bases:
Registry.plugins[name.lower()] = cls
return cls
class Plugin(metaclass=Registry):
pass
class CsvExport(Plugin):
pass
class PdfExport(Plugin):
pass
print(type(int), type(Plugin))
print(sorted(Registry.plugins))Here the metaclass's __new__ registers every subclass as it is defined. Most such jobs are easier with __init_subclass__ or a class decorator, so application code rarely needs a metaclass.
What is a descriptor in Python?
A descriptor is a class attribute that defines __get__, __set__ or __delete__, so attribute access on instances runs those methods. property, classmethod and bound methods are built on it.
class Positive:
def __set_name__(self, owner, name):
self.name = "_" + name
def __get__(self, obj, objtype=None):
return getattr(obj, self.name)
def __set__(self, obj, value):
if value <= 0:
raise ValueError(f"{self.name[1:]} must be positive")
setattr(obj, self.name, value)
class Order:
quantity = Positive()
price = Positive()
def __init__(self, quantity, price):
self.quantity = quantity
self.price = price
o = Order(2, 150)
print(o.quantity * o.price)
try:
o.price = -5
except ValueError as e:
print("ValueError:", e)A data descriptor (with __set__) takes priority over the instance __dict__. Use one to reuse validation across attributes; for one attribute, @property is simpler.
How do you make your own class iterable?
Implement __iter__, easiest as a generator, which returns a fresh iterator each time, so the object can be looped over repeatedly.
class Countdown:
def __init__(self, start):
self.start = start
def __iter__(self):
n = self.start
while n > 0:
yield n
n -= 1
class Squares:
def __init__(self, limit):
self.i, self.limit = 0, limit
def __iter__(self):
return self
def __next__(self):
if self.i >= self.limit:
raise StopIteration
self.i += 1
return self.i ** 2
c = Countdown(3)
print(list(c), list(c))
s = Squares(3)
print(list(s), list(s))Squares is its own iterator, so the second list(s) is empty. An iterable can be reused; an iterator is consumed once.
What does this print: is on integers built with int()?
a = int("256")
b = int("256")
c = int("257")
d = int("257")
print(a is b, c is d)Predict the output
CPython caches one object for each integer from -5 to 256, so both 256s are the same object, while each 257 built at runtime is new. The cache is a CPython implementation detail (constants in one code block may be shared too, hence int()), so compare numbers with ==.
What does this print: return in both try and finally?
def f():
try:
return "try"
finally:
return "finally"
print(f())Predict the output
finally always runs, and a return there replaces the value from try and silently discards any exception on its way out. Linters flag it and Python 3.14 emits a SyntaxWarning for it (PEP 765). Use finally for cleanup only.
What does this print: 1, True and 1.0 as dict keys?
d = {}
for key, value in [(1, "a"), (True, "b"), (1.0, "c")]:
d[key] = value
print(d)Predict the output
1, True and 1.0 are equal and have the same hash, so a dict treats them as one key. The first insertion fixes the key object (1), and later equal keys only replace the value, so the last value wins. {1, True, 1.0} has one element for the same reason.
How does functools.lru_cache work, and when should you not use it?
lru_cache memoizes a function by its arguments and evicts the least recently used entry once maxsize is reached; functools.cache (3.9+) is unbounded.
from functools import lru_cache
@lru_cache(maxsize=128)
def ways(n):
if n <= 1:
return 1
return ways(n - 1) + ways(n - 2)
print(ways(40))
print(ways.cache_info())Avoid it for unhashable arguments, functions with side effects, and methods, because each entry keeps self alive.
How does Python execute your code? What is bytecode?
CPython compiles source to bytecode for a stack-based virtual machine, then an interpreter loop runs it. Imported modules cache it as .pyc files in __pycache__.
import dis
def add(a, b):
return a + b
dis.dis(add)
print(add.__code__.co_varnames)Since 3.11 the interpreter specializes frequently run instructions for the types it sees (PEP 659). Bytecode changes between versions.
Python interview questions for data analyst roles
CSV files, grouping, missing values and common pandas questions for data analyst roles.
What is the difference between a NumPy array and a Python list?
A NumPy array holds one type in contiguous memory and applies operations to every element in compiled code; a list holds references to any objects and you loop over it yourself.
prices = [100, 250, 80]
print(prices * 2)
print([p * 2 for p in prices])On a list * 2 repeats it, while np.array(prices) * 2 doubles each value.
How do you read a CSV file in Python without pandas?
Use csv.DictReader, which yields one dict per row keyed by the header (io.StringIO stands in for a file here).
import csv
import io
data = io.StringIO("name,city,amount\nAsha,Pune,1200\nRavi,Delhi,800\nMeera,Pune,450\n")
rows = list(csv.DictReader(data))
print(rows[0])
total = sum(int(r["amount"]) for r in rows if r["city"] == "Pune")
print(total)Every value is a string, so convert numbers yourself. Unlike line.split(","), the module handles quoted commas. See CSV in Python.
How do you group and aggregate data in plain Python?
Use a defaultdict keyed by the group: one pass, no sorting, the plain Python version of SQL's GROUP BY with SUM.
from collections import defaultdict
from itertools import groupby
sales = [("Pune", 1200), ("Delhi", 800), ("Pune", 450), ("Mumbai", 990), ("Delhi", 300)]
totals = defaultdict(int)
for city, amount in sales:
totals[city] += amount
print(dict(totals))
sales.sort(key=lambda s: s[0])
for city, group in groupby(sales, key=lambda s: s[0]):
print(city, sum(a for _, a in group))itertools.groupby only groups consecutive items, so sort by the same key first. In pandas it is df.groupby("city")["amount"].sum().
What is the difference between loc and iloc in pandas?
loc selects by label and includes a slice's end; iloc selects by integer position and excludes it.
import pandas as pd
df = pd.DataFrame({"city": ["Pune", "Delhi", "Goa"], "sales": [120, 80, 45]},
index=["a", "b", "c"])
df.loc["a":"b", "sales"] # rows a and b: the end label is included
df.iloc[0:2, 1] # rows 0 and 1: the end position is excluded
df.loc[df["sales"] > 50] # boolean filter
To assign, use one call, df.loc[mask, "col"] = value; chained assignment warns in pandas 2 and never updates df under pandas 3's copy-on-write.
How do you handle missing values in a dataset?
Measure them first, then decide per column: drop the rows, fill with a constant, the median or the previous value, or keep "missing" as a category, depending on why values are missing.
df.isna().sum() # missing count per column
df.dropna(subset=["amount"]) # drop rows with no amount
df["amount"] = df["amount"].fillna(df["amount"].median())
df["city"] = df["city"].fillna("Unknown")
NaN is not equal to itself, so test with isna(), never ==.
What is the difference between merge, join and concat in pandas?
concat stacks frames, merge is a SQL-style join on columns, and join is a left join on the index. The common bug is duplicate keys on both sides, which multiplies rows; pass validate="many_to_one" so pandas raises an error instead.
How do you calculate the mean, median and mode in Python?
Use the standard statistics module: mean, median, mode and stdev work on any list of numbers.
import statistics as st
scores = [70, 85, 85, 90, 100, 40]
print(st.mean(scores), st.median(scores), st.mode(scores))
print(round(st.stdev(scores), 2))stdev is the sample standard deviation and pstdev the population one. NumPy's np.std defaults to population and pandas' .std() to sample, so the two can disagree.
Preparing for the interview
How should I prepare for a Python interview?
What is asked in Python interviews for freshers compared with experienced developers?
How long does it take to prepare for a Python interview?
Do I need DSA for a Python developer interview?
Counter, deque, heapq and bisect keep DSA code short in Python.Which Python topics should I revise first?
*args and **kwargs, OOP (MRO, class vs instance attributes, dunder methods), exceptions, generators and decorators. For senior roles add the GIL, threading vs multiprocessing vs asyncio, and memory management.