Python Interview Questions: Complete Guide (2025)

Python is the dominant language in machine learning, data engineering, backend APIs, and scripting. This guide covers the most frequently asked Python interview questions across all seniority levels — from L3 to L7.

Core Language Concepts

1. How does Python’s GIL affect multithreading?

The Global Interpreter Lock (GIL) allows only one thread to execute Python bytecode at a time. This means CPU-bound tasks do NOT benefit from threads — use multiprocessing instead. I/O-bound tasks (network, file, sleep) DO benefit because the GIL is released during I/O waits.

import threading
import multiprocessing
import time

def cpu_task(n):
    # Simulate CPU-bound work
    total = 0
    for i in range(n):
        total += i * i
    return total

# Threading (GIL-limited — does NOT parallelize CPU work)
start = time.time()
threads = [threading.Thread(target=cpu_task, args=(5_000_000,)) for _ in range(4)]
for t in threads: t.start()
for t in threads: t.join()
print(f"Threads: {time.time() - start:.2f}s")

# Multiprocessing (true parallelism)
start = time.time()
with multiprocessing.Pool(4) as pool:
    pool.map(cpu_task, [5_000_000] * 4)
print(f"Processes: {time.time() - start:.2f}s")

2. Explain Python generators and lazy evaluation

from typing import Iterator, Generator
import sys

def fibonacci() -> Generator[int, None, None]:
    """Infinite Fibonacci sequence — memory-efficient via lazy eval."""
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b

def chunked(iterable, size: int) -> Iterator[list]:
    """Yield successive chunks — processes large datasets without loading all."""
    chunk = []
    for item in iterable:
        chunk.append(item)
        if len(chunk) == size:
            yield chunk
            chunk = []
    if chunk:
        yield chunk

# Memory comparison: list vs generator for 1M integers
list_size   = sys.getsizeof(list(range(1_000_000)))
gen_size    = sys.getsizeof(x for x in range(1_000_000))
print(f"List: {list_size:,} bytes | Generator: {gen_size} bytes")
# List: 8,697,464 bytes | Generator: 208 bytes

# Practical use: pipeline without intermediate lists
def read_log_lines(path: str) -> Iterator[str]:
    with open(path) as f:
        for line in f:
            yield line.strip()

def filter_errors(lines: Iterator[str]) -> Iterator[str]:
    return (line for line in lines if "ERROR" in line)

def parse_timestamps(lines: Iterator[str]) -> Iterator[str]:
    return (line.split()[0] for line in lines)

3. Decorators — implementation and use cases

import functools
import time
import logging
from typing import Callable, TypeVar, Any

F = TypeVar("F", bound=Callable[..., Any])

def retry(max_attempts: int = 3, delay: float = 1.0, exceptions=(Exception,)):
    """Retry decorator with exponential backoff."""
    def decorator(func: F) -> F:
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            last_exc = None
            for attempt in range(max_attempts):
                try:
                    return func(*args, **kwargs)
                except exceptions as e:
                    last_exc = e
                    wait = delay * (2 ** attempt)
                    logging.warning(f"Attempt {attempt+1} failed: {e}. Retrying in {wait}s")
                    time.sleep(wait)
            raise last_exc
        return wrapper  # type: ignore
    return decorator

def memoize(func: F) -> F:
    """Simple memoization decorator."""
    cache = {}
    @functools.wraps(func)
    def wrapper(*args):
        if args not in cache:
            cache[args] = func(*args)
        return cache[args]
    return wrapper  # type: ignore

def rate_limit(calls_per_second: float):
    """Throttle function calls."""
    min_interval = 1.0 / calls_per_second
    last_called = [0.0]
    def decorator(func: F) -> F:
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            elapsed = time.time() - last_called[0]
            if elapsed  dict:
    # ... actual HTTP call
    pass

4. Context managers and the `with` statement

from contextlib import contextmanager, asynccontextmanager
import threading

class DatabaseTransaction:
    """Context manager using __enter__/__exit__ protocol."""
    def __init__(self, connection):
        self.conn = connection
        self.savepoint = None

    def __enter__(self):
        self.savepoint = self.conn.execute("SAVEPOINT sp1")
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        if exc_type is None:
            self.conn.execute("RELEASE SAVEPOINT sp1")
        else:
            self.conn.execute("ROLLBACK TO SAVEPOINT sp1")
        return False  # Do not suppress exceptions

@contextmanager
def timer(label: str = ""):
    """Generator-based context manager for timing blocks."""
    start = time.perf_counter()
    try:
        yield
    finally:
        elapsed = time.perf_counter() - start
        print(f"{label}: {elapsed:.4f}s")

@contextmanager
def acquire_locks(*locks):
    """Deadlock-free multi-lock acquisition (sorted by id)."""
    sorted_locks = sorted(locks, key=id)
    acquired = []
    try:
        for lock in sorted_locks:
            lock.acquire()
            acquired.append(lock)
        yield
    finally:
        for lock in reversed(acquired):
            lock.release()

# Usage
with timer("sort 1M items"):
    sorted(range(1_000_000), reverse=True)

5. Async Python — event loop and concurrency patterns

import asyncio
import aiohttp
from typing import List

async def fetch_url(session: aiohttp.ClientSession, url: str) -> dict:
    async with session.get(url, timeout=aiohttp.ClientTimeout(total=10)) as resp:
        resp.raise_for_status()
        return {"url": url, "status": resp.status, "data": await resp.json()}

async def fetch_all(urls: List[str]) -> List[dict]:
    """Fetch URLs concurrently — O(max_latency) not O(sum_latency)."""
    async with aiohttp.ClientSession() as session:
        tasks = [fetch_url(session, url) for url in urls]
        results = await asyncio.gather(*tasks, return_exceptions=True)
    return [r for r in results if not isinstance(r, Exception)]

class AsyncQueue:
    """Bounded async queue for producer-consumer pipelines."""
    def __init__(self, maxsize: int = 100):
        self._q: asyncio.Queue = asyncio.Queue(maxsize=maxsize)

    async def produce(self, items):
        for item in items:
            await self._q.put(item)
        await self._q.put(None)  # sentinel

    async def consume(self, process_fn):
        while True:
            item = await self._q.get()
            if item is None:
                break
            await process_fn(item)
            self._q.task_done()

# asyncio.run(fetch_all(["https://api.example.com/data"]))

6. Python data structures — collections module

from collections import defaultdict, Counter, deque, OrderedDict
import heapq
from typing import List, Tuple

# defaultdict — avoids KeyError for grouping
def group_by_key(items: List[Tuple]) -> dict:
    groups = defaultdict(list)
    for key, val in items:
        groups[key].append(val)
    return dict(groups)

# Counter — frequency analysis
def top_k_elements(items: List, k: int) -> List[Tuple]:
    return Counter(items).most_common(k)

# deque — O(1) append/pop from both ends
class SlidingWindow:
    def __init__(self, size: int):
        self.window = deque(maxlen=size)
        self.total = 0

    def add(self, val: float) -> float:
        if len(self.window) == self.window.maxlen:
            self.total -= self.window[0]
        self.window.append(val)
        self.total += val
        return self.total / len(self.window)

# heapq — priority queue / top-k
def merge_k_sorted(lists: List[List[int]]) -> List[int]:
    heap = []
    for i, lst in enumerate(lists):
        if lst:
            heapq.heappush(heap, (lst[0], i, 0))
    result = []
    while heap:
        val, list_idx, elem_idx = heapq.heappop(heap)
        result.append(val)
        next_idx = elem_idx + 1
        if next_idx < len(lists[list_idx]):
            heapq.heappush(heap, (lists[list_idx][next_idx], list_idx, next_idx))
    return result

Python Performance and Memory

Technique When to use Speedup
List comprehension vs loop Simple transforms 2-5x faster
Generator expressions Large datasets, streaming Saves memory
__slots__ on classes Many instances of same class 40% less RAM
numpy vectorization Numerical arrays 10-100x faster
multiprocessing.Pool CPU-bound parallel work Near-linear scale
functools.lru_cache Pure functions with repeated args Cache hits O(1)
struct / memoryview Binary protocol parsing Zero-copy reads

Common Interview Pitfalls

# WRONG: mutable default argument
def append_to(element, lst=[]):
    lst.append(element)
    return lst
# append_to(1) → [1], append_to(2) → [1, 2]  ← shared list!

# RIGHT: use None sentinel
def append_to(element, lst=None):
    if lst is None:
        lst = []
    lst.append(element)
    return lst

# WRONG: late binding in closures
fns = [lambda x: x + i for i in range(5)]
# fns[0](0) → 4, not 0 — all closures capture same i

# RIGHT: default argument captures value
fns = [lambda x, i=i: x + i for i in range(5)]

# WRONG: chained comparisons — actually fine in Python!
# 1 < x < 10 works correctly (unlike C/Java)

System Design Considerations for Python Services

  • Async frameworks: FastAPI + uvicorn for high-concurrency APIs (handles 50k+ req/s on modern hardware)
  • Worker pools: Celery + Redis/RabbitMQ for task queues; use multiprocessing for CPU-bound workers
  • Memory profiling: tracemalloc for leak detection, memory_profiler for line-by-line analysis
  • Type safety: mypy strict mode + Pydantic for runtime validation at API boundaries
  • Packaging: Poetry or uv for dependency management; pyproject.toml over setup.py

Frequently Asked Questions

What is the Python GIL and how does it affect performance?

The GIL (Global Interpreter Lock) allows only one thread to execute Python bytecode at a time. For CPU-bound tasks, threading does not achieve parallelism — use multiprocessing instead. For I/O-bound tasks (network, file I/O), the GIL is released during the wait, so threading is effective. Python 3.13+ has an experimental no-GIL mode.

What is the difference between a generator and a list comprehension?

A list comprehension creates the entire list in memory at once. A generator expression creates a lazy iterator that produces values one at a time. For large datasets, generators use O(1) memory vs O(n) for lists. Use generators when you process items sequentially and do not need random access.

How do Python decorators work?

A decorator is a function that takes a function and returns a new function, wrapping the original with additional behavior. They use functools.wraps to preserve the original function metadata. Common uses: logging, authentication, retry logic, caching, rate limiting. Stacked decorators are applied bottom-up.

{
“@context”: “https://schema.org”,
“@type”: “FAQPage”,
“mainEntity”: [
{
“@type”: “Question”,
“name”: “What is the Python GIL and how does it affect performance?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “The GIL (Global Interpreter Lock) allows only one thread to execute Python bytecode at a time. For CPU-bound tasks, threading does not achieve parallelism — use multiprocessing instead. For I/O-bound tasks (network, file I/O), the GIL is released during the wait, so threading is effective. Python 3.13+ has an experimental no-GIL mode.”
}
},
{
“@type”: “Question”,
“name”: “What is the difference between a generator and a list comprehension?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “A list comprehension creates the entire list in memory at once. A generator expression creates a lazy iterator that produces values one at a time. For large datasets, generators use O(1) memory vs O(n) for lists. Use generators when you process items sequentially and do not need random access.”
}
},
{
“@type”: “Question”,
“name”: “How do Python decorators work?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “A decorator is a function that takes a function and returns a new function, wrapping the original with additional behavior. They use functools.wraps to preserve the original function metadata. Common uses: logging, authentication, retry logic, caching, rate limiting. Stacked decorators are applied bottom-up.”
}
}
]
}

Scroll to Top