Nothing broken. Just the two things a dict already does that an if-chain does by hand.
SIZES = {'s': 'small', 'm': 'medium', 'l': 'large'}
print(SIZES['m']) # -> 'medium'
print(SIZES.get('xl')) # -> None (no KeyError)
print(SIZES.get('xl', 'unknown')) # -> 'unknown' the fallback
# The if/elif version of this is nine lines and says nothing extra.
- A mapping is data, and data can be edited without reading logic
Adding a size to the dict is adding one line to a table. Adding a branch to a chain means reading the surrounding conditions to check the new one cannot be shadowed by an earlier match. One of those is safe to do quickly and the other is not.
- get() replaces both the lookup and the else
d.get(key, fallback) returns the value if the key is there and the fallback if it is not — so the final else of the chain becomes an argument. Note d[key] and d.get(key) differ on purpose: one raises for a missing key, the other does not. Pick the one that matches whether 'missing' is an error here.
- Where the table lives matters
Defined at module level it is built once when the file loads. Defined inside the function it is rebuilt on every single call — correct, and pointless work. For nine entries nobody will notice; the habit is what you are practicing.
Every branch pairs one number with one string, and nothing else varies. Where in Python does a collection of key-to-value pairs belong?
Put the nine pairs in a dict. Then the whole function is looking up the status in it, with 'Unknown' when it is not there.
Define the table once at module level, in capitals — it is a constant. The function body becomes a single return using .get with 'Unknown' as the second argument. No if, no else.
MESSAGES = { 200: 'OK', # ... the rest of the table ... } def http_message(status): return MESSAGES.____(status, '____')
MESSAGES = {
200: "OK",
201: "Created",
204: "No Content",
301: "Moved Permanently",
400: "Bad Request",
401: "Unauthorized",
403: "Forbidden",
404: "Not Found",
500: "Internal Server Error",
}
def http_message(status):
return MESSAGES.get(status, "Unknown")
There is now one line of logic and nine lines of data. Adding a code is a one-line change that cannot break the other nine, and the table can be read, printed, counted, or loaded from a file later without touching the function. Separating the two is most of what people mean by 'data-driven'.
Building the dict inside the function would rebuild it on every call and hide it from anything else that wants it. Module level and SHOUTING_CASE is the Python convention for 'this is a fixed table', and it tells the next reader not to mutate it — a convention, not enforcement.
The real gain is that the shape now matches the problem. Anyone can see at a glance that this is a lookup with a default, whereas a chain has to be read line by line before you can be sure there is no hidden condition in the middle of it.
Keep the if/elif when the branches are not a mapping. Ranges — 400 to 499 is a client error — cannot be a dict of every code. Conditions that test more than equality, branches that do different work rather than return different values, and cases where order genuinely matters all belong in a chain. The test is simple: if every branch compares the same variable to a constant and returns a constant, it is a table. If not, leave it alone.
- Add a family() function returning 'client error' for anything 400-499 and 'server error' for 500-599, and notice that this one genuinely wants comparisons.
- Load the table from a JSON file at import time, and decide what should happen if the file is missing.
- Replace the strings with an Enum and work out what that buys you and what it costs a caller who just wants to print the message.