Skip to content
Laviyx Learn

Try

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.

Advanced8 min readUpdated
On this page

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
ToolBest forNotes
threadingI/O-bound tasks with blocking librariesshared memory; needs locks
multiprocessingCPU-bound worktrue parallelism; data is copied between processes
asynciomany concurrent I/O tasks (thousands of connections)single thread; needs async libraries

Threads overlapping I/O waits:

Python
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
Output
[0, 1, 2, 3, 4] True

Five 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:

Python
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
Output
['A done', 'B done'] True

Both 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:

Python
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)
Output
mul(3, 4) -> 12

Decorators 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:

Python
def gen():
    received = yield "ready"
    yield f"got {received}"

g = gen()
print(next(g), g.send("hello"))
Output
ready got hello

This 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:

Python
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)
Output
AttributeError: 'Point' object has no attribute 'z' and no __dict__ for setting new attributes

7. 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 (gc module) 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:

Python
print(type(int), type(type))
Output
<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:

Python
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__)
Output
Point(x=1, y=2) True
FrozenInstanceError

10. How do you find and fix a performance problem?#

Show solution
  1. Measure first: cProfile (or python -m cProfile script.py) shows where time is spent; timeit compares small alternatives.
  2. Fix the algorithm or data structure before micro-optimising. For example, membership in a set is far faster than in a list:
Python
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))
Output
True
  1. Use built-ins and the standard library (written in C), avoid repeated work (caching with lru_cache), and move heavy numeric work to NumPy.
  2. For CPU-bound tasks, consider multiprocessing; for I/O-bound ones, threads or asyncio.

Next: put it into practice with Coding Challenges.

Finished this lesson?