Menu

Earn Premium with Referrals

Invite your friends and earn Premium rewards through our referral program.

See how it works and start inviting friends.

Built-ins & Standard Library
PYTHON

Built-ins & Standard Library

Practice 34 Python questions covering built-in functions, standard library modules, common utilities, and frequently tested Python behavior.

1. Which dictionary method returns a value for a given key if present, or a default value without raising a KeyError?

Answer: dict.get(key, default).

The problem being solved: directly indexing d[key] raises KeyError when the key is missing. get is the safe accessor.

d.get(key) returns the value if the key exists, and None if it doesn’t. With the two-argument form, d.get(key, default) returns default for a missing key. No exception, ever.

The contrast matters for correctness:

d = {"a": 1}
d["b"]          # raises KeyError
d.get("b")      # returns None
d.get("b", 0)   # returns 0

Related but different: setdefault(key, default) inserts the default when the key is absent, then returns the value. And d["b"] = ... is how you write. For reading, get is the tool — the interview answer is dict.get.

Answer:

dict.get(key, default).

The problem being solved: directly indexing d[key] raises KeyError when the key is missing. get is the safe accessor.

d.get(key) returns the value if the key exists, and None if it doesn’t. With the two-argument form, d.get(key, default) returns default for a missing key. No exception, ever.

The contrast matters for correctness:

d = {"a": 1}
d["b"]          # raises KeyError
d.get("b")      # returns None
d.get("b", 0)   # returns 0

Related but different: setdefault(key, default) inserts the default when the key is absent, then returns the value. And d["b"] = ... is how you write. For reading, get is the tool — the interview answer is dict.get.

2. How does copy.deepcopy() differ from copy.copy()?

Answer: copy() makes a shallow copy — a new top-level object whose nested objects are still shared. deepcopy() recursively copies every nested object, so nothing is shared.

The difference shows up with containers inside containers.

A shallow copy, copy.copy(), creates a new container but inserts references to the same child objects. Consider a = [[1, 2], [3, 4]] and b = copy.copy(a). b is a new list, but b[0] is the very same inner list as a[0]. Mutating b[0] changes a[0] too.

A deep copy, copy.deepcopy(), walks the entire structure and duplicates every nested object. b[0] is now an independent list; mutating it leaves a untouched.

The interview answer: shallow = one new layer, inner objects shared; deep = everything duplicated recursively. The choice depends on whether sharing nested objects is acceptable — and deepcopy is also slower, because it does more work.

Answer:

copy() makes a shallow copy — a new top-level object whose nested objects are still shared. deepcopy() recursively copies every nested object, so nothing is shared.

The difference shows up with containers inside containers.

A shallow copy, copy.copy(), creates a new container but inserts references to the same child objects. Consider a = [[1, 2], [3, 4]] and b = copy.copy(a). b is a new list, but b[0] is the very same inner list as a[0]. Mutating b[0] changes a[0] too.

A deep copy, copy.deepcopy(), walks the entire structure and duplicates every nested object. b[0] is now an independent list; mutating it leaves a untouched.

The interview answer: shallow = one new layer, inner objects shared; deep = everything duplicated recursively. The choice depends on whether sharing nested objects is acceptable — and deepcopy is also slower, because it does more work.

3. Which of the following data structures allows O(1) average time complexity for checking element membership?

Answer: set.

The membership check x in container has different costs per structure:

  • list and tuple — stored in order, no hash index. Membership requires a linear scan: O(n).
  • set — backed by a hash table. Each element is hashed to a bucket, so x in my_set is O(1) on average (a fast constant-time probe).
  • deque — a double-ended queue; also O(n) for membership, because it has no hash index.

The trade-off: a set is unordered and holds only unique elements. When you need fast membership and order doesn’t matter, a set is the right tool. (A dict is the same hash-table story, with keys instead of elements.)

The interview answer: set, whose hash-based membership is O(1) on average.

Answer:

set.

The membership check x in container has different costs per structure:

  • list and tuple — stored in order, no hash index. Membership requires a linear scan: O(n).
  • set — backed by a hash table. Each element is hashed to a bucket, so x in my_set is O(1) on average (a fast constant-time probe).
  • deque — a double-ended queue; also O(n) for membership, because it has no hash index.

The trade-off: a set is unordered and holds only unique elements. When you need fast membership and order doesn’t matter, a set is the right tool. (A dict is the same hash-table story, with keys instead of elements.)

The interview answer: set, whose hash-based membership is O(1) on average.

4. What does the zip() function do when passed sequences of unequal lengths?

Answer: By default, zip() truncates — it stops as soon as the shortest input is exhausted.

zip(a, b, ...) pairs up the elements position by position: the first element of each, then the second of each, and so on. When one sequence runs out, there’s nothing left to pair, so zip simply stops.

zip([1, 2, 3], "ab")     # yields (1, 'a'), (2, 'b') — stops there

The unpaired 3 is dropped silently. This default is usually what you want, but it can hide bugs when you expect equal lengths.

Python 3.10 added strict=True as an opt-in safety net: zip(a, b, strict=True) raises ValueError if the lengths differ, which catches mismatched data instead of silently truncating.

The interview answer: default behavior is truncation at the shortest input. strict=True (3.10+) turns a length mismatch into an error.

Answer:

By default, zip() truncates — it stops as soon as the shortest input is exhausted.

zip(a, b, ...) pairs up the elements position by position: the first element of each, then the second of each, and so on. When one sequence runs out, there’s nothing left to pair, so zip simply stops.

zip([1, 2, 3], "ab")     # yields (1, 'a'), (2, 'b') — stops there

The unpaired 3 is dropped silently. This default is usually what you want, but it can hide bugs when you expect equal lengths.

Python 3.10 added strict=True as an opt-in safety net: zip(a, b, strict=True) raises ValueError if the lengths differ, which catches mismatched data instead of silently truncating.

The interview answer: default behavior is truncation at the shortest input. strict=True (3.10+) turns a length mismatch into an error.

5. What does sys.getrefcount(obj) return for a newly assigned object variable?

Answer: 2 — one reference from the variable, plus one more from the temporary reference created by passing the object into the function.

sys.getrefcount(obj) reports the number of references to an object. But the count is inflated: to even call the function, Python passes obj as an argument, and that call itself creates a temporary reference that exists for the duration of the call.

So for a freshly created object with exactly one variable referencing it:

  • 1 reference from the variable.
  • +1 reference from the argument being passed into getrefcount.

The function reports 2, not 1.

The practical lesson: the absolute number from getrefcount is always at least 1 higher than the “true” count you’d expect, precisely because of the call’s own reference. It’s a debugging tool for understanding reference counting, not an exact census — expect the temporary reference to skew it upward.

Answer:

2 — one reference from the variable, plus one more from the temporary reference created by passing the object into the function.

sys.getrefcount(obj) reports the number of references to an object. But the count is inflated: to even call the function, Python passes obj as an argument, and that call itself creates a temporary reference that exists for the duration of the call.

So for a freshly created object with exactly one variable referencing it:

  • 1 reference from the variable.
  • +1 reference from the argument being passed into getrefcount.

The function reports 2, not 1.

The practical lesson: the absolute number from getrefcount is always at least 1 higher than the “true” count you’d expect, precisely because of the call’s own reference. It’s a debugging tool for understanding reference counting, not an exact census — expect the temporary reference to skew it upward.

6. What is the result of all([]) vs any([])?

Answer: all([]) is True; any([]) is False.

These are the “vacuous truth” cases — what do you get when there are no elements to check?

  • all(iterable) returns True if every element is truthy. With no elements, there is nothing to violate that claim, so the result is True. It’s the same logic behind the convention that a product over an empty set is 1 — nothing contradicting the condition.
  • any(iterable) returns True if at least one element is truthy. With no elements, there can be no truthy element, so the result is False.

These defaults are chosen so that the functions behave consistently with their mathematical counterparts: all is a universal quantifier (vacuous truth), any is an existential quantifier (empty set → false).

The interview answer: True for all([]), False for any([]).

Answer:

all([]) is True; any([]) is False.

These are the “vacuous truth” cases — what do you get when there are no elements to check?

  • all(iterable) returns True if every element is truthy. With no elements, there is nothing to violate that claim, so the result is True. It’s the same logic behind the convention that a product over an empty set is 1 — nothing contradicting the condition.
  • any(iterable) returns True if at least one element is truthy. With no elements, there can be no truthy element, so the result is False.

These defaults are chosen so that the functions behave consistently with their mathematical counterparts: all is a universal quantifier (vacuous truth), any is an existential quantifier (empty set → false).

The interview answer: True for all([]), False for any([]).

7. What is the function of the pass keyword in Python?

Answer: pass is a null statement — a syntactic placeholder that does nothing.

Python requires an indented block after certain constructs (if, for, while, def, class, try, …). Sometimes you need the structure but have nothing to put in it yet — stubbing out a class or function, or deliberately doing nothing in an exception handler.

def not_implemented_yet():
    pass

class Placeholder:
    pass

pass fills the block syntactically while executing zero operations. Execution simply moves on.

Contrast with its relatives:

  • continue skips to the next iteration of a loop.
  • break exits the loop entirely.
  • pass does literally nothing — no jump, no exit.

The interview answer: pass is a no-op used where syntax requires a statement but no action is wanted.

Answer:

pass is a null statement — a syntactic placeholder that does nothing.

Python requires an indented block after certain constructs (if, for, while, def, class, try, …). Sometimes you need the structure but have nothing to put in it yet — stubbing out a class or function, or deliberately doing nothing in an exception handler.

def not_implemented_yet():
    pass

class Placeholder:
    pass

pass fills the block syntactically while executing zero operations. Execution simply moves on.

Contrast with its relatives:

  • continue skips to the next iteration of a loop.
  • break exits the loop entirely.
  • pass does literally nothing — no jump, no exit.

The interview answer: pass is a no-op used where syntax requires a statement but no action is wanted.

8. What is the main characteristic of a Python set element?

Answer: Elements must be hashable and unique.

A set is implemented with a hash table. That design choice dictates two properties of its elements:

  • Hashable — each element must have a stable hash value so the table can place it. Mutable types like lists and dicts are unhashable and can’t go in a set. Immutable types — int, str, float, tuple (if it contains only hashables) — qualify.
  • Unique — the hash table can’t store the same element twice. Adding an element that’s already present is a silent no-op. This is the set’s whole point: deduplication.

Two consequences follow. Sets are unordered — hash placement determines position, and iteration order isn’t meaningful. And because of hashing, membership checks are O(1) on average, unlike the O(n) linear scan of a list.

The interview answer: set elements must be hashable and unique; sets are unordered and offer fast O(1) membership.

Answer:

Elements must be hashable and unique.

A set is implemented with a hash table. That design choice dictates two properties of its elements:

  • Hashable — each element must have a stable hash value so the table can place it. Mutable types like lists and dicts are unhashable and can’t go in a set. Immutable types — int, str, float, tuple (if it contains only hashables) — qualify.
  • Unique — the hash table can’t store the same element twice. Adding an element that’s already present is a silent no-op. This is the set’s whole point: deduplication.

Two consequences follow. Sets are unordered — hash placement determines position, and iteration order isn’t meaningful. And because of hashing, membership checks are O(1) on average, unlike the O(n) linear scan of a list.

The interview answer: set elements must be hashable and unique; sets are unordered and offer fast O(1) membership.

9. What is the result of executing eval(“2 + 3 * 4”)?

Output: 14

eval() takes a string, parses it as a Python expression, and evaluates it.

The expression is 2 + 3 * 4. It follows normal Python operator precedence: multiplication binds tighter than addition, so it’s 2 + (3 * 4) = 2 + 12 = 14.

The output is 14 — the integer result, not the string "2 + 3 * 4".

The deeper point is a warning: eval() runs arbitrary code from a string. If the input isn’t trusted, this is a security hole — eval("__import__('os').system('rm -rf /')") would do exactly what it says. For evaluating user-supplied arithmetic, use ast.literal_eval (safe, literal-only) or parse the expression yourself. The interview answer is 14, with the mental note that eval on untrusted strings is dangerous.

Answer:

14

eval() takes a string, parses it as a Python expression, and evaluates it.

The expression is 2 + 3 * 4. It follows normal Python operator precedence: multiplication binds tighter than addition, so it’s 2 + (3 * 4) = 2 + 12 = 14.

The output is 14 — the integer result, not the string "2 + 3 * 4".

The deeper point is a warning: eval() runs arbitrary code from a string. If the input isn’t trusted, this is a security hole — eval("__import__('os').system('rm -rf /')") would do exactly what it says. For evaluating user-supplied arithmetic, use ast.literal_eval (safe, literal-only) or parse the expression yourself. The interview answer is 14, with the mental note that eval on untrusted strings is dangerous.

10. What is the Global Interpreter Lock (GIL) in CPython?

Answer: A mutex that prevents multiple native threads from executing Python bytecode at the same time — only one thread runs Python code in a given interpreter at any moment.

The GIL exists because CPython’s memory management (its reference counting, primarily) is not thread-safe on its own. A single mutex around bytecode execution guarantees that internal data structures are never corrupted by two threads touching them simultaneously. It trades away parallelism for safety and simplicity.

The consequences are practical:

  • CPU-bound threads don’t speed up. Two threads doing pure computation won’t run in parallel — they take turns, and you may even see a slowdown from context switching.
  • I/O-bound threads benefit. While a thread waits on a socket or file, it releases the GIL, and another thread runs. So threaded I/O concurrency works well.
  • The workaround is processes. multiprocessing spawns separate interpreters, each with its own GIL, giving true parallelism for CPU work.

Two things worth clarifying in an interview: the GIL is a property of CPython, not of Python the language — other implementations (PyPy, Jython) don’t have it. And the GIL is not a garbage-collection lock; it’s about interpreter state in general.

The interview answer: the GIL is a CPython mutex that lets only one thread execute bytecode at a time, making CPU-bound threads non-parallel while I/O-bound concurrency still works.

Answer:

A mutex that prevents multiple native threads from executing Python bytecode at the same time — only one thread runs Python code in a given interpreter at any moment.

The GIL exists because CPython’s memory management (its reference counting, primarily) is not thread-safe on its own. A single mutex around bytecode execution guarantees that internal data structures are never corrupted by two threads touching them simultaneously. It trades away parallelism for safety and simplicity.

The consequences are practical:

  • CPU-bound threads don’t speed up. Two threads doing pure computation won’t run in parallel — they take turns, and you may even see a slowdown from context switching.
  • I/O-bound threads benefit. While a thread waits on a socket or file, it releases the GIL, and another thread runs. So threaded I/O concurrency works well.
  • The workaround is processes. multiprocessing spawns separate interpreters, each with its own GIL, giving true parallelism for CPU work.

Two things worth clarifying in an interview: the GIL is a property of CPython, not of Python the language — other implementations (PyPy, Jython) don’t have it. And the GIL is not a garbage-collection lock; it’s about interpreter state in general.

The interview answer: the GIL is a CPython mutex that lets only one thread execute bytecode at a time, making CPU-bound threads non-parallel while I/O-bound concurrency still works.

11. Which of the following data structures is thread-safe for FIFO queuing operations?

Answer: queue.Queue.

The FIFO semantics are easy — a plain list can do first-in-first-out with append and pop(0). The hard part is thread safety, and that’s what queue.Queue is built for.

queue.Queue is a FIFO queue designed for producer-consumer patterns across threads. It wraps its internal storage with a lock and condition variables, so multiple producers and consumers can push and pull concurrently without races. It also adds blocking APIs — get() waits until an item is available, put() can block when the queue is full, with optional timeouts.

The other candidates fail the thread-safety test:

  • collections.deque — a fast double-ended queue, but not thread-safe for concurrent mutation.
  • list and dict — not safe for concurrent modification either.

The interview answer: queue.Queue is the thread-safe FIFO for multi-threaded work. For single-threaded high-throughput FIFO, deque is the faster choice — but concurrent access calls for queue.Queue.

Answer:

queue.Queue.

The FIFO semantics are easy — a plain list can do first-in-first-out with append and pop(0). The hard part is thread safety, and that’s what queue.Queue is built for.

queue.Queue is a FIFO queue designed for producer-consumer patterns across threads. It wraps its internal storage with a lock and condition variables, so multiple producers and consumers can push and pull concurrently without races. It also adds blocking APIs — get() waits until an item is available, put() can block when the queue is full, with optional timeouts.

The other candidates fail the thread-safety test:

  • collections.deque — a fast double-ended queue, but not thread-safe for concurrent mutation.
  • list and dict — not safe for concurrent modification either.

The interview answer: queue.Queue is the thread-safe FIFO for multi-threaded work. For single-threaded high-throughput FIFO, deque is the faster choice — but concurrent access calls for queue.Queue.

12. What is the output of the following code?

x = 256
y = 256
z = 257
w = 257
print(x is y, z is w)

Output: True False

This is about integer interning in CPython.

CPython pre-allocates a pool of small integer objects for the range -5 to 256. When you assign 256 to a variable, it doesn’t create a new object — it hands you the cached one. So x = 256 and y = 256 both point to the same pre-existing object, and x is y is True.

257 is outside that cached range. Each assignment z = 257 and w = 257 creates a fresh object (in CPython’s interactive/simple cases), so z and w are different objects and z is w is False.

The output is True False.

The deeper lesson: is compares identity, and interning is an implementation detail. Relying on it is fragile — the exact behavior can differ between interactive sessions and compiled code, and across Python versions. The reliable rule remains: use == for value comparison, is only for singletons like None.

Answer:

True False

This is about integer interning in CPython.

CPython pre-allocates a pool of small integer objects for the range -5 to 256. When you assign 256 to a variable, it doesn’t create a new object — it hands you the cached one. So x = 256 and y = 256 both point to the same pre-existing object, and x is y is True.

257 is outside that cached range. Each assignment z = 257 and w = 257 creates a fresh object (in CPython’s interactive/simple cases), so z and w are different objects and z is w is False.

The output is True False.

The deeper lesson: is compares identity, and interning is an implementation detail. Relying on it is fragile — the exact behavior can differ between interactive sessions and compiled code, and across Python versions. The reliable rule remains: use == for value comparison, is only for singletons like None.

13. What is the purpose of functools.wraps when creating decorators?

Answer: It preserves the original function’s metadata — __name__, __doc__, __module__, annotations — on the wrapper.

Without wraps, a decorator replaces the function with its wrapper, and the wrapper has its own (usually generic) name and docstring. Tooling breaks: help() shows the wrapper, tracebacks name the wrapper, and introspection like inspect.signature sees the wrong function.

import functools

def deco(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@functools.wraps(func) copies the metadata from func onto wrapper (it internally updates the wrapper’s __dict__ with the original’s). The decorated function then looks like the original — correct name, docstring, and signature — while still running the wrapper’s logic.

The interview answer: wraps copies the original function’s metadata onto the wrapper so the decorated function keeps its identity for debugging and introspection.

Answer:

It preserves the original function’s metadata — __name__, __doc__, __module__, annotations — on the wrapper.

Without wraps, a decorator replaces the function with its wrapper, and the wrapper has its own (usually generic) name and docstring. Tooling breaks: help() shows the wrapper, tracebacks name the wrapper, and introspection like inspect.signature sees the wrong function.

import functools

def deco(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@functools.wraps(func) copies the metadata from func onto wrapper (it internally updates the wrapper’s __dict__ with the original’s). The decorated function then looks like the original — correct name, docstring, and signature — while still running the wrapper’s logic.

The interview answer: wraps copies the original function’s metadata onto the wrapper so the decorated function keeps its identity for debugging and introspection.

14. What is the result of print(format(10, ‘b’))?

Output: 1010

The format() built-in converts a value using a format specification. The 'b' specifier formats an integer as a binary string.

10 in decimal is 1010 in binary (8 + 2). So format(10, 'b') produces the string '1010', and print outputs 1010.

The related specifiers follow the same pattern: 'd' is decimal, 'x' is lowercase hexadecimal, 'o' is octal. So format(10, 'x') would give 'a', and format(10, 'o') gives '12'.

The interview answer: 'b' formats as binary, so 10 becomes '1010'.

Answer:

1010

The format() built-in converts a value using a format specification. The 'b' specifier formats an integer as a binary string.

10 in decimal is 1010 in binary (8 + 2). So format(10, 'b') produces the string '1010', and print outputs 1010.

The related specifiers follow the same pattern: 'd' is decimal, 'x' is lowercase hexadecimal, 'o' is octal. So format(10, 'x') would give 'a', and format(10, 'o') gives '12'.

The interview answer: 'b' formats as binary, so 10 becomes '1010'.

15. What will be printed by this code?

def fn(x=[]):
    x.append(1)
    return x

print(fn())
print(fn([2]))
print(fn())

Output: [1], [2, 1], [1, 1]

This is the mutable-default-argument bug in its most complete form — the default list persists across calls, but an explicit argument replaces it for that one call.

  • Call 1, fn(): uses the default list (currently []), appends 1 → returns [1]. The default list now holds [1].
  • Call 2, fn([2]): an explicit list [2] is passed, so the default is untouched. Appends 1 → returns [2, 1]. The default list still holds [1].
  • Call 3, fn(): back to the default, which persists from call 1 as [1]. Appends 1 → returns [1, 1].

Output: [1], [2, 1], [1, 1].

The lesson is the standard one: defaults are created once, so a mutable default accumulates state across calls. The fix — default to None, build a fresh list inside — guarantees each no-argument call starts clean.

Answer:

[1], [2, 1], [1, 1]

This is the mutable-default-argument bug in its most complete form — the default list persists across calls, but an explicit argument replaces it for that one call.

  • Call 1, fn(): uses the default list (currently []), appends 1 → returns [1]. The default list now holds [1].
  • Call 2, fn([2]): an explicit list [2] is passed, so the default is untouched. Appends 1 → returns [2, 1]. The default list still holds [1].
  • Call 3, fn(): back to the default, which persists from call 1 as [1]. Appends 1 → returns [1, 1].

Output: [1], [2, 1], [1, 1].

The lesson is the standard one: defaults are created once, so a mutable default accumulates state across calls. The fix — default to None, build a fresh list inside — guarantees each no-argument call starts clean.

16. What does dict.fromkeys([‘a’, ‘b’], []) create?

Answer: A dict whose keys 'a' and 'b' share the exact same list instance.

dict.fromkeys(keys, value) creates a dict with the given keys, all mapped to the same value object. It does not copy the value per key.

So d = dict.fromkeys(['a', 'b'], []) gives {'a': [], 'b': []} — but both d['a'] and d['b'] point to one list. Mutating one:

d['a'].append(1)   # now d['b'] is also [1]

This is the same aliasing trap as the mutable default argument, in dict form. If you need independent lists, build the dict manually with a comprehension: {k: [] for k in ['a', 'b']}.

The interview answer: all keys share one identical list instance; fromkeys assigns the same object, it doesn’t deep-copy per key.

Answer:

A dict whose keys 'a' and 'b' share the exact same list instance.

dict.fromkeys(keys, value) creates a dict with the given keys, all mapped to the same value object. It does not copy the value per key.

So d = dict.fromkeys(['a', 'b'], []) gives {'a': [], 'b': []} — but both d['a'] and d['b'] point to one list. Mutating one:

d['a'].append(1)   # now d['b'] is also [1]

This is the same aliasing trap as the mutable default argument, in dict form. If you need independent lists, build the dict manually with a comprehension: {k: [] for k in ['a', 'b']}.

The interview answer: all keys share one identical list instance; fromkeys assigns the same object, it doesn’t deep-copy per key.

17. Which module should be used for high-precision decimal arithmetic?

Answer: decimal.

Floating-point (float) represents numbers in binary and can’t store many decimal values exactly — the classic 0.1 + 0.2 == 0.30000000000000004 problem. For money, tax, and any computation where exact decimal results matter, that’s unacceptable.

The decimal module provides exact decimal arithmetic with configurable precision. Decimal("0.1") + Decimal("0.2") equals Decimal("0.3") exactly. You can set how many significant digits to carry (getcontext().prec = 50), control rounding modes, and get deterministic, human-friendly behavior.

The contrast:

  • math — mathematical functions (sqrt, log, trig), built on floats. Right for scientific computation, wrong for exact decimals.
  • float — native binary floating point. Fast, but lossy.
  • decimal — exact decimal arithmetic. The choice for financial computing.

The interview answer: decimal — exact, configurable-precision decimal arithmetic for money and other precision-critical work.

Answer:

decimal.

Floating-point (float) represents numbers in binary and can’t store many decimal values exactly — the classic 0.1 + 0.2 == 0.30000000000000004 problem. For money, tax, and any computation where exact decimal results matter, that’s unacceptable.

The decimal module provides exact decimal arithmetic with configurable precision. Decimal("0.1") + Decimal("0.2") equals Decimal("0.3") exactly. You can set how many significant digits to carry (getcontext().prec = 50), control rounding modes, and get deterministic, human-friendly behavior.

The contrast:

  • math — mathematical functions (sqrt, log, trig), built on floats. Right for scientific computation, wrong for exact decimals.
  • float — native binary floating point. Fast, but lossy.
  • decimal — exact decimal arithmetic. The choice for financial computing.

The interview answer: decimal — exact, configurable-precision decimal arithmetic for money and other precision-critical work.

18. What is the output of the following code?

a = [1, 2, 3]
b = a[:]
print(a is b)

Output: False

A full slice a[:] creates a new list — a shallow copy. The elements are the same objects, but the list container itself is brand new.

So b is a distinct object from a, and a is b — which tests identity, not content — is False.

The contrast is with plain assignment: b = a would make both names refer to the same list, and a is b would be True. That’s the difference between copying and aliasing.

Note the subtlety: a[:] is a shallow copy. For a flat list of integers that’s a full independent copy. For a list containing lists, the inner lists would still be shared — a deepcopy would be needed for full independence. But for identity, the answer is clean: False, the slice makes a new object.

Answer:

False

A full slice a[:] creates a new list — a shallow copy. The elements are the same objects, but the list container itself is brand new.

So b is a distinct object from a, and a is b — which tests identity, not content — is False.

The contrast is with plain assignment: b = a would make both names refer to the same list, and a is b would be True. That’s the difference between copying and aliasing.

Note the subtlety: a[:] is a shallow copy. For a flat list of integers that’s a full independent copy. For a list containing lists, the inner lists would still be shared — a deepcopy would be needed for full independence. But for identity, the answer is clean: False, the slice makes a new object.

19. What is the output of print(min("", “a”, “A”, key=len))?

Output: "" (the empty string).

min() with a key function doesn’t compare the elements directly — it compares the result of applying key to each element, and returns the element with the smallest key value.

The key here is len. So the comparison is over lengths:

  • len("") = 0
  • len("a") = 1
  • len("A") = 1

The smallest length is 0, belonging to the empty string. So min returns "".

The key insight: key=len changes what we minimize over (length, not lexicographic order), but the return value is still the original element, not the key. The interview answer: the empty string, because it has the minimum length.

Answer:

"" (the empty string).

min() with a key function doesn’t compare the elements directly — it compares the result of applying key to each element, and returns the element with the smallest key value.

The key here is len. So the comparison is over lengths:

  • len("") = 0
  • len("a") = 1
  • len("A") = 1

The smallest length is 0, belonging to the empty string. So min returns "".

The key insight: key=len changes what we minimize over (length, not lexicographic order), but the return value is still the original element, not the key. The interview answer: the empty string, because it has the minimum length.

20. What is the output of print(bool(np.nan)) using standard Python logical evaluation?

Answer: True.

NaN (Not a Number) is a special floating-point value. It has quirky comparison behavior — np.nan == np.nan is False, and every comparison with it is false — which makes people assume it must be falsy. It isn’t.

Truthiness in Python is defined by __bool__, and for floats that’s simply: zero is falsy, everything else is truthy. nan is not zero — it’s a non-zero bit pattern that means “not a number.” So bool(np.nan) is True.

The interview point: NaN being weird for comparisons does not make it falsy. Any non-zero float, including nan, is truthy. Only 0.0 and 0 are falsy numbers.

Answer:

True.

NaN (Not a Number) is a special floating-point value. It has quirky comparison behavior — np.nan == np.nan is False, and every comparison with it is false — which makes people assume it must be falsy. It isn’t.

Truthiness in Python is defined by __bool__, and for floats that’s simply: zero is falsy, everything else is truthy. nan is not zero — it’s a non-zero bit pattern that means “not a number.” So bool(np.nan) is True.

The interview point: NaN being weird for comparisons does not make it falsy. Any non-zero float, including nan, is truthy. Only 0.0 and 0 are falsy numbers.

21. What does str.strip() remove by default?

Answer: Leading and trailing whitespace — spaces, tabs, newlines, and carriage returns — from both ends of the string.

strip() trims from the start and the end of the string, removing whitespace characters until it hits a non-whitespace character on each side. The default whitespace set includes space ( ), tab (\t), newline (\n), carriage return (\r), and a few others.

It does not touch whitespace in the middle of the string, and it doesn’t strip punctuation or other characters unless you pass them as arguments — s.strip(".,!") would strip those specific characters instead.

The related variants: lstrip() strips only the left end, rstrip() only the right. The interview answer: whitespace (space, tab, newline, CR) from the leading and trailing edges.

Answer:

Leading and trailing whitespace — spaces, tabs, newlines, and carriage returns — from both ends of the string.

strip() trims from the start and the end of the string, removing whitespace characters until it hits a non-whitespace character on each side. The default whitespace set includes space ( ), tab (\t), newline (\n), carriage return (\r), and a few others.

It does not touch whitespace in the middle of the string, and it doesn’t strip punctuation or other characters unless you pass them as arguments — s.strip(".,!") would strip those specific characters instead.

The related variants: lstrip() strips only the left end, rstrip() only the right. The interview answer: whitespace (space, tab, newline, CR) from the leading and trailing edges.

22. What will print(1, 2, 3, sep=’-’, end=’*’) output?

Output: 1-2-3*

print has two keyword parameters that control formatting:

  • sep — the separator placed between the positional arguments. Default is a space; here it’s '-', so the values print as 1-2-3.
  • end — the string appended after all arguments. Default is a newline; here it’s '*', so instead of a newline, an asterisk follows.

Combined: 1, then -, 2, then -, 3, then * — output 1-2-3* with no trailing newline.

The interview answer: sep='-' joins the values with hyphens and end='*' replaces the newline, giving 1-2-3*.

Answer:

1-2-3*

print has two keyword parameters that control formatting:

  • sep — the separator placed between the positional arguments. Default is a space; here it’s '-', so the values print as 1-2-3.
  • end — the string appended after all arguments. Default is a newline; here it’s '*', so instead of a newline, an asterisk follows.

Combined: 1, then -, 2, then -, 3, then * — output 1-2-3* with no trailing newline.

The interview answer: sep='-' joins the values with hyphens and end='*' replaces the newline, giving 1-2-3*.

23. What does os.path.join(“folder”, “/subfolder”) return on UNIX-like systems?

Answer: "/subfolder".

os.path.join is not a naive string concatenator — it’s path-aware. Its documented behavior: if any component is an absolute path (starts with / on Unix), every previous component is discarded.

"/subfolder" begins with a slash, so it’s absolute. Joining discards "folder" and returns just "/subfolder".

This is a well-known gotcha. You can’t “append” an absolute path onto a prefix and expect nesting — the absolute component resets everything. The safe habit when building paths incrementally is to keep all parts relative and let the last component decide, or strip the leading slash.

The interview answer: an absolute component in os.path.join discards all earlier components, so the result is /subfolder.

Answer:

"/subfolder".

os.path.join is not a naive string concatenator — it’s path-aware. Its documented behavior: if any component is an absolute path (starts with / on Unix), every previous component is discarded.

"/subfolder" begins with a slash, so it’s absolute. Joining discards "folder" and returns just "/subfolder".

This is a well-known gotcha. You can’t “append” an absolute path onto a prefix and expect nesting — the absolute component resets everything. The safe habit when building paths incrementally is to keep all parts relative and let the last component decide, or strip the leading slash.

The interview answer: an absolute component in os.path.join discards all earlier components, so the result is /subfolder.

24. Which built-in function returns both the index and value during iteration?

Answer: enumerate().

When you need both the position and the element, enumerate is the tool:

for i, v in enumerate(["a", "b", "c"]):
    print(i, v)   # 0 a, 1 b, 2 c

It wraps an iterable and yields (index, value) pairs, starting at 0 by default (enumerate(iterable, start=1) starts at 1).

The other options: zip pairs multiple iterables together, map transforms elements, and range just produces a sequence of numbers. Only enumerate gives you index and value together. The interview answer: enumerate().

Answer:

enumerate().

When you need both the position and the element, enumerate is the tool:

for i, v in enumerate(["a", "b", "c"]):
    print(i, v)   # 0 a, 1 b, 2 c

It wraps an iterable and yields (index, value) pairs, starting at 0 by default (enumerate(iterable, start=1) starts at 1).

The other options: zip pairs multiple iterables together, map transforms elements, and range just produces a sequence of numbers. Only enumerate gives you index and value together. The interview answer: enumerate().

25. What is the output of the following code?

x = (1)
print(type(x))

Output: <class 'int'>

The parentheses are just grouping — this is the single-element-tuple trap again.

(1) is not a one-element tuple. Without a comma, the parentheses act like arithmetic grouping, and (1) is simply the integer 1. So type(x) is int.

The one-element tuple requires the comma: (1,) is a tuple. The general rule: parentheses make a tuple only when they contain a comma (or when they’re empty, for the empty tuple). x = (1,) would print <class 'tuple'>.

The interview answer: int(1) is an integer in grouping parentheses, not a tuple.

Answer:

<class 'int'>

The parentheses are just grouping — this is the single-element-tuple trap again.

(1) is not a one-element tuple. Without a comma, the parentheses act like arithmetic grouping, and (1) is simply the integer 1. So type(x) is int.

The one-element tuple requires the comma: (1,) is a tuple. The general rule: parentheses make a tuple only when they contain a comma (or when they’re empty, for the empty tuple). x = (1,) would print <class 'tuple'>.

The interview answer: int(1) is an integer in grouping parentheses, not a tuple.

26. What is the time complexity of appending an element to a Python list?

Answer: O(1) amortized.

A Python list is a dynamic array — a contiguous block of memory that grows by overallocation. When you call append, there’s usually spare capacity, so the element is written at the end in constant time.

When the array fills up, Python allocates a larger block (roughly 1.125× the old size) and copies every existing element into it — that single append is O(n). But such resizes are rare: they happen only when capacity is exhausted, and the exponential growth means the average cost over many appends stays O(1).

That’s what “amortized O(1)” means: the occasional expensive copy is spread across all the cheap appends, so n appends cost O(n) total.

The interview answer: amortized O(1). A plain append is fast; infrequent resizes absorb the copying cost.

Answer:

O(1) amortized.

A Python list is a dynamic array — a contiguous block of memory that grows by overallocation. When you call append, there’s usually spare capacity, so the element is written at the end in constant time.

When the array fills up, Python allocates a larger block (roughly 1.125× the old size) and copies every existing element into it — that single append is O(n). But such resizes are rare: they happen only when capacity is exhausted, and the exponential growth means the average cost over many appends stays O(1).

That’s what “amortized O(1)” means: the occasional expensive copy is spread across all the cheap appends, so n appends cost O(n) total.

The interview answer: amortized O(1). A plain append is fast; infrequent resizes absorb the copying cost.

27. What is the output of print(“Python”.find(“z”))?

Output: -1

str.find(sub) searches for a substring and returns the index of the first occurrence. When the substring isn’t found at all, it returns -1.

There’s no substring "z" in "Python", so find returns -1. Note it does not raise — that’s the deliberate contrast with str.index(), which does the same search but raises ValueError when the substring is missing.

The rule of thumb: use find when a missing match is a normal possibility (you’ll check for -1); use index when a missing match is an error you want to surface. The interview answer: -1.

Answer:

-1

str.find(sub) searches for a substring and returns the index of the first occurrence. When the substring isn’t found at all, it returns -1.

There’s no substring "z" in "Python", so find returns -1. Note it does not raise — that’s the deliberate contrast with str.index(), which does the same search but raises ValueError when the substring is missing.

The rule of thumb: use find when a missing match is a normal possibility (you’ll check for -1); use index when a missing match is an error you want to surface. The interview answer: -1.

28. How do you define a docstring in Python?

Answer: A string literal — conventionally triple-quoted — placed as the first statement of a module, function, class, or method.

A docstring is documentation attached to a code object. It must be the very first statement in the body (before any code), and it’s automatically stored in the object’s __doc__ attribute, which tools like help() read.

def add(a, b):
    """Return the sum of a and b."""
    return a + b

print(add.__doc__)   # "Return the sum of a and b."

The convention is triple quotes, """...""", because docstrings are often multi-line. The distinguishing feature: it’s an expression statement that Python captures as metadata rather than discarding.

The interview answer: a string literal as the first statement of a module/function/class, accessible via __doc__.

Answer:

A string literal — conventionally triple-quoted — placed as the first statement of a module, function, class, or method.

A docstring is documentation attached to a code object. It must be the very first statement in the body (before any code), and it’s automatically stored in the object’s __doc__ attribute, which tools like help() read.

def add(a, b):
    """Return the sum of a and b."""
    return a + b

print(add.__doc__)   # "Return the sum of a and b."

The convention is triple quotes, """...""", because docstrings are often multi-line. The distinguishing feature: it’s an expression statement that Python captures as metadata rather than discarding.

The interview answer: a string literal as the first statement of a module/function/class, accessible via __doc__.

29. What will print(math.trunc(-2.8)) return?

Output: -2

math.trunc(x) removes the fractional part, keeping only the integer part — it truncates toward zero.

For -2.8, truncation toward zero means dropping the -0.8 and keeping -2. The result is the integer -2.

This is subtly different from floor division and int():

  • math.trunc(-2.8) = -2 (toward zero).
  • math.floor(-2.8) = -3 (toward negative infinity).
  • math.ceil(-2.8) = -2 (toward positive infinity).

The trap is assuming “truncate” means “round down.” It doesn’t — for negatives, truncation and flooring diverge. The interview answer: -2.

Answer:

-2

math.trunc(x) removes the fractional part, keeping only the integer part — it truncates toward zero.

For -2.8, truncation toward zero means dropping the -0.8 and keeping -2. The result is the integer -2.

This is subtly different from floor division and int():

  • math.trunc(-2.8) = -2 (toward zero).
  • math.floor(-2.8) = -3 (toward negative infinity).
  • math.ceil(-2.8) = -2 (toward positive infinity).

The trap is assuming “truncate” means “round down.” It doesn’t — for negatives, truncation and flooring diverge. The interview answer: -2.

30. What will print(isinstance(lambda x: x, object)) output?

Output: True

The foundational claim of Python’s object model: everything is an object — functions, classes, modules, lambdas, even type itself.

A lambda is a function object, and every object in Python is an instance of object (either directly or through its class hierarchy). So isinstance(lambda x: x, object) is True.

This is worth connecting to the other meta-questions: a lambda is an instance of function, function is an instance of type, and all of them are instances of object. There’s nothing in Python that isn’t an object. The interview answer: True.

Answer:

True

The foundational claim of Python’s object model: everything is an object — functions, classes, modules, lambdas, even type itself.

A lambda is a function object, and every object in Python is an instance of object (either directly or through its class hierarchy). So isinstance(lambda x: x, object) is True.

This is worth connecting to the other meta-questions: a lambda is an instance of function, function is an instance of type, and all of them are instances of object. There’s nothing in Python that isn’t an object. The interview answer: True.

31. What will print(str.upper(“hello”)) return?

Output: "HELLO"

Methods in Python are just functions attached to a class. str.upper is the function; when called on an instance, "hello".upper(), Python implicitly passes the instance as the first argument.

But you can also call the underlying function directly with the instance passed explicitly: str.upper("hello"). The string "hello" is provided as self, and the method uppercases it, returning "HELLO".

This is called an unbound method call (in Python 3 terms, just calling the function attribute of the class). It’s equivalent to the bound form — useful in map(str.upper, strings) and similar contexts.

The interview answer: "HELLO" — unbound method calls accept the instance explicitly as the first argument.

Answer:

"HELLO"

Methods in Python are just functions attached to a class. str.upper is the function; when called on an instance, "hello".upper(), Python implicitly passes the instance as the first argument.

But you can also call the underlying function directly with the instance passed explicitly: str.upper("hello"). The string "hello" is provided as self, and the method uppercases it, returning "HELLO".

This is called an unbound method call (in Python 3 terms, just calling the function attribute of the class). It’s equivalent to the bound form — useful in map(str.upper, strings) and similar contexts.

The interview answer: "HELLO" — unbound method calls accept the instance explicitly as the first argument.

32. What is the output of the following dictionary unpacking code?

d1 = {'a': 1}
d2 = {'a': 2, 'b': 3}
merged = {**d1, **d2}
print(merged)

Output: {'a': 2, 'b': 3}

The ** unpacking operator (Python 3.5+) splats dictionaries into a literal. When keys collide, later definitions win — the merge processes d1 first, then d2 overwrites.

  • From **d1: 'a': 1.
  • From **d2: 'a': 2 overwrites the 1; 'b': 3 is added.

Result: {'a': 2, 'b': 3}.

This is the modern, concise way to merge dicts (pre-3.9 it was {**a, **b}; 3.9+ also has a | b). The interview point is the overwrite rule: rightmost wins on overlapping keys. The values are not combined — 'a' is 2, not [1, 2].

Answer:

{'a': 2, 'b': 3}

The ** unpacking operator (Python 3.5+) splats dictionaries into a literal. When keys collide, later definitions win — the merge processes d1 first, then d2 overwrites.

  • From **d1: 'a': 1.
  • From **d2: 'a': 2 overwrites the 1; 'b': 3 is added.

Result: {'a': 2, 'b': 3}.

This is the modern, concise way to merge dicts (pre-3.9 it was {**a, **b}; 3.9+ also has a | b). The interview point is the overwrite rule: rightmost wins on overlapping keys. The values are not combined — 'a' is 2, not [1, 2].

33. What method turns a string of items separated by commas into a list?

Answer: str.split(",").

split(delimiter) divides a string around every occurrence of the delimiter and returns a list of the pieces.

"apple,banana,cherry".split(",")   # ['apple', 'banana', 'cherry']

The default — split() with no argument — splits on runs of whitespace, which is handy for whitespace-separated input. With a specific delimiter, split is the standard way to parse CSV-ish strings, key/value pairs, and similar formats.

The related operation goes the other way: ",".join(list_of_strings) builds a string from a list. The interview answer: str.split(",").

Answer:

str.split(",").

split(delimiter) divides a string around every occurrence of the delimiter and returns a list of the pieces.

"apple,banana,cherry".split(",")   # ['apple', 'banana', 'cherry']

The default — split() with no argument — splits on runs of whitespace, which is handy for whitespace-separated input. With a specific delimiter, split is the standard way to parse CSV-ish strings, key/value pairs, and similar formats.

The related operation goes the other way: ",".join(list_of_strings) builds a string from a list. The interview answer: str.split(",").

34. Which function returns True if all elements of an iterable evaluate to truthy?

Answer: all().

all(iterable) returns True only when every element is truthy. As a special case, it returns True for an empty iterable (vacuous truth — nothing contradicts the claim).

all([1, 2, 3])      # True
all([1, 0, 3])      # False — 0 is falsy
all([])             # True

The complement is any(), which returns True if at least one element is truthy (and False for empty). Both short-circuit: all stops at the first falsy element, any stops at the first truthy one.

The interview answer: all(), with any() as the existential counterpart.

Answer:

all().

all(iterable) returns True only when every element is truthy. As a special case, it returns True for an empty iterable (vacuous truth — nothing contradicts the claim).

all([1, 2, 3])      # True
all([1, 0, 3])      # False — 0 is falsy
all([])             # True

The complement is any(), which returns True if at least one element is truthy (and False for empty). Both short-circuit: all stops at the first falsy element, any stops at the first truthy one.

The interview answer: all(), with any() as the existential counterpart.

My Private Notes

Notes are auto-saved locally to this device.