Advanced Python knowledge is increasingly required for senior backend, data engineering, and ML engineering roles at Indian product companies. Decorators, generators, the GIL, and asyncio separate candidates who truly understand the language from those who only use it. This guide covers advanced Python interview questions for India in 2026.
Python decorators and closures
Python advanced concepts:
1. Closures: A closure is a function that captures variables from its enclosing scope. The inner function 'closes over' the variable — it retains access to it even after the outer function has returned: ```python def make_multiplier(n): def multiplier(x): return x * n # 'n' is captured from the enclosing scope return multiplier
double = make_multiplier(2) print(double(5)) # 10 ```
2. Decorators: A decorator is a function that takes another function as an argument and returns a modified function. It wraps the original function to add behaviour (logging, timing, caching, authentication) without modifying the function itself: ```python import functools, time
def timer(func): @functools.wraps(func) # preserves the original function's _name, doc def wrapper(*args, **kwargs): start = time.perfcounter() result = func(args, *kwargs) end = time.perfcounter() print(f'{func.name_} took {end - start:.4f}s') return result return wrapper
@timer def expensive_function(): time.sleep(1) ```
3. Decorators with arguments: A decorator with arguments is a three-level function: ```python def retry(maxattempts=3, delay=1.0): def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): for attempt in range(maxattempts): try: return func(args, *kwargs) except Exception as e: if attempt == max_attempts - 1: raise time.sleep(delay) return wrapper return decorator
@retry(maxattempts=5, delay=2.0) def callflaky_api(): ... ```
4. Class-based decorators: A class can be used as a decorator if it implements _call: ```python class memoize: def init(self, func): self.func = func self.cache = {} def call_(self, args): if args not in self.cache: self.cache[args] = self.func(args) return self.cache[args]
@memoize def fibonacci(n): return n if n < 2 else fibonacci(n-1) + fibonacci(n-2) ```
Generators, iterators, and context managers
Python lazy evaluation:
1. Iterators and the iterator protocol: An object is an iterator if it implements _iter() (returns self) and next() (returns the next value or raises StopIteration). The for loop is syntactic sugar for calling next() repeatedly. Any object that implements iter_() is iterable.
2. Generators: A generator function uses yield instead of return. Calling a generator function returns a generator object (an iterator). Each call to _next_() resumes the generator until the next yield: ```python def fibonacci(): a, b = 0, 1 while True: yield a a, b = b, a + b
gen = fibonacci() for _ in range(10): print(next(gen)) # 0, 1, 1, 2, 3, 5, 8, 13, 21, 34 ``` Memory efficiency: the Fibonacci generator above produces an infinite sequence but uses O(1) memory. Contrast with a list: [fib(n) for n in range(10000)] requires storing all 10,000 values.
3. Generator expressions: Similar to list comprehensions but lazy (do not evaluate until iterated): ```python # List comprehension: computes all values immediately; O(n) memory biglist = [x**2 for x in range(1000_000)]
# Generator expression: lazy; O(1) memory biggen = (x**2 for x in range(1000_000)) ```
4. Context managers: Context managers manage resources (file handles, database connections, locks) with the with statement. The resource is automatically released when the block exits (even if an exception occurs): ```python # Using contextlib.contextmanager from contextlib import contextmanager
@contextmanager def database_connection(url): conn = connect(url) try: yield conn finally: conn.close() # always closes, even on exception
with database_connection('postgresql://...') as db: db.execute('SELECT * FROM users') ```
The GIL, asyncio, and Python concurrency
Python concurrency model:
1. The Global Interpreter Lock (GIL): The GIL is a mutex in CPython (the reference Python implementation) that ensures only one thread executes Python bytecode at a time, even on multi-core processors. Why it exists: CPython's memory management (reference counting) is not thread-safe; the GIL prevents two threads from simultaneously modifying the reference count of the same object. Consequence: Python threads do not benefit from multiple cores for CPU-bound work. Workaround for CPU-bound work: use multiprocessing (each process has its own Python interpreter and its own GIL). GIL is released: during I/O operations (network, file, sleep); C extensions can release the GIL (NumPy, Pandas).
2. asyncio: asyncio provides cooperative multitasking for I/O-bound applications. A coroutine (async def function) can be suspended at an await point; the event loop can run another coroutine while the first is waiting for I/O. This allows one thread to handle thousands of concurrent I/O operations efficiently. ```python import asyncio, aiohttp
async def fetch(session, url): async with session.get(url) as resp: return await resp.json()
async def fetch_all(urls): async with aiohttp.ClientSession() as session: tasks = [fetch(session, url) for url in urls] return await asyncio.gather(*tasks) # run all concurrently
results = asyncio.run(fetchall(urls)) ``` asyncio.gather: run multiple coroutines concurrently. asyncio.sleep: non-blocking sleep (use instead of time.sleep in async code). asyncio.createtask: schedule a coroutine to run without waiting for it.
3. Choosing between threads, asyncio, and multiprocessing: I/O-bound, many concurrent connections (web scraping, API calls, WebSocket handlers): asyncio (most efficient; one thread handles thousands of connections). I/O-bound, existing synchronous code: threading (simpler to retrofit; GIL not an issue for I/O-bound work). CPU-bound (image processing, number crunching, ML inference): multiprocessing (bypass the GIL with separate processes; higher overhead than threads).
4. Type hints and mypy: Python 3.5+ supports type hints (PEP 484). They are optional but strongly recommended for large codebases. Type hints are checked by tools like mypy (static type checker). Common types: int, str, bool, float, list[int], dict[str, Any], Optional[int] (= int | None), Union[int, str], Tuple[int, ...], Callable[[int], str].
Practise advanced Python and backend engineering interview questions with HireStepX's AI voice interviewer. Get scored feedback on your explanations of decorators, generators, asyncio, and the GIL. First 2 sessions free.
Practice freePython metaclasses, dataclasses, and performance
Advanced Python patterns:
1. Metaclasses: A metaclass is the class of a class (a class's type). The default metaclass is type. When you write class Foo: ..., Python calls type('Foo', (object,), {...}) to create the class object itself. Override _initsubclass_, new, or initsubclass_ in a metaclass to customise class creation: ```python 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): ... ``` Real-world use: Django ORM (ModelBase metaclass inspects class attributes to create database tables), abstract base classes, plugin registration systems.
2. Dataclasses: Dataclasses (Python 3.7+) auto-generate _init, repr, eq, and optionally hash, lt_, etc. from class variable annotations: ```python from dataclasses import dataclass, field
@dataclass(order=True, frozen=True) class Point: x: float y: float label: str = '' # with default tags: list[str] = field(default_factory=list) # mutable default
p1 = Point(1.0, 2.0) p2 = Point(3.0, 4.0) print(p1 < p2) # True (order=True generates _lt_) ``` frozen=True makes the dataclass immutable and hashable (can be used as a dict key or in a set).
3. Python performance tips: Local variable lookup is faster than global or attribute lookup: assign frequently used globals to local variables inside a tight loop. List comprehensions are faster than for loops + .append() for building lists. Generator expressions are memory-efficient for large sequences. Slots (_slots = ['x', 'y']): bypasses dict for attribute storage; 40-60% less memory per instance and faster attribute access. Profiling: cProfile (CPU profiling), memoryprofiler (memory profiling), line_profiler (line-by-line profiling). For true performance, use NumPy (vectorised operations bypass the GIL and are implemented in C), Cython, or call into C extensions.
Frequently asked questions
Explore more