We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Precise Type Hints with Literals and Generics step 8 of 26
Keep Cents and Millis Apart with NewType
A billing service splits a charge into equal monthly payments. Amounts are
stored as whole cents, and another part of the system uses millis, which here
means thousandths of the currency, not milliseconds. One cent is ten millis.
Both are plain int values, so nothing stops you passing millis where cents
belong. NewType makes mypy catch the mistake.
assert solve(100, 3) == ([34, 33, 33], [340, 330, 330])
assert solve(-100, 3) == ([-33, -33, -34], [-330, -330, -340])
assert solve(7, 1) == ([7], [70])
What NewType does
from typing import NewType
Cents = NewType("Cents", int)
Millis = NewType("Millis", int)
NewType gives an existing type a new name that mypy treats as a separate
type. At runtime it is the same value, so Cents(5) is the int 5. mypy
then accepts a Cents where an int is expected. It rejects a plain int
or a Millis where a Cents is expected.
Arithmetic on Cents gives a plain int. So return amount * 10 in a
function that returns Millis fails:
Incompatible return value type (got "int", expected "Millis") [return-value]
Write Millis(amount * 10) to mark the result. The multiplication does the
conversion. Millis(...) only tells mypy which unit the number is in.
What to write
def amortise(total: Cents, periods: int) -> list[Cents]: ...
def to_millis(amount: Cents) -> Millis: ...
def solve(total: int, periods: int) -> tuple[list[int], list[int]]: ...
amortisereturns exactlyperiodsparts that add up tototal. Ifperiods < 1, raiseValueError("periods must be >= 1").- Compute
base, remainder = divmod(total, periods). The firstremainderparts arebase + 1, and the rest arebase, in that order. - Use integer arithmetic only, with no floats.
to_millismultiplies by 10 and returnsMillis.solvecallsamortise(Cents(total), periods). It returns a tuple of twolist[int], the cent parts and the same parts converted withto_millis. Let theValueErrorfromamortisepropagate.
| Total | Periods | Parts |
|---|---|---|
| 100 | 3 | [34, 33, 33] |
| -100 | 3 | [-33, -33, -34] |
| 1 | 3 | [1, 0, 0] |
| -1 | 3 | [0, 0, -1] |
| 0 | 3 | [0, 0, 0] |
Negative totals need no special case. divmod rounds down, so
divmod(-100, 3) is (-34, 2). The remainder is never negative when
periods is positive, so two parts get -33 and one gets -34.
Build new lists to return
solve returns list[int], but amortise gives you a list[Cents], and
mypy does not let you return one as the other:
Incompatible return value type (got "list[Cents]", expected "list[int]") [return-value]
The reason is that a function that takes list[int] could append a plain
int to your list of Cents. The exercise "Why a list[Dog] Is Not a
list[Animal]", later in this track, explains this rule. For now, build a new
list, such as [int(part) for part in parts].
Going further
Your code, saved here
Sign in to access your accepted solutions and saved draft history. You can keep reading the lesson without an account.
Sign in to your accountStuck?
Python reference solution
Sign in to track your attempts toward unlocking the reference solution.