See also: Machine learning terms
The Layers API (commonly written as tf.layers) was a high-level layer abstraction in TensorFlow that provided pre-built, modular components for assembling neural networks. It sat one level above the low-level tf.nn operators and offered familiar building blocks such as dense, convolutional, pooling, dropout, and batch normalization layers. tf.layers was a TensorFlow 1.x API. It was deprecated when TensorFlow 2.0 launched on September 30, 2019, in favor of tf.keras.layers, which became the canonical high-level API for the entire framework. The legacy module still ships inside TensorFlow 2 under tf.compat.v1.layers so that older code can continue to run during migration.
The deprecation of tf.layers was part of a much wider consolidation that happened with TensorFlow 2: the framework had grown several overlapping ways to build models (raw tf.nn, tf.layers, tf.contrib.slim, Estimator model functions, the standalone Keras package, and the bundled tf.keras), and the 2.0 release picked Keras as the single official high-level API. Anyone reading TF 1.x tutorials, research code from 2017 to 2019, or papers that ship reference implementations from that era will still encounter tf.layers, which is why the module is worth understanding even though it has no place in new code.
Google introduced tf.layers in TensorFlow 1.0, released on February 15, 2017. The module was promoted from tf.contrib.layers, an older incubator namespace that had grown organically out of the TF-Slim project. tf.layers gave users a stable, supported way to compose layers as Python functions that took a tensor and returned a tensor, without having to manage variable scopes, shape inference, and weight initialization by hand.
From the start, tf.layers overlapped heavily with Keras, which had been adopted into TensorFlow as tf.keras around the same period. Both APIs offered Dense, Conv2D, Dropout, BatchNormalization, and so on, with very similar arguments. Internally the two namespaces even shared some implementation code, but they remained two separate user facing surfaces. This duplication caused real confusion: tutorials disagreed about which API to use, and TF Slim formed yet a third overlapping option for the same job.
The consolidation announcement came at the TensorFlow Dev Summit in March 2019, where the team committed to making Keras the only official high-level API in TensorFlow 2. The 2.0 final release shipped on September 30, 2019. From that release onward, tf.layers was removed from the top-level tf namespace. The module stayed accessible inside the compatibility shim at tf.compat.v1.layers, and the official documentation marked every function in it with a deprecation warning that pointed to the matching tf.keras.layers class.
tf.layers was a relatively small module. The functions and classes it exposed map almost one-to-one onto modern Keras layers, which is why automated migration is feasible.
| tf.layers function | Purpose | tf.keras.layers replacement |
|---|---|---|
tf.layers.dense | Fully connected (dense) layer | tf.keras.layers.Dense |
tf.layers.conv1d | 1D convolution | tf.keras.layers.Conv1D |
tf.layers.conv2d | 2D convolution | tf.keras.layers.Conv2D |
tf.layers.conv3d | 3D convolution | tf.keras.layers.Conv3D |
tf.layers.conv2d_transpose | Transposed (deconvolution) layer for upsampling | tf.keras.layers.Conv2DTranspose |
tf.layers.separable_conv1d | Depthwise separable 1D convolution | tf.keras.layers.SeparableConv1D |
tf.layers.separable_conv2d | Depthwise separable 2D convolution | tf.keras.layers.SeparableConv2D |
tf.layers.max_pooling1d / max_pooling2d / max_pooling3d | Max pooling | tf.keras.layers.MaxPooling1D / 2D / 3D |
tf.layers.average_pooling1d / 2d / 3d | Average pooling | tf.keras.layers.AveragePooling1D / 2D / 3D |
tf.layers.batch_normalization | Batch normalization | tf.keras.layers.BatchNormalization |
tf.layers.dropout | Dropout regularization | tf.keras.layers.Dropout |
tf.layers.flatten | Flatten a multidimensional tensor to 2D | tf.keras.layers.Flatten |
Each layer existed in two forms. There was an object oriented class, for example tf.layers.Conv2D, that held its own weights and could be called multiple times to share parameters across the graph. There was also a thin functional wrapper, for example tf.layers.conv2d, that instantiated the class on the fly and immediately applied it to a tensor. The functional form was the more common style in TF 1.x example code and is what most readers will recognize.
A typical tf.layers model from the TF 1.x era was just a Python function that took an input tensor and returned an output tensor, with all weight management hidden inside the layer calls and a tf.Session driving execution. A small convolutional network for MNIST looked roughly like this:
def build_model(images, training):
net = tf.layers.conv2d(images, filters=32, kernel_size=3,
activation=tf.nn.relu)
net = tf.layers.max_pooling2d(net, pool_size=2, strides=2)
net = tf.layers.conv2d(net, filters=64, kernel_size=3,
activation=tf.nn.relu)
net = tf.layers.max_pooling2d(net, pool_size=2, strides=2)
net = tf.layers.flatten(net)
net = tf.layers.dropout(net, rate=0.4, training=training)
logits = tf.layers.dense(net, units=10)
return logits
The code reads cleanly, but a lot is happening implicitly. Each call creates trainable variables in the current variable scope, and reusing them across calls or across phases (train versus eval) was done by manipulating that scope rather than by holding a reference to the layer object. Layers that behaved differently at training and inference time, like dropout and batch normalization, took an explicit training= boolean. Batch normalization in particular required users to hand the running mean and variance update operations into the optimizer step through tf.GraphKeys.UPDATE_OPS, a frequent source of subtle bugs.
The stated reason for the deprecation was simple: TF 2.0 was unifying around Keras, and two near identical layer APIs in the same framework was bad for users. The deeper reasons line up with TensorFlow 2's broader design direction.
First, TF 2.0 made eager execution the default and dropped the placeholder-and-session programming model that tf.layers was originally designed for. The functional tf.layers.conv2d(input, ...) style assumes a static graph that gets executed later under a session. Once eager mode became the norm, the Keras style of "create a Layer object once, then call it on tensors" fit much more naturally.
Second, tf.layers had no equivalent of the Keras Model class. There was no built in way to encapsulate a stack of layers as a single object with .fit(), .evaluate(), .save(), and .summary() methods. Users either built their own thin wrapper, dropped down to TF Estimator, or used Keras anyway. TF 2.0 made the Keras Model, the Sequential builder, and the Functional API the official answer.
Third, the functional API of tf.layers leaned on global state, specifically tf.variable_scope, to control variable creation and reuse. That pattern was already brittle in TF 1.x and became actively wrong in eager mode, where there is no graph and no scopes to walk back into.
Finally, the wider research community was already on Keras: the standalone keras package on PyPI was popular long before TF 2 shipped, and PyTorch's torch.nn.Module had set the expectation that high-level deep learning APIs should be object oriented and Pythonic. Keeping tf.layers around was, at that point, a maintenance tax with no upside.
For practical migration, the mapping in the table above covers most real codebases. Beyond the symbol rename, there are a few behavior differences to watch for.
| Concern | tf.layers (TF 1.x) | tf.keras.layers (TF 2.x) |
|---|---|---|
| Programming style | Functional, layer applied inline to a tensor | Layer object created once, then called as a function |
| Variable management | Weights live in the active tf.variable_scope | Weights are attributes of the layer object, tracked automatically |
| Train versus eval | Pass training= boolean explicitly to each layer that needs it | Layer infers the right behavior from model.fit / predict, or accepts training= when called directly |
| Batch norm updates | User must add UPDATE_OPS collection to the train step | Updates are tracked on the layer and applied automatically |
| Model packaging | No built in Model abstraction | tf.keras.Model, Sequential, and Functional API |
| Saving | tf.train.Saver checkpoints, manual graph reconstruction | Native model.save() to SavedModel or HDF5 |
TensorFlow ships an automated converter, the tf_upgrade_v2 script, which is installed alongside any TensorFlow 1.13 or later (including all 2.x builds). Run it on a single file with tf_upgrade_v2 --infile old.py --outfile new.py, or on a whole tree with --intree. The script handles the mechanical symbol rewrites and writes a report.txt listing anything it could not safely convert. For tf.layers calls, the safe automatic transformation is usually to rewrite them as tf.compat.v1.layers so the file still runs under TF 2 in the v1 compatibility mode. Converting them all the way to tf.keras.layers requires a human pass, because the migration changes the surrounding control flow as well: the static graph and session disappear, the explicit training= flags often go away, and batch normalization update ops stop being a separate concern.
The TensorFlow team's own recommendation, documented in the official TF1 to TF2 migration guide, is to use tf.compat.v1 only as transitional scaffolding and to rewrite to native TF 2 idioms as time allows.
In TF 2.x and later, every modern TensorFlow layer is a subclass of tf.keras.layers.Layer. The class has a small contract that anyone who has worked with PyTorch's nn.Module will recognize. Subclasses define their state in build(input_shape), where weights are created with self.add_weight(...), and define their forward pass in call(inputs, training=None). The framework tracks the weights as attributes of the object, so layer.trainable_variables, layer.losses, and layer.weights work without the user maintaining a separate list.
Layers compose into models in three styles. The Sequential API stacks layers in order with tf.keras.Sequential([...]), which is fine for plain feedforward networks. The Functional API treats layers as callable nodes in a directed graph and is the right tool for models with skip connections, multiple inputs, or multiple outputs. Subclassing tf.keras.Model and writing an arbitrary call method is the most flexible style and the closest analogue to a PyTorch module. All three styles produce models that share the same .fit, .evaluate, .predict, .save, and .summary methods.
A dense layer in the modern API looks like:
dense = tf.keras.layers.Dense(units=64, activation='relu')
hidden = dense(inputs)
The same layer object can be called on multiple tensors to share weights, can be saved as part of a model, and reports its parameter count to model.summary() without any extra setup.
The TF 1 to TF 2 transition was not happening in a vacuum. Every major deep learning framework now offers a single, canonical, object oriented layer API, and the conventions are very similar across them.
| Framework | Base class | Composition style |
|---|---|---|
| TensorFlow 2 / Keras | tf.keras.layers.Layer, tf.keras.Model | Sequential, Functional, or Model subclass |
| PyTorch | torch.nn.Module | nn.Sequential or arbitrary forward method |
| JAX / Flax (linen) | flax.linen.Module | Functional, parameters threaded through pure functions |
| JAX / Flax NNX | flax.nnx.Module | Stateful object oriented, replaces older Haiku style |
| JAX / Equinox | eqx.Module | PyTree based, layers are just dataclasses |
| JAX / Haiku | hk.Module | Transformed pure functions; archived in favor of Flax NNX |
The broad convergence on "layer is an object that owns its weights and exposes a callable forward method" is exactly what tf.layers, in its functional form, was missing.
Looking back at tf.layers from the perspective of modern frameworks, several limitations stand out. The functional style hid the layer object from the user, which made parameter sharing, layer reuse, and serialization harder than they needed to be. Reliance on tf.variable_scope for variable management leaked into surrounding code and was easy to misuse. There was no first class model abstraction, so users had to either glue their own training loop together or wire layers into tf.estimator.EstimatorSpec, which was its own awkward API. Batch normalization required manual update op handling. None of these were fatal, but together they made tf.layers feel like a halfway step toward a proper high-level API rather than a destination.
New TensorFlow code written in 2026 should not use tf.layers. The API is gone from the top-level namespace, the documentation directs everyone to tf.keras.layers, and tooling, tutorials, and pretrained model code are all written against Keras. Where tf.layers still matters is in legacy code: research repositories from 2017 to 2019, internal pipelines at companies that adopted TF 1 early, and reference implementations attached to older papers. For those, the practical workflow is to run tf_upgrade_v2, accept the tf.compat.v1.layers rewrite as a working baseline, and then incrementally port the model to native tf.keras idioms when the surrounding training loop is rewritten for eager execution.
The broader lesson of the tf.layers story is one of API consolidation. TensorFlow 1 had several roughly equivalent ways to build a model, none of them obviously the right answer. TensorFlow 2 picked one, tf.keras, and deprecated the rest. That decision made TensorFlow easier to teach, easier to write tutorials for, and easier to compare against PyTorch on equal footing. The same pattern shows up across the ecosystem: PyTorch went through a similar consolidation around nn.Module and away from older Variable style code, and the JAX community is currently going through it with the move from Haiku to Flax NNX.
tf.layers was a box of LEGO blocks for building neural networks, shipped with TensorFlow 1. There was a different, very similar box of blocks called Keras sitting on the shelf next to it. Two boxes with mostly the same blocks was confusing, so when TensorFlow 2 came out the team threw the tf.layers box away and told everyone to use the Keras box. The old box is still in the basement under a label that says tf.compat.v1.layers, in case you find an old project that needs it, but new projects should reach for Keras.