One character apart, and the difference is not a rounding detail. Read the second line carefully.
print(3 / 4) # -> 0.75 true division, always a float print(3 // 4) # -> 0 floor division, the fraction is gone print(3 / 4 * 100) # -> 75.0 what you want print(3 // 4 * 100) # -> 0 what you get if you type one slash too many print(4 / 2) # -> 2.0 still a float, even when it divides exactly
- / always produces a float
Even 4 / 2 gives 2.0 rather than 2. That is deliberate in Python 3: division returns the mathematical answer, and you ask for an integer explicitly when you want one. It is why the contract here says 75.0 and not 75.
- // discards the fraction, it does not round
3 // 4 is 0, not 1 — floor division goes downwards, always. When the fraction was the whole point, as it is here, using it turns every answer under 100% into zero. This is a fast way to produce a progress bar that never moves.
- Order matters less than you think, but pick one
part / whole * 100 and part * 100 / whole give the same answer for these inputs. Dividing first reads as 'what fraction is it, expressed per hundred', which is what the name says, so prefer it.
The arithmetic is one expression. The interesting question is what has to happen before it, because exactly one input can make that expression fail.
Guard whole == 0 first and raise ValueError with a message that says which value was the problem. Then divide part by whole and multiply by 100, using true division.
Two lines of guard, one line of answer: return part / whole * 100. No round(), no int(), no formatting — the caller decides how it is displayed.
def percentage_of(part, whole): if whole == ____: raise ValueError(...) return part ____ whole * 100
def percentage_of(part, whole):
if whole == 0:
raise ValueError("whole cannot be zero - there is no percentage of nothing")
return part / whole * 100
Without the guard you still get an exception — ZeroDivisionError — so why write one? Because the built-in describes the *operation* that failed, and yours describes the *situation*. 'division by zero' sends a reader looking for a division; 'whole cannot be zero' tells them which argument to go and find. Elsewhere on this site the advice is to let built-ins raise; the test is whether you can say something they cannot, and here you can.
Returning 33.33333333333333 looks untidy and is correct: the function’s job is the arithmetic, and how many digits to show is a question about a screen. Round inside and every caller who needs more precision has already lost it. f'{value:.1f}%' at the point of display costs nothing and keeps the two concerns apart.
0 / 5 is a perfectly good 0.0 — nothing has happened yet. 1 / 0 has no answer at all. The two zeros look alike in a bug report and mean entirely different things, which is why one gets a case and the other gets a guard.
For money, do not compute a percentage in floats and then convert to currency — work in integer minor units or Decimal, as the bill-splitting exercise does, or the rounding will not add up. And if the caller only ever wants a formatted string, give them a formatting function instead of this plus a format spec at every call site; returning a number is right when the number gets used, not merely printed.
- Add an optional digits parameter that rounds only when asked, and argue with yourself about whether that is a convenience or the same mistake wearing a parameter.
- Write percentage_change(before, after) and notice that it has a different zero problem — and that there is no agreed answer for a change from zero.
- Make a version that returns a formatted string and one that returns a float, then decide which one your own code would import more often.