{}The Interview
Handbook

Tracks / Python

Core language & data model

junior 10 questions · 5 min read fundamentalsdata-model

Questions in this set 10
  1. 01What is the difference between a list and a tuple?
  2. 02Is Python pass-by-value or pass-by-reference?
  3. 03is vs == — when do they differ, and why does a is b sometimes surprise you?
  4. 04What is truthiness? Which objects are falsy?
  5. 05Explain shallow vs deep copy.
  6. 06What does a list comprehension actually compile to, and when should you not use one?
  7. 07Explain *args, **kwargs, and keyword-only / positional-only parameters.
  8. 08How does string formatting differ across %, .format(), and f-strings — and when must you avoid f-strings?
  9. 09What are __str__ and __repr__ for?
  10. 10What is the walrus operator good for?

These are the warm-up questions. Getting them slightly right is the difference between "junior" and "junior who has actually read the docs". Answer each in two sentences, then offer the deeper detail — interviewers reward candidates who signal depth without monologuing.

01

What is the difference between a list and a tuple?

Short answer. A list is mutable and variable-length; a tuple is immutable and fixed-length. That immutability makes tuples hashable (if their contents are), so they can be dict keys and set members.

The deeper reasons this matters:

python
point = (3, 4)
{point: "origin-ish"}          # fine — tuples hash by value
{[3, 4]: "nope"}               # TypeError: unhashable type: 'list'
  • Semantics. Convention says a tuple is a record (heterogeneous, position means something: (x, y)) and a list is a collection (homogeneous, order may be incidental).
  • Memory/speed. Tuples are allocated exactly once, are slightly smaller, and CPython caches small ones. Do not lead with this — it is a micro-optimisation, and leading with it suggests you think performance is the main difference.
  • Immutability is shallow. t = ([1], 2) — you cannot rebind t[0], but you can do t[0].append(9). This is a favourite follow-up.

Follow-up they will ask: "Is a tuple always hashable?" No — only if every element is. hash(([1],)) raises TypeError.

02

Is Python pass-by-value or pass-by-reference?

Short answer. Neither, exactly. Python passes object references by value: the function gets its own name bound to the same object. Mutating the object is visible to the caller; rebinding the name is not.

python
def f(xs, n):
    xs.append(1)   # mutation — caller sees it
    xs = [9]       # rebinding — caller does NOT see it
    n += 1         # ints are immutable, so this is also just a rebind

data, num = [], 0
f(data, num)
print(data, num)   # [1] 0

The name for this is call by sharing (or "call by object reference"). Say that phrase — it is a strong signal.

Trap: Mutable default arguments.

python
def add(item, bucket=[]):     # BUG: the list is created once, at def time
    bucket.append(item)
    return bucket

add(1); add(2)                # [1, 2] — surprise

def add(item, bucket=None):   # correct
    bucket = [] if bucket is None else bucket
    bucket.append(item)
    return bucket
03

is vs == — when do they differ, and why does a is b sometimes surprise you?

== calls __eq__ (value equality). is compares identity — the same object in memory. Use is only for singletons: None, True, False, and sentinel objects.

python
a, b = 256, 256
a is b        # True  — CPython interns small ints (-5..256)
a, b = 257, 257
a is b        # often False in a REPL, True inside one compiled function body

That inconsistency is a CPython implementation detail, not a language guarantee. The correct interview answer is: "never rely on it; compare with ==." Linters flag x is 257 for exactly this reason.

04

What is truthiness? Which objects are falsy?

An object is falsy if __bool__ returns False, or (absent that) __len__ returns 0. Falsy built-ins: None, False, 0, 0.0, Decimal(0), "", [], {}, (), set(), range(0).

Trap that shows up in real code review:

python
def f(timeout=None):
    if not timeout:          # BUG: timeout=0 means "don't wait", but is falsy
        timeout = 30
    if timeout is None:      # correct — distinguishes "unset" from "zero"
        timeout = 30
05

Explain shallow vs deep copy.

copy.copy builds a new outer container holding the same inner references. copy.deepcopy recursively copies, tracking already-seen objects with a memo dict so cycles do not blow the stack.

python
import copy
grid = [[0] * 3] * 3          # BUG: three references to ONE row
grid[0][0] = 1                # -> [[1,0,0],[1,0,0],[1,0,0]]
grid = [[0] * 3 for _ in range(3)]   # correct

deepcopy is slow and will happily copy things you did not want copied (a DB connection, a lock). In production code prefer constructing new objects explicitly, or dataclasses.replace.

06

What does a list comprehension actually compile to, and when should you not use one?

It compiles to a loop that appends into a list, run in its own scope (so the loop variable does not leak in Python 3). Prefer it when the expression fits on one line and there is at most one if.

python
[f(x) for x in xs if p(x)]          # good
(f(x) for x in xs)                  # generator: lazy, O(1) memory
{k: v for k, v in pairs}            # dict comp
{x.id for x in xs}                  # set comp

Do not use one for side effects ([print(x) for x in xs] builds a garbage list), and do not nest three levels deep — a plain loop is more readable and equally fast.

Follow-up: "How do you flatten a nested list?" [x for row in grid for x in row] — note the clause order matches the equivalent nested for loops, which is the part people get backwards.

07

Explain *args, **kwargs, and keyword-only / positional-only parameters.

python
def f(a, b=1, *args, c, d=2, **kwargs): ...
#     ^positional-or-keyword   ^keyword-only (after *)

def g(a, b, /, c, *, d): ...
#            ^ a,b positional-only    ^ d keyword-only

Positional-only (/, Python 3.8+) lets you rename parameters later without breaking callers — it is why C-implemented builtins like len(obj, /) use it. Keyword-only (*) forces call sites to be self-documenting; use it for booleans and options: connect(host, *, timeout=5, retries=3).

08

How does string formatting differ across %, .format(), and f-strings — and when must you avoid f-strings?

f-strings (3.6+) are fastest and clearest; they are evaluated eagerly at the call site. Avoid them where you want deferred interpolation:

python
logger.info("user %s failed %d times", user, n)   # correct: lazy, only formats if emitted,
                                                  # and log aggregators can group by template
logger.info(f"user {user} failed {n} times")      # formats always; every message is unique

Same reasoning applies to SQL: never f-string user input into a query — use parameter binding.

09

What are __str__ and __repr__ for?

__repr__ is for developers (unambiguous, ideally eval-able); __str__ is for users. repr() falls back to <Class at 0x…>; str() falls back to __repr__. In containers and tracebacks Python always shows __repr__ — which is why a class with only __str__ looks useless in a debugger. Define __repr__ first; define __str__ only if it genuinely differs.

10

What is the walrus operator good for?

:= assigns inside an expression, avoiding a duplicated call or a pre-loop read.

python
while (chunk := f.read(8192)):
    process(chunk)

if (m := pattern.search(line)) is not None:
    use(m.group(1))

Use it when it removes a repeated computation. Do not use it to cram two ideas into one line — readability is the whole point.