Library / SDK
pymc-devs/pytensor avatar
pymc-devs/pytensor

PyTensor: A Static-Graph Array Compiler Behind PyMC

PyTensor allows you to define, optimize, and efficiently evaluate mathematical expressions involving multi-dimensional arrays.

643 stars208 forksPythonNOASSERTION

At a glance

What is it?
PyTensor defines, optimizes and evaluates symbolic array expressions, and it serves as the computational backend for PyMC. Its static graph, in-place rewriting and choice of C, JAX or Numba compilation set it apart from eager frameworks, but the same design makes it a poor fit for workloads that need dynamic control flow.
Who is it for?
Adopt PyTensor if you are building on PyMC, writing custom symbolic operators, or need a graph you can rewrite before execution and compile through C, JAX or Numba. Do not adopt it as a general-purpose eager tensor library for dynamic models; the static graph is the point, not an inconvenience.
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 15, 2026, and from our analysis. They are not legal advice.

DEEP OPEN-SOURCE ANALYSIS

The problem PyTensor solves: symbolic graphs that can be rewritten before they run

PyTensor is a Python library for defining, optimizing and efficiently evaluating mathematical expressions over multi-dimensional arrays. That description is broad, so the useful question is what it does that an eager array library does not. The README states that PyTensor maintains a static graph which can be modified in-place to allow for advanced optimizations, and it contrasts this with PyTorch and TensorFlow. The audience is therefore narrower than the feature list suggests: people who need a symbolic expression, a rewriting pass over that expression, and then a compiled callable.

The clearest consumer is PyMC. The README says PyTensor provides the computational backend for PyMC, which means anyone using PyMC is already running PyTensor whether they think about it or not. The second audience is developers writing custom operators or symbolic optimizations, since the README describes an extensible graph framework intended for rapid development of exactly those things. A third audience is anyone who wants the same expression compiled through more than one backend without rewriting the expression itself.

How the graph is built, printed and rewritten

The README walks through the mechanism in a short example. Symbolic scalars are declared with pt.dscalar("a") and pt.dscalar("b"), an expression c = a + b is formed, and pytensor.function([a, b], c) turns that expression into a callable. The example asserts that f_c(1.5, 2.5) equals 4.0. Gradients come from the same graph: pytensor.grad(c, a) produces a symbolic derivative, which is then wrapped in its own function, and the README asserts f_dc(1.5, 2.5) equals 1.0.

The more informative part is the optimization pass. With v = pt.vector("v"), M = pt.matrix("M") and d = a/a + (M + a).dot(v), pytensor.dprint(d) prints the graph before compilation. The printed tree shows True_div applied to a and a, an ExpandDims, and a dot node. After f_d = pytensor.function([a, v, M], d), calling pytensor.dprint(f_d) prints a different tree. The README notes that a/a becomes 1 and that the dot product is replaced with a BLAS function, specifically CGemv. The rewritten graph shows a [1.] constant, an AllocEmpty node whose shape is derived through Shape_i{0} from M, and an inplace CGemv.

That is the whole architecture in miniature: build a graph, print it, compile it, print it again and observe the rewrites. The compilation targets are not limited to one runtime. The README states that the graph transpilation framework currently provides compilation via C, JAX and Numba. Choosing among them is a matter of what your environment supports and what the graph contains, not of rewriting your model code.

Installing PyTensor and getting a first function to compile

The README gives three installation paths. From PyPI: pip install pytensor. From conda-forge: conda install -c conda-forge pytensor. From the development branch: pip install git+https://github.com/pymc-devs/pytensor. There is no mention of a configuration file or required environment variables in the supplied material, so treat the config surface as something to check in the documentation rather than something the README enumerates.

The smallest working program is the one from the README: import pytensor, then from pytensor import tensor as pt, declare a = pt.dscalar("a") and b = pt.dscalar("b"), form c = a + b, and call pytensor.function([a, b], c). Two diagnostic calls are worth knowing before you write anything larger. pytensor.dprint(expr) prints the graph, and pytensor.dprint(f) prints the compiled graph with the optimizer's rewrites applied. Running dprint on both sides of the function call is the fastest way to see whether the optimization you expected actually fired.

One detail from the README is easy to miss and matters at scale. In the printed pre-compilation graph, the node for a appears with the same identifier in three places, which reflects that the graph is a DAG rather than a tree. The post-compilation graph shows an inplace CGemv, meaning the compiled version reuses buffers. Both behaviors follow from the static graph design, and both are the kind of thing you want to inspect with dprint rather than assume.

Where the static graph becomes a constraint

The same property that enables rewriting is the one that limits PyTensor. A static graph has to be defined before it runs, so a model whose structure depends on values computed at runtime does not map onto it cleanly. The README does not discuss control flow, dynamic shapes or data-dependent branching, and the supplied material gives no guidance on how those are expressed. If your workload is dominated by Python-level control flow that changes the graph on every call, you are paying the compilation cost repeatedly for little rewriting benefit.

A second limitation is the compilation step itself. pytensor.function is where the optimizations happen, so there is a cost between defining an expression and evaluating it. For a one-off calculation on small arrays, that cost is pure overhead compared with an eager library. The README example is deliberately tiny, and it exists to demonstrate graph rewriting, not to argue that compilation is free.

A third consideration is backend availability. The README lists C, JAX and Numba as the current compilation targets. That means the practical set of backends depends on what is installed and what the graph can express in each. The supplied material does not state which operators are unsupported on which backend, so that is a question to answer against the documentation before you commit to a pipeline.

PyTensor versus an eager array library

The obvious comparison is with an eager tensor library, and the README makes the distinction explicitly: contrary to PyTorch and TensorFlow, PyTensor maintains a static graph which can be modified in-place. The difference is not speed or API style, it is when the graph exists. In an eager library, each operation executes as you write it, and any fusion or buffer reuse has to happen inside the runtime. In PyTensor, the expression is a first-class object you can print, inspect and rewrite before anything executes.

That has a concrete consequence visible in the README's own example. The expression a/a is simplified to the constant 1, and the dot product becomes a BLAS call. Those rewrites are graph-level transformations, and they are the reason the framework is described as suitable for developing custom symbolic optimizations. If your goal is to add a new transformation that applies across a whole model, PyTensor gives you a place to put it. An eager library gives you the operator and little else.

The trade runs the other way too. Eager execution handles Python control flow naturally because there is no separate graph to build. PyTensor asks you to express the computation symbolically first, and the README does not describe an escape hatch for cases where that is awkward. The choice is between a graph you can rewrite and a graph you never have to think about.

Maintenance, releases and the licence question

The repository is not archived, and the release cadence visible in the supplied material is tight: rel-3.3.1 on 2026-09-07, rel-3.3.0 on 2026-08-12 and rel-3.2.4 on 2026-08-01. Three releases in roughly five weeks, with the last push to main on 2026-09-09. For an engineer deciding whether to adopt, that pattern suggests active maintenance and a willingness to ship patch releases quickly. It also means you should pin versions. A dependency that moves that often will surface incompatibilities eventually, and pinning is cheap.

Upgrade cost is harder to judge from the README alone. The material does not include a changelog or a deprecation policy, so the practical answer is to read the release notes for each version between the one you are on and the one you want. The README does point to a contributing guide and to the issue tracker as a place to start, which is where you would look for known breakage.

The licence needs a direct statement. The repository metadata reports NOASSERTION, which means no standard licence identifier was detected, and the README does not name a licence. PyTensor is a fork of Aesara, which is itself a fork of Theano, and forks can carry licence terms inherited from upstream. None of that is resolvable from the supplied material. If you are adopting this in a commercial or redistributed product, read the actual licence file in the repository and, if the terms matter to your organization, get them reviewed. The metadata alone will not answer the question.

Editorial conclusion

Adopt PyTensor if you are building on PyMC, writing custom symbolic operators, or need a graph you can rewrite before execution and compile through C, JAX or Numba. Do not adopt it as a general-purpose eager tensor library for dynamic models; the static graph is the point, not an inconvenience. Before committing, verify the licence text in the repository, since the metadata reports NOASSERTION rather than a named licence, and confirm that your target backend (C, JAX or Numba) is available in your environment.

Official sources

  1. Issues
  2. Project website
  3. pymc-devs/pytensor on GitHub
  4. README
  5. Releases
Community notes

Community notes