{"id":15546720,"url":"https://github.com/rsokl/phantom-tensors","last_synced_at":"2026-03-02T13:12:55.871Z","repository":{"id":59377780,"uuid":"536894247","full_name":"rsokl/phantom-tensors","owner":"rsokl","description":"Tensor-like types – with variadic shapes – that support both static and runtime type checking, and convenient parsing","archived":false,"fork":false,"pushed_at":"2025-10-27T07:25:28.000Z","size":165,"stargazers_count":19,"open_issues_count":4,"forks_count":2,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-11-06T02:19:19.402Z","etag":null,"topics":["array-like","jax","numpy","parsing","runtime","static","tensor","tensor-types","torch","type-checking","variadic-shapes"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/rsokl.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-09-15T06:32:02.000Z","updated_at":"2025-04-30T04:10:39.000Z","dependencies_parsed_at":"2024-05-15T13:22:07.750Z","dependency_job_id":"9d7e5caf-0a23-4b21-ab4c-27a0df998b25","html_url":"https://github.com/rsokl/phantom-tensors","commit_stats":{"total_commits":130,"total_committers":4,"mean_commits":32.5,"dds":"0.30000000000000004","last_synced_commit":"9569cb2fee80ae12b71f398c1bdcabdb15addf36"},"previous_names":[],"tags_count":4,"template":false,"template_full_name":null,"purl":"pkg:github/rsokl/phantom-tensors","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rsokl%2Fphantom-tensors","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rsokl%2Fphantom-tensors/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rsokl%2Fphantom-tensors/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rsokl%2Fphantom-tensors/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/rsokl","download_url":"https://codeload.github.com/rsokl/phantom-tensors/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rsokl%2Fphantom-tensors/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30003748,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-02T12:19:43.414Z","status":"ssl_error","status_checked_at":"2026-03-02T12:19:02.215Z","response_time":60,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["array-like","jax","numpy","parsing","runtime","static","tensor","tensor-types","torch","type-checking","variadic-shapes"],"created_at":"2024-10-02T13:03:52.154Z","updated_at":"2026-03-02T13:12:55.850Z","avatar_url":"https://github.com/rsokl.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Phantom Tensors\n\u003e Tensor types with variadic shapes, for any array-based library, that work with both static and runtime type checkers\n\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"https://pypi.python.org/pypi/phantom-tensors\"\u003e\n    \u003cimg src=\"https://img.shields.io/pypi/v/phantom-tensors.svg\" alt=\"PyPI\" /\u003e\n  \u003c/a\u003e\n  \u003ca\u003e\n    \u003cimg src=\"https://img.shields.io/badge/python-3.8%20\u0026#8208;%203.12-blue.svg\" alt=\"Python version support\" /\u003e\n  \u003c/a\u003e\n\u003c/p\u003e\n\n\n**This project is currently just a rough prototype! Inspired by: [phantom-types](https://github.com/antonagestam/phantom-types)**\n\nThe goal of this project is to let users write tensor-like types with variadic shapes (via [PEP 646](https://peps.python.org/pep-0646/)) that are: \n- Amendable to **static type checking (without mypy plugins)**. \n    \u003e E.g., pyright can tell the difference between `Tensor[Batch, Channel]` and `Tensor[Batch, Feature]`\n- Useful for performing **runtime checks of tensor types and shapes**. \n    \u003e E.g.,  can validate -- at runtime -- that arrays of types `NDArray[A, B]` and `NDArray[B, A]` indeed have transposed shapes with respect with each other.\n- Compatible with *any* array-based library (numpy, pytorch, xarray, cupy, mygrad, etc.)\n    \u003e E.g. A function annotated with `x: torch.Tensor` can be passed `phantom_tensors.torch.Tensor[N, B, D]`. It is trivial to write custom phantom-tensor flavored types for any array-based library.\n\n`phantom_tensors.parse` makes it easy to declare shaped tensor types in a way that static type checkers understand, and that are validated at runtime:\n\n```python\nfrom typing import NewType\n\nimport numpy as np\n\nfrom phantom_tensors import parse\nfrom phantom_tensors.numpy import NDArray\n\nA = NewType(\"A\", int)\nB = NewType(\"B\", int)\n\n# static: declare that x is of type NDArray[A, B]\n#         declare that y is of type NDArray[B, A]\n# runtime: check that shapes (2, 3) and (3, 2)\n#          match (A, B) and (B, A) pattern across\n#          tensors\nx, y = parse(\n    (np.ones((2, 3)), NDArray[A, B]),\n    (np.ones((3, 2)), NDArray[B, A]),\n)\n\nx  # static type checker sees: NDArray[A, B]\ny  # static type checker sees: NDArray[B, A]\n\n```\n\nPassing inconsistent types to `parse` will result in a runtime validation error.\n```python\n# Runtime: Raises `ParseError` A=10 and A=2 do not match\nz, w = parse(\n    (np.ones((10, 3)), NDArray[A, B]),\n    (np.ones((3, 2)), NDArray[B, A]),\n)\n```\n\nThese shaped tensor types are amenable to static type checking:\n\n```python\nfrom typing import Any\n\nimport numpy as np\n\nfrom phantom_tensors import parse\nfrom phantom_tensors.numpy import NDArray\nfrom phantom_tensors.alphabet import A, B  # these are just NewType(..., int) types\n\ndef func_on_2d(x: NDArray[Any, Any]): ...\ndef func_on_3d(x: NDArray[Any, Any, Any]): ...\ndef func_on_any_arr(x: np.ndarray): ...\n\n# runtime: ensures shape of arr_3d matches (A, B, A) patterns\narr_3d = parse(np.ones((3, 5, 3)), NDArray[A, B, A])\n\nfunc_on_2d(arr_3d)  # static type checker: Error!  # expects 2D arr, got 3D\n\nfunc_on_3d(arr_3d)  # static type checker: OK\nfunc_on_any_arr(arr_3d)  # static type checker: OK\n```\n\n\nWrite easy-to-understand interfaces using common dimension names (or make up your own):\n\n```python\nfrom phantom_tensors.torch import Tensor\nfrom phantom_tensors.words import Batch, Embed, Vocab\n\ndef embedder(x: Tensor[Batch, Vocab]) -\u003e Tensor[Batch, Embed]:\n    ...\n```\n\n\nUsing a runtime type checker, such as [beartype](https://github.com/beartype/beartype) or [typeguard](https://github.com/agronholm/typeguard), in conjunction with `phantom_tensors` means that the typed shape information will be validated at runtime across a function's inputs and outputs, whenever that function is called.\n\n```python\nfrom typing import TypeVar, cast\nfrom typing_extensions import assert_type\n\nimport torch as tr\nfrom beartype import beartype\n\nfrom phantom_tensors import dim_binding_scope, parse\nfrom phantom_tensors.torch import Tensor\nfrom phantom_tensors.alphabet import A, B, C\n\nT1 = TypeVar(\"T1\")\nT2 = TypeVar(\"T2\")\nT3 = TypeVar(\"T3\")\n\n\n@dim_binding_scope\n@beartype  # \u003c- adds runtime type checking to function's interfaces\ndef buggy_matmul(x: Tensor[T1, T2], y: Tensor[T2, T3]) -\u003e Tensor[T1, T3]:\n    # This is the wrong operation!\n    # Will return shape-(T1, T1) tensor, not (T1, T3)\n    out = x @ x.T\n    \n    # We lie to the static type checker to try to get away with it\n    return cast(Tensor[T1, T3], out)\n\nx, y = parse(\n    (tr.ones(3, 4), Tensor[A, B]),\n    (tr.ones(4, 5), Tensor[B, C]),\n)\n\n# At runtime beartype raises:\n#   Function should return shape-(A, C) but returned shape-(A, A)\nz = buggy_matmul(x, y)  # Runtime validation error!\n\n```\n\n## Installation\n\n```shell\npip install phantom-tensors\n```\n\n`typing-extensions` is the only strict dependency. Using features from `phantom_tensors.torch(numpy)` requires that `torch`(`numpy`) is installed too. \n\n## Some Lower-Level Details and Features\n\nEverything on display here is achieved using relatively minimal hacks (no mypy plugin necessary, no monkeypatching). Presently, `torch.Tensor` and `numpy.ndarray` are explicitly supported by phantom-tensors, but it is trivial to add support for other array-like classes.\n\n\u003e Note that mypy does not support PEP 646 yet, but pyright does. You can run pyright on the following examples to see that they do, indeed type-check as expected! \n\n\n### Dimension-Binding Contexts\n\n`phantom_tensors.parse` validates inputs against types-with-shapes and performs [type narrowing](https://mypy.readthedocs.io/en/latest/type_narrowing.html) so that static type checkers are privy to the newly proven type information about those inputs. It performs inter-tensor shape consistency checks within a \"dimension-binding context\". Tensor-likes that are parsed simultaneously are automatically checked within a common dimension-binding context.\n\n\n```python\nimport numpy as np\nimport torch as tr\n\nfrom phantom_tensors import parse\nfrom phantom_tensors.alphabet import A, B, C\nfrom phantom_tensors.numpy import NDArray\nfrom phantom_tensors.torch import Tensor\n\nt1, arr, t2 = parse(\n    # \u003c- Runtime: enter dimension-binding context\n    (tr.rand(9, 2, 9), Tensor[B, A, B]),  # \u003c-binds A=2 \u0026 B=9\n    (np.ones((2,)), NDArray[A]),  # \u003c- checks A==2\n    (tr.rand(9), Tensor[B]),  # \u003c- checks B==9\n)  # \u003c- Runtime: exit dimension-binding scope \n   #    Statically: casts t1, arr, t2 to shape-typed Tensors\n\n# static type checkers now see\n# t1: Tensor[B, A, B] \n# arr: NDArray[A]\n# t2: Tensor[B]\n\nw = parse(tr.rand(78), Tensor[A]);  # \u003c- binds A=78 within this context\n```\n\nAs indicated above, the type-checker sees the shaped-tensor/array types. Additionally, these are subclasses of their rightful parents, so we can pass these to functions typed with vanilla `torch.Tensor` and `numpy.ndarry` annotations, and type checkers will be a-ok with that.\n\n```python\ndef vanilla_numpy(x: np.ndarray): ...\ndef vanilla_torch(x: tr.Tensor): ...\n\nvanilla_numpy(arr)  # type checker: OK\nvanilla_torch(arr)  # type checker: Error! \nvanilla_torch(t1)  # type checker: OK \n```\n\n#### Basic forms of runtime validation performed by `parse`\n\n```python\n# runtime type checking\n\u003e\u003e\u003e parse(1, Tensor[A])\n---------------------------------------------------------------------------\nParseError: Expected \u003cclass 'torch.Tensor'\u003e, got: \u003cclass 'int'\u003e\n\n# dimensionality mismatch\n\u003e\u003e\u003e parse(tr.ones(3), Tensor[A, A, A])\n---------------------------------------------------------------------------\nParseError: shape-(3,) doesn't match shape-type (A=?, A=?, A=?)\n\n# unsatisfied shape pattern\n\u003e\u003e\u003e parse(tr.ones(1, 2), Tensor[A, A])\n---------------------------------------------------------------------------\nParseError: shape-(1, 2) doesn't match shape-type (A=1, A=1)\n\n# inconsistent dimension sizes across tensors\n\u003e\u003e\u003e x, y = parse(\n...     (tr.ones(1, 2), Tensor[A, B]),\n...     (tr.ones(4, 1), Tensor[B, A]),\n... )\n\n---------------------------------------------------------------------------\nParseError: shape-(4, 1) doesn't match shape-type (B=2, A=1)\n```\n\nTo reiterate, `parse` is able to compare shapes across multiple tensors by entering into a \"dimension-binding scope\".\nOne can enter into this context explicitly:\n\n```python\n\u003e\u003e\u003e from phantom_tensors import dim_binding_scope\n\n\u003e\u003e\u003e x = parse(np.zeros((2,)), NDArray[B])  # binds B=2\n\u003e\u003e\u003e y = parse(np.zeros((3,)), NDArray[B])  # binds B=3\n\u003e\u003e\u003e with dim_binding_scope:\n...     x = parse(np.zeros((2,)), NDArray[B])  # binds B=2\n...     y = parse(np.zeros((3,)), NDArray[B])  # raises!\n---------------------------------------------------------------------------\nParseError: shape-(3,) doesn't match shape-type (B=2,)\n```\n\n#### Support for `Literal` dimensions:\n\n```python\nfrom typing import Literal as L\n\nfrom phantom_tensors import parse\nfrom phantom_tensors.torch import Tensor\n\nimport torch as tr\n\nparse(tr.zeros(1, 3), Tensor[L[1], L[3]])  # static + runtime: OK\nparse(tr.zeros(2, 3), Tensor[L[1], L[3]])  #  # Runtime: ParseError - mismatch at dim 0\n```\n\n#### Support for `Literal` dimensions and variadic shapes:\n\nIn Python 3.11 you can write shape types like `Tensor[int, *Ts, int]`, where `*Ts` represents 0 or more optional entries between two required dimensions. phantom-tensor supports this \"unpack\" dimension. In this README we opt for `typing_extensions.Unpack[Ts]` instead of `*Ts` for the sake of backwards compatibility.\n\n```python\nfrom phantom_tensors import parse\nfrom phantom_tensors.torch import Tensor\n\nimport torch as tr\nfrom typing_extensions import Unpack as U, TypeVarTuple\n\nTs = TypeVarTuple(\"Ts\")\n\n# U[Ts] represents an arbitrary number of entries\nparse(tr.ones(1, 3), Tensor[int, U[Ts], int)  # static + runtime: OK\nparse(tr.ones(1, 0, 0, 0, 3), Tensor[int, U[Ts], int])  # static + runtime: OK\n\nparse(tr.ones(1, ), Tensor[int, U[Ts], int])  # Runtime: Not enough dimensions\n```\n\n#### Support for [phantom types](https://github.com/antonagestam/phantom-types):\n\nSupports phatom type dimensions (i.e. `int` subclasses that override `__isinstance__` checks):\n\n```python\nfrom phantom_tensors import parse\nfrom phantom_tensors.torch import Tensor\n\nimport torch as tr\nfrom phantom import Phantom\n\nclass EvenOnly(int, Phantom, predicate=lambda x: x%2 == 0): ...\n\nparse(tr.ones(1, 0), Tensor[int, EvenOnly])  # static return type: Tensor[int, EvenOnly] \nparse(tr.ones(1, 2), Tensor[int, EvenOnly])  # static return type: Tensor[int, EvenOnly] \nparse(tr.ones(1, 4), Tensor[int, EvenOnly])  # static return type: Tensor[int, EvenOnly] \n\nparse(tr.ones(1, 3), Tensor[int, EvenOnly])  # runtime: ParseError (3 is not an even number)\n```\n\n\n\n## Compatibility with Runtime Type Checkers\n\n`parse` is not the only way to perform runtime validation using phantom tensors – they work out of the box with 3rd party runtime type checkers like [beartype](https://github.com/beartype/beartype)! How is this possible?\n\n...We do something tricky here! At, runtime `Tensor[A, B]` actually returns a [phantom type](https://github.com/antonagestam/phantom-types). This means that `isinstance(arr, NDArray[A, B])` is, at runtime, *actually* performing `isinstance(arr, PhantomNDArrayAB)`, which dynamically generated and is able to perform the type and shape checks.\n\nThanks to the ability to bind dimensions within a specified context, all `beartype` needs to do is faithfully call `isinstance(...)` within said context and we can have the inputs and ouputs of a phantom-tensor-annotated function get checked!\n\n```python\nfrom typing import Any\n\nfrom beartype import beartype  # type: ignore\nimport pytest\nimport torch as tr\n\nfrom phantom_tensors.alphabet import A, B, C\nfrom phantom_tensors.torch import Tensor\nfrom phantom_tensors import dim_binding_scope, parse\n\n# @dim_binding_scope:\n#   ensures A, B, C consistent across all input/output tensor shapes\n#   within scope of function\n@dim_binding_scope \n@beartype  # \u003c-- adds isinstance checks on inputs \u0026 outputs\ndef matrix_multiply(x: Tensor[A, B], y: Tensor[B, C]) -\u003e Tensor[A, C]:\n    a, _ = x.shape\n    _, c = y.shape\n    return parse(tr.rand(a, c), Tensor[A, C])\n\n@beartype\ndef needs_vector(x: Tensor[Any]): ...\n\nx, y = parse(\n    (tr.rand(3, 4), Tensor[A, B]),\n    (tr.rand(4, 5), Tensor[B, C]),\n)\n\nz = matrix_multiply(x, y)\nz  # type revealed: Tensor[A, C]\n\nwith pytest.raises(Exception):\n    # beartype raises error: input Tensor[A, C] doesn't match Tensor[A]\n    needs_vector(z)  # \u003c- pyright also raises an error!\n\nwith pytest.raises(Exception):\n    # beartype raises error: inputs Tensor[A, B], Tensor[A, B] don't match signature\n    matrix_multiply(x, x)  # \u003c- pyright also raises an error!\n```\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frsokl%2Fphantom-tensors","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frsokl%2Fphantom-tensors","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frsokl%2Fphantom-tensors/lists"}