Failures and limits#

The error classes#

Error

Import from

Base

Raised when

AnnotationError

tenspec

TypeError

a declaration is invalid, unsupported, or reached Pydantic with no entry point

TensorMismatch

tenspec.errors

ValueError

an array disagrees with its declared requirement

BindingError

tenspec.errors

ValueError

a dimension lookup found no extent. Its reason is no_scope, unbound or not_scalar

ConstraintEvaluationError

tenspec

RuntimeError

a backend cannot inspect a declared property of this value, such as Finite on a meta tensor. It reports an unavailable inspection, not necessarily a defect

TransformContractError

tenspec.errors

RuntimeError

a transform returned an array that broke its preservation contract

Where the error is raised decides how you catch it. Pydantic wraps a ValueError raised inside its own validation callbacks, and Tenspec’s check runs in one of those. So a TensorMismatch, and a BindingError raised while a value is being checked, arrive inside a ValidationError. A model validator is one of those callbacks too. An error raised in the body of a checked function is not: it propagates as itself. axis_size in a function body, or outside every boundary, therefore raises BindingError directly, as Axis scope and axis_size shows.

TensorMismatch keeps its parts readable, and it holds no array:

from typing import Literal as Shape

import numpy as np

from pydantic import ValidationError
from tenspec import NonEmpty, validate
from tenspec.errors import TensorMismatch
from tenspec.numpy import Float

try:
    validate((np.ones(3), np.ones(5)), tuple[Float[Shape["rows"]], Float[Shape["rows"]]])
except ValidationError as refusal:
    entry = refusal.errors()[0]
    original = entry["ctx"]["error"]
    print(entry["loc"], isinstance(original, TensorMismatch))
    print(original.requirement, "|", original.observed, "|", original.bindings)

try:
    validate(np.zeros((2, 0)), Float[Shape["rows cols"], NonEmpty])
except ValidationError as refusal:
    print(refusal.errors()[0]["msg"])

What this release does not check#

Case

What happens

an async function

checked raises AnnotationError

a callable that is not a plain Python function

checked raises AnnotationError

a union of alternative tensor structures

AnnotationError. Write one declaration, and add None for an optional tensor

a tensor alias that contains its own name

AnnotationError

a device requirement on NumPy

AnnotationError, because NumPy places no array

a Tenspec declaration on a model without the mixin

AnnotationError when Pydantic builds that model’s schema, which is the class statement unless the caller defers the build

A declaration relates values inside one boundary. It does not relate the length of a Python sequence to an axis, and it does not relate an axis of a nested model to one of its parent. Write those by hand, as in A relation the declaration cannot express.

What a declaration does not promise#

  • No immutability. CopyReadOnly and ReadOnly control a copy and a flag, not access through a base array.

  • No promise about a later value. A check happens at its boundary. See An owned computed result.

  • Nothing about what an outer validator returns. An AfterValidator runs after the check and owns its own result.

  • No byte order. A logical alias such as Float64 accepts either byte order. Transform preservation compares the native dtype, so a transform may not change it.

  • No claim about operability. A backend that represents a format says nothing about whether your installed NumPy or Torch can compute with it.

  • No static shape check. No type checker compares two shapes.