Skip to main content

The API that does not exist, and the one that does something else

Level 5: Level 5: Judging Code You Did Not Writemedium24 minstdlib semanticsapi verificationcode reviewdebugging

An invented method fails loudly. The dangerous case is the method that is real, spelled right, and does not mean what the name suggests.

Two different problems wearing one name

"Hallucinated API" usually gets described as a model inventing a method that was never in the library. That happens, and it is the easy version: AttributeError: 'str' object has no attribute 'rreplace' on the first run, fixed in a minute, no harm done.

The expensive version is the call that is real. The method exists, it is spelled correctly, it accepts the arguments given, and it does something other than what the surrounding code assumes. Nothing raises. The tests the author ran happen not to distinguish the two behaviors.

def strip_prefix(url):
    return url.strip("https://")     # real method, wrong meaning

str.strip does not remove a prefix. It removes any character in the string you gave it, from both ends, repeatedly, until it hits a character that is not in the set. So the argument "https://" is really the set {h, t, p, s, :, /}.

"https://shop.example.com".strip("https://")   # 'op.example.com'
"shop.example.com".strip("https://")           # 'op.example.com'

The leading s and h of shop are in the set, so they go too. On a URL like https://api.example.com the result looks close enough to right that a quick glance passes it.

Check yourself
What does 'https://status.example.com'.strip('https://') return?

The stdlib calls most often used with the wrong meaning

The callWhat people assumeWhat it does
text.strip("abc")removes the prefix "abc"removes any of a, b, c from both ends
text.replace(old, new)replaces the first occurrencereplaces every occurrence
items.remove(x)removes every xremoves only the first one, and raises if absent
sorted(items, reverse=True)reverses the sorted listsorts descending, and keeps the original order of ties
sorted(items)[::-1]the same as reverse=Truealso flips the order of tied elements
dict.get(key)raises when missingreturns None when missing
round(2.5)rounds up to 3rounds to even, giving 2
datetime.utcnow()a UTC-aware timestampa naive datetime with no timezone attached

Nothing on that list is exotic and every row is a bug somebody shipped this month. The pattern is always the same: the name suggests one behavior, the implementation has another, and the difference only shows on inputs the author did not try.

Check yourself

Sort each generated line by how it will fail.

sorted(items, cmp=my_compare)
url.strip('https://')
text.rreplace('a', 'b')
path.replace('.tmp', '')
config.get('retries').upper()

Verifying in ten seconds

You do not need to memorise the table. You need the habit of checking, and Python makes it cheap.

help(str.strip)          # the docstring says "characters to be removed", not "prefix"
dir(str)                 # every real method on str, so an invented one is absent
"abcabc".replace("a", "")   # try it on an input where the two readings differ

The last line is the one that matters, and it is the same technique as the whole level: pick an input on which the two candidate meanings give different answers, and run it. If strip removed a prefix, "shop".strip("https://") would be "shop". It is "op". Question settled, no documentation required.

Check yourself
You are unsure whether sorted(items, reverse=True) and sorted(items)[::-1] behave identically. What is the fastest way to find out?

Version drift

The other half of this problem is code that was correct, for a different version. str.removeprefix arrived in Python 3.9, so it is exactly right today and an AttributeError on an old runtime. The cmp argument to sorted was correct in Python 2 and has been gone for over a decade, which does not stop it appearing in generated code, because a great deal of Python 2 was written.

The check is the same either way. Know which version you actually run, and when a call looks unfamiliar, confirm it exists there rather than assuming the code was written for your environment.

Interview nuance: if you use an API in an interview and you are not certain of its behavior, say so and test it: "I think strip takes a character set rather than a prefix, let me check with a quick example." That is not a confession of ignorance, it is the exact habit that prevents this bug class, and interviewers read it as maturity. Guessing confidently and being wrong reads as the opposite.

Check yourself
Which of these generated lines is most dangerous to merge?
Worked example (Python)
print("https://shop.example.com".strip("https://"))   # op.example.com
print("shop.example.com".strip("https://"))           # op.example.com
print("https://shop.example.com".removeprefix("https://"))   # shop.example.com

Apply

Your turn

The task this lesson builds to.

Write a function check(name) that returns True only when the candidate stored under name is a correct strip_prefix, and False otherwise.

The contract: strip_prefix(url) returns url with a leading "https://" removed when it is there, and returns url unchanged when it is not. Only the leading occurrence is removed, so an https:// appearing later inside the string stays exactly where it is.

The starter holds four candidates in CANDIDATES. Two are correct, and the two that are not use real string methods that mean something other than what their names suggest here. Keep check 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.

An activity feed on your product shows records newest first, and an assistant produced the newest_first function in the starter. It crashes immediately, which is the good news. The less obvious problem is that the first repair most people reach for changes the order of records that share a timestamp, and the feed is expected to keep those in the order they arrived.

Repair newest_first so it satisfies its contract.

The contract: newest_first(records) returns the records sorted by their "created" value with the largest first. Records that share a "created" value keep their original order relative to each other. The list that comes back holds the same record dictionaries, and the caller's list is not reordered in place.

3 hints and 4 automated checks are waiting in the workspace.