All jobs / Automation developer
Programming interview questions
Programming questions a Automation developer can be asked, from first job to senior. Open one to see the strongest answer and why it works.
def add(item, bag=[]): bag.append(item); return bag — you call add(1) then add(2). What comes back the second time?
The strongest answer: [1, 2] — the default list is created once when the function is defined, so every call without a bag shares it; use bag=None and build a new list inside
Why it works: Exactly. One default object, shared for the life of the function. None plus a fresh list inside is the fix.
- Default arguments are evaluated once, at definition.
- A mutable default is shared by every call.
- Use None and create the value inside the function.
- The bug is silent, not an error.
When would you choose a tuple over a list?
The strongest answer: When the collection is fixed and its position means something — a coordinate, a record, a dict key — since tuples are immutable and therefore hashable
Why it works: Right. Immutability is the point, and hashability is what it buys you.
- Tuple = fixed, positional, hashable.
- List = a sequence you intend to change.
- Only a tuple can key a dict or join a set.
- Choose on meaning, not on length.
What is the difference between == and is?
The strongest answer: == asks whether two values are equal; is asks whether they are the very same object — use is only for None, True, False and sentinels
Why it works: Exactly. Identity versus equality, and is belongs to singletons.
- == compares values, is compares identity.
- Use is None, not == None.
- Equal objects need not be the same object.
- Caching makes is look right by accident.
You want a value from a dict, or a default if the key is missing, without raising. What do you use?
The strongest answer: d.get(key, default) — or d.setdefault / defaultdict when the missing key should also be stored
Why it works: Right. get for a read; setdefault or defaultdict when the miss should create the entry.
- get(key, default) for a safe read.
- setdefault / defaultdict when the miss should stick.
- in then index looks up twice.
- or swallows falsy stored values.
if items: — what exactly is being tested?
The strongest answer: Whether items is truthy: an empty list, empty string, empty dict, 0 and None are all falsy, so this is not the same as testing for None
Why it works: Exactly — which is why an empty list and None take the same branch unless you separate them.
- Empty containers, 0 and None are all falsy.
- if x is not None when None is the thing you mean.
- if x when any empty value should take the same branch.
- Be explicit when the two differ.
Building a long string in a loop with s += piece is slow on big inputs. Why?
The strongest answer: Strings are immutable, so each += builds a whole new string and copies what came before; collect the pieces in a list and "".join(parts) once
Why it works: Right. Copy per iteration versus one allocation at the end.
- Strings cannot be changed in place.
- += in a loop copies repeatedly.
- Collect into a list, then join.
- The cost grows with the square of the length.
What does xs[1:4] give you, and what does xs[:] do?
The strongest answer: Items at index 1, 2 and 3 — the stop is exclusive — and xs[:] is a shallow copy of the whole list
Why it works: Exactly. Half-open ranges, and a bare slice copies the outer list only.
- Stop is exclusive: [1:4] is 1, 2, 3.
- xs[:] is a shallow copy.
- Slices clamp instead of raising.
- Nested objects are still shared after a shallow copy.
You need both the index and the item while looping. What is the idiomatic way?
The strongest answer: for i, item in enumerate(items) — and enumerate(items, start=1) when the count should begin at one
Why it works: Right, and the start argument saves the off-by-one arithmetic.
- enumerate gives index and item together.
- start=1 for human-facing numbering.
- range(len(x)) is a smell in review.
- Manual counters drift.
When is a list comprehension the right choice over a for loop?
The strongest answer: When you are building one collection from another with a simple expression and filter; once it needs branching, side effects or two levels of nesting, a loop reads better
Why it works: Exactly. It is a readability call, and the line where it flips is nesting.
- Build-from-iterable with a simple expression.
- Side effects belong in a loop.
- Two levels of nesting is the readability limit.
- A generator expression when you do not need the list.
rows2 = rows[:] then rows2[0].append(9). Why did rows change too?
The strongest answer: The slice copied the outer list but not the inner lists, so both lists hold the same row objects — copy.deepcopy, or build new inner lists, if you need them separate
Why it works: Right. One level deep is exactly what a shallow copy means.
- A slice copies one level.
- Inner objects stay shared.
- deepcopy, or rebuild the inner values.
- The same trap applies to dicts of dicts.
What is wrong with except: pass?
The strongest answer: It swallows everything including typos, KeyboardInterrupt and bugs you needed to see; catch the specific exception you can actually handle, and log anything you deliberately ignore
Why it works: Exactly. It hides the failures that would have told you what was wrong.
- Catch the exception you can handle.
- A bare except hides typos and interrupts.
- Log anything you choose to ignore.
- except Exception is broad; bare except is broader.
When does a finally block run?
The strongest answer: On the way out of the try either way — after a successful run, after a handled or unhandled exception, and on return or break — which is why it is where cleanup goes
Why it works: Right. That guarantee is the whole point of the construct.
- finally always runs on the way out.
- except handles, else runs on success.
- Cleanup belongs in finally, or in a with block.
- It runs on return and break too.
A teammate’s Playwright test passes on every run, yet the checkout it covers has been broken for a week. Every call in the test is written without await.
Why does the test pass, and what do you change?
The strongest answer: The un-awaited calls return promises nothing waits for, so the body finishes before the assertions resolve; await every action and assertion, and lint for floating promises
Why it works: Exactly — an un-awaited promise asserts nothing. The lint rule is what stops the next one landing.
- An un-awaited promise asserts nothing.
- Await every action and assertion.
- Lint floating promises in the suite.
- Prove it fails when the app breaks.
A flaky spec was “fixed” by wrapping the whole test body in try / catch, with a retry inside the catch.
What is wrong with that fix?
The strongest answer: A catch around everything swallows assertion failures too, so a real bug retries until it passes; catch only the one operation you know is unstable and let assertions throw
Why it works: Right — assertion libraries signal failure by throwing. A broad catch turns a failing test into a passing one.
- Catch narrowly; let assertions throw.
- Wrap the unstable call, not the test.
- Never swallow an assertion error.
- Use the runner’s retry, visibly.
Your page objects are six levels deep — base page, authenticated page, dashboard page and on — and a change to the header breaks tests in four squads.
How do you restructure them?
The strongest answer: Compose instead of inherit: small components — header, table, modal — that a page object holds as fields, so a page owns what it uses and a header change touches one class
Why it works: Exactly — pages are made of parts. Composition gives the header one owner and one place to fix.
- Compose page parts; inherit rarely.
- One class per real component.
- Pages hold components as fields.
- Shared UI gets a named owner.