The deeper questions asked for mid/senior Python roles — internals, the object model, concurrency and design — with answers you can say in under a minute.
Object model & internals
What is the difference between __new__ and __init__?
How does attribute lookup work for obj.x?
What is a descriptor? Where does Python use them?
What is a metaclass? When would you use one?
Why can't a list be a dict key, and what makes an object hashable?
What does __slots__ do and what are the trade-offs?
Functions, generators & scope
How are closures implemented?
How does a generator work under the hood?
Explain yield from.
What's the difference between an iterable and an iterator?
Concurrency & performance
If the GIL exists, why do we still need locks?
When would you choose asyncio over threads?
How would you speed up a slow Python program?
What is the difference between deepcopy and pickling for copying?
Design & practice
How do you implement a singleton in Python?
How do you make a class iterable, comparable and usable with with?
What are * and / in a function signature?
How do you manage dependencies and environments for a project?
Code you should be able to write on a whiteboard
from collections import OrderedDict
class LRUCache:
"""O(1) get/put: OrderedDict remembers recency order."""
def __init__(self, capacity: int):
self.capacity = capacity
self.data = OrderedDict()
def get(self, key):
if key not in self.data:
return -1
self.data.move_to_end(key) # mark as most recently used
return self.data[key]
def put(self, key, value):
self.data[key] = value
self.data.move_to_end(key)
if len(self.data) > self.capacity:
self.data.popitem(last=False) # evict least recently used
c = LRUCache(2)
c.put("a", 1); c.put("b", 2); c.get("a"); c.put("c", 3)
print(list(c.data), c.get("b")) # ['a', 'c'] -1import functools, time
def rate_limited(max_calls, per_seconds, clock=time.monotonic):
"""Allow at most max_calls in any per_seconds window."""
def decorator(func):
calls = []
@functools.wraps(func)
def wrapper(*args, **kwargs):
now = clock()
while calls and now - calls[0] >= per_seconds:
calls.pop(0)
if len(calls) >= max_calls:
raise RuntimeError("rate limit exceeded")
calls.append(now)
return func(*args, **kwargs)
return wrapper
return decorator
fake_now = [0.0]
@rate_limited(2, 1.0, clock=lambda: fake_now[0])
def ping():
return "pong"
print(ping(), ping()) # pong pong
try:
ping()
except RuntimeError as e:
print(e) # rate limit exceeded
fake_now[0] = 1.5 # time passes
print(ping()) # pongdef flatten(nested):
"""Flatten arbitrarily nested lists/tuples lazily."""
for item in nested:
if isinstance(item, (list, tuple)):
yield from flatten(item)
else:
yield item
print(list(flatten([1, [2, (3, [4])], 5]))) # [1, 2, 3, 4, 5]