Array annotations¤
The shape and dtypes of arrays can be annotated in the form dtype[array, shape]
, such as Float[Array, "batch channels"]
.
Shape¤
Symbols
The shape should be a string of space-separated symbols, such as "a b c d"
. Each symbol can be either an:
int
: fixed-size axis, e.g."28 28"
.str
: variable-size axis, e.g."channels"
.- A symbolic expression in terms of other variable-size axes, e.g.
def remove_last(x: Float[Array, "dim"]) -> Float[Array, "dim-1"]
.
Symbolic expressions must not use any spaces, otherwise each piece is treated as as a separate axis.
When calling a function, variable-size axes and symbolic axes will be matched up across all arguments and checked for consistency. (See Runtime type checking.)
Modifiers
In addition some modifiers can be applied:
- Prepend
*
to an axis to indicate that it can match multiple axes, e.g."*batch"
will match zero or more batch axes. - Prepend
#
to an axis to indicate that it can be that size or equal to one -- i.e. broadcasting is acceptable, e.g.
def add(x: Float[Array, "#foo"], y: Float[Array, "#foo"]) -> Float[Array, "#foo"]
. - Prepend
_
to an axis to disable any runtime checking of that axis (so that it can be used just as documentation). This can also be used as just_
on its own: e.g."b c _ _"
. - Documentation-only names (i.e. they're ignored by jaxtyping) can be handled by prepending a name followed by
=
e.g.Float[Array, "rows=4 cols=3"]
. - Prepend
?
to an axis to indicate that its size can vary within a PyTree structure. (See PyTree annotations.)
When using multiple modifiers, their order does not matter.
As a special case:
...
: anonymous zero or more axes (equivalent to*_
) e.g."... c h w"
Notes
- To denote a scalar shape use
""
, e.g.Float[Array, ""]
. - To denote an arbitrary shape (and only check dtype) use
"..."
, e.g.Float[Array, "..."]
. - You cannot have more than one use of multiple-axes, i.e. you can only use
...
or*name
at most once in each array. - A symbolic expression cannot be evaluated unless all of the axes sizes it refers to have already been processed. In practice this usually means that they should only be used in annotations for the return type, and only use axes declared in the arguments.
- Symbolic expressions are evaluated in two stages: they are first evaluated as f-strings using the arguments of the function, and second are evaluated using the processed axis sizes. The f-string evaluation means that they can use local variables by enclosing them with curly braces, e.g.
{variable}
, e.g.def full(size: int, fill: float) -> Float[Array, "{size}"]: return jax.numpy.full((size,), fill) class SomeClass: some_value = 5 def full(self, fill: float) -> Float[Array, "{self.some_value}+3"]: return jax.numpy.full((self.some_value + 3,), fill)
Dtype¤
The dtype should be any one of (all imported from jaxtyping
):
- Any dtype at all:
Shaped
- Boolean:
Bool
- PRNG key:
Key
- Any integer, unsigned integer, floating, or complex:
Num
- Any floating or complex:
Inexact
- Any floating point:
Float
- Of particular precision:
BFloat16
,Float16
,Float32
,Float64
- Of particular precision:
- Any complex:
Complex
- Of particular precision:
Complex64
,Complex128
- Of particular precision:
- Any floating point:
- Any integer or unsigned intger:
Integer
- Any unsigned integer:
UInt
- Of particular precision:
UInt4
,UInt8
,UInt16
,UInt32
,UInt64
- Of particular precision:
- Any signed integer:
Int
- Of particular precision:
Int4
,Int8
,Int16
,Int32
,Int64
- Of particular precision:
- Any unsigned integer:
- Any floating, integer, or unsigned integer:
Real
.
- Any floating or complex:
- Boolean:
Unless you really want to force a particular precision, then for most applications you should probably allow any floating-point, any integer, etc. That is, use
from jaxtyping import Array, Float
Float[Array, "some_shape"]
from jaxtyping import Array, Float32
Float32[Array, "some_shape"]
Array¤
The array should typically be either one of:
jaxtyping.Array / jax.Array / jax.numpy.ndarray # these are all aliases of one another
np.ndarray
torch.Tensor
tf.Tensor
Some other types are also supported here:
Unions: these are unpacked. For example, SomeDtype[Union[A, B], "some shape"]
is equivalent to Union[SomeDtype[A, "some shape"], SomeDtype[B, "some shape"]]
. A common example of a union type here is np.typing.ArrayLike
.
Any: use typing.Any
to check just the shape/dtype, but not the array type.
Duck-type arrays: anything with .shape
and .dtype
attributes. For example,
class MyDuckArray:
@property
def shape(self) -> tuple[int, ...]:
return (3, 4, 5)
@property
def dtype(self) -> str:
return "my_dtype"
class MyDtype(jaxtyping.AbstractDtype):
dtypes = ["my_dtype"]
x = MyDuckArray()
assert isinstance(x, MyDtype[MyDuckArray, "3 4 5"])
# checks that `type(x) == MyDuckArray`
# and that `x.shape == (3, 4, 5)`
# and that `x.dtype == "my_dtype"`
TypeVars: in this case the runtime array is checked for matching the bounds or constraints of the typing.TypeVar
.
Existing jaxtyped annotations:
Image = Float[Array, "channels height width"]
BatchImage = Float[Image, "batch"]
BatchImage = Shaped[Image, "batch"]
would work just as well. But Bool[Image, "batch"]
would throw an error, as there are no dtypes that are both bools and floats.) Thus the above is equivalent to
BatchImage = Float[Array, "batch channels height width"]
Note that jaxtyping.{Array, ArrayLike}
are only available if JAX has been installed.
Scalars, PRNG keys¤
For convenience, jaxtyping also includes jaxtyping.Scalar
, jaxtyping.ScalarLike
, and jaxtyping.PRNGKeyArray
, defined as:
Scalar = Shaped[Array, ""]
ScalarLike = Shaped[ArrayLike, ""]
# Left: new-style typed keys; right: old-style keys. See JEP 9263.
PRNGKeyArray = Union[Key[Array, ""], UInt32[Array, "2"]]
Recalling that shape-and-dtype specified jaxtyping arrays can be nested, this means that e.g. you can annotate the output of jax.random.split
with Shaped[PRNGKeyArray, "2"]
, or e.g. an integer scalar with Int[Scalar, ""]
.
Note that jaxtyping.{Scalar, ScalarLike, PRNGKeyArray}
are only available if JAX has been installed.