PyTensor: a static-graph compiler for math expressions, and the engine under PyMC
PyTensor allows you to define, optimize, and efficiently evaluate mathematical expressions involving multi-dimensional arrays.
At a glance
- What is it?
- PyTensor defines, optimizes and evaluates multi-dimensional array expressions, compiling them through C, JAX or Numba. It is the computational backend for PyMC, and its static graph is the main thing that separates it from PyTorch and JAX.
- Who is it for?
- Adopt PyTensor if you need a modifiable static graph with autodiff and a choice of C, JAX or Numba backends, or if you are already inside PyMC. Do not adopt it as a general deep learning framework; the README positions it against PyTorch and TensorFlow on graph semantics, not on training infrastructure.
- Can I use it commercially?
- Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
- Is it still maintained?
- Yes. The repository received new commits within the last day.
- What is it written in?
- Mainly Python, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on September 30, 2026, and from our analysis. They are not legal advice.
Editorial analysis
What PyTensor does that NumPy does not
NumPy evaluates arrays immediately. PyTensor builds an expression graph first and only evaluates it when you wrap it in pytensor.function. That delay is the whole point: between definition and evaluation the library rewrites the graph, removing operations and substituting faster ones.
The README's own example makes the payoff visible. You write d = a/a + (M + a).dot(v), and after compilation the printed graph shows a/a replaced by the constant 1 and the dot product replaced by a BLAS call, CGemv. Nothing in the source expression asked for either change.
The intended audience is narrower than "anyone doing numerics". PyTensor is the computational backend for PyMC, and the topics list bayesian-inference, statistics, computational-science and deep-learning. If you are writing a sampler, a probabilistic model, or a custom operator that needs gradients, the graph is an asset. If you want to multiply two matrices once, NumPy is the shorter path.
The static graph, autodiff and the pytensor.function boundary
Everything in PyTensor is symbolic until you call pytensor.function. The README declares a = pt.dscalar("a") and b = pt.dscalar("b"), forms c = a + b, and then converts the expression into a callable that takes (a, b) values. Calling f_c(1.5, 2.5) returns 4.0. The assertion in the README is the contract: the callable takes the inputs you declared, in order.
Gradients come from the same graph. pytensor.grad(c, a) produces a new symbolic expression, which you compile the same way. The README compiles it into f_dc and asserts f_dc(1.5, 2.5) == 1.0, the derivative of a + b with respect to a.
The design choice worth noting is that PyTensor maintains a static graph which can be modified in-place. That is the stated contrast with PyTorch and TensorFlow. A static graph can be rewritten by optimizers before execution, which is exactly what the dprint output demonstrates. The cost is that a graph you build is a graph you must reason about; there is no eager fallback where you can inspect intermediate values by printing a tensor.
Compilation targets: C, JAX and Numba
PyTensor implements an extensible graph transpilation framework that currently provides compilation via C, JAX, and Numba, according to the README. The same expression graph can therefore be lowered to different execution backends without rewriting the model.
The compiled C path is not purely optional at build time. setup.py defines an extension named pytensor.scan.scan_perform built from pytensor/scan/scan_perform.pyx, with numpy.get_include() on the include path. There is an explicit escape hatch: when the PYODIDE environment variable is set to "1", setup.py sets ext_modules to an empty list and builds a pure-Python wheel, omitting the Cython version of scan. That tells you scan has a Python fallback, and that the compiled version is the default on normal installs.
Dependencies are declared in pyproject.toml: scipy>=1,<2, numpy>=2.0, numba>=0.58,<=0.67.0, and filelock>=3.15. The numba upper bound is pinned at 0.67.0, so a Numba release beyond that range is outside what this version declares.
Installing PyTensor and compiling a first gradient
The README gives two install routes. From PyPI:
pip install pytensorOr from conda-forge:
conda install -c conda-forge pytensorThe development branch installs directly from GitHub with pip install git+https://github.com/pymc-devs/pytensor. Note the Python constraint in pyproject.toml: requires-python is >=3.12,<3.15, so 3.11 and earlier are outside the declared range for this version.
Once installed, the README's first example is the smallest thing that exercises definition, compilation and evaluation:
import pytensor
from pytensor import tensor as pt
a = pt.dscalar("a")
b = pt.dscalar("b")
c = a + b
f_c = pytensor.function([a, b], c)
assert f_c(1.5, 2.5) == 4.0You should see no output; the assertion passes silently. To watch the optimizer work, build a slightly larger expression and print the graph before and after compilation:
v = pt.vector("v")
M = pt.matrix("M")
d = a/a + (M + a).dot(v)
pytensor.dprint(d)
f_d = pytensor.function([a, v, M], d)
pytensor.dprint(f_d)The first dprint shows the expression as written, with True_div and dot nodes. The second shows the rewritten graph, where a/a has become the constant 1. and the dot has become CGemv{inplace}. If the second print looks identical to the first, the optimizer did not fire and that is the first thing to investigate.
Where PyTensor is the wrong tool
The README does not document rollback, and it does not describe a training loop, a data loader, or a model zoo. PyTensor is an expression compiler, not a deep learning framework. If your work is defined by nn.Module-style layers, distributed training and a large ecosystem of pretrained weights, the static graph is friction rather than an advantage.
The compilation boundary is also a debugging boundary. Errors surface when you call pytensor.function or when you execute it, not where you wrote the expression, and the traceback points into generated or rewritten graph nodes. The README's dprint output is the intended tool for that, but it means reading a graph dump rather than stepping through Python.
There is a build-time dependency to plan for. A normal install compiles the scan_perform extension, which needs a C toolchain and Cython, both listed in build-system requires alongside setuptools>=59.0.0, numpy>=2.0 and versioneer[toml]==0.29. Environments without a compiler should expect the pure-Python path that setup.py reserves for Pyodide, not the compiled one.
PyTensor against Theano, PyMC, JAX and PyTorch
PyTensor is a fork of Aesara, which is a fork of Theano. The lineage matters because Theano is unmaintained while PyTensor's last push was on 2026-09-09, with rel-3.3.1 published on 2026-09-07. If you have Theano code, the migration is a rename and a re-check of the graph API, not a rewrite from a different paradigm.
The relationship with PyMC is not a comparison but a dependency: PyTensor provides the computational backend for PyMC. Choosing PyMC means choosing PyTensor underneath, whether or not you call it directly.
Against JAX, the README's transpilation framework is the interesting overlap: PyTensor can compile via JAX. So the difference is not "JAX or not" but who owns the graph. JAX traces functions and transforms them; PyTensor keeps a mutable static graph you can edit in place, which is what the README cites as the contrast with PyTorch and TensorFlow. If you want custom symbolic optimizations on a graph you control, that mutability is the reason to pick PyTensor. If you want transformations applied to ordinary Python functions, JAX's model fits better.
Licence, releases and upgrade cost
pyproject.toml declares license = "BSD-3-Clause" with license-files = ["LICENSE.txt"], while the repository metadata reports NOASSERTION. Treat the pyproject declaration as the project's own statement and read LICENSE.txt before redistributing; this is a description of what the files say, not legal advice.
The release cadence visible here is tight: rel-3.3.1 on 2026-09-07, rel-3.3.0 on 2026-08-12, rel-3.2.4 on 2026-08-01. Three releases in roughly five weeks means pinning is worth the effort. The declared dependency floors and ceilings are the ones to watch: numpy>=2.0 is a hard floor, and numba is capped at <=0.67.0. An upgrade that moves Numba past that ceiling is outside the declared range.
The Python range, >=3.12,<3.15, is the other upgrade gate. A project still on 3.11 cannot install this version at all, and the classifier list marks 3.12, 3.13 and 3.14 as supported, with free-threading listed as 1 - Unstable. That last classifier is the project telling you the free-threaded build is not settled.
Editorial conclusion
Adopt PyTensor if you need a modifiable static graph with autodiff and a choice of C, JAX or Numba backends, or if you are already inside PyMC. Do not adopt it as a general deep learning framework; the README positions it against PyTorch and TensorFlow on graph semantics, not on training infrastructure. Before committing, verify that your Python version falls inside the >=3.12,<3.15 range declared in pyproject.toml, and check that a C compiler is available if you want the compiled path rather than the Python fallback for scan.
Frequently asked questions
What is PyTensor?
It is a Python library for defining, optimizing and efficiently evaluating mathematical expressions over multi-dimensional arrays. The README also states that it provides the computational backend for PyMC.
How is PyTensor different from NumPy?
NumPy evaluates arrays as you write the operations, while PyTensor builds a symbolic graph and only evaluates it once you call pytensor.function. Between those two steps the library rewrites the graph, for example replacing a/a with the constant 1 and a dot product with a BLAS CGemv call.
How is PyTensor different from Theano?
PyTensor is a fork of Aesara, which is itself a fork of Theano. That lineage is stated in the README's Background section; the README does not list the individual API differences.
How is PyTensor different from TensorFlow?
The README states that PyTensor maintains a static graph which can be modified in-place to allow for advanced optimizations, and draws that contrast with PyTorch and TensorFlow. The README does not describe a training stack, data pipeline or model repository.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/pymc-devs-pytensor)