Inventory¶
Untyped Python that converts cleanly, with no hand editing afterwards.
inventory.py has no annotations. inventory.ppy beside it has every
parameter and return typed, Sequence where the body only reads, and
@ppy.pure where the checker proved it. examples/verify_conversions.py
regenerates it on every run to keep that claim true.
Run it¶
What it prints¶
ppy convert inventory.py --promote-buffers --dry-run
81 lines
# ---- ./inventory.ppy ----
import math
from collections.abc import Sequence
import ppy
@ppy.pure
def line_value(item: tuple[str, int, float]) -> float:
return item[1] * item[2]
@ppy.pure
def total_value(items: Sequence[tuple[str, int, float]]) -> float:
total: float = 0.0
for item in items:
total += line_value(item)
return total
@ppy.pure
def total_units(items: Sequence[tuple[str, int, float]]) -> int:
total: int = 0
for item in items:
total += item[1]
return total
@ppy.pure
def discounted(price: float, discount: float) -> float:
return price * (1.0 - discount)
def apply_discount(
items: Sequence[tuple[str, int, float]],
discount: float,
) -> list[tuple[str, int, float]]:
out: list[tuple[str, int, float]] = []
for item in items:
out.append((item[0], item[1], discounted(item[2], discount)))
return out
def below_stock(items: Sequence[tuple[str, int, float]], threshold: int) -> list[str]:
low: list[str] = []
for item in items:
if item[1] < threshold:
low.append(item[0])
return low
@ppy.pure
def summary(items: Sequence[tuple[str, int, float]]) -> tuple[float, float, float]:
value: float = total_value(items)
count: int = len(items)
if not count:
return (0.0, 0.0, 0.0)
return (value, value / count, math.sqrt(value))
@ppy.pure
def labeled(name: str, value: float) -> str:
return name + "=" + str(round(value, 2))
def main() -> None:
items: list[tuple[str, int, float]] = [
("bolt", 120, 0.25),
("nut", 340, 0.1),
("washer", 80, 0.05),
("bracket", 15, 12.5),
]
value, average, root = summary(items)
print(labeled("total", value), labeled("avg", average), labeled("root", root))
print(apply_discount(items, 0.2)[0])
print(below_stock(items, 100))
print("units:", total_units(items))
main()
python inventory.ppy, ppy run inventory.ppy
What the converter recovered¶
@ppy.pure
def total_value(items: Sequence[tuple[str, int, float]]) -> float:
total: float = 0.0
for item in items:
total += line_value(item)
return total
list[tuple[str, int, float]]for everyitemsparameter, from the call sites; widened toSequence[...]where the function only reads it.- The return type of every function, propagated along the call graph:
summaryreturns a three-tuple because that is what both of itsreturnstatements build. @ppy.pureon the six functions the checker proved pure, and on nothing it could not.items: list[tuple[str, int, float]]on the local inmain, becausewrite-local-annotationsis on by default.
What the input had to get right¶
An earlier version of this file had one calc(items, kind) that did three
jobs behind an integer flag. It returned int | float, so nothing
downstream could be typed and the conversion produced four errors.
Splitting it into total_value and total_units made the file
convertible.
No tool makes that decision. It is a design change, not a mechanical one, and the converter renames nothing and splits nothing.
Where the code comes from¶
Generated, not hand-written: inventory.ppy is exactly what
ppy convert inventory.py --promote-buffers writes, and
examples/verify_conversions.py checks that on every run.
Read on: Conversion and inference ยท Inference
20_inventory/inventory.ppy¶
import math
from collections.abc import Sequence
import ppy
@ppy.pure
def line_value(item: tuple[str, int, float]) -> float:
return item[1] * item[2]
@ppy.pure
def total_value(items: Sequence[tuple[str, int, float]]) -> float:
total: float = 0.0
for item in items:
total += line_value(item)
return total
@ppy.pure
def total_units(items: Sequence[tuple[str, int, float]]) -> int:
total: int = 0
for item in items:
total += item[1]
return total
@ppy.pure
def discounted(price: float, discount: float) -> float:
return price * (1.0 - discount)
def apply_discount(
items: Sequence[tuple[str, int, float]],
discount: float,
) -> list[tuple[str, int, float]]:
out: list[tuple[str, int, float]] = []
for item in items:
out.append((item[0], item[1], discounted(item[2], discount)))
return out
def below_stock(items: Sequence[tuple[str, int, float]], threshold: int) -> list[str]:
low: list[str] = []
for item in items:
if item[1] < threshold:
low.append(item[0])
return low
@ppy.pure
def summary(items: Sequence[tuple[str, int, float]]) -> tuple[float, float, float]:
value: float = total_value(items)
count: int = len(items)
if not count:
return (0.0, 0.0, 0.0)
return (value, value / count, math.sqrt(value))
@ppy.pure
def labeled(name: str, value: float) -> str:
return name + "=" + str(round(value, 2))
def main() -> None:
items: list[tuple[str, int, float]] = [
("bolt", 120, 0.25),
("nut", 340, 0.1),
("washer", 80, 0.05),
("bracket", 15, 12.5),
]
value, average, root = summary(items)
print(labeled("total", value), labeled("avg", average), labeled("root", root))
print(apply_discount(items, 0.2)[0])
print(below_stock(items, 100))
print("units:", total_units(items))
main()
Source: examples/20_inventory.