Check it against a version that is obviously correct
Write the slow, dull, clearly-right implementation and make the clever one agree with it. A brute force you trust is a specification you can execute.
When you cannot tell by reading
Some code is optimized past the point where reading it settles the question. A single pass with a dictionary replaces a double loop, and now correctness depends on an argument about what the dictionary holds at each step. You can follow that argument, and you can also be wrong about it, confidently, in about forty seconds.
There is a way out that costs almost nothing. Write the version that is too slow to ship and too simple to be wrong, then make the fast one agree with it on every input you can generate.
def count_pairs_slow(nums, target):
"""Every pair, checked directly. Quadratic and obviously right."""
total = 0
for i in range(len(nums)):
for j in range(i + 1, len(nums)):
if nums[i] + nums[j] == target:
total += 1
return total
Nobody ships this. But as a reference oracle it is worth more than the clever version, because you can verify it by reading it once, and then it verifies everything else for free.
The differential loop
Generate inputs, run both, compare, stop at the first disagreement.
def first_disagreeing_target(nums, targets):
for target in targets:
if count_pairs_fast(nums, target) != count_pairs_slow(nums, target):
return target
return -1
That is the whole technique. Its power comes from where the inputs come from: you no longer have to be clever about choosing them, because you are not predicting the answer, only comparing two answers. So you can throw thousands of ugly inputs at it, including inputs you would never have thought to write down.
The inputs that find bugs fastest are the ones with structure a human would not bother typing: lots of duplicates, all the same value, everything negative, one element, empty.
When the oracle is the shipped code
Differential testing is not only for speed. It is the fastest way to review a rewrite, a refactor, or a migration, because you already have a version everyone trusts: the one in production.
for row in sample_rows:
assert new_pricing(row) == old_pricing(row)
Every disagreement is either a bug in the new code or an undocumented behavior of the old code that somebody depends on. Both are things you want to find before the deploy rather than after.
Sort each review situation by whether a reference oracle is available to you cheaply.
The two ways this goes wrong
The oracle is wrong too. If you write the reference from the same misunderstanding of the contract, both versions agree and both are wrong. Guard against it by making the reference dumb enough that its correctness is visible in one reading, and by keeping a couple of hand-checked examples alongside.
Nothing generates interesting inputs. A differential loop over ten hand-picked ordinary inputs finds nothing that ten ordinary example tests would not. The inputs have to include the shapes you find uncomfortable.
Interview nuance: offering to write the brute force first is a strong move in an interview, not a weak one. "Let me get the obviously correct version down so I have something to check the optimized one against" shows you know that the optimization is the risky part. Plenty of candidates jump straight to the clever solution and then cannot tell whether it works.
def count_pairs_slow(nums, target):
total = 0
for i in range(len(nums)):
for j in range(i + 1, len(nums)):
if nums[i] + nums[j] == target:
total += 1
return total
def count_pairs_fast(nums, target):
seen = set()
total = 0
for n in nums:
if target - n in seen:
total += 1
seen.add(n)
return total
print(count_pairs_slow([1, 1, 1], 2), count_pairs_fast([1, 1, 1], 2)) # 3 2Apply
Your turn
The task this lesson builds to.
Write a function first_disagreeing_target(nums, targets) that returns the first value in
targets for which count_pairs_fast(nums, target) disagrees with count_pairs_slow(nums, target),
or -1 when the two agree on every target in the list.
Both functions are in the starter. count_pairs_slow is the reference oracle: it checks every pair
directly, so it is slow and correct. count_pairs_fast is the single-pass version under review.
Neither should be changed. No target in the graded inputs is negative, so -1 is safe as the
"they always agreed" answer.
Keep first_disagreeing_target as the last function in the file.
3 hints and 4 automated checks are waiting in the workspace.
Practice
Make it stick
A second problem on the same idea, so it survives past today.
A leaderboard service needs the top scores for a game, and four implementations are sitting in a pull request. All four look reasonable, all four return the right answer in the reviewer's manual test, and the service has started serving a leaderboard that is subtly wrong for some games.
Write a function check(name) that returns True only when the candidate stored under name is a
correct top_k, and False otherwise.
The contract: top_k(scores, k) returns the k largest scores in descending order, keeping
duplicates. k may be 0, and it may be larger than the list, in which case every score is
returned. The caller's list must not be modified.
sorted(scores, reverse=True)[:k] is a correct and obviously right oracle, so use it to produce the
expected value rather than writing expectations by hand. Keep check as the last function in the
file.
3 hints and 4 automated checks are waiting in the workspace.