Python course menu
Advanced Python Interview Questions and Answers
Advanced Python interview questions for experienced developers: the GIL, threads vs processes vs asyncio, decorators, generators with send, __slots__, dataclasses, memory management, metaclasses, and performance.
On this page
- 1. What is the GIL, and how does it affect concurrency?
- 2. Threads, processes or asyncio: when do you use each?
- 3. Explain async / await.
- 4. How do decorators work internally?
- 5. What does generator.send() do?
- 6. What are slots for?
- 7. How does Python manage memory?
- 8. What is a metaclass?
- 9. What do frozen dataclasses give you?
- 10. How do you find and fix a performance problem?
Senior Python interviews explore how Python works underneath: concurrency, memory, the object model, and performance trade-offs. Clear reasoning about trade-offs matters more than recalling every detail.
1. What is the GIL, and how does it affect concurrency?#
Show solution
The Global Interpreter Lock in CPython (the standard implementation) allows only one thread to execute Python bytecode at a time. Consequences:
- I/O-bound work (network calls, file and database access) still benefits from threads, because threads release the GIL while waiting.
- CPU-bound pure-Python work does not speed up with threads; use
multiprocessing(separate processes, each with its own GIL) or libraries that run in C (NumPy).
Python 3.13 introduced an experimental free-threaded build without the GIL, but the default build still has it.
2. Threads, processes or asyncio: when do you use each?#
Show solution
Threads overlapping I/O waits:
import threading, time
def work(n, out):
time.sleep(0.2)
out.append(n)
out = []
threads = [threading.Thread(target=work, args=(i, out)) for i in range(5)]
t0 = time.perf_counter()
for t in threads: t.start()
for t in threads: t.join()
print(sorted(out), time.perf_counter() - t0 < 1.0) # one after another takes at least 1 s[0, 1, 2, 3, 4] TrueFive 0.2-second waits finished in less than the 1 second they need one after another, because they overlapped.
3. Explain async / await.#
Show solution
async def defines a coroutine. await pauses it until another awaitable finishes, letting the event loop run other coroutines in the meantime:
import asyncio, time
async def fetch(name, delay):
await asyncio.sleep(delay)
return f"{name} done"
async def main():
return await asyncio.gather(fetch("A", 0.2), fetch("B", 0.2))
t0 = time.perf_counter()
print(asyncio.run(main()), time.perf_counter() - t0 < 0.4) # one after another takes at least 0.4 s['A done', 'B done'] TrueBoth waits ran concurrently. Blocking calls (like time.sleep or requests.get) inside a coroutine freeze the whole loop; use async equivalents.
4. How do decorators work internally?#
Show solution
@decorator above def f is equivalent to f = decorator(f). A decorator returns a wrapper function (a closure over the original) that adds behaviour. functools.wraps copies the original's name and docstring:
from functools import wraps
def debug(func):
@wraps(func)
def wrapper(*a, **k):
r = func(*a, **k)
print(f"{func.__name__}{a} -> {r}")
return r
return wrapper
@debug
def mul(x, y):
return x * y
mul(3, 4)mul(3, 4) -> 12Decorators with arguments add a third layer (a function that returns the decorator). See Decorators.
5. What does generator.send() do?#
Show solution
send(value) resumes a generator and makes the paused yield expression evaluate to value, enabling two-way communication:
def gen():
received = yield "ready"
yield f"got {received}"
g = gen()
print(next(g), g.send("hello"))ready got helloThis is the mechanism coroutines were originally built on.
6. What are slots for?#
Show solution
__slots__ declares a fixed set of attributes. Instances then have no per-object __dict__, which saves memory when you create millions of objects and prevents typos from creating new attributes:
class Point:
__slots__ = ("x", "y")
def __init__(self, x, y):
self.x, self.y = x, y
p = Point(1, 2)
try:
p.z = 3
except AttributeError as e:
print("AttributeError:", e)AttributeError: 'Point' object has no attribute 'z' and no __dict__ for setting new attributes7. How does Python manage memory?#
Show solution
- Every object has a reference count; when it drops to zero, the object is freed immediately.
- A cyclic garbage collector (
gcmodule) finds and frees groups of objects that reference each other but are otherwise unreachable. - Small objects use a specialised allocator (pymalloc) for speed.
Practical points: large objects held in global caches or long-lived lists are not freed; weakref lets you refer to objects without keeping them alive; tools like tracemalloc help find leaks.
8. What is a metaclass?#
Show solution
Classes are objects too, and a metaclass is the class of a class. The default metaclass is type:
print(type(int), type(type))<class 'type'> <class 'type'>A custom metaclass (class Meta(type): ...) can change how classes are created, for example to register subclasses or validate class definitions. They are rarely needed; class decorators and __init_subclass__ solve most of the same problems more simply.
9. What do frozen dataclasses give you?#
Show solution
@dataclass(frozen=True) makes instances immutable and hashable, so they can be dictionary keys or set members, and cannot be changed by accident:
from dataclasses import dataclass
@dataclass(frozen=True)
class Point:
x: int
y: int
p = Point(1, 2)
print(p, hash(p) == hash(Point(1, 2)))
try:
p.x = 5
except Exception as e:
print(type(e).__name__)Point(x=1, y=2) True
FrozenInstanceError10. How do you find and fix a performance problem?#
Show solution
- Measure first:
cProfile(orpython -m cProfile script.py) shows where time is spent;timeitcompares small alternatives. - Fix the algorithm or data structure before micro-optimising. For example, membership in a set is far faster than in a list:
import timeit
print(timeit.timeit("x in s", setup="s=set(range(10000)); x=9999", number=1000)
< timeit.timeit("x in s", setup="s=list(range(10000)); x=9999", number=1000))True- Use built-ins and the standard library (written in C), avoid repeated work (caching with
lru_cache), and move heavy numeric work to NumPy. - For CPU-bound tasks, consider
multiprocessing; for I/O-bound ones, threads orasyncio.
Next: put it into practice with Coding Challenges.
Finished this lesson?