40 questions · Updated
Python Interview Questions and Answers
Python interviews test two things at once: whether you understand the language's data model, and whether you can write clean, idiomatic code quickly. For freshers, expect questions on lists versus tuples, mutability, dictionaries and comprehensions. Experienced candidates get decorators, generators, context managers, the GIL and asyncio, plus questions about memory and performance. This page gathers 40 Python interview questions and answers, ordered from beginner to advanced, followed by the Python coding interview questions asked most often — reversing strings, finding duplicates, two-sum, anagram grouping, an LRU cache and similar exercises. Answers target Python 3.12 and later, so you will see f-string improvements, structural pattern matching, the new generic type syntax, and notes on the free-threaded build that arrived experimentally in 3.13 and became officially supported in 3.14. Each answer is short enough to say aloud in an interview, with a snippet where code makes the point faster. Work through the beginner section if you are preparing for your first role, and keep the Python cheat sheet open for quick syntax revision.
Python interview questions for freshers
1.What are the key features of Python?
Python is a high-level, dynamically typed, garbage-collected language with a readable, indentation-based syntax. It supports procedural, object-oriented and functional styles, has a large standard library, and runs on an interpreter (CPython is the reference implementation, compiling to bytecode). Its weaknesses are raw CPU speed and, historically, multi-threaded CPU parallelism because of the GIL.
2.What is the difference between a list and a tuple?
Lists are mutable: you can add, remove and change elements. Tuples are immutable once created. Because they are immutable, tuples containing only hashable items are hashable and can be used as dictionary keys or set members. Tuples are typically used for fixed records, such as coordinates, and lists for collections that change.
3.Which built-in types are mutable and which are immutable?
Immutable: int, float, complex, bool, str, bytes, tuple, frozenset and None. Mutable: list, dict, set, bytearray and most user-defined objects. Mutability matters for function arguments, default values and dictionary keys: only hashable (in practice, immutable) objects can be keys.
4.What is the difference between == and is?
== compares values by calling __eq__. is compares identity: whether two names point to the same object in memory. Use is only for singletons such as None, True and False. Small integers and some strings may be cached by CPython, so is can appear to work for them, but relying on that is a bug.
5.How do list, dict and set comprehensions work?
A comprehension builds a collection from an iterable in a single expression, optionally with a filter. They are usually faster and clearer than an equivalent loop with append. A generator expression uses parentheses and produces values lazily instead of building a list in memory.
squares = [n * n for n in range(10) if n % 2 == 0]
lengths = {word: len(word) for word in words}
unique = {c.lower() for c in text if c.isalpha()}
total = sum(n * n for n in range(1_000_000)) # generator, no list6.What are *args and **kwargs?
*args collects extra positional arguments into a tuple, and **kwargs collects extra keyword arguments into a dict. They let a function accept a variable number of arguments, and are common in decorators and wrappers that forward arguments. The same operators unpack sequences and mappings when calling a function.
def log(*args, **kwargs):
print(args, kwargs)
log(1, 2, level="info") # (1, 2) {'level': 'info'}7.What is the mutable default argument pitfall?
Default argument values are evaluated once, when the function is defined, not on every call. A mutable default such as a list is therefore shared between calls and accumulates data. Use None as the default and create the object inside the function.
def add(item, items=None):
if items is None:
items = []
items.append(item)
return items8.How is Python code organised into modules and packages?
A module is a single .py file; a package is a directory of modules, traditionally marked with an __init__.py file. import executes a module once and caches it in sys.modules. The if __name__ == "__main__": guard runs code only when the file is executed directly, not when it is imported.
9.How do exceptions work in Python?
Use try with except blocks for specific exception types, an optional else that runs if no exception occurred, and finally for cleanup that always runs. Catch the narrowest exception you can handle; a bare except also catches KeyboardInterrupt and SystemExit. raise ... from err chains exceptions to preserve the original cause, and Python 3.11 added ExceptionGroup with except* for handling several errors at once.
10.What are f-strings?
F-strings are string literals prefixed with f that evaluate expressions inside braces at runtime, such as f"{name} is {age}". They support format specs like {value:.2f} and the debugging form {x=} which prints the expression and its value. Since Python 3.12 they can contain nested quotes of the same type, backslashes and comments inside the braces.
11.What is the difference between a shallow copy and a deep copy?
A shallow copy (list.copy(), slicing, copy.copy) creates a new outer container but shares the inner objects. A deep copy (copy.deepcopy) recursively copies everything, so changes to nested objects do not affect the original. Assignment with = copies neither; it just binds another name to the same object.
12.How do you work with dictionaries efficiently?
Use dict.get(key, default) to avoid KeyError, setdefault or collections.defaultdict to build grouped values, and collections.Counter for counting. Dictionaries preserve insertion order (guaranteed since Python 3.7), and the | operator merges two dicts into a new one since 3.9. Average lookup, insert and delete are O(1).
Intermediate Python interview questions
13.What is a decorator?
A decorator is a callable that takes a function and returns a replacement, usually a wrapper that adds behaviour before or after the original call. The @decorator syntax is shorthand for func = decorator(func). Use functools.wraps so the wrapper keeps the original name and docstring. Decorators are used for logging, caching, authorisation and timing.
import functools, time
def timed(func):
@functools.wraps(func)
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 wrapper14.What are generators and the yield keyword?
A generator function uses yield to produce a sequence of values lazily: each call to next() resumes the function until the next yield. Generators use constant memory regardless of sequence length, which makes them ideal for streaming large files or infinite sequences. yield from delegates to another iterable or generator.
def read_lines(path):
with open(path) as f:
for line in f:
yield line.rstrip("\n")15.What is the difference between an iterable and an iterator?
An iterable is any object that can return an iterator, via __iter__ (lists, dicts, strings, files). An iterator is an object with __next__ that returns the next item and raises StopIteration when exhausted; it is also iterable itself. Iterators are single-pass: once consumed, iterating again yields nothing, whereas a list can be iterated many times.
16.What are context managers and the with statement?
A context manager defines setup and teardown around a block through __enter__ and __exit__. The with statement guarantees teardown even if the block raises, which is why it is used for files, locks and database transactions. contextlib.contextmanager lets you write one as a generator with a single yield.
from contextlib import contextmanager
@contextmanager
def transaction(conn):
try:
yield conn
conn.commit()
except Exception:
conn.rollback()
raise17.Explain the LEGB scope rule.
Python resolves names by searching Local, Enclosing (outer function), Global (module) and Built-in scopes, in that order. Assigning to a name inside a function makes it local unless declared global or nonlocal. nonlocal rebinds a variable in the nearest enclosing function scope, which is needed when a closure updates a counter.
18.What is the difference between @staticmethod, @classmethod and instance methods?
Instance methods receive the instance as self and can read or change its state. Class methods receive the class as cls and are often used as alternative constructors, such as from_json, that work correctly with subclasses. Static methods receive neither; they are plain functions grouped in the class namespace for organisation.
19.What are dunder (magic) methods?
Dunder methods, such as __init__, __repr__, __eq__, __hash__, __len__ and __iter__, let your classes integrate with Python syntax and built-ins. Defining __eq__ without __hash__ makes instances unhashable, which surprises people. __repr__ should give an unambiguous developer-facing representation, and __str__ a readable one.
20.What are dataclasses?
The @dataclass decorator generates __init__, __repr__ and __eq__ from annotated class fields, removing boilerplate for data-holding classes. Options include frozen=True for immutability, slots=True for lower memory usage and order=True for comparisons. Use field(default_factory=list) for mutable defaults.
from dataclasses import dataclass, field
@dataclass(frozen=True, slots=True)
class Point:
x: float
y: float
tags: tuple[str, ...] = field(default_factory=tuple)21.How does structural pattern matching (match/case) work?
match compares a value against patterns that can destructure sequences, mappings and class instances, bind variables and use guards with if. It was added in Python 3.10. It is more than a switch statement: it is useful for parsing commands, ASTs or JSON-like data with varying shapes. The wildcard case _ matches anything.
match event:
case {"type": "click", "x": x, "y": y}:
handle_click(x, y)
case {"type": "key", "key": str(k)} if k.isalpha():
handle_key(k)
case _:
pass22.How do type hints work, and are they enforced?
Type hints annotate expected types for parameters, return values and variables. Python does not enforce them at runtime; static checkers such as mypy or pyright and IDEs use them to catch errors. Python 3.12 introduced the type statement for aliases and a compact generic syntax, such as def first[T](items: list[T]) -> T. Libraries like Pydantic do validate data at runtime using hints.
23.What does the collections module offer?
Counter counts hashable items, defaultdict supplies default values for missing keys, deque is a double-ended queue with O(1) appends and pops on both ends, namedtuple creates lightweight immutable records, and OrderedDict offers move_to_end and popitem(last=False), which are handy for LRU caches. Knowing these often shortens coding interview answers considerably.
24.How do you manage dependencies and virtual environments?
A virtual environment isolates a project's packages from the system Python, created with python -m venv .venv. Dependencies are declared in pyproject.toml and pinned with a lock file. Common tools are pip with pip-tools, Poetry, and uv, which handles environments, installs and Python versions quickly. Mentioning reproducible, locked installs is what interviewers look for.
Advanced Python interview questions
25.What is the GIL and how does it affect concurrency?
The Global Interpreter Lock in CPython allows only one thread to execute Python bytecode at a time. Threads still help for I/O-bound work because the GIL is released while waiting, but CPU-bound threads do not run in parallel. For CPU-bound work use multiprocessing, native extensions that release the GIL (like NumPy), or the free-threaded CPython build, which was experimental in 3.13 and is officially supported, though still optional, in 3.14.
26.When do you use threading, multiprocessing or asyncio?
Use asyncio for many concurrent I/O operations, like thousands of network requests, with a single thread and cooperative scheduling. Use threading for I/O-bound work with blocking libraries that are not async. Use multiprocessing (or concurrent.futures.ProcessPoolExecutor) for CPU-bound work that must use multiple cores. concurrent.futures gives a common interface for threads and processes.
27.How does asyncio work?
async def defines a coroutine; await suspends it until the awaited operation completes, letting the event loop run other coroutines meanwhile. asyncio.run starts the loop, and asyncio.TaskGroup (3.11+) runs tasks concurrently with structured error handling. A single blocking call, such as time.sleep or a synchronous HTTP request, stalls every coroutine, so use async libraries or asyncio.to_thread.
import asyncio
async def main(urls):
async with asyncio.TaskGroup() as tg:
tasks = [tg.create_task(fetch(u)) for u in urls]
return [t.result() for t in tasks]
asyncio.run(main(urls))28.How does Python manage memory?
CPython uses reference counting: each object tracks how many references point to it and is freed immediately when the count drops to zero. A cyclic garbage collector periodically finds reference cycles that counting alone cannot free. Small objects are allocated from pools by pymalloc. Leaks in Python usually come from lingering references, such as global caches or closures, rather than unfreed memory.
29.What are __slots__ and why use them?
Defining __slots__ in a class replaces the per-instance __dict__ with fixed storage for the named attributes. That reduces memory per instance and speeds up attribute access, which matters when you create millions of objects. The trade-off is that you cannot add attributes that are not listed. Dataclasses support this via slots=True.
30.What is a metaclass?
A metaclass is the class of a class: it controls how classes are created, just as classes control how instances are created. type is the default metaclass. Frameworks use metaclasses to register subclasses or build ORM fields, but __init_subclass__ and class decorators cover most of those use cases more simply, which is a good point to make in an interview.
31.What are descriptors?
A descriptor is an object that defines __get__, __set__ or __delete__ and is stored as a class attribute; it customises attribute access on instances. property, classmethod, staticmethod and functions binding to methods are all built on descriptors. They are used for validation, lazy attributes and ORM fields.
32.What is the method resolution order (MRO)?
The MRO is the order Python searches classes for a method in an inheritance hierarchy, computed with the C3 linearisation algorithm. You can inspect it with ClassName.__mro__. super() follows the MRO rather than simply calling the parent, which is what makes cooperative multiple inheritance work.
33.How do you profile and speed up slow Python code?
Measure first: cProfile or a sampling profiler like py-spy show where time goes, and timeit benchmarks small snippets. Typical fixes are choosing better data structures (a set instead of a list for membership), moving work into built-ins and comprehensions, caching with functools.cache, and vectorising numeric work with NumPy. For hot loops, consider Cython, Rust extensions via PyO3, or PyPy.
Python coding interview questions
34.Reverse a string and check whether it is a palindrome.
Slicing with a step of -1 reverses any sequence. For a palindrome check, normalise first by keeping only alphanumeric characters and lowercasing. A two-pointer loop avoids the extra copy if memory matters.
def is_palindrome(s: str) -> bool:
cleaned = [c.lower() for c in s if c.isalnum()]
return cleaned == cleaned[::-1]35.Two sum: return indices of two numbers that add up to a target.
Store each number's index in a dictionary as you scan, and for each element check whether target - n has already been seen. This is O(n) time and O(n) space, compared with O(n²) for checking every pair.
def two_sum(nums: list[int], target: int) -> tuple[int, int] | None:
seen: dict[int, int] = {}
for i, n in enumerate(nums):
if target - n in seen:
return seen[target - n], i
seen[n] = i
return None36.Find duplicates in a list.
Counter gives counts in one pass; keep items with a count above one. This is O(n). If order of first appearance matters, iterate the original list with a seen set instead.
from collections import Counter
def duplicates(items):
return [x for x, n in Counter(items).items() if n > 1]37.Group anagrams together.
Anagrams share the same sorted letters, so use the sorted string (or a 26-letter count tuple) as a dictionary key. Sorting each word costs O(k log k), so the total is O(n·k log k).
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())38.Implement an LRU cache.
OrderedDict keeps keys in access order: move a key to the end on every get or put and pop the oldest item when capacity is exceeded. Both operations are O(1). In everyday code, functools.lru_cache does this for function results.
from collections import OrderedDict
class LRUCache:
def __init__(self, capacity: int):
self.capacity = capacity
self.data: OrderedDict[int, int] = OrderedDict()
def get(self, key: int) -> int:
if key not in self.data:
return -1
self.data.move_to_end(key)
return self.data[key]
def put(self, key: int, value: int) -> None:
self.data[key] = value
self.data.move_to_end(key)
if len(self.data) > self.capacity:
self.data.popitem(last=False)39.Flatten a nested list of arbitrary depth.
Recurse into lists and yield everything else. Writing it as a generator with yield from keeps memory use low and reads cleanly. Check for list (or a broader iterable) but exclude strings, which are iterable too.
def flatten(items):
for item in items:
if isinstance(item, list):
yield from flatten(item)
else:
yield item
list(flatten([1, [2, [3, [4]]]])) # [1, 2, 3, 4]40.Return the first non-repeating character in a string.
Count characters in one pass, then scan the string again and return the first character with a count of one. Two linear passes give O(n) time; dictionaries preserve insertion order, but scanning the original string makes the intent explicit.
from collections import Counter
def first_unique(s: str) -> str | None:
counts = Counter(s)
return next((c for c in s if counts[c] == 1), None)Preparing for a Python interview
Common questions about getting ready
What Python interview questions are asked for freshers?
Freshers are mostly asked about core data types, the difference between lists, tuples, sets and dictionaries, mutability, comprehensions, functions with *args and **kwargs, exceptions and basic OOP. Expect one or two simple coding problems such as reversing a string, counting characters or two-sum.
How should experienced developers prepare for a Python interview?
Go beyond syntax: be ready to discuss decorators, generators, context managers, the GIL, when to use asyncio versus threads versus processes, memory and performance profiling, packaging, and testing with pytest. System design and framework questions (Django, FastAPI) are common for senior back-end roles.
Which Python version should I use in interviews?
Use a current Python 3 release (3.12 or later). Modern features such as f-string debugging, match/case, the | union type syntax and TaskGroup show you are up to date, but avoid features too new for the interviewer's environment if you are coding in a shared editor.
Are Python coding interview questions mostly algorithms?
For general software roles, yes: arrays, strings, hash maps, two pointers, BFS/DFS and basic dynamic programming. Python's built-ins, such as Counter, defaultdict, deque, heapq and bisect, make many solutions much shorter, so know them well.
How long does it take to prepare for a Python interview?
With working Python knowledge, two to four weeks of focused practice is typical: one week on language concepts from this list, and the rest on timed coding problems and, for senior roles, system design.

