The short answer: Python interviews check that you understand how Python’s data model behaves, beyond the syntax: mutable vs immutable objects, references and copies, hashing, scope and closures. Expect questions on decorators, generators and context managers, classes (properties, class methods, inheritance and the MRO, dataclasses), exceptions, concurrency (the GIL, threads vs processes vs asyncio), type hints, and performance, plus a live coding exercise. Interviewers increasingly ask what’s current. Python 3.15 (October 9, 2026) added lazy imports, a built-in frozendict and UTF-8 as the default encoding. Python 3.14 (October 2025) made the free-threaded, GIL-free build officially supported and added template strings. The answers below are checked against the Python documentation and the official programming FAQ, with sources at the end.
Applying for your first role? Start with our Python interview questions for junior developers, which cover the basics in more depth.
Python versions in 2026
| Version | Released | Status in November 2026 | End of support |
|---|---|---|---|
| Python 3.15 | October 9, 2026 | Bug fixes | October 2031 |
| Python 3.14 | October 7, 2025 | Bug fixes | October 2030 |
| Python 3.13 | October 7, 2024 | Security fixes only | October 2029 |
| Python 3.12 | October 2, 2023 | Security fixes only | October 2028 |
| Python 3.11 | October 24, 2022 | Security fixes only | October 2027 |
| Python 3.10 | October 4, 2021 | End of life | October 2026 |
What interviewers ask about from the last two releases:
- Python 3.14: template strings (
t"...", PEP 750), deferred evaluation of annotations (PEPs 649 and 749), multiple interpreters in the standard library (PEP 734),exceptwithout brackets (PEP 758), Zstandard compression (PEP 784), the free-threaded build “officially supported” (PEP 779), and the experimental JIT compiler in the Windows and macOS installers. - Python 3.15: explicit lazy imports (
lazy import json, PEP 810), built-infrozendict(PEP 814) andsentinel(PEP 661) types, UTF-8 as the default encoding (PEP 686), unpacking in comprehensions ([*L for L in lists], PEP 798), and a newprofilingpackage with a sampling profiler (PEP 799).
Core Python
“Mutable vs immutable: why does it matter?”
Immutable objects (int, float, str, tuple, frozenset, and now frozendict) can’t change after creation; “changing” one makes a new object. Mutable objects (list, dict, set, most class instances) change in place, and every name that refers to them sees the change. This explains most Python surprises: variables are names bound to objects, and function arguments pass references to objects (often called “pass by object reference”), so a function can mutate a list you passed in, but rebinding the parameter inside the function doesn’t affect the caller.
“== or is?”
== compares values (it calls __eq__); is checks identity: are these the same object? Use is only for singletons such as None (if x is None). Using is to compare numbers or strings can appear to work because CPython reuses some objects, but that’s an implementation detail.
“What can be a dictionary key? Are dictionaries ordered?”
A key must be hashable: in the glossary’s words, it “has a hash value which never changes during its lifetime” and can be compared with other objects. So strings, numbers and tuples of hashable items work; lists and dicts don’t. Lookups are O(1) on average because of hashing. Since Python 3.7, “dictionary order is guaranteed to be insertion order.”
“Shallow copy or deep copy?”
copy.copy(x) (or list(x), x[:], d.copy()) creates a new outer container that still points to the same inner objects; copy.deepcopy(x) copies recursively. With a list of lists, a shallow copy shares the inner lists, so changing copy[0][0] also changes the original.
“Why shouldn’t you use a mutable default argument?”
“Default values are created exactly once, when the function is defined,” so a default list is shared between calls. Use None and create the list inside:
def add_item(item, items=None):
if items is None:
items = []
items.append(item)
return items
“What are *args and **kwargs? What’s the scope rule?”
*args collects extra positional arguments into a tuple and **kwargs collects extra keyword arguments into a dict; the same symbols unpack them in calls. Python looks up names in the LEGB order: Local, Enclosing function, Global, Built-in. Assigning to a name makes it local unless you declare it global or, for an enclosing function’s variable, nonlocal.
Functions, closures and decorators
“What is a closure?”
A function that remembers variables from the scope where it was defined, even after that scope has returned. Closures read those variables when called, not when defined, which causes the classic “late binding” bug (see the puzzles below).
“What is a decorator? Write one.”
The glossary: “a function returning another function, usually applied as a function transformation using the @wrapper syntax.” Decorators add behavior such as timing, caching, retries or access checks without changing the function’s code:
import functools
import time
def timed(func):
@functools.wraps(func) # keep func's name
def wrapper(*args, **kwargs):
start = time.perf_counter()
try:
return func(*args, **kwargs)
finally:
print(f"{func.__name__} took {time.perf_counter() - start:.3f}s")
return wrapper
@timed
def build_report():
...
Follow-ups: a decorator that takes arguments (@retry(times=3)) needs one more level of nesting, and functools.lru_cache and functools.cache are built-in decorators for memoization.
“Iterator, iterable or generator?”
An iterable can be looped over (it has __iter__); an iterator produces values one at a time (it has __next__ and raises StopIteration at the end). A generator is the easy way to write an iterator: a function with yield, which pauses and resumes, producing values lazily. Generator expressions ((x * x for x in data)) do the same inline. Use them for large or infinite sequences, such as reading a big file line by line without loading it into memory. yield from delegates to another generator.
“What does with do? How do you write a context manager?”
with guarantees setup and cleanup: a file is closed, a lock released or a transaction committed even if an exception occurs. Write one as a class with __enter__ and __exit__, or with contextlib.contextmanager:
from contextlib import contextmanager
import time
@contextmanager
def timer(label):
start = time.perf_counter()
try:
yield
finally:
print(f"{label}: {time.perf_counter() - start:.3f}s")
with timer("load"):
data = load_data()
Object-oriented Python
“Instance method, @classmethod, @staticmethod or @property?”
class Temperature:
def __init__(self, celsius):
self.celsius = celsius
# Read like an attribute: t.fahrenheit
@property
def fahrenheit(self):
return self.celsius * 9 / 5 + 32
# Alternative constructor; receives the class
@classmethod
def from_fahrenheit(cls, f):
return cls((f - 32) * 5 / 9)
# Utility; receives neither self nor cls
@staticmethod
def is_valid(celsius):
return celsius >= -273.15
“How does multiple inheritance work? What is the MRO?”
The method resolution order is “the order in which base classes are searched for a member during lookup.” Python computes it with the C3 linearization, so each class appears once and before its parents. super() follows the MRO, not just “the parent,” which is what makes cooperative multiple inheritance work.
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'
D.__mro__ # D, B, C, A, object
“What are dataclasses? When would you use __slots__?”
@dataclass generates __init__, __repr__ and __eq__ from type-annotated fields; frozen=True makes instances immutable (and hashable), and slots=True adds __slots__. __slots__ stops Python from giving each instance a __dict__, which saves memory when you create millions of objects and blocks typos that would create new attributes.
“What are dunder methods?”
Special methods with double underscores that hook into Python’s syntax: __init__, __repr__ and __str__, __eq__ and __hash__, __len__, __iter__, __getitem__, __enter__ and __exit__, and operators like __add__. Implementing __eq__ without __hash__ makes instances unhashable, a common follow-up.
“Abstract base class or Protocol?”
An abstract base class (abc.ABC with @abstractmethod) requires subclasses to inherit from it and implement the methods. A typing.Protocol describes the methods an object must have, checked structurally by type checkers, with no inheritance needed: the static version of duck typing.
Errors and exceptions
“How do you handle exceptions well?”
Catch specific exceptions, not bare except:; use else for code that should run only if nothing failed and finally for cleanup; raise custom exceptions that subclass Exception; and chain with raise NewError(...) from err to keep the cause. Pythonic code often follows EAFP, “easier to ask for forgiveness than permission”: try the operation and catch the exception, instead of checking first. Since Python 3.11, ExceptionGroup and except* handle several errors raised together, for example by tasks in an asyncio.TaskGroup. Python 3.14 lets you drop the brackets when catching several exception types without as: except ValueError, TypeError:.
Concurrency
“What is the GIL?”
The global interpreter lock is “the mechanism used by the CPython interpreter to assure that only one thread executes Python bytecode at a time.” Threads still help with I/O-bound work, because the GIL is released while waiting on the network or disk, but they don’t speed up CPU-bound Python code on multiple cores.
“Threads, processes or asyncio?”
- I/O-bound with a modest number of tasks:
threadingorconcurrent.futures.ThreadPoolExecutor. - CPU-bound:
multiprocessingorProcessPoolExecutor, which use separate processes with their own interpreter and GIL, or a library that releases the GIL, such as NumPy. - Many concurrent I/O tasks (thousands of network calls):
asyncio, cooperative multitasking on one thread withasyncandawait.
import asyncio
async def fetch(i):
# Stands in for a network call
await asyncio.sleep(1)
return i
async def main():
async with asyncio.TaskGroup() as tg:
tasks = [tg.create_task(fetch(i)) for i in range(3)]
# Takes about 1 second, not 3
print([t.result() for t in tasks])
asyncio.run(main())
A blocking call inside a coroutine (like time.sleep or a synchronous HTTP client) freezes the event loop for every task; that’s the most common asyncio mistake.
“Is Python getting rid of the GIL?”
Partly, and optionally. Since 3.13, CPython has a separate free-threaded build “where the global interpreter lock (GIL) is disabled,” so threads can run in parallel on multiple cores; Python 3.14 made it officially supported. It isn’t the default build, and it has some overhead: the docs measure about 1% to 8% on single-threaded code, depending on the platform. C extensions must be marked as compatible, or the GIL is turned back on. Check with sys._is_gil_enabled(). Python 3.14 also added multiple interpreters to the standard library (PEP 734), another way to get parallelism within one process.
Type hints
“Does Python enforce type hints?”
No. Annotations are documentation for people and tools; Python doesn’t check them at runtime, so add("a", "b") on a function annotated with int parameters happily returns "ab". Type checkers such as mypy and Pyright catch the mistake before running, and libraries like Pydantic validate data at runtime. Know the modern syntax: list[int], X | None, and since Python 3.12, generic functions written as def first[T](items: list[T]) -> T:. Since Python 3.14, annotations are evaluated lazily, only when something asks for them, which removes most of the need for quoted forward references.
Performance
“How fast are the built-in data structures?”
| Operation | Average cost | Note |
|---|---|---|
list.append, list.pop() | O(1) | At the end of the list |
list.insert(0, x), list.pop(0) | O(n) | Use collections.deque for a queue |
x in list | O(n) | Scans the list |
x in set, x in dict | O(1) | Hashing |
sorted(items) | O(n log n) | Stable sort |
So “deduplicate while keeping order” is list(dict.fromkeys(items)), membership checks on large collections should use a set, and building a long string should use "".join(parts) rather than += in a loop.
“How do you find a performance problem?”
Measure before optimizing: timeit for small snippets, cProfile for whole programs, and in Python 3.15 the new profiling package, which includes Tachyon, a high-frequency statistical sampling profiler. Then fix the algorithm first (a set instead of a list scan, caching), move hot loops to vectorized libraries like NumPy or pandas, and only then reach for C extensions or other runtimes.
Testing and tooling
“How do you test Python code?”
Most teams use pytest: plain assert statements, fixtures for setup, parametrize for many inputs, and unittest.mock (or pytest’s monkeypatch) to replace network calls and clocks. Know how to isolate dependencies: a project gets its own virtual environment (python -m venv .venv), and its metadata and dependencies go in pyproject.toml. Tools such as uv, Ruff (linting and formatting) and a type checker in CI are common in 2026. Since Python 3.15, text files default to UTF-8 everywhere, but passing encoding="utf-8" to open() is still a good habit for code that runs on older versions.
Output puzzles (“what does this print?”)
Lambdas in a loop
funcs = [lambda: i for i in range(3)]
print([f() for f in funcs])
Answer: [2, 2, 2]. The official FAQ explains: the variable “is accessed when the lambda is called — not when it is defined,” and by then the loop has finished. Fix it by binding the value: lambda i=i: i.
The shared default list
def add_item(item, items=[]):
items.append(item)
return items
add_item(1)
print(add_item(2))
Answer: [1, 2]. The default list was created once, when the function was defined, and both calls append to it.
The grid that changes everywhere
grid = [[0] * 3] * 3
grid[0][0] = 1
print(grid)
Answer: [[1, 0, 0], [1, 0, 0], [1, 0, 0]]. “Replicating a list with * doesn’t create copies, it only creates references to the existing objects”: the three rows are the same list. Use [[0] * 3 for _ in range(3)].
+= on a list inside a tuple
t = ([1],)
t[0] += [2]
Answer: a TypeError (“‘tuple’ object does not support item assignment”), and t is now ([1, 2],). += first extends the list in place, then tries to assign it back into the tuple, which fails. The FAQ walks through exactly this case.
A class attribute shared by every instance
class Team:
members = []
def add(self, name):
self.members.append(name)
a, b = Team(), Team()
a.add("Ana")
print(b.members)
Answer: ['Ana']. members belongs to the class, so every instance shares it. Create it in __init__ instead.
Coding exercises with solutions
Two sum in one pass
def two_sum(nums, target):
seen = {} # value -> index
for i, n in enumerate(nums):
if target - n in seen:
return seen[target - n], i
seen[n] = i
return None
O(n) time with a dictionary, instead of O(n²) with nested loops. State your assumptions: exactly one answer? Can the same element be used twice?
Group anagrams
from collections import defaultdict
def group_anagrams(words):
groups = defaultdict(list)
for w in words:
groups["".join(sorted(w))].append(w)
return list(groups.values())
Flatten a nested list
def flatten(items):
for x in items:
if isinstance(x, list):
yield from flatten(x)
else:
yield x
list(flatten([1, [2, [3, 4]], 5]))
# [1, 2, 3, 4, 5]
For exactly one level of nesting, Python 3.15’s comprehension unpacking also works: [*L for L in lists].
An LRU cache
from collections import OrderedDict
class LRUCache:
def __init__(self, capacity):
self.capacity = capacity
self.data = OrderedDict()
def get(self, key):
if key not in self.data:
return -1
# Mark as recently used
self.data.move_to_end(key)
return self.data[key]
def put(self, key, value):
self.data[key] = value
self.data.move_to_end(key)
if len(self.data) > self.capacity:
# Evict the least recently used
self.data.popitem(last=False)
Both operations are O(1). Mention functools.lru_cache for caching function results.
Merge overlapping intervals
def merge(intervals):
merged = []
for start, end in sorted(intervals):
if merged and start <= merged[-1][1]:
merged[-1][1] = max(merged[-1][1], end)
else:
merged.append([start, end])
return merged
More to practice: word frequency with collections.Counter, a retry decorator with exponential backoff, reading a large CSV with the csv module and a generator, and a rate limiter.
Questions by level and role
- Junior: data types and mutability, lists vs tuples, dictionaries,
==vsis, functions and default arguments, comprehensions, basic classes, exceptions, files. See our junior Python guide. - Mid-level: closures and decorators, generators, context managers, properties and class methods, dataclasses, copying, complexity of built-ins, testing with pytest, threads vs processes vs
asyncio. - Senior: the GIL and the free-threaded build,
asynciopitfalls, the MRO andsuper(), memory and profiling, typing at scale, packaging and dependency management, and designing a service or data pipeline in Python. - By role: backend developers get web framework questions (Django’s ORM and migrations, FastAPI and Pydantic); data roles get pandas, NumPy and SQL (see our SQL interview questions and data science interview prep); machine learning roles add model and pipeline questions.
Behavioral questions for Python developers
Use the STAR method with a real example:
- “Tell me about a bug that took you a long time to find. How did you track it down?”
- “Describe a time you made slow Python code faster. What did you measure?”
- “How did you upgrade a project to a newer Python version or library?”
- “Tell me about code you wrote that someone else had to maintain. What did you do to make it easy?”
More: our software engineer interview questions, backend developer interview questions, Java interview questions and data engineer interview questions.
Frequently asked questions
What Python questions are asked in interviews?
What is the latest version of Python?
What's new in Python 3.15?
Does Python still have a GIL?
What is the difference between a list and a tuple?
How should I practice for a Python coding interview?
Can Huru help me practice Python interview questions?
Sources
- Python.org, Download Python: active Python releases (release dates, support status and end of support). Read on 4 November 2026.
- Python documentation: What’s New in Python 3.15 (released 9 October 2026) and What’s New in Python 3.14. Read on 4 November 2026.
- Python documentation: Programming FAQ (lambdas in loops, shared default values, multidimensional lists,
+=on tuple items, copying), Glossary (GIL, decorator, hashable, EAFP, MRO), Built-in types:dictand Python support for free threading. Read on 4 November 2026.