Python Interview Questions
Core Language
Q: What is the difference between is and ==?
==checks value equality.ischecks identity — whether both variables point to the same object in memory.
a = [1, 2, 3]
b = [1, 2, 3]
a == b # True — same value
a is b # False — different objects
# Gotcha: small ints and interned strings are cached
x = 256; y = 256
x is y # True (CPython caches ints -5 to 256)
x = 257; y = 257
x is y # False
Q: What is the difference between mutable and immutable types?
Mutable — can be changed after creation:
list,dict,set,bytearrayImmutable — cannot be changed:int,float,str,tuple,frozenset,bytes
# Immutable — reassignment creates a new object
s = "hello"
s[0] = "H" # TypeError
# Mutable — changes in place
lst = [1, 2, 3]
lst[0] = 99 # ✅
# Key implication: immutables are hashable → can be dict keys / set members
d = {(1, 2): "tuple key"} # ✅
d = {[1, 2]: "list key"} # TypeError — list is unhashable
Q: What is the mutable default argument gotcha?
Default arguments are evaluated once at function definition time, not on each call. A mutable default (like
[]) is shared across all calls.
# Bug
def append_to(item, lst=[]):
lst.append(item)
return lst
append_to(1) # [1]
append_to(2) # [1, 2] ← not a fresh list!
# Fix
def append_to(item, lst=None):
if lst is None:
lst = []
lst.append(item)
return lst
Q: What is the difference between deepcopy and copy?
copy.copy()— shallow copy: creates a new container but the nested objects are still references.copy.deepcopy()— deep copy: recursively copies all nested objects.
import copy
original = [[1, 2], [3, 4]]
shallow = copy.copy(original)
deep = copy.deepcopy(original)
original[0][0] = 99
shallow[0][0] # 99 — shared inner list
deep[0][0] # 1 — independent copy
Q: What are Python's scoping rules (LEGB)?
Python resolves names in this order: Local → Enclosing → Global → Built-in
x = "global"
def outer():
x = "enclosing"
def inner():
x = "local"
print(x) # "local"
inner()
print(x) # "enclosing"
outer()
print(x) # "global"
Use global x to write to a global variable from inside a function.
Use nonlocal x to write to an enclosing (not global) variable from a nested function.
Q: What is the GIL and why does it matter?
The Global Interpreter Lock (GIL) allows only one thread to execute Python bytecode at a time, even on multi-core CPUs.
- I/O-bound tasks →
threadingworks fine (GIL released during I/O waits)- CPU-bound tasks → use
multiprocessing(separate processes, no GIL)asynciois cooperative concurrency — not truly parallel but efficient for I/O
Q: What is the difference between @staticmethod and @classmethod?
class MyClass:
class_var = "shared"
def instance_method(self):
# has access to instance (self) and class
return self.class_var
@classmethod
def class_method(cls):
# receives class as first arg — can access/modify class state
# often used as alternative constructors
return cls()
@staticmethod
def static_method():
# receives neither self nor cls
# just a regular function namespaced inside the class
return "no access to class or instance"
Q: What is __slots__ and when would you use it?
__slots__restricts a class's attributes to a fixed set, preventing the creation of__dict__per instance. Saves memory when creating many instances.
class Point:
__slots__ = ('x', 'y')
def __init__(self, x, y):
self.x = x
self.y = y
p = Point(1, 2)
p.z = 3 # AttributeError — z not in __slots__
OOP
Q: What are the four pillars of OOP in Python?
Encapsulation — bundle data + methods; hide internals with
__privateconvention Abstraction — expose what, hide how (abc.ABC,@abstractmethod) Inheritance — child class inherits from parent (class Dog(Animal)) Polymorphism — same interface, different behaviour (method overriding, duck typing)
Q: What is duck typing?
"If it walks like a duck and quacks like a duck, it's a duck." Python doesn't check types — it checks whether an object has the required methods/attributes.
class Dog:
def speak(self): return "Woof"
class Cat:
def speak(self): return "Meow"
def make_sound(animal):
print(animal.speak()) # works for any object with .speak()
make_sound(Dog())
make_sound(Cat())
Q: What is super() and when do you use it?
super()returns a proxy to the parent class, allowing you to call overridden methods without hardcoding the parent's name. Essential in multiple inheritance (follows MRO).
class Animal:
def __init__(self, name):
self.name = name
class Dog(Animal):
def __init__(self, name, breed):
super().__init__(name) # call Animal.__init__
self.breed = breed
Q: What is MRO (Method Resolution Order)?
The order Python searches classes when looking up a method. Uses C3 linearisation. Check with
ClassName.__mro__orClassName.mro().
class A: pass
class B(A): pass
class C(A): pass
class D(B, C): pass
D.__mro__ # (D, B, C, A, object)
Q: What are dunder (magic) methods? Give examples.
Special methods with double underscores that Python calls implicitly.
| Method | Called when |
|---|---|
__init__ | Object created |
__str__ | str(obj) or print(obj) |
__repr__ | repr(obj), debugging |
__len__ | len(obj) |
__getitem__ | obj[key] |
__setitem__ | obj[key] = val |
__contains__ | x in obj |
__eq__ | obj == other |
__lt__ | obj < other |
__add__ | obj + other |
__enter__ / __exit__ | with obj: |
__iter__ / __next__ | for x in obj: |
Functions & Closures
Q: What is a closure?
A function that remembers variables from its enclosing scope even after the outer function has returned.
def make_multiplier(n):
def multiplier(x):
return x * n # n is captured from enclosing scope
return multiplier
double = make_multiplier(2)
double(5) # 10 — n=2 is still alive
Q: What is a decorator and how does it work?
A decorator is a function that wraps another function to add behaviour before/after it runs. Uses closure +
functools.wrapsto preserve metadata.
from functools import wraps
def log(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
result = func(*args, **kwargs)
print(f"Done {func.__name__}")
return result
return wrapper
@log
def greet(name):
print(f"Hello, {name}")
greet("Vatsal")
# Calling greet
# Hello, Vatsal
# Done greet
Without @wraps(func), greet.__name__ would be "wrapper" — breaking introspection tools.
Decorator with arguments requires a 3-level nested structure:
def repeat(n): # level 1: receives config
def decorator(func): # level 2: receives function
@wraps(func)
def wrapper(*args, **kwargs): # level 3: runs on each call
for _ in range(n):
func(*args, **kwargs)
return wrapper
return decorator
@repeat(3)
def say_hi():
print("Hi!")
Q: What is the difference between *args and **kwargs?
def func(*args, **kwargs):
print(args) # tuple of positional args
print(kwargs) # dict of keyword args
func(1, 2, name="Vatsal", age=25)
# (1, 2)
# {'name': 'Vatsal', 'age': 25}
# Spread operator — unpack when calling
nums = [1, 2, 3]
print(*nums) # 1 2 3
d = {"sep": "-"}
print(1, 2, 3, **d) # 1-2-3
Q: What is a lambda function?
Anonymous single-expression function. Useful for short callbacks.
square = lambda x: x ** 2
square(5) # 25
# Common use with sorted, map, filter
nums = [3, 1, 4, 1, 5]
sorted(nums, key=lambda x: -x) # [5, 4, 3, 1, 1]
Avoid for complex logic — use a named function for readability.
Generators & Iterators
Q: What is the difference between a generator and a list?
A list stores all values in memory at once. A generator yields values lazily — one at a time, only when asked. Uses
O(1)memory regardless of size.
# List — all in memory
squares_list = [x**2 for x in range(1_000_000)]
# Generator — one at a time
squares_gen = (x**2 for x in range(1_000_000))
next(squares_gen) # 0
next(squares_gen) # 1
Q: What is yield and how does it work?
yieldturns a function into a generator. Each call tonext()runs until the nextyield, then pauses — the local state is preserved between calls.
def countdown(n):
while n > 0:
yield n
n -= 1
for x in countdown(3):
print(x) # 3, 2, 1
Q: What is a context manager and how do you create one?
An object that manages resources — guarantees setup (
__enter__) and cleanup (__exit__) even if an exception occurs.
# Class-based
class ManagedFile:
def __init__(self, path):
self.path = path
def __enter__(self):
self.file = open(self.path)
return self.file
def __exit__(self, exc_type, exc_val, exc_tb):
self.file.close()
return False # don't suppress exceptions
with ManagedFile("data.txt") as f:
data = f.read()
# Generator-based (simpler)
from contextlib import contextmanager
@contextmanager
def managed_file(path):
f = open(path)
try:
yield f
finally:
f.close()
Data Structures
Q: When would you use a tuple over a list?
Tuples when data is fixed and shouldn't change (coordinates, RGB colours, DB rows). Tuples are immutable → hashable → usable as dict keys or set elements. Also slightly faster to iterate than lists.
Q: What is the time complexity of common Python operations?
| Operation | list | dict / set |
|---|---|---|
x in collection | O(n) | O(1) average |
append | O(1) amortised | — |
insert(0, x) | O(n) | — |
pop() (end) | O(1) | — |
pop(0) (front) | O(n) | — |
get / set / delete | — | O(1) average |
sorted() | O(n log n) | — |
Never use
list.pop(0)for queues — usecollections.dequeinstead (popleft()is O(1)).
Q: What are defaultdict, Counter, and OrderedDict?
from collections import defaultdict, Counter, OrderedDict
# defaultdict — no KeyError for missing keys
graph = defaultdict(list)
graph["a"].append("b") # no need to check if "a" exists
# Counter — frequency map
words = ["apple", "banana", "apple", "cherry", "banana", "apple"]
c = Counter(words)
# Counter({'apple': 3, 'banana': 2, 'cherry': 1})
c.most_common(2) # [('apple', 3), ('banana', 2)]
# OrderedDict — maintains insertion order (less needed since Python 3.7+)
od = OrderedDict()
od["first"] = 1
od["second"] = 2
Advanced Topics
Q: What is @property and why use it?
@propertylets you define a method that is accessed like an attribute. Allows adding validation/logic later without breaking the API.
class Circle:
def __init__(self, radius):
self._radius = radius
@property
def radius(self):
return self._radius
@radius.setter
def radius(self, value):
if value < 0:
raise ValueError("Radius cannot be negative")
self._radius = value
@property
def area(self):
import math
return math.pi * self._radius ** 2
c = Circle(5)
c.radius # 5 — looks like attribute access
c.area # 78.5...
c.radius = -1 # ValueError
Q: What is dataclass and when would you use it?
@dataclassauto-generates__init__,__repr__,__eq__(and optionally__hash__,__lt__, etc.) from class variable annotations. Less boilerplate than manual classes.
from dataclasses import dataclass, field
@dataclass
class Point:
x: float
y: float
label: str = "origin"
tags: list = field(default_factory=list) # mutable default — use field()
p = Point(1.0, 2.0)
print(p) # Point(x=1.0, y=2.0, label='origin', tags=[])
Q: What is the difference between __str__ and __repr__?
__repr__— for developers: should be unambiguous, ideally eval-able back to the object. Shown in REPL,repr(), logging.__str__— for end users: readable, human-friendly. Shown byprint(),str().
class Point:
def __init__(self, x, y):
self.x, self.y = x, y
def __repr__(self):
return f"Point({self.x!r}, {self.y!r})" # Point(1, 2)
def __str__(self):
return f"({self.x}, {self.y})" # (1, 2)
If only __repr__ is defined, it's used as fallback for str().
Q: How does Python handle memory management?
Python uses reference counting + a cyclic garbage collector.
- Each object has a reference count; when it hits 0, memory is freed immediately
- The GC handles cyclic references (A → B → A) that ref counting alone can't break
del xdecrements the ref count but doesn't guarantee immediate deallocation
Common Gotchas Cheatsheet
| Gotcha | Rule |
|---|---|
| Mutable default arg | Use None as default, initialise inside |
is vs == | Use == for value comparison; is only for None checks |
| Late binding in closures | Closures capture variable, not value — use default=x to bind early |
list * n shallow copy | [[]] * 3 makes 3 refs to same list — use list comprehension |
+= on tuple | Creates new tuple; += on list mutates in place |
| Integer division | 7 / 2 = 3.5 (float); 7 // 2 = 3 (floor div) |
except Exception | Catches most errors but not SystemExit, KeyboardInterrupt — use except Exception not bare except |
| Chained comparison | 1 < x < 10 works in Python (evaluates as 1 < x and x < 10) |
# Late binding gotcha
funcs = [lambda: i for i in range(3)]
[f() for f in funcs] # [2, 2, 2] — all capture same i
# Fix — bind at definition time
funcs = [lambda i=i: i for i in range(3)]
[f() for f in funcs] # [0, 1, 2]
# Shallow list multiplication gotcha
matrix = [[0] * 3] * 3
matrix[0][0] = 1
# [[1, 0, 0], [1, 0, 0], [1, 0, 0]] ← all rows are same object!
# Fix
matrix = [[0] * 3 for _ in range(3)]