Practice Python interview answers with simple explanations first, then examples, project context, and professional notes for OOP, decorators, generators, collections, Django, Flask, and Python 3 features.
Python is a high-level, interpreted, dynamically typed programming language. In simple words, it lets developers write readable code for many kinds of work: automation, web apps, APIs, data analysis, AI, testing, and scripting. In an interview, do not only say "Python is easy." Mention why it is useful in real projects: clean syntax, large standard library, automatic memory management, multiple programming styles, cross-platform support, and a strong ecosystem such as Django, Flask, FastAPI, NumPy, pandas, and PyTorch.
lst = [1, 2, 2, 3]
tpl = (1, 2, 2, 3)
st = {1, 2, 2, 3} # {1, 2, 3} - duplicates removed
lst = ["a", "b", "c"]
print(lst[0]) # "a"
dct = {"name": "Alice", "age": 30}
print(dct["name"]) # "Alice"
A decorator is a function that takes another function and returns a new function with extra behavior. It lets you add behavior without changing the original function body. In real projects, decorators are used for logging, authentication, permissions, caching, retry logic, timing, validation, and route handling in frameworks. A good interview answer should include the idea of "wrapping" plus one practical use case.
def log(func):
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
return func(*args, **kwargs)
return wrapper
@log
def greet(name):
return f"Hello, {name}"
greet("Alice") # prints "Calling greet", returns "Hello, Alice"
def func(*args, **kwargs):
print(args) # (1, 2, 3)
print(kwargs) # {"a": 4, "b": 5}
func(1, 2, 3, a=4, b=5)
A generator yields values one at a time using yield, enabling memory-efficient iteration over large datasets. Generators are lazy - they produce values on demand.
def fibonacci(n):
a, b = 0, 1
for _ in range(n):
yield a
a, b = b, a + b
for num in fibonacci(10):
print(num)
List comprehension provides a concise way to create lists. It is more readable and often faster than equivalent for loops.
# Traditional
squares = []
for x in range(10):
squares.append(x**2)
# List comprehension
squares = [x**2 for x in range(10)]
# With condition
evens = [x for x in range(10) if x % 2 == 0]
lst = [x**2 for x in range(1000000)] # entire list in memory
gen = (x**2 for x in range(1000000)) # lazy, one at a time
a = [1, 2, 3]
b = [1, 2, 3]
c = a
print(a == b) # True (same values)
print(a is b) # False (different objects)
print(a is c) # True (same object)
import copy
original = [[1, 2], [3, 4]]
shallow = copy.copy(original)
deep = copy.deepcopy(original)
shallow[0][0] = 99 # affects original
deep[0][0] = 88 # does not affect original
class MyClass:
count = 0
@staticmethod
def add(a, b): return a + b
@classmethod
def increment(cls): cls.count += 1
class Point:
def __init__(self, x, y): self.x, self.y = x, y
def __repr__(self): return f"Point({self.x}, {self.y})"
def __str__(self): return f"({self.x}, {self.y})"
a = [1, 2]
a.append([3, 4]) # [1, 2, [3, 4]]
b = [1, 2]
b.extend([3, 4]) # [1, 2, 3, 4]
lst = [1, 2, 3, 2]
lst.remove(2) # [1, 3, 2]
lst.pop() # returns 2, lst is [1, 3]
del lst[0] # [3]
xrange() existed in Python 2 and returned a generator. range() in Python 2 returned a list. In Python 3, range() behaves like Python 2 xrange() (returns a lazy range object), and xrange() was removed.
The GIL is a mutex that allows only one thread to execute Python bytecode at a time, even on multi-core systems. This simplifies memory management but limits CPU-bound parallelism. Use multiprocessing for CPU-bound tasks; threading still works for I/O-bound tasks.
from multiprocessing import Pool
def square(x): return x * x
with Pool(4) as pool:
results = pool.map(square, range(10))
# lambda
square = lambda x: x**2
# def
def square(x):
return x**2
# lambda in sorted()
students.sort(key=lambda s: s.age)
nums = [1, 2, 3, 4, 5]
list(map(lambda x: x*2, nums)) # [2,4,6,8,10]
list(filter(lambda x: x%2==0, nums)) # [2,4]
from functools import reduce
reduce(lambda a,b: a+b, nums) # 15
class Singleton:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
class MyClass:
class_var = 0 # class variable
def __init__(self, x):
self.x = x # instance variable
@property allows you to define methods that are accessed like attributes, providing controlled access to private variables without explicit getter/setter calls.
class User:
def __init__(self, name):
self._name = name
@property
def name(self): return self._name
@name.setter
def name(self, value):
if not value: raise ValueError("Name required")
self._name = value
user.name = "Alice" # calls setter
is None checks object identity (recommended). == None checks equality (calls __eq__). Always use is None because None is a singleton - there is only one None object in Python.
if value is None: # correct
pass
if value == None: # works but not idiomatic
pass
try:
result = risky_operation()
except ValueError as e:
handle_error(e)
else:
process_result(result)
finally:
cleanup()
if age < 0:
raise ValueError("Age cannot be negative")
assert age >= 0, "Age must be non-negative"
The with statement ensures proper resource cleanup using context managers (__enter__ and __exit__ methods). Commonly used for file handling, database connections, and locks.
with open("file.txt", "r") as f:
data = f.read()
# file is automatically closed
# Custom context manager
from contextlib import contextmanager
@contextmanager
def timer():
start = time.time()
yield
print(f"Elapsed: {time.time() - start}s")
import pickle, json
data = {"name": "Alice", "age": 30}
pickle.dumps(data) # binary
json.dumps(data) # text
for i in range(len(items)):
print(i, items[i])
# Better with enumerate
for i, item in enumerate(items):
print(i, item)
a = [1, 2, 3]
b = ["a", "b"]
list(zip(a, b)) # [(1,"a"), (2,"b")]
from itertools import zip_longest
list(zip_longest(a, b, fillvalue="-")) # [(1,"a"), (2,"b"), (3,"-")]
print(any([False, 0, 1])) # True
print(all([True, 1, "a"])) # True
print(all([True, 0, "a"])) # False
a = [3, 1, 2]
a.sort() # a is now [1, 2, 3]
b = [3, 1, 2]
c = sorted(b) # b unchanged, c is [1, 2, 3]
s = "a,b,c"
parts = s.split(",") # ["a", "b", "c"]
joined = ",".join(parts) # "a,b,c"
__call__ makes an instance callable like a function. Regular methods are called with dot notation.
class Multiplier:
def __init__(self, factor): self.factor = factor
def __call__(self, x): return x * self.factor
double = Multiplier(2)
print(double(5)) # 10 - instance called like a function
__init__.py marks a directory as a Python package, allowing imports from it. It can be empty or contain package initialization code. Python 3.3+ supports namespace packages without __init__.py, but it is still recommended for explicit package definition.
import math
math.sqrt(16)
from math import sqrt
sqrt(16)
Code under if __name__ == "__main__": only runs when the script is executed directly, not when imported as a module. Module-level code (outside the if) runs on both direct execution and import.
def main():
print("Running main")
if __name__ == "__main__":
main() # only runs when script is executed directly
def func(a, b, c): print(a, b, c)
args = [1, 2, 3]
func(*args) # unpacks to func(1, 2, 3)
kwargs = {"a": 1, "b": 2, "c": 3}
func(**kwargs) # unpacks to func(a=1, b=2, c=3)
class Animal: pass
class Dog(Animal): pass
d = Dog()
print(type(d) == Dog) # True
print(type(d) == Animal) # False
print(isinstance(d, Animal)) # True
d = {"name": "Alice"}
print(d["age"]) # KeyError
print(d.get("age")) # None
print(d.get("age", 0)) # 0
itertools.chain(*iterables) creates a lazy iterator that chains multiple iterables without creating a new list in memory. list1 + list2 creates a new list immediately. Use chain() for memory efficiency with large iterables.
from itertools import chain
for item in chain([1,2], [3,4], [5,6]):
print(item) # 1,2,3,4,5,6 - no intermediate list created
defaultdict automatically creates a default value for missing keys using a factory function. Regular dict raises KeyError for missing keys.
from collections import defaultdict
# Regular dict
d = {}
d["key"] += 1 # KeyError
# defaultdict
d = defaultdict(int) # int() returns 0
d["key"] += 1 # works, d["key"] is now 1
Counter is a dict subclass for counting hashable objects. It provides convenient methods like most_common(), elements(), and arithmetic operations on counts.
from collections import Counter
c = Counter(["a", "b", "a", "c", "a"])
print(c) # Counter({"a": 3, "b": 1, "c": 1})
print(c.most_common(2)) # [("a", 3), ("b", 1)]
from collections import namedtuple
Point = namedtuple("Point", ["x", "y"])
p = Point(1, 2)
from dataclasses import dataclass
@dataclass
class Point:
x: int
y: int = 0 # default value
name = "Alice"
age = 30
print(f"{name} is {age} years old") # f-string (preferred)
print("{} is {} years old".format(name, age))
print("%s is %d years old" % (name, age))
Use curated frontend, Java, Python, or cloud and DevOps packs with 200 questions, model answers, code examples, and a seven-day plan.
Combine four related topic areas into one focused role-preparation path.
Practise explaining code, design decisions, limitations, and failure modes.
Move from baseline review to a timed mock interview and final checklist.
Explore 500+ free tutorials across 20+ languages and frameworks.