Python Interview Questions: Core Language, Data Structures & Best Practices
Reviewed by Mark Dickie · Last updated
Python is a high-level, interpreted programming language that emphasises readable syntax and supports multiple programming styles, including procedural, object-oriented, and functional. For interviews at any difficulty level, you should be solid on Python's data model (how __dunder__ methods work), mutability rules, the GIL and its concurrency implications, and the differences between Python's built-in data structures. Interviewers at top companies lean hard on "why did you choose this structure over that one" — so knowing the trade-offs matters more than memorising syntax.
What interviewers actually test
Python interviews vary by role, but the subject matter follows a recognisable pattern across difficulty bands:
| Difficulty | Typical focus areas | Example topics |
|---|---|---|
| 1 — Beginner | Built-in types, control flow | Lists vs. tuples, range, string methods |
| 2 — Elementary | Functions, comprehensions | *args/**kwargs, list/dict/set comprehensions |
| 3 — Intermediate | OOP, standard library | Inheritance vs. composition, collections, decorators |
| 4 — Advanced | Internals, concurrency | GIL, asyncio, generators, memory management |
| 5 — Expert | CPython, performance | Bytecode, __slots__, profiling, C extensions |
How to prepare systematically
Working through questions in difficulty order helps, but don't skip straight to hard problems before you can explain the basics clearly — interviewers notice when foundations are shaky.
- Lock down the data model first: understand how Python resolves attribute lookup (MRO), what makes an object hashable, and why mutating a default argument is a classic bug.
- Practise writing generators and context managers from scratch, not just recognising them in code.
- Study the
collectionsmodule —defaultdict,Counter,deque, andOrderedDictcome up far more than most candidates expect. - Understand threading vs. multiprocessing vs.
asyncio: know which to reach for given I/O-bound vs. CPU-bound work, and why the GIL matters in each case. - Read at least one real piece of Python source code (e.g.
functools.lru_cache) before the interview — it signals genuine curiosity and gives you concrete examples to discuss.
The quiz below covers the full range from difficulty 1 through 5, so you can warm up on straightforward questions and keep going until the problems genuinely stretch you.
At a glance
| Questions | 15 |
|---|---|
| Difficulty | 2–5 of 5 |
| Formats | Multiple choice, Multiple answer, True / false, Code output |
What you'll review
- mutability identity
- mutable default args
- gil
- generators
- classmethod staticmethod
- asyncio
- mro
- numeric model
- truthiness
- context managers
- threading multiprocessing
- string handling
- comprehensions
- dict set
- slicing
Practice questions
Python/data-model/mutability-identity
Given a = [1, 2] and b = [1, 2], what are a == b and a is b?
Options
TrueandFalseTrueandTrueFalseandFalseFalseandTrue
Show answer
a == b is True and a is b is False. The == operator compares values, and both lists hold the same elements, so it is True. The is operator compares identity — whether two names point at the same object in memory — and because these are two separately-constructed lists, it is False.
== compares values, and the two lists hold the same elements, so it is True. is compares identity — whether both names point at the same object in memory — and these are two separately-constructed lists, so it is False.
Python/functions-scope/mutable-default-args
What does the second call return? def add(x, acc=[]): acc.append(x) return acc add(1) print(add(2))
Options
[1, 2][2][1]- It raises a
TypeError
Show answer
The second call returns [1, 2]. A default argument is evaluated only once, when the function is defined, so every call that omits acc shares the same list. The first call appends 1 and the second appends 2 to that same list. The fix is to default acc=None and create a fresh list inside the function.
A default argument is evaluated once, when the function is defined, so all calls that omit acc share the same list. The first call appends 1, the second appends 2 to that same list, giving [1, 2]. The fix is acc=None plus if acc is None: acc = [].
Python/concurrency/gil
In CPython, what is the practical effect of the Global Interpreter Lock (GIL) on a pure-Python, CPU-bound workload?
Options
- Only one thread executes Python bytecode at a time, so threads do not speed up CPU-bound work — use multiprocessing for true parallelism
- Threads run Python bytecode on all cores in parallel, so the GIL has no effect on CPU-bound speed
- The GIL prevents I/O-bound threads from ever overlapping their waits
- The GIL makes every operation on built-in types fully thread-safe regardless of how it is used
Show answer
Only one thread executes Python bytecode at a time, so threads do not speed up CPU-bound work — the work is serialized. The GIL is released during blocking I/O, so threads still help I/O-bound work, but for true CPU parallelism you reach for multiprocessing, which runs separate interpreters in separate processes.
The GIL lets only one thread run Python bytecode at any instant, so threading gives no speedup on CPU-bound Python code — the work is serialized. Threads still help for I/O-bound work because the GIL is released during blocking I/O, but for CPU parallelism you reach for multiprocessing (separate interpreters) instead.
Python/iteration/generators
What is the key difference between [x*x for x in range(10**9)] and (x*x for x in range(10**9))?
Options
- The generator expression produces values lazily one at a time, using O(1) memory; the list comprehension builds all 10**9 values up front
- They are identical — both build a full list, just with different syntax
- The generator is faster because it stores results in a tuple instead of a list
- The list comprehension is lazy and the generator is eager
Show answer
The generator expression produces values lazily, one at a time, using O(1) memory, whereas the list comprehension builds all 10**9 values up front. Square brackets eagerly materialize every element into a list, while parentheses create a generator that computes each value on demand and holds only the current state. The trade-off is that a generator is single-pass with no len() or indexing.
Square brackets eagerly materialize every element into a list, so the comprehension here would try to allocate a billion integers. Parentheses create a generator that computes each value on demand and yields it once, holding only the current state — constant memory. The trade-off is that a generator is single-pass and has no len() or indexing.
Python/oop/classmethod-staticmethod
What distinguishes a @classmethod from a @staticmethod?
Options
- A classmethod receives the class as an implicit first argument (
cls); a staticmethod receives no implicit first argument at all - A staticmethod receives the instance (
self); a classmethod receives nothing - A classmethod can be called on the class but a staticmethod can only be called on an instance
- There is no difference; the two decorators are aliases
Show answer
A @classmethod receives the class as an implicit first argument (cls), while a @staticmethod receives no implicit first argument at all. The cls binding lets a classmethod reference class attributes, act as an alternate constructor, and respect subclassing; a staticmethod is just a plain function namespaced on the class. Both can be called via the class or an instance.
@classmethod binds the class to an implicit first parameter cls, which is why it can reference class attributes or act as an alternate constructor and respects subclassing. @staticmethod gets no implicit argument — it is just a plain function namespaced on the class. Both can be called via the class or an instance.
Python/concurrency/asyncio
How does asyncio achieve concurrency for many coroutines on a single thread?
Options
- Cooperative multitasking on one event loop: a coroutine runs until it hits
await, then yields control so the loop can run others while it waits - It transparently spawns one OS thread per coroutine and pre-empts them on a timer
- It runs each coroutine in a separate process to bypass the GIL
- It blocks on each coroutine in turn, running them strictly sequentially with no overlap
Show answer
asyncio uses cooperative multitasking on a single event loop: a coroutine runs until it hits await, then yields control so the loop can run other ready tasks while it waits. There is no pre-emption and no extra threads or processes, so a CPU-bound coroutine that never awaits will starve the loop.
asyncio is single-threaded cooperative multitasking: one event loop runs a coroutine until it awaits something not yet ready, at which point the coroutine yields control and the loop advances another ready task. There is no pre-emption and no extra threads, so a CPU-bound coroutine that never awaits will starve the loop.
Python/oop/mro
Given this diamond, what does D().who() return? class A: def who(self): return "A" class B(A): def who(self): return "B" class C(A): def who(self): return "C" class D(B, C): pass
Options
"B""C""A"- It raises a
TypeErrorfor ambiguous inheritance
Show answer
D().who() returns "B". Python resolves attributes along the C3-linearized method resolution order, which for D(B, C) is D -> B -> C -> A -> object. The first class on that chain that defines who is B, so its version wins. There is no TypeError — the MRO resolves the diamond unambiguously.
Python resolves attributes along the C3-linearized method resolution order, which for D(B, C) is D -> B -> C -> A -> object. The first class on that chain defining who is B, so D().who() returns "B". The MRO is what makes cooperative super() calls work without re-running shared bases.
Python/data-model/numeric-model
What does isinstance(True, int) return, and why?
Options
True—boolis a subclass ofint, soTruebehaves as1andFalseas0False—boolandintare unrelated typesFalse—intis a subclass ofbool, not the other way around- It raises a
TypeErrorbecauseTrueis not a class instance
Show answer
isinstance(True, int) returns True, because bool is defined as a subclass of int in Python. As a result True behaves as 1 and False as 0 in arithmetic — True + True is 2. This is why isinstance(x, int) accidentally accepts booleans, and why summing a list of bools counts the True values.
bool is defined as a subclass of int in Python, so isinstance(True, int) is True and booleans participate in arithmetic — True + True is 2. This is why isinstance(x, int) accidentally accepts booleans, and why summing a list of bools counts the True values.
Python/data-model/truthiness
Which of these values are falsy (i.e. bool(value) is False)? Select all that apply.
Options
[]0"""0"[0]
Show answer
The falsy values are the empty list [], the number zero 0, and the empty string — but not the string "0" or the list [0]. Truthiness depends on emptiness, not on the contents being zero-like, so a non-empty string "0" and a non-empty list [0] are both truthy even though they contain a zero.
Empty containers ([]), the number zero (0), and the empty string ("") are all falsy. The string "0" is a non-empty string and the list [0] is a non-empty list — both are truthy, because truthiness depends on emptiness, not on the contents being zero-like.
Python/context-resources/context-managers
Which statements about the with statement and the context-manager protocol are true? Select all that apply.
Options
- An object is a context manager if it defines
__enter__and__exit__ __exit__runs even when thewithbody raises an exception- Returning a truthy value from
__exit__suppresses the exception @contextlib.contextmanagerlets you write one without__exit__, using a generator thatyields once
Show answer
All four statements are true. An object is a context manager if it defines __enter__ and __exit__; __exit__ runs even when the body raises, which is what makes with reliable for cleanup; returning a truthy value from __exit__ suppresses the exception; and @contextlib.contextmanager lets you write one with a single-yield generator instead of those methods.
The protocol is exactly __enter__/__exit__. __exit__ is guaranteed to run on the way out — normal or exceptional — which is what makes with reliable for cleanup; if it returns a truthy value the propagating exception is suppressed. @contextlib.contextmanager wraps a single-yield generator: code before the yield is the enter, code after (in a finally) is the exit.
Python/concurrency/threading-multiprocessing
For which workloads is multiprocessing (rather than threading or asyncio) the right tool in CPython? Select all that apply.
Options
- A CPU-bound numeric computation you want to spread across multiple cores
- Pure-Python work where the GIL would otherwise serialize threads
- Thousands of concurrent network requests that spend most of their time waiting on I/O
- Sharing a large mutable in-memory object across workers with zero copying or serialization cost
Show answer
Multiprocessing is the right tool for CPU-bound numeric work you want to spread across cores and for pure-Python work the GIL would otherwise serialize — but not for thousands of I/O-bound network requests, which suit asyncio, nor for sharing a large mutable object cheaply. Each process has its own GIL, so data crosses process boundaries via pickling and IPC at real cost.
Multiprocessing runs separate interpreters in separate processes, each with its own GIL, so it delivers real parallelism for CPU-bound Python work. It is the wrong choice for high-concurrency I/O — that is asyncio's domain (far cheaper than thousands of processes) — and it does not give cheap shared mutable state: data crosses process boundaries via pickling/IPC, which has real copy and serialization cost.
Python/testing-idioms/string-handling
Python strings are immutable, so s[0] = 'z' raises a TypeError.
Show answer
True. Python strings are immutable, so there is no in-place item assignment and s[0] = 'z' raises a TypeError reporting that a str object does not support item assignment. Methods like s.replace(...) return a brand-new string rather than mutating the original.
str is immutable — there is no in-place item assignment, so s[0] = 'z' raises TypeError: 'str' object does not support item assignment. Methods like s.replace(...) return a brand-new string rather than mutating the original.
Python/iteration/comprehensions
In Python 3, the loop variable of a list comprehension leaks into the enclosing scope, overwriting a same-named outer variable.
Show answer
False. In Python 3 a list comprehension runs in its own implicit scope, so its loop variable does not leak and an outer x keeps its value after [x for x in range(3)]. This leaking behaviour was true in Python 2, where comprehensions shared the surrounding scope, but it was deliberately fixed.
In Python 3 a comprehension runs in its own implicit scope, so its loop variable does not leak — an outer x keeps its value after [x for x in range(3)]. This was true in Python 2, where comprehensions shared the surrounding scope, but it was deliberately fixed.
Python/pythonic-dsa/dict-set
As of Python 3.7, a regular dict preserves insertion order when iterated.
Show answer
True. As of Python 3.7 a regular dict preserving insertion order is a guaranteed language feature, not just a CPython implementation detail. Iterating a dict, its keys(), values(), or items() yields entries in the order they were first inserted, so OrderedDict is now needed only for its extra ordering methods.
Ordered iteration by insertion became a guaranteed language feature in Python 3.7 (it was a CPython implementation detail in 3.6). Iterating a dict, its keys(), values(), or items() yields entries in the order they were first inserted, so OrderedDict is now needed only for its extra ordering methods.
Python/testing-idioms/slicing
What does this print, one value per line?
s = "abcdef"
print(s[::-1])
print(s[1:4])
print(s[-2:])Show answer
fedcba
bcd
ef
s[::-1] walks the string with step -1, reversing it to "fedcba". s[1:4] takes indices 1, 2, 3 (stop is exclusive) giving "bcd". s[-2:] starts two from the end and runs to the end, giving "ef".
Sources
The official documentation these questions are checked against:
Related interview questions
Job market
See python salaries and hiring demand from live job postings.
Practice this for real
CodePrep turns your target job description into an adaptive quiz from a bank of tagged questions, scores your answers, and resurfaces the topics you miss.