Skip to main content
Python intermediate Lesson 27 of 28

Python Interview Prep

Top 30 Python interview questions with detailed answers and production-quality code examples for all levels.

Core Language

Q1: What is the GIL and how does it affect concurrency?

The Global Interpreter Lock is a mutex in CPython that allows only one thread to execute Python bytecode at a time. This means:

  • Threads don’t parallelize CPU-bound work — only one core is used
  • Threads do help I/O-bound work — the GIL is released during I/O, so threads can overlap waiting
  • multiprocessing bypasses the GIL by spawning separate processes
# CPU-bound: threads don't help
import threading, time

def cpu_task():
    sum(i**2 for i in range(5_000_000))

# Sequential and threaded take roughly the same time for CPU work
# Use multiprocessing.Pool for real CPU parallelism

Q2: Explain mutable default arguments — what’s the trap?

Default argument values are evaluated once at function definition time, not on every call.

# Bug
def append(item, lst=[]):
    lst.append(item)
    return lst

append(1)   # [1]
append(2)   # [1, 2] — NOT [2]! Same list object reused.

# Fix: use None as sentinel
def append(item, lst=None):
    if lst is None:
        lst = []
    lst.append(item)
    return lst

Q3: What is a decorator and how do you write one?

A decorator is a callable that takes a function and returns a replacement function. It adds behavior without modifying the original.

import functools, time

def timeit(func):
    @functools.wraps(func)  # preserves __name__, __doc__
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        elapsed = time.perf_counter() - start
        print(f"{func.__name__} took {elapsed:.4f}s")
        return result
    return wrapper

@timeit
def slow():
    time.sleep(0.1)

slow()   # "slow took 0.1003s"

Q4: What’s the difference between @staticmethod and @classmethod?

class MyClass:
    count = 0

    @staticmethod
    def utility(x):
        # No access to class or instance — like a regular function in the class namespace
        return x * 2

    @classmethod
    def create(cls):
        # cls is the class itself — works correctly with subclasses
        obj = cls()
        cls.count += 1
        return obj

@classmethod is used for alternative constructors. @staticmethod for namespace organization.

Q5: How does Python’s memory management work?

  1. Reference counting — every object has a reference count. Reaches zero → immediately freed.
  2. Cyclic GC — handles reference cycles (A → B → A) that ref counting can’t break.
  3. Memory pools — CPython pre-allocates pools for small objects to avoid fragmentation.
import sys
x = [1, 2, 3]
sys.getrefcount(x)   # 2 (x + argument)
y = x
sys.getrefcount(x)   # 3
del y
sys.getrefcount(x)   # 2

Data Structures and Algorithms

Q6: What are Python’s built-in data structures and their complexities?

StructureAccessSearchInsertDelete
listO(1)O(n)O(1) amortized (end)O(n)
dictO(1) avgO(1) avgO(1) avgO(1) avg
setO(1) avgO(1) avgO(1) avg
dequeO(n)O(n)O(1) both endsO(1) both ends

Q7: How do you reverse a list in Python? Name all the ways.

lst = [1, 2, 3, 4, 5]

lst[::-1]          # new reversed list (slicing)
list(reversed(lst))# new reversed list (iterator)
lst.reverse()      # in-place, returns None
sorted(lst, reverse=True)  # sorted reversed copy

Q8: How do you find duplicates in a list?

from collections import Counter

def find_duplicates(lst):
    return [item for item, count in Counter(lst).items() if count > 1]

find_duplicates([1, 2, 2, 3, 3, 3, 4])   # [2, 3]

Q9: What is a generator and when would you use one?

A generator function uses yield to produce values lazily. Use when the dataset is large or you only need to iterate once.

def read_large_file(path):
    with open(path) as f:
        for line in f:
            yield line.strip()

# Processes line by line — constant memory regardless of file size
for line in read_large_file("10gb_file.log"):
    process(line)

Q10: How does list.sort() differ from sorted()?

list.sort() sorts in place and returns None. sorted() returns a new sorted list and works on any iterable.

lst = [3, 1, 4, 1, 5]
lst.sort()        # modifies lst, returns None
sorted(lst)       # returns new list, lst unchanged
sorted("hello")   # works on any iterable: ['e', 'h', 'l', 'l', 'o']

OOP and Design

Q11: What is duck typing?

Python doesn’t check types — it checks behavior. If an object has the right methods, it works.

def process(stream):
    for line in stream:   # works for file, list, generator, StringIO...
        print(line.strip())

process(open("file.txt"))
process(["line 1\n", "line 2\n"])
process(line + "\n" for line in ["a", "b"])

Q12: What are dunder (magic) methods?

Double-underscore methods customize Python’s built-in operations:

class Vector:
    def __init__(self, x, y):
        self.x, self.y = x, y

    def __repr__(self):             # repr(v)
        return f"Vector({self.x}, {self.y})"

    def __add__(self, other):       # v1 + v2
        return Vector(self.x + other.x, self.y + other.y)

    def __len__(self):              # len(v)
        return 2

    def __eq__(self, other):        # v1 == v2
        return self.x == other.x and self.y == other.y

Q13: What is multiple inheritance and the MRO?

Python supports multiple inheritance. The Method Resolution Order (MRO) determines which class’s method is called using the C3 linearization algorithm.

class A:
    def hello(self): return "A"

class B(A):
    def hello(self): return "B"

class C(A):
    def hello(self): return "C"

class D(B, C):
    pass

D().hello()     # "B" — follows MRO
D.__mro__       # (D, B, C, A, object)

Q14: What is super() and when do you use it?

super() delegates method calls to the next class in the MRO. Always use it instead of naming the parent class directly.

class Animal:
    def __init__(self, name):
        self.name = name

class Dog(Animal):
    def __init__(self, name, breed):
        super().__init__(name)   # calls Animal.__init__
        self.breed = breed

Functional Python

Q15: What is map(), filter(), and when should you use them?

numbers = [1, 2, 3, 4, 5]

list(map(lambda x: x**2, numbers))         # [1, 4, 9, 16, 25]
list(filter(lambda x: x % 2 == 0, numbers)) # [2, 4]

# Prefer comprehensions for readability:
[x**2 for x in numbers]
[x for x in numbers if x % 2 == 0]

Use map/filter with named functions when the function already exists.

Q16: What is functools.partial?

Creates a new function with some arguments pre-filled:

from functools import partial

def power(base, exp):
    return base ** exp

square = partial(power, exp=2)
cube = partial(power, exp=3)

square(5)   # 25
cube(3)     # 27

Q17: Explain closures.

A closure is a function that captures variables from its enclosing scope:

def make_multiplier(factor):
    def multiply(x):
        return x * factor   # factor is captured from outer scope
    return multiply

double = make_multiplier(2)
triple = make_multiplier(3)
double(5)   # 10
triple(5)   # 15

Concurrency and I/O

Q18: When would you use asyncio vs threading vs multiprocessing?

  • asyncio — I/O-bound, async-native code (web APIs, database, sockets). Thousands of concurrent connections in one thread.
  • threading — I/O-bound, blocking libraries. GIL limits true CPU parallelism.
  • multiprocessing — CPU-bound work. Each process has its own GIL and memory.

Q19: What is the difference between @property and a regular attribute?

@property makes a method behave like an attribute, adding computed logic and validation:

class Circle:
    def __init__(self, radius):
        self._radius = radius

    @property
    def radius(self):
        return self._radius

    @radius.setter
    def radius(self, value):
        if value < 0:
            raise ValueError("Radius cannot be negative")
        self._radius = value

    @property
    def area(self):
        import math
        return math.pi * self._radius ** 2

c = Circle(5)
c.radius = -1   # raises ValueError
c.area          # computed, not stored

Advanced Topics

Q20: What are __slots__ and when do they help?

__slots__ restricts instance attributes to a fixed set, eliminating the per-instance __dict__. Saves 40-60% memory when creating millions of instances.

class FastPoint:
    __slots__ = ("x", "y")
    def __init__(self, x, y):
        self.x = x; self.y = y

Q21: What is a context manager and how do you create one?

from contextlib import contextmanager

@contextmanager
def managed_resource():
    resource = acquire()
    try:
        yield resource
    finally:
        release(resource)

with managed_resource() as r:
    use(r)

Q22: How does @lru_cache work?

from functools import lru_cache

@lru_cache(maxsize=128)
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n-1) + fibonacci(n-2)

It caches results in a dict keyed by arguments. The “LRU” (Least Recently Used) eviction policy keeps the most recently used results up to maxsize.

Q23: What is the difference between is and ==?

== tests value equality (calls __eq__). is tests identity (same object in memory).

a = [1, 2, 3]
b = [1, 2, 3]
a == b    # True
a is b    # False

x = None
x is None    # correct
x == None    # works but style violation

Q24: How do you handle circular imports?

Move the import inside the function that uses it, or restructure to extract shared code into a third module.

# Circular: a.py imports b.py, b.py imports a.py
# Fix: import inside the function
def get_user():
    from myapp.models import User   # deferred import
    return User.find(1)

Q25: What is *args and **kwargs?

def variadic(*args, **kwargs):
    print(args)     # tuple of positional arguments
    print(kwargs)   # dict of keyword arguments

variadic(1, 2, 3, name="Alice", age=30)
# (1, 2, 3)
# {'name': 'Alice', 'age': 30}

# Unpack into a function call
def add(a, b, c): return a + b + c
args = (1, 2, 3)
add(*args)   # 6

Q26: What is a descriptor?

An object that defines __get__, __set__, or __delete__ to customize attribute access. property, staticmethod, and classmethod are all descriptors.

class Validated:
    def __set_name__(self, owner, name):
        self.name = name

    def __get__(self, obj, objtype=None):
        if obj is None: return self
        return obj.__dict__.get(self.name)

    def __set__(self, obj, value):
        if not isinstance(value, int) or value < 0:
            raise ValueError(f"{self.name} must be a non-negative int")
        obj.__dict__[self.name] = value

class Order:
    quantity = Validated()
    price = Validated()

Q27: How do you flatten a nested list?

# Recursive generator
def flatten(lst):
    for item in lst:
        if isinstance(item, list):
            yield from flatten(item)
        else:
            yield item

list(flatten([1, [2, [3, 4]], [5]]))   # [1, 2, 3, 4, 5]

# One level with itertools
from itertools import chain
list(chain.from_iterable([[1,2],[3,4]]))   # [1, 2, 3, 4]

Q28: What is the difference between shallow and deep copy?

import copy

original = [[1, 2], [3, 4]]

shallow = original.copy()        # or original[:]
deep = copy.deepcopy(original)

original[0].append(99)
print(shallow[0])   # [1, 2, 99] — inner list is shared
print(deep[0])      # [1, 2]     — completely independent

Q29: How do you make a class hashable?

Implement both __hash__ and __eq__. If you define __eq__ without __hash__, Python sets __hash__ = None making the class unhashable.

class Point:
    def __init__(self, x, y):
        self.x = x; self.y = y

    def __eq__(self, other):
        return self.x == other.x and self.y == other.y

    def __hash__(self):
        return hash((self.x, self.y))

p = Point(1, 2)
{p}            # works — hashable
{p: "origin"}  # works as dict key

Q30: What is typing.Protocol and how is it different from ABC?

Protocol enables structural subtyping — an object matches a Protocol if it has the right attributes, regardless of inheritance.

from typing import Protocol

class Drawable(Protocol):
    def draw(self) -> None: ...

class Circle:
    def draw(self) -> None:
        print("Drawing circle")

# Circle never inherits from Drawable
def render(shape: Drawable) -> None:
    shape.draw()

render(Circle())   # works — structural compatibility, no ABC needed

Use Protocol for interfaces that third-party types should satisfy. Use ABC when you own the hierarchy and want to enforce implementation.

Frequently Asked Questions

What level are these questions aimed at?
The questions cover beginner through advanced topics. Junior roles focus on Q1-15, mid-level on Q1-22, senior/staff on all 30.
Should I memorize these answers?
No. Understand the concepts. Interviewers probe with follow-ups — if you memorized an answer without understanding it, you'll stall immediately.
What's the most commonly asked Python interview topic?
The GIL and concurrency, mutable default arguments, generators vs lists, and list/dict/set complexity come up most often in technical screens.