The API that does not exist, and the one that does something else
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.
The stdlib calls most often used with the wrong meaning
| The call | What people assume | What 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 occurrence | replaces every occurrence |
items.remove(x) | removes every x | removes only the first one, and raises if absent |
sorted(items, reverse=True) | reverses the sorted list | sorts descending, and keeps the original order of ties |
sorted(items)[::-1] | the same as reverse=True | also flips the order of tied elements |
dict.get(key) | raises when missing | returns None when missing |
round(2.5) | rounds up to 3 | rounds to even, giving 2 |
datetime.utcnow() | a UTC-aware timestamp | a 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.
Sort each generated line by how it will fail.
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.
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.
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.comApply
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.