Python interviews test language fundamentals, OOP design, data structures, and runtime behaviour. This guide covers 50 of the most common questions — with concise answers and runnable code examples.
Quick reference
| Topic | Most asked questions |
|---|---|
| Types & mutability | Mutable vs immutable, is vs ==, interning |
| Data structures | List vs tuple vs set vs dict, comprehensions |
| Functions | *args/**kwargs, closures, default arg trap |
| Decorators | @functools.wraps, factory decorators, @property |
| OOP | __init__ vs __new__, MRO, dunder methods |
| Generators | yield, lazy evaluation, itertools |
| Error handling | Exception hierarchy, raise ... from, context managers |
| Concurrency | GIL, threading vs multiprocessing vs asyncio |
| Modules | import system, __name__ == "__main__" |
| Tricky questions | Mutable defaults, late binding, None checks |
Types and mutability
1. What is the difference between mutable and immutable types?
Immutable objects cannot be changed after creation. Mutable objects can.
| Immutable | Mutable |
|---|---|
int, float, bool |
list |
str |
dict |
tuple |
set |
frozenset |
bytearray |
bytes |
user-defined classes (usually) |
# Immutable — reassignment creates a new object
x = "hello"
id_before = id(x)
x += " world"
print(id(x) == id_before) # False — new string object
# Mutable — modified in place
lst = [1, 2, 3]
id_before = id(lst)
lst.append(4)
print(id(lst) == id_before) # True — same object
Mutability matters for dictionary keys (must be hashable/immutable) and function default arguments.
2. What is the difference between is and ==?
==checks value equality (calls__eq__)ischecks identity — whether both variables point to the same object in memory
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
# None check — always use `is`, not `==`
x = None
print(x is None) # True ✓
print(x == None) # True but fragile (can be overridden by __eq__)
Use is only for singletons: None, True, False.
3. What is integer interning?
CPython caches small integers (typically −5 to 256) and short strings, so is may return True even for "different" objects.
a = 100
b = 100
print(a is b) # True — cached (CPython implementation detail)
a = 1000
b = 1000
print(a is b) # False — not cached (implementation detail, may vary)
Never rely on is for value comparison of integers or strings.
4. How does Python handle variable scoping (LEGB)?
Python looks up names in this order:
- Local — inside the current function
- Enclosing — enclosing function scopes (closures)
- Global — module-level
- Built-in — built-in names (
len,print, etc.)
x = "global"
def outer():
x = "enclosing"
def inner():
# x = "local" # if this line is uncommented, local wins
print(x) # "enclosing" — found in enclosing scope
inner()
outer()
Use global to write to a global variable, nonlocal to write to an enclosing variable.
Data structures
5. What are the differences between list, tuple, set, and dict?
| Feature | list |
tuple |
set |
dict |
|---|---|---|---|---|
| Ordered | Yes (insertion) | Yes | No (3.7+ insertion order) | Yes (3.7+ insertion order) |
| Mutable | Yes | No | Yes | Yes |
| Duplicates | Yes | Yes | No | Keys: No, Values: Yes |
| Indexable | Yes | Yes | No | By key |
| Use case | General sequence | Fixed record, dict key | Unique items, set ops | Key-value mapping |
6. When would you use a tuple instead of a list?
- When the data should not change (coordinates, RGB colour, DB row)
- As dictionary keys (lists are unhashable)
- For named records via
collections.namedtupleortyping.NamedTuple - Slightly faster than lists for iteration (no overhead for mutability)
from typing import NamedTuple
class Point(NamedTuple):
x: float
y: float
p = Point(1.0, 2.0)
print(p.x, p.y) # 1.0 2.0
print(p[0]) # 1.0 — still indexable
coords = {p: "origin"} # usable as dict key
7. How does a Python dict work internally?
Python dicts are hash tables — each key is hashed to find a slot.
dict[key]is O(1) average for lookup, insert, delete- Keys must be hashable (immutable)
- As of Python 3.7+, insertion order is guaranteed
d = {"a": 1, "b": 2}
# Safe access patterns
value = d.get("c", 0) # default 0 if missing
d.setdefault("d", []).append(1) # create key if absent
# Merging (Python 3.9+)
merged = d | {"e": 5}
# defaultdict avoids KeyError
from collections import defaultdict
counts = defaultdict(int)
for ch in "hello":
counts[ch] += 1
8. What is a list comprehension and when should you prefer a generator expression?
# List comprehension — materialises all values in memory
squares = [x**2 for x in range(1000)]
# Generator expression — lazy, one value at a time
squares_gen = (x**2 for x in range(1000))
# Prefer generator when:
# 1. You only iterate once
# 2. The dataset is large
total = sum(x**2 for x in range(1_000_000)) # no list in memory
# Prefer list when:
# 1. You need indexing / len()
# 2. You iterate multiple times
Nested comprehensions are readable up to two levels; beyond that, use explicit loops.
Functions
9. What is the difference between *args and **kwargs?
*argscaptures positional arguments as atuple**kwargscaptures keyword arguments as adict
def demo(*args, **kwargs):
print(args) # (1, 2, 3)
print(kwargs) # {'x': 10, 'y': 20}
demo(1, 2, 3, x=10, y=20)
# Unpacking into a function call
def add(a, b, c):
return a + b + c
nums = [1, 2, 3]
print(add(*nums)) # 6
params = {"a": 1, "b": 2, "c": 3}
print(add(**params)) # 6
10. What is the mutable default argument trap?
Default argument values are evaluated once when the function is defined, not on each call.
# BUG: same list is shared across all calls
def append_to(value, lst=[]):
lst.append(value)
return lst
print(append_to(1)) # [1]
print(append_to(2)) # [1, 2] ← NOT [2]!
# FIX: use None as sentinel
def append_to_fixed(value, lst=None):
if lst is None:
lst = []
lst.append(value)
return lst
print(append_to_fixed(1)) # [1]
print(append_to_fixed(2)) # [2] ✓
11. What is a closure in Python?
A closure is a function that remembers the variables from its enclosing scope even after the outer function has returned.
def make_counter(start=0):
count = start
def increment(step=1):
nonlocal count
count += step
return count
return increment
counter = make_counter(10)
print(counter()) # 11
print(counter()) # 12
print(counter(5)) # 17
Closures are the basis for decorators, factories, and partial application.
12. What is the late binding problem in closures?
Variables in closures are looked up at call time, not at definition time.
# BUG: all lambdas see the same `i` = 9 after the loop
funcs = [lambda: i for i in range(10)]
print(funcs[0]()) # 9, not 0!
print(funcs[3]()) # 9, not 3!
# FIX: capture the current value as a default argument
funcs = [lambda i=i: i for i in range(10)]
print(funcs[0]()) # 0 ✓
print(funcs[3]()) # 3 ✓
Decorators
13. What is a decorator?
A decorator is a function that wraps another function to extend its behaviour without modifying it.
import functools
import time
def timer(func):
@functools.wraps(func) # preserves __name__, __doc__
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"{func.__name__} took {elapsed:.4f}s")
return result
return wrapper
@timer
def slow_sum(n):
return sum(range(n))
slow_sum(1_000_000) # slow_sum took 0.0312s
Without @functools.wraps, slow_sum.__name__ would be "wrapper".
14. How do you write a decorator that accepts arguments?
Add an extra outer function to receive the arguments.
import functools
def retry(times=3, exceptions=(Exception,)):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(1, times + 1):
try:
return func(*args, **kwargs)
except exceptions as e:
if attempt == times:
raise
print(f"Attempt {attempt} failed: {e}")
return wrapper
return decorator
@retry(times=3, exceptions=(IOError,))
def fetch(url):
raise IOError("connection refused")
# fetch("http://example.com") # retries 3 times then raises
15. What does @property do?
@property lets you define a getter (and optionally setter/deleter) for a class attribute using method syntax while providing attribute access syntax.
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)
print(c.radius) # 5 — calls getter
c.radius = 10 # calls setter
print(c.area) # 314.159...
# c.radius = -1 # raises ValueError
Object-oriented programming
16. What is the difference between __init__ and __new__?
__new__creates the instance (class method, called first)__init__initialises the already-created instance
class Singleton:
_instance = None
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self, value):
self.value = value # called every time, but same object
a = Singleton(1)
b = Singleton(2)
print(a is b) # True — same instance
print(a.value) # 2 — __init__ ran again
Override __new__ for singletons, immutable types (subclassing int, str), or metaclasses.
17. What is the MRO (Method Resolution Order)?
MRO determines the order in which base classes are searched for a method. Python uses the C3 linearisation algorithm.
class A:
def hello(self): print("A")
class B(A):
def hello(self): print("B")
class C(A):
def hello(self): print("C")
class D(B, C):
pass
D().hello() # B — left-first depth
print(D.__mro__) # (<class 'D'>, <class 'B'>, <class 'C'>, <class 'A'>, <class 'object'>)
Use super() to walk the MRO cooperatively:
class B(A):
def hello(self):
super().hello() # continues to next in MRO
print("B")
18. What are dunder (magic) methods?
Dunder methods (double underscore) let you define how your objects behave with operators and built-in functions.
class Vector:
def __init__(self, x, y):
self.x, self.y = x, y
def __repr__(self): # unambiguous string representation
return f"Vector({self.x}, {self.y})"
def __str__(self): # user-friendly string
return f"({self.x}, {self.y})"
def __add__(self, other): # v1 + v2
return Vector(self.x + other.x, self.y + other.y)
def __eq__(self, other): # v1 == v2
return self.x == other.x and self.y == other.y
def __len__(self): # len(v)
return 2
def __getitem__(self, idx): # v[0], v[1]
return (self.x, self.y)[idx]
v1 = Vector(1, 2)
v2 = Vector(3, 4)
print(v1 + v2) # (4, 6)
print(v1 == v2) # False
print(len(v1)) # 2
print(v1[0]) # 1
19. What is the difference between @staticmethod and @classmethod?
@staticmethod |
@classmethod |
|
|---|---|---|
| First arg | None | cls (the class) |
| Accesses class | No | Yes |
| Accesses instance | No | No |
| Inheritance | Doesn't change | Follows subclass |
class Date:
def __init__(self, year, month, day):
self.year, self.month, self.day = year, month, day
@classmethod
def from_string(cls, date_str):
year, month, day = map(int, date_str.split("-"))
return cls(year, month, day) # works correctly in subclasses
@staticmethod
def is_valid(date_str):
parts = date_str.split("-")
return len(parts) == 3 and all(p.isdigit() for p in parts)
d = Date.from_string("2024-07-14")
print(Date.is_valid("2024-07-14")) # True
20. What are abstract base classes?
ABCs enforce that subclasses implement specific methods.
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self) -> float: ...
@abstractmethod
def perimeter(self) -> float: ...
def describe(self):
return f"Area={self.area():.2f}, Perimeter={self.perimeter():.2f}"
class Circle(Shape):
def __init__(self, r): self.r = r
def area(self): return 3.14159 * self.r ** 2
def perimeter(self): return 2 * 3.14159 * self.r
# Shape() # TypeError: Can't instantiate abstract class
print(Circle(5).describe()) # Area=78.54, Perimeter=31.42
Generators and iterators
21. What is a generator?
A generator is a function that uses yield to produce values lazily — one at a time, on demand — instead of returning them all at once.
def fibonacci():
a, b = 0, 1
while True:
yield a
a, b = b, a + b
gen = fibonacci()
print([next(gen) for _ in range(8)]) # [0, 1, 1, 2, 3, 5, 8, 13]
Generators are memory-efficient — they don't build a list in memory.
22. What is the difference between return and yield?
return |
yield |
|
|---|---|---|
| Returns | Once | Many times |
| State | Discarded | Preserved between calls |
| Function type | Regular | Generator |
| On call | Runs immediately | Returns generator object |
def squares_list(n):
return [x**2 for x in range(n)] # builds whole list
def squares_gen(n):
for x in range(n):
yield x**2 # one at a time
# Both iterable, but generator uses O(1) memory
for s in squares_gen(1_000_000):
if s > 100: break
23. What is yield from?
yield from delegates to a sub-generator, forwarding values and allowing two-way communication.
def flatten(nested):
for item in nested:
if isinstance(item, list):
yield from flatten(item) # recurse into sublists
else:
yield item
data = [1, [2, [3, 4], 5], 6]
print(list(flatten(data))) # [1, 2, 3, 4, 5, 6]
24. What is the iterator protocol?
Any object implementing __iter__ (returns self) and __next__ (returns next value or raises StopIteration) is an iterator.
class CountUp:
def __init__(self, start, stop):
self.current = start
self.stop = stop
def __iter__(self):
return self
def __next__(self):
if self.current >= self.stop:
raise StopIteration
val = self.current
self.current += 1
return val
for n in CountUp(1, 4):
print(n) # 1, 2, 3
Error handling
25. What is the Python exception hierarchy?
BaseException
├── SystemExit
├── KeyboardInterrupt
├── GeneratorExit
└── Exception
├── AttributeError
├── ImportError
│ └── ModuleNotFoundError
├── LookupError
│ ├── IndexError
│ └── KeyError
├── NameError
├── TypeError
├── ValueError
├── OSError
│ ├── FileNotFoundError
│ ├── PermissionError
│ └── TimeoutError
├── RuntimeError
│ └── RecursionError
└── StopIteration
Catch specific exceptions, not bare except: (which also catches KeyboardInterrupt).
26. What does raise ... from do?
It chains exceptions, preserving the original cause.
def parse_config(text):
try:
return int(text)
except ValueError as e:
raise RuntimeError(f"Invalid config: {text!r}") from e
try:
parse_config("abc")
except RuntimeError as e:
print(e) # Invalid config: 'abc'
print(e.__cause__) # invalid literal for int() with base 10: 'abc'
Use raise X from None to suppress the original exception in the traceback.
27. How do context managers work (with statement)?
A context manager implements __enter__ and __exit__. It ensures resources are cleaned up even if an exception occurs.
class ManagedFile:
def __init__(self, path, mode="r"):
self.path, self.mode = path, mode
def __enter__(self):
self.file = open(self.path, self.mode)
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:
content = f.read()
# Using contextlib — simpler
from contextlib import contextmanager
@contextmanager
def managed_file(path, mode="r"):
f = open(path, mode)
try:
yield f
finally:
f.close()
Modules and imports
28. What does if __name__ == "__main__" do?
Python sets __name__ to "__main__" when a script is run directly, and to the module name when it's imported.
# mymodule.py
def greet(name):
return f"Hello, {name}!"
if __name__ == "__main__":
# This block runs only when executed directly: python mymodule.py
print(greet("World"))
# Not executed when: import mymodule
This pattern lets a file serve as both a reusable module and a standalone script.
29. What is the difference between import X and from X import Y?
import math
print(math.sqrt(9)) # 3.0 — explicit namespace
from math import sqrt
print(sqrt(9)) # 3.0 — direct access
from math import * # imports all public names — avoid in production
import numpy as np # alias for long names
from X import Y doesn't save memory — the whole module is still loaded. It just binds the name differently.
30. How does Python's import system work?
- Check
sys.modules(cache) — if found, return cached module - Find the module: check
sys.pathdirectories - Load the module: execute its code in a new namespace
- Cache in
sys.modules - Bind name in caller's namespace
import sys
# Inspect what's cached
print("json" in sys.modules) # False initially
import json
print("json" in sys.modules) # True
# See the search path
print(sys.path[:3])
Concurrency
31. What is the GIL (Global Interpreter Lock)?
The GIL is a mutex in CPython that allows only one thread to execute Python bytecode at a time.
Implications:
- CPU-bound tasks: threading gives no speedup — use
multiprocessingorProcessPoolExecutor - I/O-bound tasks: threading still helps — the GIL is released during I/O, so threads can interleave
import concurrent.futures
# I/O-bound: threading works well
def fetch(url):
import urllib.request
return urllib.request.urlopen(url).read()
urls = ["https://example.com"] * 5
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as ex:
results = list(ex.map(fetch, urls)) # concurrent HTTP requests
# CPU-bound: use ProcessPoolExecutor
def cpu_task(n):
return sum(range(n))
with concurrent.futures.ProcessPoolExecutor() as ex:
results = list(ex.map(cpu_task, [10**6]*4))
32. What is the difference between threading, multiprocessing, and asyncio?
threading |
multiprocessing |
asyncio |
|
|---|---|---|---|
| Parallelism | No (GIL) | Yes | No |
| Concurrency | Yes | Yes | Yes |
| Best for | I/O-bound | CPU-bound | I/O-bound (network) |
| Memory | Shared | Separate | Shared |
| Overhead | Low | High | Very low |
| Complexity | Medium | High | Medium |
# asyncio — cooperative multitasking for I/O
import asyncio
async def fetch(session, url):
async with session.get(url) as resp:
return await resp.text()
async def main():
import aiohttp
async with aiohttp.ClientSession() as session:
tasks = [fetch(session, f"https://example.com/{i}") for i in range(10)]
results = await asyncio.gather(*tasks)
asyncio.run(main())
33. How does asyncio work?
asyncio uses an event loop to run coroutines cooperatively. When a coroutine awaits an I/O operation, control returns to the event loop, which runs another coroutine.
import asyncio
async def say_after(delay, message):
await asyncio.sleep(delay) # yields control
print(message)
async def main():
# Sequential: takes 3 seconds
# await say_after(1, "one")
# await say_after(2, "two")
# Concurrent: takes 2 seconds (overlap)
await asyncio.gather(
say_after(1, "one"),
say_after(2, "two"),
)
asyncio.run(main())
Memory and performance
34. How does Python manage memory?
- Reference counting: each object has a count of references; when it reaches 0, memory is freed immediately
- Cyclic garbage collector: handles reference cycles (A → B → A) that reference counting misses
- Memory pools: CPython uses
pymalloc— pools of fixed-size blocks for small objects (< 512 bytes)
import gc
# Force garbage collection of cycles
gc.collect()
# Disable GC in performance-critical code (if you know you have no cycles)
gc.disable()
# ... performance-critical section ...
gc.enable()
35. What are __slots__?
__slots__ prevents the creation of a __dict__ for each instance, saving memory and speeding up attribute access.
class Point:
__slots__ = ("x", "y") # only these attributes allowed
def __init__(self, x, y):
self.x, self.y = x, y
p = Point(1, 2)
# p.z = 3 # AttributeError — can't add arbitrary attributes
# Memory comparison (approximate):
# Without __slots__: ~232 bytes per instance
# With __slots__: ~56 bytes per instance
Use __slots__ when creating millions of instances of a class.
String handling
36. How do you format strings in Python?
name = "Alice"
score = 95.678
# f-strings (Python 3.6+) — recommended
print(f"Hello, {name}! Score: {score:.2f}")
# format() method
print("Hello, {}! Score: {:.2f}".format(name, score))
# % formatting (old style, avoid)
print("Hello, %s! Score: %.2f" % (name, score))
# Multiline f-string
msg = (
f"Name: {name}\n"
f"Score: {score:.1f}\n"
f"Grade: {'A' if score >= 90 else 'B'}"
)
F-strings support arbitrary expressions: f"{2 + 2}", f"{obj!r}", f"{value:{width}.{precision}f}".
37. How is string immutability relevant in Python?
Strings are immutable — any "modification" creates a new string.
s = "hello"
# s[0] = "H" # TypeError!
# Concatenation in a loop is O(n²) — each + creates a new string
parts = []
for i in range(1000):
parts.append(str(i))
result = "".join(parts) # O(n) — use join!
# str.join is always preferred over repeated +=
words = ["Hello", "World"]
sentence = " ".join(words) # "Hello World"
Functional programming
38. What are map, filter, and reduce?
from functools import reduce
nums = [1, 2, 3, 4, 5]
# map: apply function to each element
doubled = list(map(lambda x: x * 2, nums)) # [2, 4, 6, 8, 10]
# Prefer: [x * 2 for x in nums]
# filter: keep elements where function returns True
evens = list(filter(lambda x: x % 2 == 0, nums)) # [2, 4]
# Prefer: [x for x in nums if x % 2 == 0]
# reduce: fold list into single value
product = reduce(lambda a, b: a * b, nums) # 120
# Prefer: math.prod(nums) in Python 3.8+
In Python, list comprehensions and generator expressions are usually preferred over map/filter for clarity.
39. What is functools.partial?
partial creates a new function with some arguments pre-filled.
from functools import partial
def power(base, exp):
return base ** exp
square = partial(power, exp=2)
cube = partial(power, exp=3)
print(square(4)) # 16
print(cube(3)) # 27
# Useful with map
nums = [1, 2, 3, 4]
squares = list(map(square, nums)) # [1, 4, 9, 16]
Common patterns and gotchas
40. What are Python's truthy and falsy values?
Falsy: False, 0, 0.0, "", [], {}, set(), None, objects with __bool__ returning False or __len__ returning 0.
Everything else is truthy.
# Pythonic checks
if not my_list: # empty list
print("empty")
if user: # None or falsy user
do_something(user)
# TRAP: 0 is falsy — check explicitly if 0 is valid
count = 0
if count is None: # ✓ explicit None check
count = default
# TRAP: empty string vs missing value
if value is not None: # ✓ distinguishes "" from None
process(value)
41. How does Python's copy vs deepcopy work?
import copy
original = {"a": [1, 2, 3], "b": [4, 5]}
# Shallow copy — new dict, but inner lists are shared
shallow = copy.copy(original) # or: original.copy() / dict(original)
shallow["a"].append(99)
print(original["a"]) # [1, 2, 3, 99] — modified!
# Deep copy — completely independent
original = {"a": [1, 2, 3], "b": [4, 5]}
deep = copy.deepcopy(original)
deep["a"].append(99)
print(original["a"]) # [1, 2, 3] — unchanged ✓
42. What is the difference between sorted() and .sort()?
sorted() |
.sort() |
|
|---|---|---|
| Works on | Any iterable | Lists only |
| Returns | New list | None |
| Original | Unchanged | Modified in place |
nums = [3, 1, 4, 1, 5]
# sorted — non-destructive
ascending = sorted(nums) # [1, 1, 3, 4, 5]
descending = sorted(nums, reverse=True)
# .sort() — in place
nums.sort(key=lambda x: -x) # descending, modifies nums
# Sort by multiple keys
people = [("Alice", 30), ("Bob", 25), ("Carol", 30)]
people.sort(key=lambda p: (p[1], p[0])) # age then name
43. What is a decorator factory (parameterised decorator)?
import functools
def validate_types(**expected_types):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for name, expected in expected_types.items():
if name in kwargs and not isinstance(kwargs[name], expected):
raise TypeError(f"{name} must be {expected.__name__}")
return func(*args, **kwargs)
return wrapper
return decorator
@validate_types(name=str, age=int)
def create_user(name, age):
return {"name": name, "age": age}
print(create_user(name="Alice", age=30))
# create_user(name="Alice", age="thirty") # TypeError
44. How do you implement a Singleton in Python?
# Method 1: using __new__
class Singleton:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
# Method 2: using a module (most Pythonic)
# config.py — just import it, modules are loaded once
# import config; config.value = 42
# Method 3: using a metaclass
class SingletonMeta(type):
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super().__call__(*args, **kwargs)
return cls._instances[cls]
class Database(metaclass=SingletonMeta):
def __init__(self, url):
self.url = url
db1 = Database("postgres://localhost/mydb")
db2 = Database("postgres://localhost/other")
print(db1 is db2) # True
print(db1.url) # "postgres://localhost/mydb"
45. What is collections.Counter?
Counter is a dict subclass for counting hashable objects.
from collections import Counter
text = "the quick brown fox jumps over the lazy dog"
word_counts = Counter(text.split())
print(word_counts.most_common(3)) # [('the', 2), ('quick', 1), ...]
# Arithmetic on counters
c1 = Counter(a=3, b=1)
c2 = Counter(a=1, b=2)
print(c1 + c2) # Counter({'a': 4, 'b': 3})
print(c1 - c2) # Counter({'a': 2}) — negatives dropped
# Count characters
char_count = Counter("banana") # Counter({'a': 3, 'n': 2, 'b': 1})
Typing and dataclasses
46. What are Python type hints?
Type hints (PEP 484) let you annotate types for static analysis tools like mypy. They are not enforced at runtime by default.
from typing import Optional, Union, List, Dict
def greet(name: str, times: int = 1) -> str:
return (name + " ") * times
# Modern syntax (Python 3.10+)
def find(items: list[int], target: int) -> int | None:
for i, v in enumerate(items):
if v == target:
return i
return None
# TypedDict for structured dicts
from typing import TypedDict
class User(TypedDict):
name: str
age: int
email: str | None
47. What is a dataclass?
@dataclass auto-generates __init__, __repr__, __eq__ from field declarations.
from dataclasses import dataclass, field
from typing import List
@dataclass(order=True, frozen=False)
class Employee:
name: str
department: str
salary: float = 50_000.0
skills: List[str] = field(default_factory=list)
def give_raise(self, amount: float) -> None:
self.salary += amount
emp = Employee("Alice", "Engineering", 80_000)
emp.give_raise(10_000)
print(emp) # Employee(name='Alice', department='Engineering', salary=90000.0, skills=[])
# frozen=True makes it immutable (hashable)
@dataclass(frozen=True)
class Point:
x: float
y: float
p = Point(1.0, 2.0)
# p.x = 3.0 # FrozenInstanceError
Common interview traps
48. What will this code print?
def foo(x, lst=[]):
lst.append(x)
return lst
print(foo(1)) # [1]
print(foo(2)) # [1, 2] ← NOT [2]
print(foo(3)) # [1, 2, 3]
Why: The default lst=[] is created once. All calls share the same list object.
49. What is the output of this code?
class A:
x = []
a = A()
b = A()
a.x.append(1)
print(b.x) # [1] — class attribute shared
a.x = [99] # now a.x is an INSTANCE attribute
print(b.x) # [1] — b.x still refers to class attribute
print(A.x) # [1] — unchanged class attribute
Mutation via a.x.append() modifies the shared class attribute. Assignment (a.x = ...) creates a new instance attribute, shadowing the class attribute for that instance.
50. How does Python's sorted handle complex objects?
from dataclasses import dataclass
@dataclass
class Student:
name: str
grade: float
age: int
students = [
Student("Alice", 3.8, 22),
Student("Bob", 3.8, 21),
Student("Carol", 3.5, 23),
]
# Sort by grade descending, then name ascending
ranked = sorted(students, key=lambda s: (-s.grade, s.name))
for s in ranked:
print(f"{s.name}: {s.grade}")
# Alice: 3.8
# Bob: 3.8
# Carol: 3.5
# Using attrgetter — faster than lambda for simple attributes
from operator import attrgetter
ranked = sorted(students, key=attrgetter("grade", "name"))
Common mistakes
| Mistake | Wrong | Right |
|---|---|---|
| Mutable default | def f(lst=[]) |
def f(lst=None) |
| Late binding | lambda: i in loop |
lambda i=i: i |
is for values |
if x is 1000 |
if x == 1000 |
None check |
if x == None |
if x is None |
| Modify while iterate | for i in lst: lst.remove(i) |
Iterate copy or use list comprehension |
Catching bare except |
except: |
except Exception: or specific type |
Ignoring StopIteration |
— | Use for loops or next(gen, default) |
| String concatenation in loop | s += x in loop |
"".join(parts) |
FAQ
Q: Is Python pass-by-value or pass-by-reference?
A: Neither exactly. Python uses pass-by-object-reference (pass-by-assignment). You pass a reference to the object. Immutable objects can't be changed; mutable objects can be mutated but not rebound in the caller.
Q: What is the difference between deepcopy and pickle?
A: Both create independent copies, but pickle serialises to bytes (for file storage/network), while deepcopy creates an in-memory copy. pickle handles more types but is slower.
Q: When should I use asyncio vs threading?
A: Use asyncio for network I/O with many concurrent connections (web scraping, APIs). Use threading for blocking I/O (legacy libs without async support). Use multiprocessing for CPU-bound work.
Q: What is the difference between __str__ and __repr__?
A: __repr__ should be unambiguous and ideally valid Python to recreate the object. __str__ should be human-readable. print() uses __str__; the REPL and repr() use __repr__. If only __repr__ is defined, it's used for both.
Q: How do you avoid circular imports?
A: Restructure code to remove the circular dependency (move shared code to a third module), use local imports inside functions, or import the module rather than the name (import module instead of from module import name).
Q: What is __all__ in a module?
A: __all__ is a list of names that are exported when a user does from module import *. It also signals the public API. Names not in __all__ are still importable directly — it only affects import *.