Loading...
Loading...
Python interview guide
Questions from the interview-preparation section: core Python, OOP, collections and output prediction. Every answer is short enough to say aloud, and every question links to the lesson that teaches it.
60 questionsLast updated
Make Book and Member small classes, perhaps dataclasses. A Library holds a dict of books and a dict of members, and a Loan links one member with one book and a due date. The library has-a collection and is not a list itself, which keeps its interface small.
Follow-up: How would you add late fees? Add a fee() method on Loan, or a fee policy object passed to Library, so the rule can change without touching the other classes.
__slots__ and __dict__ for a dataclass?By default each instance has a __dict__. @dataclass(slots=True) creates __slots__ instead, which saves memory and blocks stray attributes. It is helpful when you create many small objects.
Instance methods use the object through self. Class methods take cls and suit alternative constructors. Static methods take neither and are plain functions kept in the class for organisation. If a static method does not belong to the class, make it a module function.
Say the sentence: "B is an A" means inheritance, "B has an A" means composition. Then ask whether the child works everywhere the parent is used. If not, or if you are only reusing code, hold an object instead.
Lesson: OOP Interview Drill and CheckpointUse a property setter that raises ValueError for bad data, and assign through the property in __init__ so the first value is checked too. For simple value objects, __post_init__ in a dataclass works.
Code uses an object by its methods, not its class. It keeps code flexible and avoids needless base classes. The trade-off is that mistakes appear at run time, so tests, type hints and protocols help.
Lesson: OOP Interview Drill and Checkpointlen, for and in?Define __len__, __iter__ and __contains__, or just __getitem__, which gives iteration and in as fallbacks. Define __bool__ only if truthiness should differ from length.
Does each class do one job? Are is-a relationships true? Are mutable defaults shared by mistake? Are __eq__ and __hash__ paired, and are collaborators passed in? These five checks catch most problems.
Follow-up: Which one catches most bugs? Mutable defaults and shared class attributes, because they look correct until two objects are used at once.
Lesson: OOP Interview Drill and CheckpointNo. A tuple's hash is built from the hashes of its items, so one unhashable item such as a list makes hash(t) raise TypeError. A tuple of ints and strings is hashable and works as a dictionary key.
append is O(1) amortized. The list keeps spare capacity, so most appends only write one slot. When the block is full, a bigger block is allocated and items are copied, which costs O(n) but happens rarely, so the average per append stays constant.
pop(), pop(0) and pop(i)?pop() removes the last item in O(1). pop(0) and pop(i) have to shift the items after the removed one, so they cost O(n - i), which is O(n) in the worst case. Use pop() for stacks.
Shallow. The new list holds the same objects as the old one, so a nested list is shared. Changing copy[0][0] shows up in the original. Use copy.deepcopy when nested data must be independent.
a[::-1] do, and how does it differ from a.reverse()?a[::-1] builds a new reversed list and leaves a alone. a.reverse() reverses in place and returns None. reversed(a) gives a lazy iterator without copying, which is best when you only loop once.
References. The list holds pointers, so the objects live elsewhere and can be shared between lists. That is why small ints and strings can appear in many lists without copies, and why aliasing bugs are possible.
Lesson: Lists, Tuples and SlicingIt is a hash table. hash(key) selects a slot in an index array, and the index points into a compact array of entries that stores the hash, key and value in insertion order. Equality confirms the match. This layout, used since CPython 3.6, saves memory and is why dicts keep order.
Python sets __hash__ to None, so instances raise TypeError when hashed and cannot go in sets or be dict keys. Define both and build the hash from the same fields used in __eq__. A frozen dataclass does this for you.
No. A set is unordered, and its iteration order depends on hashes and table size. Small ints often look sorted, and strings change order between runs because of hash randomization. Use sorted(s) when you need a fixed order.
d[key] and d.get(key)?d[key] raises KeyError if the key is missing. d.get(key) returns None, or a default you pass as the second argument. Use get when a missing key is normal, and the index when it would be a bug.
A frozenset is an immutable, hashable set. Use it as a dict key or as a member of another set, for example to store a group of items whose order does not matter. It supports the same read operations as a set.
Lesson: Dicts, Sets and Hashingpopitem() removes and returns the last inserted pair, so a dict can act as a stack of pairs. Before 3.7 it returned an arbitrary pair. It raises KeyError on an empty dict.
hash() return for an int?For small ints it returns the int itself, with one exception: hash(-1) is -2, because -1 is reserved as an error code in the C API. Large ints are reduced modulo a large prime. Equal numbers like 3 and 3.0 give the same hash.
get(k, default) reads without storing. setdefault(k, default) reads and stores the default. defaultdict(factory) does the storing automatically on any d[k] for a missing key. defaultdict is the neatest for grouping and counting.
It keeps only the newest n items: once full, an append on one end drops an item from the other end automatically. It is a neat fit for a moving window, recent history and 'last n lines' tasks.
Lesson: collections, heapq and Choosing a StructureIt stores a binary min-heap in a normal list: for index k, the children are at 2k+1 and 2k+2, and every parent is less than or equal to its children. heappush and heappop take O(log n); heap[0] is the smallest.
Push tuples (priority, item). If priorities tie, Python compares the items, which fails for objects that cannot be ordered. Insert a unique counter, (priority, count, item), so the comparison never reaches the item.
PriorityQueue wraps heapq with locks, so it is safe across threads, but it is slower and has no peek. For single-threaded code and interviews, plain heapq is the usual choice.
Lesson: collections, heapq and Choosing a StructureA set (or dict keys). x in s is O(1) on average, against O(n) for a list. Choose a list only when order or duplicates matter.
It groups several dicts into one view that searches them in order and writes only to the first. It is useful for layered settings such as command line, then environment, then defaults. It copies nothing.
Lesson: collections, heapq and Choosing a StructureA filter goes at the end: [x for x in a if x > 0] drops items. A choice between two values goes at the front: [x if x > 0 else 0 for x in a] keeps every item. Mixing them up is a very common mistake.
The for clauses are in the same order as nested loops: [x for row in grid for x in row] flattens a grid. Transposing uses zip(*grid). If it needs more than two clauses, write loops for readability.
Yes. Changing a set's size while looping raises RuntimeError: Set changed size during iteration. Loop over list(s) or build a new set instead. Note that the check can miss a change that adds and removes one item in the same step.
Timsort, a hybrid of merge sort and insertion sort. It is O(n log n) in the worst case, O(n) on data that is already sorted or has long sorted runs, and needs up to O(n) extra space. It is stable.
Lesson: Comprehensions, Safe Iteration and SortingBoth give descending order, but reverse=True keeps equal items in their original order, as stability promises. reversed(sorted(x)) also reverses the equal items. This matters when the items are records with the same key.
Both return the first item that has the best key. max(rows, key=lambda r: r[1]) returns the earliest row among equal marks. They run in O(n), so use them instead of sorting when you need one item.
No. A comprehension is meant to build a value, so calling print or append inside it hides the effect and creates a throwaway list. Use a normal for loop when the point is the side effect.
Count with Counter in O(n), then use most_common(k), which uses a heap and costs about O(m log k) for m distinct words. For stable tie-breaking, sort the items with the key (-count, word).
Use collections.deque: append neighbours and popleft the next node. Both are O(1). A list with pop(0) would make the search slower than it should be.
x in range(n) fast for ints but x in list(range(n)) is not?A range object checks membership with arithmetic in O(1) for ints. Converting it to a list creates n items and then scans them in O(n). Keep the range object when you only need the test.
Lesson: Collections Interview Drill and CheckpointFor one query, heapq.nlargest(k, a)[-1] is O(n log k) and a size-k min-heap keeps memory small. Sorting is O(n log n) and simplest. Quickselect averages O(n) but is longer to write; mention it, then choose by constraints.
Dicts keep insertion order, guaranteed since Python 3.7. Sets have no order guarantee, and the order of strings in a set can change between runs. Sort the set when output must be stable.
Lesson: Collections Interview Drill and CheckpointDo not change it. Build a new collection with a comprehension, or loop over a snapshot such as list(d). Assign back with a[:] = ... when other code holds a reference to the same list.
A generator is an iterator written as a function that uses yield. Each yield pauses the function and gives a value, and the local variables are kept until the next request. It is the simplest way to write a lazy iterator.
Lazy evaluation computes a value only when it is requested. The downside is that a generator can be read only once, has no len(), and cannot be indexed. If you need those, build a list.
An iterator remembers its position and never goes back. After the first pass it is exhausted, so the second pass sees nothing. To loop again, call iter() on the original iterable or store the items in a list.
Follow-up: Is it an error? No, there is no exception; you just get no items, which is why this bug is easy to miss.
Common mistake: Reusing a generator and expecting the data again; it silently gives an empty result.
Lesson: Iterators and GeneratorsUse next() with a generator expression, such as next((x for x in data if x > 10), None). It stops at the first match and the default avoids StopIteration when nothing matches. This is faster than building a filtered list.
It raises GeneratorExit at the paused yield, so the generator can clean up with try/finally and stop. After that the generator is finished. Python calls it automatically when the generator is garbage collected.
Using the generator twice, for example total = sum(g) and then count = len(list(g)), where the second one gets nothing. Another is expecting side effects to run immediately, when they run only on iteration. Create a list if you need two passes.
A decorator is a function that takes a function and returns a new function, usually wrapping the original with extra behaviour. The syntax @name above a def is the same as func = name(func). Typical uses are logging, timing, caching, retries and access checks.
A closure looks up the variable when it runs, not when it is created. Lambdas created in a loop all see the final loop value, such as [2, 2, 2]. Fix it with a default argument like lambda n=n: n or with functools.partial.
Follow-up: Does the same happen with a list comprehension of values? No, a comprehension that stores values like [n for n in ...] copies each value; only functions that read n later share it.
Common mistake: Blaming the lambda. The same happens with a def inside a loop.
Lesson: Decorators and ClosuresMemoization stores results of calls for reuse, trading memory for time. It is a bad idea for functions with side effects, for results that change over time, and for calls that almost never repeat. An unbounded cache can also grow without limit.
Lesson: Decorators and ClosuresIt turns a method into an attribute that is computed on access, so callers write obj.area without brackets. A matching @name.setter can validate assignments. It lets you start with a plain attribute and add logic later without changing callers.
A plain *args, **kwargs wrapper hides the signature from tools, although functools.wraps records __wrapped__, which inspect.signature follows. A decorator can also return a completely different object. Keep the signature the same unless there is a strong reason.
A context manager is an object that sets something up when a with block starts and cleans it up when the block ends, whatever happens inside. It removes the risk of forgetting to close files, release locks or commit transactions. It also makes the setup and cleanup easy to read.
Follow-up: Name three standard context managers. open() for files, threading.Lock for locks, and decimal.localcontext or tempfile.TemporaryDirectory.
Common mistake: Saying it only works for files. Any pair of setup and cleanup steps can be a context manager.
Lesson: Context Managers and the with StatementA true value tells Python the exception was handled, so it is swallowed. False or None lets the exception continue. A manager should hide an exception only when it really dealt with it, as a database transaction does with a rollback.
Follow-up: What if the block did not fail? The return value is ignored because there is no exception to hide.
Lesson: Context Managers and the with StatementBoth do the same job. The generator form is shorter and good for simple setup and cleanup. The class form is better when you need state, extra methods, reuse or full control over exceptions in __exit__.
It runs a block and ignores the exception types you name, then carries on after the block. It replaces a try/except: pass with clearer intent. Statements after the failing line in the block are skipped.
__enter__ starts the transaction and returns the connection. In __exit__, commit if exc_type is None, otherwise rollback and let the exception continue by returning False. Libraries like sqlite3 connections already behave this way.
Then the block is never entered and __exit__ is not called, because the setup did not complete. So __enter__ should clean up after itself if it fails half way through.
CPython uses reference counting for memory, and the GIL protects those counts from being changed by two threads at once. It also made single-threaded programs fast and C extensions easy to write. Removing it needs other protection, which is why free-threaded work was a large project.
Lesson: Threads, Processes, asyncio and the GILasyncio runs many tasks in one thread using an event loop, and tasks switch only at await. Threads are switched by the operating system at any moment. asyncio needs fewer resources for thousands of waits and gives more predictable switching, but the code must use async libraries.
Each lesson has quizzes, practice and more interview questions on the same topic.
Open the first lesson