Python Advanced Interview Questions: Decorators, Generators, and Concurrency
15 advanced questions on decorators, generators, context managers, the GIL, threading, multiprocessing, and asyncio.
1. What is a decorator, and how do you write one?
A decorator is a function that takes a function and returns a new function that adds behavior around it. The @ syntax is shorthand for reassigning the name. Use functools.wraps to keep the original name and docstring.
import functools, time def timed(fn): @functools.wraps(fn) def wrapper(*args, **kwargs): start = time.perf_counter() result = fn(*args, **kwargs) print(fn.__name__, time.perf_counter() - start) return result return wrapper @timed def work(): return sum(range(1_000_000))2. How do you write a decorator that takes arguments?
Add one more level: an outer function receives the arguments and returns the actual decorator.
def repeat(times): def decorator(fn): def wrapper(*args, **kwargs): for _ in range(times): result = fn(*args, **kwargs) return result return wrapper return decorator @repeat(3) def hello(): print('hi')3. What is a closure in Python?
A closure is an inner function that remembers variables from its enclosing function after that function has returned. Use nonlocal to rebind an enclosing variable. Closures underlie decorators and factory functions.
4. What is the difference between an iterator and an iterable?
An iterable is an object you can loop over: it implements __iter__, which returns an iterator. An iterator implements __next__, returning the next item and raising StopIteration when finished, and its __iter__ returns itself. Lists are iterables; calling iter(list) gives an iterator that can be consumed only once.
5. What are generators, and why use them?
A function with yield returns a generator, which produces values lazily and pauses between them while keeping its local state. They save memory for large or infinite sequences and make pipelines easy.
def fibonacci(): a, b = 0, 1 while True: yield a a, b = b, a + b from itertools import islice print(list(islice(fibonacci(), 8))) # [0, 1, 1, 2, 3, 5, 8, 13]6. What is yield from?
It delegates to another iterable or generator, passing its values straight through (and also its return value and sent values). It replaces a manual for loop that re-yields each item.
7. What are context managers and the with statement?
A context manager guarantees setup and cleanup around a block, even if an exception occurs. Implement __enter__ and __exit__, or use contextlib.contextmanager with a generator.
from contextlib import contextmanager @contextmanager def opened(path): f = open(path) try: yield f finally: f.close() with opened('data.txt') as f: print(f.readline())8. What is the GIL?
The Global Interpreter Lock is a mutex in CPython that lets only one thread execute Python bytecode at a time. It simplifies memory management, but it means CPU-bound code in threads does not run in parallel. It is released during blocking I/O and in many C extensions, so threads still help I/O-bound programs. Recent Python versions also offer an optional free-threaded build that removes the GIL.
9. When do you use threading, multiprocessing, or asyncio?
Use threading for I/O-bound work with blocking libraries. Use multiprocessing (or concurrent.futures.ProcessPoolExecutor) for CPU-bound work, since each process has its own interpreter and GIL. Use asyncio for large numbers of concurrent I/O operations with async-aware libraries, running on a single thread with cooperative scheduling.
10. How does asyncio work?
Functions declared with async def are coroutines. An event loop runs them, and await suspends a coroutine until the awaited operation completes, letting other coroutines run in the meantime. Start the loop with asyncio.run, and run coroutines concurrently with asyncio.gather or a TaskGroup (Python 3.11 and newer).
import asyncio async def fetch(n): await asyncio.sleep(1) return n * 2 async def main(): results = await asyncio.gather(fetch(1), fetch(2), fetch(3)) print(results) # [2, 4, 6] after about 1 second asyncio.run(main())11. What is the difference between a coroutine and a thread?
A thread is scheduled preemptively by the operating system and can be interrupted at any time, so shared data needs locks. A coroutine yields control only at await points, so switching is predictable and cheap, but a coroutine that blocks or does heavy computation stalls the whole event loop.
12. How does memory management work in Python?
CPython uses reference counting: each object tracks how many references point to it and is freed when the count reaches zero. A cyclic garbage collector additionally finds and frees groups of objects that reference each other. Memory is allocated through a private heap with pooled allocators for small objects.
13. What is functools.lru_cache?
A decorator that caches function results by arguments, keeping the most recently used entries. It is a quick way to memoize pure functions, such as recursive calculations. Arguments must be hashable.
from functools import lru_cache @lru_cache(maxsize=None) def fib(n): return n if n < 2 else fib(n - 1) + fib(n - 2)14. What are metaclasses?
A metaclass is the class of a class: it controls how classes are created, just as a class controls how instances are created. type is the default metaclass. They are used by frameworks (ORMs, enums), but most problems are better solved with decorators, __init_subclass__, or plain inheritance.
15. What are type hints, and does Python enforce them?
Type hints (annotations such as def f(x: int) -> str) document expected types and let tools like mypy, pyright, and editors check code. The interpreter itself does not enforce them at runtime. Libraries such as pydantic and dataclass validators can use them to validate data.
Interview Tips
- Be able to write a decorator from memory, including functools.wraps.
- Choose a concurrency model by naming the bottleneck: CPU or I/O.
- Describe the GIL accurately: it limits threads running Python bytecode, not I/O waiting.