Nothing broken — just the arithmetic that makes integer cents the professional default. Read both outputs.
print(0.1 + 0.2) # -> 0.30000000000000004 print(0.1 + 0.2 == 0.3) # -> False shares = [round(10.00 / 3, 2)] * 3 print(shares, sum(shares)) # -> [3.33, 3.33, 3.33] 9.99 base, extra = divmod(1000, 3) print(base, extra) # -> 333 1 (333*3 + 1 = 1000)
- Binary floating point cannot hold 0.10 exactly
A tenth is a repeating fraction in binary the same way a third is in decimal, so it is stored as the nearest value it can manage. The error is tiny and it accumulates, which is why every accounting system in the world stores whole cents — or uses Decimal, which does base-10 properly.
- A penny has to go somewhere
Ten pounds split three ways is not 3.33 each; that is 9.99 and the bill was ten. Rounding each share independently is how the missing cent appears. The remainder is real money and the function has to decide who gets it, rather than letting rounding decide by accident.
- divmod gives you both halves of the answer
divmod(1000, 3) returns (333, 1): the base share and how many people owe one more. That is the whole calculation, and it makes the 'who pays extra' question explicit rather than hiding it inside a rounding rule.
Two questions: how much does everyone pay, and how many people pay one cent more than that? One built-in answers both at once.
divmod(total_cents, people) gives (base, extra) — the share everyone gets, and the number of cents left over. Since extra is always smaller than people, it is exactly how many people pay base + 1.
Guard both bad inputs first. Then base, extra = divmod(...). Build a list of one entry per person, where the first `extra` of them get base + 1 and the rest get base. A comprehension over range(people) with a conditional expression does it in one line.
def split_bill(total_cents, people): if people < 1 or total_cents < 0: raise ValueError(...) base, extra = ____(total_cents, people) return [base + 1 if i < ____ else base for i in range(____)]
def split_bill(total_cents, people):
if people < 1:
raise ValueError(f"people must be at least 1, got {people}")
if total_cents < 0:
raise ValueError(f"total_cents cannot be negative, got {total_cents}")
base, extra = divmod(total_cents, people)
return [base + 1 if index < extra else base for index in range(people)]
base * people + extra is total_cents — that is what divmod guarantees — and the comprehension hands out exactly `extra` extra cents. There is no rounding anywhere, so there is no accumulated error to worry about and no need to check the sum afterwards. Designing so the invariant cannot break beats asserting that it did not.
Earliest-first is one defensible answer; so is random, so is rotating so the same person does not always lose out across a weekly bill. What is not defensible is leaving it to whichever way round() happens to break ties. Any function that distributes money should be able to say, in words, who is favoured and why.
The function never sees a currency symbol, a decimal point or a locale. Keeping the arithmetic in integers and doing presentation at the edge means the same code works for a currency with three decimal places, or one with none, without touching this line.
Integer cents stop being enough when you need fractions of the smallest unit — interest, currency conversion, per-unit pricing at four decimal places. That is what decimal.Decimal is for: exact base-10 arithmetic with an explicit rounding mode you choose rather than inherit. Never use float for money in either case. And if this is a real ledger, expect to record the rounding decision itself, because a reconciliation that cannot explain a one-cent difference is a reconciliation that gets escalated.
- Add weights so a table can split by what each person ordered, and work out where the remainder should go when the weights are uneven.
- Rotate who absorbs the extra cent based on a seed, so a group splitting a weekly bill shares the burden over time.
- Rewrite it with decimal.Decimal and a chosen rounding mode, then compare which version you would rather debug at the end of a quarter.