Getting started#
Tenspec declares the shape and the dtype of a NumPy array or a PyTorch tensor, in the annotation beside the value. A PyTorch declaration can also require a device. Tenspec checks that declaration at one boundary: a function, a Pydantic model, or one standalone call.
A declaration is an ordinary annotation, so a type checker reads it and untouched code still runs. Tenspec accepts or refuses the array the caller passed. It converts nothing, moves nothing, and copies nothing unless the declaration names a transform that copies.
Install#
Install it with pip, or with uv. Either one is enough, so run the line for the tool you use:
pip install "tenspec[numpy]" # or: uv add "tenspec[numpy]"
Name the array library you need as an extra: numpy or torch. import tenspec loads neither
one.
This is an early release, at version 0.1.0, and the interface can change before version 1.0. It needs Python 3.13 or newer. The tested interpreter and array-library combinations are in the support matrix.
The three forms#
Three forms declare the same relation: a matrix of rows by features, and one weight per
feature. Each one opens a boundary of its own, and each refuses the same disagreement.
A checked function#
from typing import Literal as Shape
import numpy as np
from tenspec import checked
from tenspec.numpy import Float
@checked
def weigh_columns(
values: Float[Shape["rows features"]], weights: Float[Shape["features"]]
) -> Float[Shape["rows features"]]:
return values * weights
print(weigh_columns(np.ones((3, 4)), np.array([1.0, 2.0, 3.0, 4.0])).shape)
weights must cover as many features as values has columns, and the returned matrix must keep
the shape it came with. Remove @checked and the declarations stay as ordinary annotations that
a type checker still reads, with no Tenspec check at run time.
A model#
from typing import Literal as Shape
import numpy as np
from pydantic import BaseModel, ConfigDict, Field
from tenspec import TensorContracts
from tenspec.numpy import Float
class ProjectBase(BaseModel):
model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True)
class WeighedColumns(TensorContracts, ProjectBase):
values: Float[Shape["rows features"]] = Field(description="one row per observation")
weights: Float[Shape["features"]] = Field(description="one weight per feature")
block = WeighedColumns(values=np.ones((3, 4)), weights=np.array([1.0, 2.0, 3.0, 4.0]))
print(block.values.shape, block.weights.shape)
The field annotations establish the relation at construction, with no handwritten shape validator. The caller’s own base owns the configuration, and it must permit arbitrary types, because an array is not a Pydantic type.
One standalone call#
from typing import Literal as Shape
import numpy as np
from pydantic import ValidationError
from tenspec import validate
from tenspec.numpy import Float
weighed = tuple[Float[Shape["rows features"]], Float[Shape["features"]]]
print([item.shape for item in validate((np.ones((3, 4)), np.ones(4)), weighed)])
try:
validate((np.ones((3, 4)), np.ones(5)), weighed)
except ValidationError as refusal:
print(refusal.errors()[0]["msg"])
One tuple is one boundary, which is what relates the two arrays. Two separate validate calls
relate nothing. The refusal names the axis, both extents, and the bindings the boundary held:
Value error, expected features=4, but the array has features=5, with {'rows': 3, 'features': 4}
Write from typing import Literal as Shape. tenspec.Shape is a public re-export of
typing.Literal and works at run time, but Ruff 0.16.6 does not follow it. See
Ruff and the Shape alias.
Where to read next#
Page |
What it answers |
|---|---|
What a declaration says about shape, dtype and device |
|
How |
|
How |
|
How to write a property or a transform of your own |
|
A tutorial this site runs on every build |
|
The checkers, the settings they need, and the measured support matrix |
|
The names you import, and the advanced interfaces |