Hello! In our last lesson, we explored jax.lax.scan and saw how its carry argument can pass state through iterations. We concluded by noting that this state can become quite complex—for example, a tuple containing an LSTM's hidden and cell states, or a dictionary of layer parameters.
Today, we will formally introduce the JAX mechanism for handling such complex, nested data structures: the pytree. This concept is absolutely central to idiomatic JAX programming. For you, it's doubly important as it forms the philosophical core of Equinox, the library you've chosen for building models. Understanding pytrees is the key that unlocks how JAX and Equinox represent everything from model parameters to optimizer states.
By the end of this lesson, you will be able to define what a pytree is in JAX and distinguish its two fundamental components: the leaves and the container nodes.
1. The Anatomy of a Pytree
At its heart, a pytree is a surprisingly simple idea: it's any nested structure built from standard Python container types. The most common containers are lists, tuples, and dictionaries. These containers can hold other containers, creating a tree-like structure. The values at the "ends" of the tree branches are called leaves.
The image below provides an excellent visualization of this concept.

Let's break down what this diagram shows:
- The Python Object:
[1, (2, {'a': 3, 'b': 4}, 5), [6, 7]]. This is a list containing an integer, a tuple, and another list. The tuple itself contains integers and a dictionary. This is a perfect example of a nested data structure. - Container Nodes: These are the elements that give the pytree its shape. In the diagram, these are the
List,Tuple, andDictnodes. They don't hold the final data themselves; they organize it. The specific arrangement of these containers is often called the "treedef" (tree definition). - Leaf Nodes: These are the actual data values within the containers. In this case, they are the integers
1, 2, 3, 4, 5, 6, 7. JAX transformations, likejax.grad, primarily operate on these leaves. - Flattening: The process of extracting all the leaves from the tree in a defined order results in a simple, flat list of values. JAX internally performs this kindof operation frequently: it separates the structure (the treedef) from the contents (the leaves), operates on the leaves, and then uses the saved treedef to reassemble the results into the original structure.
This separation of structure from content is a fundamental design principle in JAX. It allows JAX to apply functions and transformations to arbitrarily complex data structures in a uniform way.
2. Models as Pytrees in Equinox
This pytree concept isn't just a low-level utility; it's the foundation upon which entire libraries are built. Equinox, in particular, elevates this idea to its central design philosophy: models are pytrees.
This is a significant departure from the PyTorch paradigm. In PyTorch, an nn.Module is an object that contains parameters, which you might access via model.parameters(). The model object and its parameters are distinct entities. In Equinox, the model object is the pytree of its own parameters and other attributes.
The Equinox documentation provides the best introduction to this idea.
This section from the Equinox documentation defines what a pytree is in the context of JAX and immediately demonstrates how an Equinox model is constructed as one.
Please read the first section, 1. Models as PyTrees. As you read, focus on: The formal definition of a pytree and its leaves. The structure of the NeuralNetwork class. Notice how its attributes (layers, extra_bias) are JAX arrays or other containers. The final tree_map call. This is a preview of the next lesson, but observe how it applies an operation (the gradient update) to the model and grads pytrees, treating them as corresponding structures.
Let's dissect the NeuralNetwork example from the reading.
class NeuralNetwork(eqx.Module):
layers: tuple
extra_bias: jax.Array
When you create an instance of this class, model = NeuralNetwork(key), that model object is the root of your pytree.
- Containers: The
modelobject itself is a container node. Themodel.layersattribute, being a tuple ofeqx.nn.Linearobjects, is also a container. Eacheqx.nn.Linearobject is also a container, holding its ownweightandbias. - Leaves: The actual numerical data—the JAX arrays—are the leaves. These include
model.extra_bias, and within each linear layer,model.layers[0].weightandmodel.layers[0].bias.
When you call jax.grad(loss)(model, x, y), JAX sees that model is a pytree. It traverses this tree, finds all the array leaves (the parameters), computes gradients for them, and returns a new pytree, grads, which has the exact same structure as model but with the gradients as its leaves.
3. What Can Be a Leaf?
So far, we've focused on JAX arrays as the leaves of our model pytrees, as these are the elements that are typically differentiated or updated. However, the "leaf" concept is more general. A leaf is simply any element in the tree that is not a standard container (list, tuple, dict).
This means leaves can also be non-array objects. For instance, a model might include a Python function as its activation function or a boolean flag to control behavior like dropout.
This next section of the documentation explains how Equinox handles pytrees containing non-array leaves.
Please read the beginning of the section on Filtering. Pay attention to the examples of non-array leaves, like a boolean flag or an activation function. You don't need to dive into the partition/combine mechanics yet; just absorb the idea that pytrees can have heterogeneous leaf types.
As the text highlights, you might want to store the jax.nn.relu function directly inside your model pytree. This function is not a JAX array, but it is still a leaf.
This distinction is critical:
- Array Leaves: These are the components JAX transformations like
jax.gradand optimizers typically operate on. They hold the dynamic, trainable state of your model. - Static Leaves: These are other non-container objects, like functions, strings, or integers specifying model configuration. They are part of the model's structure but are typically held constant during transformations.
Identifying which parts of your model are dynamic (array leaves) and which are static (other leaves and the container structure) is a key skill for building complex models in JAX, and we will see tools for managing this in upcoming lessons.
Conclusion
In this lesson, we have formally defined the pytree, the cornerstone data structure of JAX. You have learned to see nested Python objects through the lens of JAX, identifying their structural containers and their data-carrying leaves.
Key Takeaways:
- A pytree is a nested data structure composed of standard Python containers (lists, tuples, dicts).
- It consists of container nodes, which define the structure (the "treedef"), and leaf nodes, which hold the actual data.
- JAX functions and transformations often work by flattening a pytree, operating on its leaves, and then re-building the original structure.
- In Equinox, models are pytrees, where trainable parameters are typically JAX arrays serving as leaves.
- Leaves are not limited to arrays; they can be any Python object that isn't a registered pytree container, leading to a distinction between dynamic (array) and static (non-array) leaves.
Now that we have a solid conceptual understanding of what pytrees are, our next step is to learn how to work with them programmatically. In the next lesson, we will explore the jax.tree_util library, focusing on tree_map, the powerhouse function for applying operations to every leaf in a pytree. This will give you the practical tools to manipulate your Equinox models effectively.