Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Narrowing through a predicate

Project overview and reading path

A helper recognizes nonempty text in a str | int value. Its caller wants to strip the text after the check.

Typical Python

def is_nonempty_text(value: str | int) -> bool:
    return isinstance(value, str) and bool(value.strip())


def normalize_name(value: str | int) -> str:
    if is_nonempty_text(value):
        return value.strip()
    raise ValueError("name must be nonempty text")


name = normalize_name(7)

The checker rejects .strip() because a plain bool return does not tell it which type the predicate recognized. For 7, the caller also gets an unannounced ValueError at runtime.

Alternative

def is_nonempty_text(value: str | int) -> TypeGuard[str]:
    return isinstance(value, str) and bool(value.strip())


def normalize_name(value: str | int) -> str | InvalidName:
    if is_nonempty_text(value):
        return value.strip()
    return InvalidName(value)


outcome = normalize_name(" training ")
match outcome:
    case str() as name:
        normalized = name
    case InvalidName():
        normalized = "rejected"
    case _:
        assert_never(outcome)

The failure is a typed outcome as in Result and match.

Source

TypeGuard[str] communicates the narrowing. normalize_name(" training ") returns "training", and the checker accepts .strip() in the guarded branch. The return type also forces callers to distinguish InvalidName before calling string methods. For a single use, an inline type check may be simpler.

The checker trusts the guard’s author; it does not prove the predicate correct or encode nonemptiness in str. Invalid input returns InvalidName; external schema validation belongs at a boundary.