Skip to content

Fixed and named sizes

Named sizes: one name, everywhere it matters

Magic numbers in two places drift apart. A Literal alias gives a size one name that works in every annotation, and every message still spells the resolved numbers:

from typing import Literal
import numpy as np
from numty import F64

N = Literal[128]  # samples in
M = Literal[127]  # samples out: one difference (the checker verifies this)
C = Literal[6]    # channels

def profile(frames: F64[N, C], mix: F64[C]) -> F64[N]:
    return np.average(frames, axis=1, weights=mix)

def step_back(y: F64[N, C]) -> F64[M, C]:
    return np.diff(y, axis=0)   # checked: 128 - 1 = 127

Rename M to 126 and the return no longer lines up; pass five weights where six channels are named and the call fails with the numbers, not the names:

error: Incompatible return value type (got "ndarray[tuple[Literal[127], Literal[6]], dtype[float64]]", expected "ndarray[tuple[Literal[126], Literal[6]], dtype[float64]]")
error: Argument 2 to "profile" has incompatible type "ndarray[tuple[Literal[5]], dtype[float64]]"; expected "ndarray[tuple[Literal[6]], dtype[float64]]"

The clean file is tests/fixtures/named_dims.py (checked clean, runs to (128,) (127, 6)); the two errors are pinned by tests/fixtures/bad_diff_size.py and tests/fixtures/bad_named_dims_weights.py.

Your own sizes, in one line

No pre-baked list from the library — exact sizes live in your code, one line each, in standard typing that needs no plugin to write (what each checker catches without the plugin is in How it works):

from typing import Literal
from numty import FloatArray

A2x3 = FloatArray[tuple[Literal[2], Literal[3]]]

def use_2x3(m: A2x3) -> None: ...

use_2x3(np.zeros((3, 2)))  # error: 3x2 is not 2x3

Rank-only aliases (Vector, Matrix) catch transposed/flattened mixups even more cheaply. Named sizes work in expressions — mark them Final and the solver folds names and arithmetic (H * 2, W + 1, M // 2) just like literals, so np.zeros((H * 2, 3)) is (56, 3) for H: Final = 28, and len() of an IntEnum class folds to its member count (aliases share one value, so they count once). A Final name is not a dim in an annotation: I64[H, 2] fails with I64 dim "H" must be a TypeVar, an integer, Unpack[Ts], or Literal[...] and the parameter becomes Any. Name an annotation size with a Literal alias instead (H = Literal[28], see Named sizes). Plain variables and parameters stay unknown: their values are gone from the types before the plugin sees them, so they bail out instead of erroring. A generic int parameter can never be verified against a shape — the contract says "every int", and no annotation survives the join to int.