On this page
Before you start
This lesson uses ideas from these earlier lessons. Read them first if any idea is new to you.
01Why interviewers ask this
The hidden tests of a coding round do not call your code with good values only. They also try bad ones: an item that does not exist, a count of 0, an order that is bigger than the stock. Then they check two things. Did the call fail in the way the statement says? And is the data still exactly as it was before the call?
A weak answer fails in the middle. It changes some data, meets the problem, and stops. The object is left half changed, and an id may be used up for an order that never happened. A strong answer checks everything first and changes data last. It also reports the failure in the way the statement asks.
At SDE-1 level you should reject bad input without a crash. At SDE-2 level you should also make an order of many lines work as one step: all of it happens, or none of it does. You should explain why a method stops with an error in one case and returns a plain "no" in another. Interviewers often ask: "What happens if the third line of the order fails?"
02The idea in plain words
Think of a cashier at a canteen counter. Your basket has three items. A good cashier first looks at all three: are they on the menu, and in stock? Only then does the cashier take your money and print the bill number. A bad cashier takes money for the first item, then finds that the third is finished. Now you have paid for something you did not get, and a bill number is used.
The rule for code is the same: look first, act last. In the lesson on encapsulation a class guarded its own facts. This lesson shows what a method does when a call would break them.
Kinds of bad input
Bad input is a value that the method should not accept. Five kinds appear in almost every problem:
| Kind | Example in a canteen |
|---|---|
| Wrong type | the count is the text "two", not the number 2 |
| Out of range | the count is 0 or -3 |
| Unknown id | the item "vada" is not on the menu |
| Wrong state | cancelling an order that is already cancelled |
| Duplicate | adding an order id that already exists |
Two ways to say no
A method has two usual ways to say "I cannot do this".
- Raise an exception. An exception is the stop signal from the lesson on encapsulation. The method stops at the
raiseline, and the caller gets an error with a message. The caller can catch it withtryandexcept. - Return a marker value. The method returns a value that means "no", such as
False,None,-1or an empty text"". It is also called a sentinel value.
class StockFlag:
def take(self, item, count):
"""Return True if taken, False if not enough."""
if self._stock.get(item, 0) < count:
return False
self._stock[item] -= count
return Trueclass StockRaise:
def take(self, item, count):
"""Take count of item. Raise OutOfStock if not enough."""
if self._stock.get(item, 0) < count:
raise OutOfStock(f"not enough {item}")
self._stock[item] -= countBoth classes keep the stock in a dictionary self._stock. Some code features:
d.get(item, 0)gives the value ofitem, or 0 if the key is missing.d.items()gives each key with its value.dict(stock)makes a copy of a dictionary.- An empty dictionary or list counts as false, so
if not lines:means "the order is empty".
The statement decides which way to use. Many practice statements say two things at once: "throws for bad input, and returns false when there is not enough stock". A bad value is a mistake by the caller. "Not enough stock" is a normal answer. Do both, as written, and write the rule in the docstring (the contract from the lesson on interfaces). This lesson follows that statement. If the statement says nothing, say "I assume bad input raises an error. Tell me if not", and use that everywhere.
A marker value has one danger. The caller can forget to check it, and then the marker acts like a real value.
def price_or_minus_one(item):
prices = {"dosa": 40, "tea": 15}
return prices.get(item, -1)
print(80 + price_or_minus_one("vdaa"))The item name has a typo, so the call returns -1, and the bill is 80 + (-1) = 79. Nothing fails. The customer gets Rs 1 off and nobody is told. An exception cannot be forgotten like this: if nobody catches it, the program stops.
Your own exception classes
Python has built-in errors such as ValueError for a wrong value. You can also make your own. The line class OutOfStock(Exception): pass makes a new error type. The word Exception in brackets makes the new class a child of Python's own Exception class. This is the parent and child idea of the lesson on composition and inheritance. The word pass means "nothing more to add".
Make your own type when the caller must react to one case in a special way: except OutOfStock: catches only that case. Write a message that a person can read, with the item name in it: not enough tea, not error 7.
Never write an empty except:, with no error name. It catches every error, including a typo in your own code:
try:
shop.place_oder([("dosa", 1)])
except:
pass
print("no sign of the typo")The method name place_oder is wrong, but the program prints no sign of the typo and goes on. The order was never placed, and nobody knows. Catch the one error you expect, by name.
Check first, change last
Here is the first draft of an order method. A line is a pair of an item and a count, such as ("dosa", 2). The draft changes the stock as it checks.
class BrokenStock:
def __init__(self, stock):
self._stock = dict(stock)
self._next_id = 1
def place_order(self, lines):
order_id = self._next_id
self._next_id += 1
for item, count in lines:
if self._stock.get(item, 0) < count:
return None
self._stock[item] -= count
return order_idfor item, count in lines takes each pair and names its two parts. The bug is easy to miss. The loop takes the dosa out of the stock, and only then finds that there is not enough tea. The method returns None, but the dosa is gone, and the order id is used up.
The fix has three steps, always in this order:
- Check everything. Look at all the input and the current state. Change nothing.
- Change the data. Now no step can fail for a reason you could have checked.
- Move counters, ids and the status last. The state says "this happened" only when it did.
All or nothing for many items
An order with many lines must be all or nothing: either every line is applied, or no line is. The fix above does this. It checks every line before it applies any line.
Be careful when the same item can appear on two lines. Each line may pass the check alone, but together they ask for too much. So add up the counts for each item first, and check the totals:
def total_wanted(lines):
totals = {}
for item, count in lines:
totals[item] = totals.get(item, 0) + count
return totalsWith a stock of 3 dosa, the lines [("dosa", 2), ("dosa", 2)] give {'dosa': 4}. That is more than 3, so the order is refused before any change.
The other way is undo on failure. You apply each line and remember it. If a line fails, you put back what you already took.
def place_order(self, lines):
done = []
for item, count in lines:
if self._stock.get(item, 0) < count:
for taken, number in done:
self._stock[taken] += number
return None
self._stock[item] -= count
done.append((item, count))
# the id moves here, as in the fixed versionThis works, but it costs more: you must remember every step, and the code that puts things back can have a bug. Use it only when you cannot know in advance whether a step will work.
Cleanup that must always happen: try and finally
Sometimes you change something for a short time and must put it back, whatever happens. Use finally. The code in finally runs when the try part works, and also when it raises an error.
def attempt(fail):
print("start")
try:
if fail:
raise ValueError("boom")
print("work done")
finally:
print("cleanup")attempt(False) prints start, work done, cleanup. attempt(True) prints start, then cleanup, and then the error goes on to the caller. You need finally rarely, because checking first avoids most cleanup.
Check at the front door, trust the inside
In lesson 14 the front door class is the class that the caller talks to. Put your checks there. A helper that outside code can never call may trust its input. A method that outside code can call must check, as the lesson on encapsulation said.
Prove that a failed call changed nothing
A method that "changes nothing" is a claim, so test it. Take a snapshot, a copy of the whole state, before the call. Make the refused call, take a second snapshot and compare. An assert line stops the program with an error if its condition is false (the lesson on interfaces).
Want to check your understanding? Try explaining this idea in your own words. You can continue reading first, or ask the AI tutor a question. Sign up free to ask the tutor
03Pause and predict
Pause. Guess first.
The canteen has 3 dosa and 5 tea, and the next order id is 1. In the first draft above, someone orders 2 dosa and 6 tea, and the order is refused. What is the dosa stock afterwards? What id will the next good order get?
Write your guess before you read on. The answer is in the worked example.
04Worked example
Run with Python 3.11. Assumption: the canteen's statement says that bad input raises an error, and that a short stock returns None and changes nothing. The stock is 3 dosa and 5 tea, and the next id is 1. Both classes have a method snapshot that returns a copy of the stock and the next id: return (dict(self._stock), self._next_id).
shop = BrokenStock({"dosa": 3, "tea": 5})
print(shop.place_order([("dosa", 2), ("tea", 6)]))
print(shop.snapshot())
print(shop.place_order([("dosa", 1)]))It prints None, then ({'dosa': 1, 'tea': 5}, 2), then 2. The dosa went from 3 to 3 - 2 = 1 before the tea check failed. The id 1 was used up, so the next good order gets id 2. This answers the predict box: the stock is 1, and the next id is 2. The refused order left two marks.
Now the fixed class. The public method checks, then changes, then moves the id. It uses total_wanted from above.
class CanteenStock:
def __init__(self, stock):
self._stock = dict(stock)
self._next_id = 1
self._orders = {}
def place_order(self, lines):
"""Return the order id, or None if the stock is short."""
self._check_input(lines)
wanted = total_wanted(lines)
for item, count in wanted.items():
if count > self._stock[item]:
return None
for item, count in wanted.items():
self._stock[item] -= count
order_id = self._next_id
self._next_id += 1
self._orders[order_id] = wanted
return order_idThe check of the input is a private method of the same class. It raises ValueError for bad values and UnknownItem for an item that is not on the menu. (UnknownItem is made like OutOfStock.) In Python, True counts as the number 1, so isinstance(count, int) would accept it. The test type(count) is int does not.
def _check_input(self, lines):
if not lines:
raise ValueError("the order is empty")
for item, count in lines:
if item not in self._stock:
raise UnknownItem(f"unknown item: {item}")
if type(count) is not int or count < 1:
raise ValueError(f"bad count for {item}: {count}")Now run the same order, and a good order after it.
shop = CanteenStock({"dosa": 3, "tea": 5})
before = shop.snapshot()
print(shop.place_order([("dosa", 2), ("tea", 6)]))
assert shop.snapshot() == before
print(shop.snapshot())
print(shop.place_order([("dosa", 2), ("tea", 4)]))
print(shop.snapshot())The output is None, then ({'dosa': 3, 'tea': 5}, 1), then 1, then ({'dosa': 1, 'tea': 1}, 2). The refused order changed nothing: the stock is still 3 and 5, and the next id is still 1. The good order gets id 1. The stock falls to 3 - 2 = 1 dosa and 5 - 4 = 1 tea, and the next id is 2.
If the statement said that a short stock raises OutOfStock, only one line of the first loop would change. The return None would become raise OutOfStock(f"not enough {item}"). The snapshot check would be the same.
Next, five kinds of bad input. Each is followed by the snapshot check.
for bad in [[], [("vada", 1)], [("dosa", 0)], [("dosa", "two")], [("dosa", True)]]:
before = shop.snapshot()
try:
shop.place_order(bad)
except (ValueError, UnknownItem) as error:
print(type(error).__name__, error)
assert shop.snapshot() == beforeThe brackets after except catch either of the two named errors. It prints one line for each call:
ValueError the order is empty
UnknownItem unknown item: vada
ValueError bad count for dosa: 0
ValueError bad count for dosa: two
ValueError bad count for dosa: TrueEvery assert passes, so none of the five calls changed anything.
Two more cases. The stock is now 1 dosa and 1 tea. The lines [("dosa", 1), ("dosa", 1)] each pass alone, but together they need 2 dosa. The call returns None and the snapshot is unchanged.
Last, a wrong state. The method cancel puts the items of an order back and marks the order as cancelled. A second cancel of the same order raises NotAllowed. (NotAllowed is made like OutOfStock.)
def cancel(self, order_id):
if order_id not in self._orders:
raise ValueError(f"unknown order: {order_id}")
if self._orders[order_id] is None:
raise NotAllowed("order is already cancelled")
for item, count in self._orders[order_id].items():
self._stock[item] += count
self._orders[order_id] = NoneAfter shop.cancel(1), the stock is 3 dosa and 5 tea again, and the next id is still 2. The second shop.cancel(1) raises order is already cancelled, and the snapshot is unchanged. In the round, ask this of every method: "if this call is refused, is anything different afterwards?"
05Trade-offs
Checking first solves the half-changed object and the used-up id. A refused call leaves nothing changed, and the tests that look for a trace pass. It costs a second walk over the input and a separate check method. I would change to undo on failure when you cannot know ahead of time whether a step will work.
Raising an error solves the forgotten check, but it costs a crash when nobody catches it. Returning a marker value solves the common "no" of normal work and needs no try. It costs the danger that the marker is used as a real value. I would change from one to the other when the statement says so.
06Common misconceptions
"If a method fails, the object is unchanged." Only if the checks came first. In the first draft, the call returned None after the dosa stock fell from 3 to 1.
"A bare except: keeps my program safe." It hides every error, including a typo such as place_oder. Catch the error you expect, by name, and let all others stop the program.
"Returning -1 is gentler than raising an error." The caller can forget to check, and then -1 is used as a number. In the example, a typo in an item name gave a bill of 79 instead of a failure.
07How to say it in the interview
Say it like this
I check all the input first, then change the data, and I move ids and counters last, so a refused call leaves nothing changed. I follow the statement for how to say no: I raise an error for bad input and return the marker it asks for.
08Check your understanding
- Name the three steps of a safe method in order. Name the two ways to say no.
- The stock is 3 dosa, 5 tea and 2 vada, and the next id is 7. An order asks for 2 dosa, 3 vada and 1 tea. The first draft (
BrokenStock) takes the id first and goes through the lines in the order given. What are the stock and the next id after the refusal? What are they withCanteenStock? - A teammate wrote
get_price(item)so that it returns -1 for an unknown item. The teammate also wrapped the call toplace_orderintry: ... except: pass"so the demo never crashes". What can go wrong, and what do you do?
09Answers
Show the answers
- Check everything, change the data, then move ids, counters and status. The two ways to say no are to raise an exception and to return a marker value such as
False,Noneor-1. - First draft: the id 7 is taken, so the next id is 8. Dosa: 3 - 2 = 1 is taken out. Then vada fails, because 3 is more than 2. So the stock is 1 dosa, 5 tea and 2 vada, and the next id is 8. With
CanteenStock, the check fails before anything changes. The stock stays 3 dosa, 5 tea and 2 vada, and the next id is still 7. - A typo in an item name gives -1, and the bill goes down instead of failing, as in 80 + (-1) = 79. The bare
excepthides every error, so a failed order looks like a good one. Raise an error for an unknown item. Catch only the named errors you expect, such asUnknownItem.
10Summary card: bad input and errors
One-page summary
- Idea. A method either does its whole job or changes nothing.
- Bad input. Wrong type, out of range, unknown id, wrong state, duplicate.
- Saying no. Raise an exception, or return a marker value. The statement decides. Write it in the docstring.
- Order. Check everything, change the data, then move ids, counters and status.
- Many lines. Check all lines first. Add up repeated items. Undo on failure is the costly second choice.
- Cleanup.
finallyruns in every case. Never write an emptyexcept:.- Proof. Take a snapshot before and after a refused call, and compare.
- Trade-off. It solves half changes and used-up ids. It costs a second pass.
- Trap. Changing the stock before the last check, or taking the id first.
11Sources
- Exceptions,
finally, catching a named error, and your own exception classes: Python tutorial, "Errors and exceptions", https://docs.python.org/3/tutorial/errors.html (opens in a new tab) (read 29 Oct 2026). - The statement style "throws for bad input and returns false when not enough" is how Tradeoff's own practice problems are written. Lesson 7 describes the round.
- The canteen, the stock and the prices are made up for teaching. All code was run with Python 3.11.
Try what you just learned
Put this idea into practice with a short exercise or a relevant tool. Free to try. No sign-up needed.
More practice. This needs a free account.
What to learn next
Continue learning with the lessons that build on this one.
- SOLID with small examples
- Composition or inheritance: has-a or is-a
- Interfaces and polymorphism: one call, many behaviours
Practise this idea
Have a question about this lesson?
Ask the AI tutor to explain a difficult idea, walk through an example or check your understanding. It uses the lesson to help you work through the topic.
A free account includes 10 AI tutor questions a day. The lesson remains free to read without an account.
All lessons in Module 2: Low-level design and machine coding
Prefer another topic? Browse all 169 lessons.