Data Types
Lists, tuples, strings, sets, and dicts — mutability, slicing, comprehensions.
34 questions
JuniorTheoryVery commonWhat is the difference between a list and a tuple in Python?
What is the difference between a list and a tuple in Python?
A list is mutable — you append, insert, or remove; a tuple is immutable once created. They also differ in CPython: list over-allocates so append is amortized O(1), while tuple is fixed-size, more compact, and cached via free lists.
Common mistakes
- ✗Believing list and tuple are identical under the hood — CPython stores and optimizes them differently
- ✗Thinking a tuple is fully immutable even when it holds a mutable element like a list
- ✗Assuming tuples are always faster — the real win is memory and hashability, not blanket speed
Follow-up questions
- →Can a tuple be used as a dict key? What's the catch with nested mutables?
- →Why does
listover-allocate instead of growing by one slot per append?
JuniorTheoryVery commonWhat is a mapping in Python, and what is the canonical example?
What is a mapping in Python, and what is the canonical example?
A mapping gives keyed access to values via methods like get, keys, values, items. The canonical one is dict; the stdlib adds defaultdict, OrderedDict, Counter. Keys must be hashable; values may be anything.
Common mistakes
- ✗Thinking dict values must be hashable — only keys must be
- ✗Believing
dictkeeps keys sorted; since 3.7 it keeps insertion order, not sorted order - ✗Confusing key access by hash with positional access by index
Follow-up questions
- →What's the difference between
d[key]andd.get(key)? - →What does
defaultdictadd over a plaindict?
JuniorTheoryVery commonWhat is a set, and how does frozenset differ from it?
What is a set, and how does frozenset differ from it?
A set is an unordered collection of unique, hashable elements with no indexing or slicing, with fast membership and union/intersection/difference. set is mutable (add, remove); frozenset is its immutable, hashable variant for a dict key.
Common mistakes
- ✗Expecting a set to preserve insertion order or support indexing — it does neither
- ✗Trying to put a
listordictinto a set — elements must be hashable - ✗Forgetting that only
frozenset(notset) can be a dict key or set member
Follow-up questions
- →Why must set elements be hashable?
- →When would you reach for
frozensetoverset?
JuniorTheoryVery commonHow does slicing s[start:stop:step] work on a sequence?
How does slicing s[start:stop:step] work on a sequence?
It returns a new sub-sequence from start (inclusive) up to stop (exclusive), taking every step-th item. All three are optional; negative indices count from the end and s[:] makes a shallow copy. Out-of-range bounds are clamped, not errors.
Common mistakes
- ✗Expecting
stopto be inclusive - ✗Assuming an out-of-range bound raises instead of being clamped
- ✗Thinking
s[:]is a deep copy rather than a shallow one
Follow-up questions
- →What does a negative step like
s[::-1]produce, and why? - →How does slice assignment
s[1:3] = [...]differ from single-item assignment?
JuniorTheoryVery commonCan you change a single character in a Python str in place?
Can you change a single character in a Python str in place?
No — str is immutable. s[0] = 'x' raises TypeError; every "edit" like replace or + builds a brand-new string object and rebinds the name, leaving the original untouched. Immutability is what makes strings hashable and valid dict keys.
Common mistakes
- ✗Trying
s[i] = cand expecting it to work like a list - ✗Thinking
s.replace(...)mutatessin place instead of returning a new string - ✗Building strings with
+=in a tight loop, unaware each step allocates a new object
Follow-up questions
- →Why does repeated
+=on a string in a loop scale poorly, and what is the fix? - →How does string immutability let CPython safely intern and cache literals?
MiddleTheoryVery commonWhat can and cannot be used as a dict key, and why?
What can and cannot be used as a dict key, and why?
Any hashable object can be a key: int, str, a hashable tuple, even a function or module. list, dict, set cannot — they're mutable and unhashable. A tuple's hash is recursive, so a tuple holding a dict inside is itself unhashable.
Common mistakes
- ✗Thinking every tuple is a valid key — a tuple containing a
list/dictis unhashable - ✗Assuming any collection is barred as a key — a
frozensetis hashable and works fine - ✗Confusing immutability with hashability for the key requirement
Follow-up questions
- →Why is
(1, [2])not a valid dict key? - →How does giving a class an
__eq__affect its usability as a key?
JuniorTheoryCommonWhat is a sequence in Python, and which built-in types are sequences?
What is a sequence in Python, and which built-in types are sequences?
A sequence is an ordered iterable with integer-index access via __getitem__ and a length via __len__. Built-ins: list, tuple, range, str, bytes. It supports indexing, slicing, in, len(), and iteration.
Common mistakes
- ✗Confusing 'sequence' with 'iterable' — sets and dicts are iterable yet not sequences
- ✗Assuming a sequence must be mutable —
tuple,str,rangeare immutable sequences - ✗Forgetting that
strandbytesare sequences, not opaque text blobs
Follow-up questions
- →How does slicing with a negative step like
s[::-1]work? - →What must
__getitem__accept for an object to support slicing?
JuniorCodeCommonWhat do these or / and expressions print?
What do these or / and expressions print?
(1) default — or returns the first truthy operand (an empty list is falsy). (2) 5 — the first truthy value. (3) '' — and returns the first falsy operand, or the last if all are truthy. or/and return one of the operands, not a bool — the basis of x = val or default.
Common mistakes
- ✗Believing
or/andalways return a boolean - ✗Thinking they always return the first operand regardless of truthiness
- ✗Not knowing the
x = val or defaultidiom relies on this
Follow-up questions
- →Why does short-circuit evaluation return an operand rather than a bool?
- →What is the risk of
x = val or defaultwhenvalcan be0or''?
JuniorTheoryCommonHow do a, b, c = seq and starred *rest unpacking work?
How do a, b, c = seq and starred *rest unpacking work?
The right side is iterated and bound positionally to the targets; the counts must match or you get a ValueError. A single starred target like a, *rest = seq absorbs the surplus into a list, so it can sit anywhere. It works on any iterable.
Common mistakes
- ✗Forgetting a count mismatch raises
ValueError, not a silent truncation - ✗Thinking
*restcollects into atupleinstead of alist - ✗Believing the starred target must be last rather than allowed anywhere
Follow-up questions
- →What does
a, *b, c = range(5)bind to each name? - →How does unpacking make the swap
a, b = b, awork without a temp?
JuniorTheoryCommonWhat do zip, enumerate, and range produce when iterating?
What do zip, enumerate, and range produce when iterating?
range(n) yields integers 0..n-1 lazily; enumerate(seq) yields (index, item) pairs; zip(a, b) yields tuples pairing elements until the shortest input ends. All three are lazy iterators in Python 3 — wrap in list() to materialize. zip(*rows) transposes.
Common mistakes
- ✗Expecting these to return lists rather than lazy iterators in Python 3
- ✗Thinking
zippads to the longest input instead of stopping at the shortest - ✗Assuming
enumeratestarts at 1 or thatrange(n)includesn
Follow-up questions
- →How do you make
enumeratestart counting from 1? - →What does
zip(*matrix)do, and why does it transpose?
MiddleTheoryCommonWhat do dict.items(), keys(), and values() return in Python 3?
What do dict.items(), keys(), and values() return in Python 3?
They return dynamic view objects, not lists. A view reflects later changes to the dict live, supports len(), iteration, and in, and the keys/items views behave set-like. Python 2 returned fresh lists here; Python 3 made views the default.
Common mistakes
- ✗Thinking
keys()returns a list — it's a live view in Python 3 - ✗Editing a dict while iterating its view — raises
RuntimeError - ✗Assuming a view is a one-shot iterator; it's re-iterable and supports
len()
Follow-up questions
- →Why might iterating a view while mutating the dict raise
RuntimeError? - →How are the keys and items views 'set-like'?
MiddleTheoryCommonWhat makes an object hashable in Python?
What makes an object hashable in Python?
An object is hashable if it has a stable __hash__ (constant over its lifetime) plus __eq__, and equal objects must hash equally. Immutable built-ins (int, str, a hashable tuple) qualify; mutable list, dict, set do not.
Common mistakes
- ✗Thinking any object with
__eq__is hashable — defining__eq__without__hash__makes it unhashable - ✗Believing a tuple is always hashable — it isn't if it contains a mutable element
- ✗Assuming mutable objects can be hashed as long as you avoid changing them
Follow-up questions
- →What happens to hashability if you define
__eq__but not__hash__? - →Why must equal objects have equal hashes?
MiddleCodeCommonWhat does this is vs == integer test print?
What does this is vs == integer test print?
True False then True True. CPython caches small integers from −5 to 256, so a and b are the same object (is → True); 257 is outside that range, so c and d are distinct objects (is → False). == compares value and is always True. Use is only for identity (e.g. x is None).
Common mistakes
- ✗Using
isto compare integer values instead of== - ✗Believing equal integers are always the same object
- ✗Thinking
==delegates tois
Follow-up questions
- →Why does CPython cache the integers from −5 to 256?
- →When is using
isthe correct choice instead of==?
MiddleTheoryCommonWhy is lookup faster in dict/set than in list/tuple?
Why is lookup faster in dict/set than in list/tuple?
dict and set are hash tables: lookup averages O(1) — Python hashes the key and jumps to a slot. list and tuple have no value index, so x in seq scans linearly at O(n). The trade-off: hashing needs hashable elements and more memory.
Common mistakes
- ✗Calling dict lookup O(log n) — it's average O(1) via hashing, not a tree search
- ✗Assuming a list
incheck is cheap — it's an O(n) linear scan - ✗Forgetting hash tables trade memory and hashability for that speed
Follow-up questions
- →What is the worst-case complexity of a dict lookup, and when does it occur?
- →Why can't you do an O(1) membership test on a
list?
MiddleTheoryCommonHow does Python compare two sequences with < and ==?
How does Python compare two sequences with < and ==?
Element by element, left to right (lexicographic). == is True only when lengths match and every pair is equal. For <, Python compares items at the first difference; if one side runs out first, the shorter is smaller. Items must be comparable or it raises TypeError.
Common mistakes
- ✗Thinking longer sequences are automatically greater
- ✗Assuming
==compares identity rather than element-wise value - ✗Expecting cross-type element comparison to coerce instead of raising
TypeError
Follow-up questions
- →How does
(1, 2) < (1, 2, 3)resolve when the shorter is a prefix of the longer? - →Why can comparing
[1, 'a']with[1, 2]raise only sometimes?
MiddleTheoryCommonHow does str differ from bytes, and what do encode/decode do?
How does str differ from bytes, and what do encode/decode do?
str is an immutable sequence of Unicode code points; bytes is an immutable sequence of raw 0–255 octets. encode turns a str into bytes through a codec like UTF-8; decode reverses it. Mixing them, or guessing the wrong codec, raises UnicodeError — Python 3 does no implicit conversion.
Common mistakes
- ✗Confusing the direction of
encode(str→bytes) anddecode(bytes→str) - ✗Expecting implicit str/bytes conversion like Python 2 did
- ✗Forgetting a wrong codec raises
UnicodeDecodeErrorrather than silently producing mojibake
Follow-up questions
- →Why does
'café'.encode('ascii')raise, and how do error handlers like'ignore'change that? - →How many bytes does a non-ASCII character take in UTF-8 versus UTF-16?
JuniorCodeOccasionalWhat does print(True + 4) output, and why?
What does print(True + 4) output, and why?
Prints 5, 2, 2. In Python bool is a subclass of int, with True == 1 and False == 0, so booleans participate directly in arithmetic. True + 4 is 1 + 4, and sum of booleans counts the True values. This is why summing a list of conditions counts how many are true.
Common mistakes
- ✗Thinking
boolis unrelated tointand cannot do arithmetic - ✗Expecting
True + 4to raise aTypeError - ✗Forgetting
sumof booleans counts theTruevalues
Follow-up questions
- →Why can
Trueand1collapse to one key in adict? - →What does
isinstance(True, int)return, and why?
JuniorCodeOccasionalWhat do these chained comparisons print?
What do these chained comparisons print?
All three print True. Python chains comparisons with an implicit and: (1) is 1 < 2 and 2 < 3; (2) is 3 > 2 and 2 == 2; (3) is (False == False) and (False in [False]) → True and True. The third surprises people because == and in chain together.
Common mistakes
- ✗Reading
a < b < cas(a < b) < cinstead of an implicitand - ✗Not realizing
==andinchain together in one expression - ✗Expecting a chained comparison to return an operand rather than a bool
Follow-up questions
- →How does Python evaluate the middle operand of a chain exactly once?
- →Why is
False == False in [False]not(False == False) in [False]?
JuniorTheoryOccasionalWhat is a hash collision?
What is a hash collision?
A hash collision is two different keys producing the same hash value, or landing in the same table slot. dict and set resolve it by probing other slots, so a collision costs lookup speed, not correctness — it never returns a wrong value.
Common mistakes
- ✗Thinking collisions make a dict return wrong values — they only add probing overhead
- ✗Believing built-in types never collide
- ✗Confusing equal hashes with equal keys — equal hash doesn't mean equal key
Follow-up questions
- →How does CPython's dict pick the next slot after a collision?
- →What happens to dict performance as the load factor rises?
JuniorCodeOccasionalAfter b = a on a dict, what do both print after edits?
After b = a on a dict, what do both print after edits?
Both print {1: 1, 2: 2, 3: 3}. b = a binds a second name to the same dict object — it does not copy it, so edits through either name are visible through both. To get an independent dict use a.copy() or dict(a) (a shallow copy). a is b is True here.
Common mistakes
- ✗Thinking
b = acopies the dict instead of aliasing it - ✗Expecting edits through one name not to affect the other
- ✗Confusing name binding with object duplication
Follow-up questions
- →How would you make
ban independent shallow copy ofa? - →What does
a is breturn here, and what does that test?
JuniorCodeOccasionalWhat does [1, 2, 3].extend('abc') produce, and why?
What does [1, 2, 3].extend('abc') produce, and why?
Prints [1, 2, 3, 'a', 'b', 'c']. extend iterates its argument and appends each item; a string is iterable over its characters, so each char is added separately. To add the whole string as one element use l.append('abc'), which gives [1, 2, 3, 'abc'].
Common mistakes
- ✗Expecting
extendto add the string as one element likeappend - ✗Thinking
extendrejects anything that is not a list - ✗Forgetting a string is iterable over its characters
Follow-up questions
- →How would you append the whole string as a single element instead?
- →What does
[1, 2].extend(3)raise, and why?
JuniorCodeOccasionalConvert a list of one-element tuples into a list of strings
Convert a list of one-element tuples into a list of strings
Take the first element of each tuple: [t[0] for t in data]. A one-element tuple is written ('x',) with a trailing comma, so indexing [0] pulls the single value out. Equivalent forms are [v for (v,) in data] (tuple unpacking in the loop target) or list(map(lambda t: t[0], data)).
Common mistakes
- ✗Forgetting a one-element tuple needs
[0]to unwrap it - ✗Thinking
str(t)cleanly yields the inner value - ✗Confusing the outer list index with the inner tuple index
Follow-up questions
- →How would you flatten tuples that may hold more than one value?
- →Why does
('x')differ from('x',)?
JuniorCodeOccasionalWhat does 0.1 + 0.2 == 0.3 print?
What does 0.1 + 0.2 == 0.3 print?
False. 0.1 + 0.2 is 0.30000000000000004 because these values are not exactly representable in binary floating point, so the sum differs from the literal 0.3. Compare with a tolerance instead — math.isclose(0.1 + 0.2, 0.3).
Common mistakes
- ✗Assuming float literals are stored exactly
- ✗Comparing floats with
==instead of a tolerance - ✗Thinking Python uses decimal arithmetic for
floatby default
Follow-up questions
- →How does
math.isclosedecide whether two floats are equal enough? - →When should you reach for
decimal.Decimalorfractions.Fractioninstead?
JuniorCodeOccasionalHow do you print a literal {} inside an f-string?
How do you print a literal {} inside an f-string?
Double the braces: print(f'Curly brackets: {{}}') prints Curly brackets: {}. In an f-string {{ is an escaped literal { and }} a literal }; a single {...} is a replacement field that must contain an expression, so an empty {} raises a SyntaxError at compile time.
Common mistakes
- ✗Trying to escape braces with a backslash instead of doubling them
- ✗Thinking an empty
{}is allowed and silently skipped - ✗Confusing f-string
{{}}escaping withstr.formatdifferences
Follow-up questions
- →How would you embed
{x}literally next to an interpolatedx? - →When is the
SyntaxErrorfor an empty{}raised — at compile or run time?
JuniorCodeOccasionalWhat does negative indexing print for 'abyz'[-1]?
What does negative indexing print for 'abyz'[-1]?
Prints z then y. A negative index counts from the end: -1 is the last element, -2 the second-to-last. It is equivalent to var[len(var) - 1]. This works on any sequence — str, list, tuple — and an out-of-range negative index raises IndexError.
Common mistakes
- ✗Thinking
-1points at the first element instead of the last - ✗Believing negative indices raise
IndexErrorby default - ✗Forgetting negative indexing works on any sequence, not just strings
Follow-up questions
- →What does the slice
var[-2:]return for this string? - →When does a negative index actually raise
IndexError?
MiddleCodeOccasionalWhat do += and + print for these aliased lists?
What do += and + print for these aliased lists?
[1, 2, 3, 4] then [1, 2, 3]. += on a list calls __iadd__, mutating the object in place, so b (same object) sees it. a = a + [4] builds a new list and rebinds a, leaving b pointing at the original — mutation versus rebinding.
Common mistakes
- ✗Believing
+=and+behave identically on lists - ✗Thinking
b = acopies the list - ✗Not distinguishing in-place mutation from rebinding
Follow-up questions
- →What does
__iadd__return, and why does that matter for the rebinding? - →How would the result differ if
awere a tuple instead of a list?
MiddleCodeOccasionalWhat does [[0]*3]*3 print after a write?
What does [[0]*3]*3 print after a write?
[[1, 0, 0], [1, 0, 0], [1, 0, 0]]. The outer * 3 copies the reference to the same inner list three times, so mutating one row mutates all. Fix with a comprehension that builds independent rows: grid = [[0] * 3 for _ in range(3)].
Common mistakes
- ✗Believing
* 3deep-copies the inner list - ✗Confusing list multiplication with NumPy broadcasting
- ✗Not knowing the comprehension fix for independent rows
Follow-up questions
- →Why does
[[0] * 3 for _ in range(3)]build independent rows? - →Does the same aliasing trap apply to
[0] * 3of immutable ints?
MiddleCodeOccasionalWhat happens with t[0] += [100] on a tuple?
What happens with t[0] += [100] on a tuple?
(1) succeeds → ([1, 99], 2): the tuple is immutable, but the list it references is mutable. (2) is the famous trap: += mutates the list AND tries to reassign t[0], so it raises TypeError — yet the list is still mutated to [1, 99, 100]. The operation half-succeeds.
Common mistakes
- ✗Thinking tuple immutability protects the mutable objects it holds
- ✗Expecting line (2) to leave the list unchanged after the error
- ✗Believing the
TypeErrorrolls back the in-place mutation
Follow-up questions
- →Why does
+=both mutate the list and attempt an assignment tot[0]? - →What does this reveal about whether a tuple is truly 'immutable'?
MiddleDebuggingRareWhy does this average return the wrong value?
Why does this average return the wrong value?
// is floor division, so average([1, 2]) returns 1, not 1.5. An empty list also raises ZeroDivisionError. Fix: use / for true division and guard the empty case: return sum(nums) / len(nums) if nums else 0.
Common mistakes
- ✗Confusing
//(floor) with/(true division) - ✗Thinking
//rounds to nearest rather than toward negative infinity - ✗Forgetting the empty-list ZeroDivisionError
Follow-up questions
- →What does
//do for negative operands like-7 // 2? - →How do you compute an integer average while still rounding correctly?
MiddleDebuggingRareWhy does this word counter raise KeyError?
Why does this word counter raise KeyError?
counts[w] += 1 reads counts[w] before the key exists → KeyError on the first sight of each word. Fix: counts[w] = counts.get(w, 0) + 1, or use collections.defaultdict(int), or simply collections.Counter(text.split()).
Common mistakes
- ✗Assuming
dict[k] += 1auto-initializes a missing key to 0 - ✗Blaming
splitor the return rather than the missing-key read - ✗Not reaching for
defaultdictorCounter
Follow-up questions
- →How does
collections.defaultdict(int)change what happens on a missing key? - →When is
dict.setdefaultpreferable togetfor this pattern?
MiddleDebuggingRareWhy does removing items while iterating skip elements?
Why does removing items while iterating skip elements?
Removing items during iteration shifts the indices under the iterator, so it skips elements. Never mutate a list while iterating it. Fix: iterate over a copy (for n in nums[:]:), or rebuild with a comprehension: nums = [n for n in nums if n % 2].
Common mistakes
- ✗Assuming removal leaves the iterator aligned with the next element
- ✗Expecting a RuntimeError on
remove(that fires for size change, but indices still shift) - ✗Mutating the list in place instead of iterating a copy or rebuilding
Follow-up questions
- →Why does iterating over
nums[:]make the removal safe? - →How does a comprehension avoid mutating the list at all?
MiddleDebuggingRareWhy is this string-building loop slow on large input?
Why is this string-building loop slow on large input?
Strings are immutable, so each s += ... builds a brand-new string — O(n²) for n words, slow on large inputs. It is functionally correct, but the complexity is the bug. Fix: return ' '.join(words), which is O(n) and idiomatic.
Common mistakes
- ✗Believing
+=on a string mutates a buffer in place - ✗Blaming the trailing space or
stripinstead of repeated allocation - ✗Micro-optimizing the concatenation instead of using
join
Follow-up questions
- →Why does string immutability force a new allocation on each
+=? - →When is
io.StringIOor a list-plus-joinpreferable for building text?
SeniorTheoryRareHow does CPython's dict store entries and handle resizing?
How does CPython's dict store entries and handle resizing?
A CPython dict is a sparse hash table; each slot holds the key's hash plus key and value refs. Lookup takes the hash's low bits as a slot offset and probes on collision. About a third of slots stay empty; when full it grows and entries reinsert.
Common mistakes
- ✗Thinking CPython dict uses separate chaining — it uses open addressing with probing
- ✗Believing the table never resizes — it grows once it gets too full
- ✗Assuming the full hash indexes the slot — only the low bits do, initially
Follow-up questions
- →Why keep a third of the slots empty instead of filling the table densely?
- →How did the 3.6 'compact dict' change the layout and add insertion ordering?
SeniorTheoryRareWhat does {True: 'a', 1: 'b', 1.0: 'c'} evaluate to, and why?
What does {True: 'a', 1: 'b', 1.0: 'c'} evaluate to, and why?
It evaluates to {True: 'c'}. True, 1, 1.0 are equal (True == 1 == 1.0) and hash identically, so they're one key. The first inserted key (True) is kept, but each assignment overwrites the value — final value 'c'.
Common mistakes
- ✗Thinking different types mean different keys — equality plus equal hash collapses them
- ✗Believing the surviving key becomes the last one written — it's the first inserted
- ✗Forgetting that the value still updates on each re-assignment to an equal key
Follow-up questions
- →Which object identity does the surviving key have —
Trueor1? - →How could this collapse cause a real bug in a lookup table?