Free Handbook · Runs in your browser

Interview Questions

Sixty Python interview questions in three tiers — junior, mid-level and senior — each with a model answer and, where it matters, what the interviewer is actually testing. Then how a coding round is run and the checklist for a take-home that gets you to the next stage.

0 / 108 lessons🔥 0 day streak
ShareXLinkedIn

Module 15 · what you'll be able to do

  • Answer the 25 questions every junior Python interview draws from
  • Handle the 25 mid-level questions on internals, concurrency, OOP and testing
  • Reason through the 10 senior questions on design, performance and judgement
  • Run a coding round the way strong candidates do: clarify, brute force, improve, test
  • Submit a take-home that reviewers rate highly
01

Junior — 25 questions

These are asked in nearly every first-round Python interview and in most screening calls. Answer each out loud before opening it; an answer you can say in two or three sentences is what they want. Every one is covered in Modules 01–07.

JuniorWhat is the difference between a list and a tuple?

A list is mutable — you can append, remove and reassign items; a tuple is immutable and fixed-size. Because a tuple cannot change, it can be hashed, so it can be a dict key or set member; a list cannot. Use a tuple for a fixed record (a coordinate, a row), a list for a collection you build up.

What they are really testing: Whether you know mutability is the real distinction, not "tuples are faster".

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

Neither in the C++ sense — it is "pass by object reference" (or "call by sharing"). The function receives the same object the caller has. If the object is mutable (a list), changes inside the function are visible outside; if you rebind the parameter name to a new object, the caller is unaffected. So items.append(x) leaks out; items = [] does not.

What they are really testing: Whether you can predict what a function does to its arguments.

JuniorWhat does if __name__ == "__main__": do?

When a file is run directly, Python sets its __name__ to "__main__"; when it is imported, __name__ is the module name. The guard lets a file be both a script (the block runs) and an importable module (the block does not run), so importing your code never triggers its side effects.

JuniorWhat is the difference between == and is?

== compares values (it calls __eq__); is compares identity — whether both names point at the very same object. Use is only for singletons: None, True, False. Two equal strings or ints may or may not be the same object depending on interning, so is on them is a bug that appears only sometimes.

JuniorWhat is a dictionary and when would you use it?

A hash map from keys to values with O(1) average lookup, insertion and deletion. Keys must be hashable (immutable). Use it whenever you look things up by a key — counting occurrences, grouping records, caching, representing JSON. Since 3.7 it preserves insertion order.

JuniorWhat are *args and **kwargs?

They collect extra arguments: *args gathers positional arguments into a tuple, **kwargs gathers keyword arguments into a dict. The names are convention; the stars are the syntax. In a call, the same stars unpack a sequence or dict into arguments. They are how decorators forward everything to the wrapped function.

JuniorWhat is the mutable default argument problem?

Default values are evaluated once, at def time. def f(items=[]) creates one list shared by every call that omits the argument, so appends accumulate across calls. The fix is items=None and if items is None: items = [] inside the function.

What they are really testing: A classic; not knowing it suggests you have not been bitten by it yet.

JuniorHow do you handle exceptions in Python?

try around the risky code, except SpecificError for what you expect, optional else for the success path and finally for cleanup. Catch the narrowest type you can, never a bare except:, and do not swallow errors silently — log, re-raise, or return something meaningful.

JuniorWhat is a list comprehension? Give an example.

A one-expression way to build a list from an iterable with an optional filter: [x * 2 for x in nums if x > 0]. It replaces the create-loop-append pattern, reads as "what, from where, under what condition", and is usually faster. Dict and set comprehensions use the same shape with braces.

JuniorWhat is the difference between / and //?

/ is true division and always returns a float (7 / 2 == 3.5, 4 / 2 == 2.0). // is floor division: it rounds toward negative infinity, so 7 // 2 == 3 and -7 // 2 == -4, not -3.

JuniorWhat is None?

The single object that represents "no value". Functions without a return return it, dict.get returns it for a missing key, and many in-place methods (list.sort, append) return it — which is why x = items.sort() makes x None. Test for it with is None.

JuniorHow do you read a file line by line?

With with open(path, encoding="utf-8") as f: and for line in f:. The file object is an iterator, so lines are read one at a time and the file can be any size. with guarantees the file is closed even if an exception is raised. Strip the trailing newline with line.rstrip("\n").

JuniorWhat is the difference between a shallow and a deep copy?

A shallow copy (list(a), a[:], a.copy(), copy.copy) creates a new outer container whose items are the same objects as the original. A deep copy (copy.deepcopy) recursively copies everything. With nested lists, a shallow copy shares the inner lists — changing one changes "both".

JuniorWhat is a set and when would you use it?

An unordered collection of unique hashable items with O(1) membership tests and set algebra (union, intersection, difference). Use it to de-duplicate, and whenever you repeatedly ask "is x in this collection?" — a list answers that by scanning, a set by hashing.

JuniorWhat are f-strings?

String literals prefixed with f where {expression} is replaced with the expression's value, with an optional format spec: f"{price:.2f}", f"{n:,}", f"{name=}" for debugging. They are the modern replacement for % formatting and str.format, and faster than both.

JuniorWhat does enumerate() do?

It wraps an iterable and yields (index, item) pairs, optionally starting the index somewhere other than 0. It replaces for i in range(len(items)) followed by items[i], which is both clumsier and the source of most IndexErrors.

JuniorWhat is PEP 8?

The official Python style guide: 4-space indentation, snake_case for functions and variables, PascalCase for classes, UPPER_CASE for constants, 79/99-character lines, two blank lines between top-level definitions. Tools like black and ruff enforce it automatically; teams use them so reviews are about logic, not spacing.

JuniorHow do you swap two variables?

a, b = b, a. The right-hand side builds a tuple (b, a) first, then unpacks it into the names — no temporary variable needed. The same tuple unpacking is how functions return multiple values.

JuniorWhat is the difference between append and extend?

append(x) adds x as one item — if x is a list, you get a nested list. extend(iterable) adds each item of the iterable individually. a + b is like extend but returns a new list.

JuniorWhat is a lambda?

An anonymous single-expression function: lambda x: x * 2. Its purpose is to be passed to something — a key= for sorting, map, a callback — where a named def would be noise. Anything needing a statement, a docstring or a name should be a def.

JuniorHow is memory managed in Python?

By reference counting plus a cycle-detecting garbage collector. Every object tracks how many references point at it; when the count hits zero it is freed immediately. Reference cycles (a → b → a) cannot reach zero, so a separate collector finds and frees them periodically. You rarely manage memory by hand; you avoid holding references you do not need.

JuniorWhat is the difference between a module and a package?

A module is one .py file; a package is a directory of modules with an __init__.py. import os loads a module; from collections import Counter pulls a name from a package's module. Packages let large codebases be organised into namespaces.

JuniorWhat does range(5) produce?

A lazy sequence of 0, 1, 2, 3, 4 — the stop value is exclusive. It is not a list: it computes each value on demand, so range(10**9) uses no memory. range(start, stop, step) takes a start and a step, including negative steps.

JuniorWhat is the output of print(0.1 + 0.2 == 0.3) and why?

False. Floats are binary fractions and 0.1, 0.2 and 0.3 cannot be represented exactly, so the sum is 0.30000000000000004. Compare floats with math.isclose, and use decimal.Decimal or integer cents for money.

JuniorHow do you check the type of a variable?

type(x) returns the exact class; isinstance(x, SomeClass) is what you use in code because it also accepts subclasses and can take a tuple of types. isinstance(x, (int, float)) is the idiomatic "is it a number".

02

Mid-level — 25 questions

For roles with two to five years of experience. The interviewer is checking that you understand what Python does underneath and can choose between its tools. Modules 08–10 and 13 cover the material.

Mid-levelExplain the GIL. Does it make Python single-threaded?

The Global Interpreter Lock lets only one thread execute Python bytecode at a time in CPython. Threads still help for I/O-bound work (network, disk) because the lock is released while waiting; for CPU-bound work, threads do not run in parallel — use multiprocessing (separate processes, separate GILs) or a library that releases the GIL in C, like NumPy. Python 3.13 has an experimental free-threaded build.

What they are really testing: Whether you can choose between threads, processes and asyncio for a given workload.

Mid-levelWhat is a generator, and why would you use one over a list?

A function with yield returns an iterator that produces values lazily, pausing between them and keeping its local state. It uses constant memory regardless of how many items it produces, so it is how you process a file larger than RAM, stream from a database, or build pipelines of transformations. A list materialises everything up front.

Mid-levelWhat is a decorator? Write one.

A callable that takes a function and returns a replacement, usually a wrapper that adds behaviour. @timed above def f is f = timed(f). Use functools.wraps so the wrapper keeps the original's name and docstring. A minimal one: def log(fn): @wraps(fn) def w(*a, **k): print(fn.__name__); return fn(*a, **k); return w.

Mid-levelExplain * and / in a function signature.

Parameters after a bare * are keyword-only — callers must name them, which keeps calls readable and lets you add parameters later without breaking positional callers. Parameters before / are positional-only — callers cannot name them, which lets you rename them freely. def f(a, /, b, *, c) has one of each plus a normal one.

Mid-levelWhat is the difference between __str__ and __repr__?

__repr__ is the unambiguous developer representation — ideally code that recreates the object — used by the REPL, by containers, and as the fallback when __str__ is missing. __str__ is the readable user representation used by print and f-strings. Always define __repr__; define __str__ when users see the object.

Mid-levelHow does super() work with multiple inheritance?

It does not mean "the parent class" — it means "the next class in this object's MRO after the current one". With class C(A, B), super() inside A resolves to B, not to A's own parent. That is what makes cooperative multiple inheritance (mixins) work: each class calls super().__init__() and the chain visits every class exactly once.

Mid-levelWhat are context managers and how do you write one?

Objects that define __enter__ and __exit__ for use with with: setup on entry, guaranteed cleanup on exit even if an exception is raised. Files, locks and DB transactions are context managers. The easiest way to write one is @contextlib.contextmanager on a generator: code before yield is setup, after it is cleanup.

Mid-levelWhat is the difference between @staticmethod and @classmethod?

A classmethod receives the class as its first argument (cls), so it can build instances — the alternative-constructor pattern, Date.from_iso("2026-01-01"). A staticmethod receives nothing implicit; it is a plain function namespaced in the class because it belongs there conceptually. If it needs neither self nor cls, it is a staticmethod.

Mid-levelExplain asyncio in one minute.

A single-threaded event loop that runs coroutines (async def) and switches between them at await points — while one waits on the network, another runs. It gives high concurrency for I/O-bound work without threads. It does nothing for CPU-bound work, and a blocking call inside a coroutine (like time.sleep or requests.get) stalls the whole loop.

Mid-levelWhat is __slots__?

A class attribute listing the instance attributes allowed. It replaces the per-instance __dict__ with fixed slots, cutting memory per object substantially and speeding attribute access. Worth it when you create millions of small objects; not worth it otherwise, because it also prevents adding attributes dynamically.

Mid-levelHow would you make a class hashable and comparable?

Define __eq__ and __hash__ together — defining __eq__ alone sets __hash__ to None. Hash the same fields you compare. For ordering, define __lt__ and use functools.total_ordering to derive the rest. Or use @dataclass(frozen=True, order=True), which generates all of it.

Mid-levelWhat is the difference between is None and == None?

is None checks identity with the one None object — always correct and fast. == None calls __eq__, which a class can override to return anything (NumPy arrays return an array of booleans, and pandas raises). PEP 8 says use is.

Mid-levelHow do you profile a slow Python program?

Measure before guessing. cProfile (python -m cProfile -s cumulative app.py) shows where the time goes by function; time.perf_counter for a specific block; line_profiler for line-level detail; tracemalloc for memory. Nine times out of ten the fix is algorithmic — an O(n²) loop, or a list where a set was needed — not micro-optimisation.

Mid-levelWhat is duck typing and how do type hints support it?

Duck typing: you use an object for what it can do, not for what class it is — anything with .read() is file-like. Type hints support it with typing.Protocol: a class that declares the methods required, which any object satisfies structurally without inheriting from it. So you get static checking without giving up the flexibility.

Mid-levelHow does Python's sort work, and what does "stable" mean?

Timsort — a hybrid merge/insertion sort that is O(n log n) worst case and exploits existing runs in real data. Stable means equal elements keep their original relative order, which lets you sort by several keys in successive passes (least important first) or in one pass with a tuple key like (-count, name).

Mid-levelExplain LEGB and give an example of a scope bug.

Name lookup order: Local, Enclosing, Global, Built-in. The classic bug: a function reads a global count and later assigns to it — the assignment makes count local for the whole function, so the earlier read raises UnboundLocalError. Fix by passing values in and returning them, or with global/nonlocal if you must.

Mid-levelWhat is a closure and where does Python use them?

A function that captures variables from its enclosing scope and keeps them after that scope has returned. Decorators are closures over the wrapped function; factories like make_multiplier(3) are closures over the argument. The late-binding trap: closures capture the variable, not its value at creation, so lambdas in a loop all see the final value unless bound with a default argument.

Mid-levelWhat happens when you import a module twice?

The second import is a dictionary lookup in sys.modules and returns the already-loaded module object — the file is executed only once per process. That is why module-level code is effectively a singleton, and why a circular import fails: the second module finds a partially-initialised entry in sys.modules.

Mid-levelHow would you process a 20 GB CSV on a laptop with 8 GB RAM?

Stream it: iterate the file line by line (or with csv.reader) and aggregate as you go, keeping only the running results in memory. With pandas, read_csv(chunksize=…) and combine per-chunk results. If it must be sorted or joined, use a database (sqlite3 or DuckDB) or move to Spark. The wrong answer is f.read().

Mid-levelWhat is the difference between threading, multiprocessing and concurrent.futures?

threading: threads sharing memory, good for I/O-bound concurrency, limited by the GIL for CPU. multiprocessing: separate processes, true parallelism for CPU-bound work, data must be pickled between them. concurrent.futures: one high-level API (ThreadPoolExecutor/ProcessPoolExecutor) over both, with submit and map — use this one.

Mid-levelHow do you write a unit test in Python?

With pytest: a file named test_*.py, functions named test_*, plain assert statements. Fixtures provide setup; @pytest.mark.parametrize runs one test over many inputs; pytest.raises checks exceptions. Tests should be fast, independent, and test behaviour through the public interface, not implementation details.

Mid-levelWhat is the difference between a shallow copy of a dict and dict.copy()?

They are the same thing — d.copy(), dict(d) and {**d} all produce a new dict with the same value objects. The nuance interviewers want: nested dicts or lists inside are shared, so mutating copy["nested"]["k"] changes the original. copy.deepcopy when the values are mutable.

Mid-levelExplain Python's data model in one sentence, then give an example.

Every operator and built-in function is dispatched to a dunder method on the object, so a class can participate in the language by implementing them. Example: len(x) calls x.__len__(); x + y calls x.__add__(y); for i in x calls x.__iter__(); with x calls __enter__/__exit__.

Mid-levelWhat are dataclasses and when do you prefer them to a namedtuple or a dict?

@dataclass generates __init__, __repr__, __eq__ (and optionally ordering and hashing) from annotated fields. Prefer it over a dict when the fields are known — you get attribute access, defaults, type hints and editor support. Prefer it over a namedtuple when you need mutability or methods; use frozen=True for an immutable record.

Mid-levelHow do you make an HTTP call robust in production?

Set a timeout (there is none by default), check the status (raise_for_status), retry idempotent requests on connection errors, timeouts, 429 and 5xx with exponential backoff and a cap, reuse a Session, and log the request id from the response. Never retry a non-idempotent POST blindly.

03

Senior — 10 questions

Senior questions have no single right answer. They test judgement: how you diagnose, what you would ask first, what you would refuse to do. The model answers show the shape of a strong response — a method, the trade-offs named, and a concrete first step.

SeniorA batch job that took 10 minutes now takes 3 hours after a "small" change. How do you find the cause?

Reproduce on a sample, then profile — cProfile sorted by cumulative time — rather than reading the diff for guesses. Look for the shape of the regression: an O(n) step that became O(n²) (a lookup that moved from a set to a list, a join done with nested loops), a query in a loop (N+1), or a cache that stopped working. Compare the profiles before and after; the top entry changes. Then add a test that asserts the runtime bound on the sample.

What they are really testing: Method over cleverness: measure, compare, bisect.

SeniorDesign the error-handling strategy for a data pipeline that processes millions of records nightly.

Separate record-level errors from run-level errors. A bad record is expected: validate, write it to a dead-letter store with the reason, count it, and continue; fail the run only if the bad-record rate crosses a threshold. A run-level error (source unavailable, schema changed) should fail fast and loudly, be idempotent to retry, and alert. Make every step re-runnable — write outputs atomically, key on a run id — so a 3 am failure is a re-run, not a cleanup.

SeniorWhen would you choose asyncio over threads, and what are its failure modes?

For thousands of concurrent I/O operations — web scraping, a service fanning out to many APIs, websockets — where threads would be heavy. Its failure modes: one blocking call (time.sleep, a sync DB driver, CPU work) freezes every coroutine; every library in the path must be async-aware; error handling and cancellation are subtle (gather versus TaskGroup); and debugging is harder. For a handful of concurrent requests, a ThreadPoolExecutor with sync code is simpler and just as fast.

SeniorHow do you structure a Python project so it survives five years and ten engineers?

A src/ layout with one installable package, pyproject.toml with pinned dependencies and a lockfile, a clear public API in __init__.py, layers that only depend downward (I/O at the edges, pure logic in the middle), type hints checked in CI, ruff and black so style is never discussed, tests that run in under a minute, and an ADR-style docs folder for the decisions that were not obvious. The goal is that the next engineer can change one thing without reading everything.

SeniorExplain how you would make a function idempotent and why it matters.

Idempotent: calling it twice with the same input has the same effect as once. It matters because retries happen — network failures, re-run pipelines, at-least-once queues. Techniques: key writes on a deterministic id and upsert instead of insert; check-then-act inside a transaction; make the operation a pure function of its input plus a version; record processed ids. The practice problem "idempotent upsert" is exactly this.

SeniorWhat is wrong with except Exception: pass, and when is catching Exception legitimate?

It hides every bug — typos, wrong types, missing keys — as silence, and the failure surfaces somewhere unrelated, hours later, with no traceback. Catching Exception is legitimate at a boundary where the process must not die: the top of a worker loop, a request handler — provided you log the traceback (logging.exception), count it, and either re-raise, return an error response, or continue deliberately. The sin is the pass, not the breadth.

SeniorHow does CPython execute code, and where does that matter in practice?

Source → AST → bytecode (cached as .pyc) → executed by a stack-based interpreter loop, with objects on a heap managed by refcounting plus a cycle GC. Where it matters: attribute and global lookups are dictionary operations (so hot loops benefit from local variables), function calls are relatively expensive (so vectorise with NumPy or batch), and the GIL serialises bytecode execution across threads. Knowing this is why "move the loop into C" — NumPy, pandas, Polars — is the standard performance answer.

SeniorA colleague proposes rewriting a working 2,000-line script into classes "to make it object-oriented". How do you respond?

Ask what problem the rewrite solves. Classes earn their place when there is state that several functions share and an interface others depend on; a script that reads, transforms and writes is often best as a small set of pure functions with one main(), which is easier to test and to reason about. If the real problems are duplication or untestability, address those directly — extract functions, add tests, introduce a dataclass for the record type — and let structure emerge. Rewrites for their own sake lose the bug fixes nobody remembers.

SeniorHow would you add type checking to a large untyped codebase without stopping feature work?

Incrementally and enforced. Turn on mypy (or pyright) with everything allowed to fail, then ratchet: new files must be typed, touched functions get annotated, and a CI check ensures the error count only goes down. Start at the boundaries — public APIs, data models, anything a dataclass or Pydantic model can describe — because that is where hints catch real bugs. Avoid a "type everything" sprint; it produces Any everywhere and no safety.

SeniorWhat would you look for reviewing a junior engineer's Python pull request?

In order: does it do what the ticket asked and is that visible in a test; is the failure behaviour deliberate (what happens on bad input, missing file, network error); are mutable defaults, bare excepts, string-built SQL and hard-coded secrets absent; are names and function sizes such that I can read it without the description; and only then style, which the linter should have handled. The review comment that teaches the most is a question — "what happens if the list is empty?" — not a rewrite.

04

The coding round, walked through

A live coding round is 30–45 minutes on one or two problems, usually shared in an online editor. The interviewer is not grading whether you finish; they are grading how you think, communicate and test. Strong candidates follow the same script every time — the one from Module 12.

  1. 1
    Clarify (2 min)

    Restate the problem. Ask about input size, empty input, duplicates, negatives, what to return when there is no answer. Write the contract as a comment at the top.

  2. 2
    Example (1 min)

    Work one small case by hand and write it down. It becomes your first test.

  3. 3
    Brute force out loud (2 min)

    "The obvious approach is every pair, O(n²)." Say it, then say what is being repeated. Many interviewers will accept the brute force if you can name its complexity and the better approach.

  4. 4
    Pick the pattern (1 min)

    Hashmap? Two pointers? Sort first? Stack? Name it, and why. Then code.

  5. 5
    Code (15 min)

    Talk while you type — what each line is for. Use real names. Handle the edge cases you listed. Do not optimise yet.

  6. 6
    Test (5 min)

    Walk through your hand example line by line, then an edge case. Find your own bug before they do — it scores higher than never having one.

  7. 7
    Complexity + improvements (2 min)

    State time and space. Say what you would change with more time. Stop talking.

pythonEdit it. ⌘/Ctrl + Enter runs.
You should see
['b', 'a']
[]
[]
['y']
['b', 'a']

What loses the round

  • Silence for ten minutes, then a wall of code
  • Starting to type before the problem is clear
  • Arguing with a test case instead of running it
  • "It should work" without tracing an example
  • Optimising a solution that is not yet correct

What wins it

  • Narrating your reasoning, including dead ends
  • A written contract and example before code
  • Testing your own code and finding your own bug
  • Naming the complexity without being asked
  • Saying "I would use a set here because…"
05

Take-home assignment checklist

A take-home is judged on what it is like to work with you. Reviewers open the README first, then the tests, then the code. Most submissions that fail do so on the first two, not the third.

  • README: what it does, how to run it in three commands (python -m venv, pip install -r requirements.txt, python -m app), how to run the tests, and the decisions you made and why — including what you deliberately left out.
  • It runs on a clean machine: pinned requirements.txt, no absolute paths, no reliance on files outside the repo, python3 -m pytest is green.
  • Tests that cover the happy path, the empty case, a malformed input, and the one tricky rule in the spec. Five good tests beat fifty trivial ones.
  • Structure: a small package, not one 600-line file. Pure logic separated from I/O so the tests do not need files or a network.
  • Errors are handled on purpose: bad rows are reported, not silently dropped; missing files give a message, not a traceback.
  • Types and style: type hints on public functions, ruff/black clean. It signals that you work on a team.
  • Scope: do exactly what was asked, well. One extra you are proud of, mentioned in the README, is fine; five half-done extras are a red flag.
  • Commits: a handful of meaningful commits, not one "final" dump. Reviewers read the history.
  • No secrets, no .venv, no data dumps in the repo — a proper .gitignore.
  • Time-box to what they said (usually 3–4 hours) and say so. Over-investing reads as either dishonest about the time or unable to prioritise.
The sentence reviewers want to write
"Clear README, ran first time, tests cover the edge cases, and the code is easy to follow." Aim every decision at that sentence.

Finish the Python handbook, then get hired

Sit the exam for your certificate, run your resume through the ATS checker, and see the jobs that ask for exactly this.

Check my resume
Found this course useful? Share it.
ShareXLinkedIn

Comments

0

Join the conversation. Sign in to leave a comment — we'd love to hear your thoughts.