Interview Questions
A collection of Python interview questions ranging from fundamentals to details that trip up experienced developers. Each answer aims to be correct, concise, and to explain the reasoning — not just state a rule to memorize.
1. What's the difference between a list and a tuple?
Both are ordered sequences, but lists are mutable and tuples are immutable. Once a tuple is created, its contents can't be changed, added to, or removed — which makes tuples hashable (usable as dict keys or set members) as long as their elements are also hashable, and slightly more memory-efficient than lists.
point = (3, 4) # tuple — fixed, hashable scores = [90, 85, 77] # list — can grow, shrink, mutate scores.append(100) # fine point.append(5) # AttributeError: tuple has no attribute 'append'
2. What is the GIL (Global Interpreter Lock)?
The GIL is a mutex in CPython that allows only one thread to execute Python bytecode at a time, even on a multi-core machine. It exists to simplify memory management (reference counting isn't thread-safe without it). The practical consequence: threading doesn't give real parallelism for CPU-bound Python code, though it works fine for I/O-bound work (network calls, file I/O) since the GIL is released during blocking I/O. For true CPU parallelism, use multiprocessing (separate processes, each with its own GIL) or offload work to a C extension that releases the GIL.
3. Explain mutable vs immutable types
Mutable objects can be changed in place after creation; immutable objects cannot. list, dict, and set are mutable. int, float, str, tuple, and frozenset are immutable.
s = "hello" s += " world" # creates a brand new string object; 'hello' is untouched lst = [1, 2, 3] lst.append(4) # mutates the same list object in place
This matters most around function arguments and default values — passing a mutable object into a function lets that function modify the caller's data, which is sometimes intended and sometimes a nasty surprise.
4. What are decorators?
A decorator is a function that takes another function (or class) and returns a modified version of it, typically adding behavior before/after the original call without changing its source code.
import time
from functools import wraps
def timed(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
print(f"{func.__name__} took {time.time() - start:.4f}s")
return result
return wrapper
@timed
def slow_add(a, b):
time.sleep(0.1)
return a + b5. What's the difference between __str__ and __repr__?
__str__ returns a readable, user-facing string (used by print() and str()). __repr__ returns an unambiguous, developer-facing representation, ideally one that could recreate the object (used by the REPL, debuggers, and as a fallback when __str__ isn't defined).
class Point:
def __init__(self, x, y):
self.x, self.y = x, y
def __repr__(self):
return f"Point(x={self.x}, y={self.y})"
def __str__(self):
return f"({self.x}, {self.y})"
p = Point(1, 2)
print(p) # (1, 2) -> __str__
print([p]) # [Point(x=1, y=2)] -> __repr__ (lists always use repr on their items)6. How does Python manage memory?
CPython uses automatic reference counting: every object tracks how many references point to it, and is freed the moment that count hits zero. A supplemental cyclic garbage collector periodically detects and cleans up reference cycles (e.g. two objects referencing each other) that reference counting alone can't catch. Small objects are also managed through internal memory pools (pymalloc) to reduce allocation overhead.
7. What are generators and why use them?
A generator is a function containing yield that produces values lazily, one at a time, instead of building an entire collection in memory upfront. Each call to next() resumes the function from where it last paused.
def read_large_file(path):
with open(path) as f:
for line in f:
yield line.strip()
# Only one line is in memory at a time, no matter the file size
for line in read_large_file('huge_log.txt'):
process(line)Generators trade the convenience of a full list for constant memory usage and the ability to represent infinite or very large sequences.
8. Explain *args and **kwargs
*args collects extra positional arguments into a tuple; **kwargs collects extra keyword arguments into a dict. They let a function accept a flexible, unknown-in-advance number of arguments.
def describe(*args, **kwargs):
print("positional:", args)
print("keyword:", kwargs)
describe(1, 2, name="Ada", age=36)
# positional: (1, 2)
# keyword: {'name': 'Ada', 'age': 36}9. What is the difference between == and is?
== checks value equality (are these two objects considered equal?). is checks identity (are these the exact same object in memory?). Two equal values are not necessarily the same object.
a = [1, 2, 3] b = [1, 2, 3] print(a == b) # True — same contents print(a is b) # False — two different list objects
10. What are Python's list, dict, and set comprehensions?
Comprehensions are a concise, often faster way to build a collection from an iterable in a single expression, replacing a manual loop with .append().
squares = [n * n for n in range(10) if n % 2 == 0]
lookup = {n: n * n for n in range(5)}
unique_lengths = {len(word) for word in ["hi", "python", "ok"]}11. What is a Python virtual environment and why use one?
A virtual environment is an isolated Python installation with its own set of installed packages, separate from the system Python and from other projects. It prevents dependency version conflicts between unrelated projects and keeps each project's requirements reproducible.
python -m venv .venv source .venv/bin/activate pip install requests
12. What's the difference between a shallow copy and a deep copy?
A shallow copy creates a new outer object but keeps references to the same nested objects. A deep copy recursively duplicates everything, so nested objects are independent too.
import copy original = [[1, 2], [3, 4]] shallow = copy.copy(original) shallow[0].append(99) print(original) # [[1, 2, 99], [3, 4]] — inner list was shared! deep = copy.deepcopy(original) deep[0].append(100) print(original) # unaffected
13. What are Python's magic (dunder) methods?
Methods surrounded by double underscores (__init__, __len__, __eq__, __add__, etc.) let custom objects hook into Python's built-in syntax and functions — len(obj) calls obj.__len__(), obj1 + obj2 calls obj1.__add__(obj2), and so on.
class Vector:
def __init__(self, x, y):
self.x, self.y = x, y
def __add__(self, other):
return Vector(self.x + other.x, self.y + other.y)
def __repr__(self):
return f"Vector({self.x}, {self.y})"
print(Vector(1, 2) + Vector(3, 4)) # Vector(4, 6)14. What is duck typing?
Duck typing means an object's suitability is determined by whether it has the methods/attributes being used, not by its declared type — "if it walks like a duck and quacks like a duck, it's a duck." Python doesn't require inheritance from a common interface; it just calls the method and lets it fail at runtime if it's missing.
def make_it_speak(thing):
thing.speak() # works for any object with a .speak() method,
# regardless of its class or inheritance15. What's the difference between a module and a package?
A module is a single .py file. A package is a directory of modules containing an __init__.py file (implicit in modern Python via namespace packages, but still commonly used explicitly), letting you organize related modules under one importable namespace.
16. How do exceptions work, and what's the role of finally?
try runs code that might fail; except catches and handles specific exception types; else runs only if no exception occurred; finally always runs, regardless of whether an exception was raised or caught — commonly used for cleanup.
try:
value = int(user_input)
except ValueError:
print("Not a valid number")
else:
print(f"Parsed: {value}")
finally:
print("Done processing input")17. What is the difference between a class method, a static method, and an instance method?
Type | First parameter | Typical use |
|---|---|---|
Instance method |
| Reads/writes instance state |
Class method ( |
| Alternative constructors, class-level state |
Static method ( | none required | Utility logic related to the class but needing no instance/class data |
class Pizza:
def __init__(self, toppings):
self.toppings = toppings
@classmethod
def margherita(cls):
return cls(["mozzarella", "tomato"])
@staticmethod
def is_vegetarian(topping):
return topping not in {"pepperoni", "bacon"}18. What does the walrus operator (:=) do?
Introduced in Python 3.8, := assigns a value to a variable as part of a larger expression, avoiding a separate assignment statement.
data = [1, 2, 3, 4, 5]
# Without walrus: compute the length twice or use an extra line
if (n := len(data)) > 3:
print(f"List has {n} items")19. What's the difference between multithreading and multiprocessing in Python?
Threads share memory and are lightweight but, due to the GIL, don't achieve true parallel execution of Python bytecode — they help with I/O-bound work. Processes have separate memory and their own interpreter/GIL, giving real parallelism for CPU-bound work, at the cost of higher memory usage and the need to explicitly share data (via pickling, queues, or shared memory).
20. What are Python's built-in data types for grouping data?
list— ordered, mutable sequence.tuple— ordered, immutable sequence.dict— key-value mapping, insertion-ordered since Python 3.7.set— unordered collection of unique, hashable elements.frozenset— immutable version ofset.
These questions cover the areas interviewers return to most often: core data structures, memory and concurrency model, and the language features (decorators, generators, dunder methods) that distinguish someone who has written real Python from someone who has only read about it.