Skip to content

Narrowing

This example shows each form the checker narrows on, one function each. Flow typing is what lets a strict checker accept ordinary Python: after if name is None: return 0, name is a str; after isinstance(value, str) fails, value is an int.

Run it

python  narrowing.ppy
ppy     narrowing.ppy
ppy run narrowing.ppy

Guards, boolean operators, the walrus

@ppy.pure
def boolean_guard(name: str | None) -> bool:
    return name is not None and len(name) > 0


@ppy.pure
def walrus_guard(values: list[int]) -> int:
    if (count := len(values)) == 0:
        return 0
    return count
  • and narrows its right operand: len(name) is checked with name: str.
  • or narrows the code after it. In early_default, the body after if name is None or len(name) == 0: return -1 sees a non-empty str.
  • A walrus binding is a local the checker tracks like any other.
  • by_class narrows with isinstance.

match

@ppy.pure
def by_match(value: int | str | None) -> str:
    match value:
        case None:
            return "none"
        case int():
            return f"int:{value + 1}"
        case _:
            return f"str:{value.upper()}"

A case sees the subject with the earlier cases subtracted. The int() case sees int | str, and the wildcard sees only str, so value.upper() checks without a cast.

A case with a guard rules nothing out for the cases after it, because a guard can fail for reasons the pattern does not express.

All six functions are pure and native.

What it prints

python narrowing.ppy, ppy narrowing.ppy, ppy run narrowing.ppy

3 0
True False False
3 -1 -1
4 7
none int:42 str:HI
3 0

Read on: Classes ยท The subset

narrowing.ppy is hand-written; there is no .py source and no conversion step.

10_narrowing/narrowing.ppy

import ppy


@ppy.pure
def optional_guard(name: str | None) -> int:
    if name is None:
        return 0
    return len(name)


@ppy.pure
def boolean_guard(name: str | None) -> bool:
    return name is not None and len(name) > 0


@ppy.pure
def early_default(name: str | None) -> int:
    if name is None or len(name) == 0:
        return -1
    return len(name)


@ppy.pure
def by_class(value: int | str) -> int:
    if isinstance(value, str):
        return len(value)
    return value


@ppy.pure
def by_match(value: int | str | None) -> str:
    match value:
        case None:
            return "none"
        case int():
            return f"int:{value + 1}"
        case _:
            return f"str:{value.upper()}"


@ppy.pure
def walrus_guard(values: list[int]) -> int:
    if (count := len(values)) == 0:
        return 0
    return count


def main() -> None:
    print(optional_guard("ppy"), optional_guard(None))
    print(boolean_guard("ppy"), boolean_guard(""), boolean_guard(None))
    print(early_default("ppy"), early_default(""), early_default(None))
    print(by_class("four"), by_class(7))
    print(by_match(None), by_match(41), by_match("hi"))
    print(walrus_guard([1, 2, 3]), walrus_guard([]))


main()

Source: examples/10_narrowing.