Two small decisions that this function is entirely made of. The second one is the one people get wrong.
def greet(name, greeting=None):
if greeting is None: # not `if not greeting`
greeting = 'Hello'
return f'{greeting}, {name}'
# and the counting rule:
for n in (-1, 0, 1, 2):
print(n, 'singular' if n == 1 else 'plural')
# -> -1 plural / 0 plural / 1 singular / 2 plural
- None means 'not supplied', and nothing else does
`if greeting is None` asks precisely whether the caller passed anything. `if not greeting` also fires for an empty string, so a caller who deliberately passes '' silently gets the default instead. Here that distinction is theoretical; forming the habit on the easy case is how you get it right on the hard one.
- Exactly one is singular — everything else is not
count > 1 looks equivalent and quietly produces '0 file'. English has one singular form and uses the plural for zero, for negatives and for everything above one. Testing == 1 says that rule directly instead of approximating it.
- f-strings convert for you
f'{count} {word}' turns the number into text without str() anywhere, so there is no conversion to forget and no chance of accidentally adding a number to a string.
There are only two possible shapes of answer, and one condition decides between them. Write the condition down in words first — being careful about zero.
If the count is exactly 1, the answer is the count and the word. Otherwise it is the count and the plural — which is either what the caller supplied, or the word with s added.
Return early for the singular case. For the plural, work out the plural form with `plural if plural is not None else word + 's'`, then build the string with an f-string.
def pluralise(count, word, plural=None): if count == ____: return f'{count} {word}' ending = plural if plural is not ____ else word + '____' return f'{count} {ending}'
def pluralise(count, word, plural=None):
if count == 1:
return f"{count} {word}"
ending = plural if plural is not None else word + "s"
return f"{count} {ending}"
Handling the simple case first and leaving means the rest of the function only ever deals with plurals, so the 'which plural?' decision appears once. The alternative — one f-string with a nested conditional inside it — fits on a line and takes twice as long to read.
`plural or word + 's'` is shorter and treats an empty string as 'not supplied'. That is the same falsy-value bug as the deep-get and first-match exercises, and it is worth writing the longer form every time so the habit is automatic by the time it matters.
Returning '1 file' rather than 'file' means the caller cannot accidentally format the number differently from the word it agrees with. Small functions that hand back a complete, consistent phrase are harder to misuse than ones that hand back a fragment.
This is English-only, and it is not a small limitation. Plenty of languages have more than two number forms — Polish and Arabic have several, chosen by rules far more involved than 'is it one?' — and some have none at all. Any interface that will be translated needs a real i18n library with plural rules per locale, such as gettext’s ngettext or ICU MessageFormat. Write this for an internal tool or a log line; do not let it become the foundation of a product that will later be localised.
- Add a `number_words` option so counts under ten render as 'three files', which is what most style guides ask for in prose.
- Handle the common English endings automatically — y to ies, s/x/ch/sh to es — then count how many exceptions you have to hard-code before you decide the parameter was better.
- Write the ngettext version of the same call and compare what the translator has to be given in each case.