Theano
scan function
updates
machine learning
Python

how does theano.scan's updates work?

ML System Design practice on Codemia

Design recommenders, ranking systems and training pipelines the way ML interviews actually ask for them, with worked solutions.

Practice ML system design

Introduction

theano.scan is Theano's way to build loop-like behavior inside a symbolic computation graph. The part that confuses many people is the updates object returned by scan. The short version is that updates is not how you pass state from one iteration to the next. It is how Theano records changes that should be applied to shared variables after the compiled function runs.

What scan Returns

A basic scan call returns two things:

  • the symbolic outputs produced across iterations
  • an updates mapping that must be passed to theano.function if you want those shared-variable updates to happen

A simplified shape looks like this:

python
1import numpy as np
2import theano
3import theano.tensor as T
4
5x = T.vector('x')
6
7def step(x_t, running_sum):
8    return running_sum + x_t
9
10outputs, updates = theano.scan(
11    fn=step,
12    sequences=x,
13    outputs_info=T.constant(np.float32(0.0)),
14)
15
16f = theano.function([x], outputs, updates=updates)
17print(f(np.array([1, 2, 3], dtype=np.float32)))

In this example, updates is usually empty because the loop state is carried through outputs_info, not through shared-variable mutation.

State Between Iterations Uses outputs_info

This is the key idea: if you want one iteration to influence the next, use recurrent outputs and outputs_info.

In the previous example, running_sum is the state being threaded through the loop. Each iteration returns a new value, and scan feeds it into the next step automatically.

That is different from shared-variable updates. Shared-variable updates are not the normal mechanism for per-step recurrence inside scan.

What updates Is Actually For

updates matters when the computation touches shared state that should change across calls to the compiled function. A common case is random number generation, where internal random state needs to advance after execution.

python
1import theano
2import theano.tensor as T
3from theano.tensor.shared_randomstreams import RandomStreams
4
5srng = RandomStreams(seed=123)
6
7def step():
8    return srng.normal(size=())
9
10samples, updates = theano.scan(fn=step, n_steps=3)
11f = theano.function([], samples, updates=updates)
12
13print(f())
14print(f())

Here, the updates returned by scan advance the random stream state so the next outer function call produces new samples. If you ignore the returned updates, the behavior is wrong or stale because the shared RNG state is not updated properly.

Shared Variables Outside the Loop

You can also combine scan results with explicit shared-variable updates at the outer function level.

python
1import numpy as np
2import theano
3import theano.tensor as T
4
5counter = theano.shared(np.int32(0))
6x = T.ivector('x')
7
8outputs, scan_updates = theano.scan(fn=lambda item: item * 2, sequences=x)
9result = outputs.sum()
10
11all_updates = dict(scan_updates)
12all_updates[counter] = counter + result
13
14f = theano.function([x], result, updates=all_updates)
15print(f(np.array([1, 2, 3], dtype=np.int32)))
16print(counter.get_value())

In this example, the loop itself is simple, but the compiled function also updates a shared counter after computing the result.

Timing Matters

Another source of confusion is timing. The updates mapping is attached to the compiled function call, not applied as ordinary Python side effects while you define the graph. Theano builds a symbolic description first. The actual shared-variable mutation happens when the compiled function executes.

That means updates is part of the runtime execution plan, not an immediate assignment.

The Practical Rule

Use outputs_info when the loop needs recurrent state between timesteps.

Use updates when shared variables or random streams must be advanced across calls to the compiled function.

If you mix those ideas up, you usually end up with code that either fails to propagate state correctly or mutates shared variables in places where a recurrent output would have been clearer.

Common Pitfalls

The most common mistake is trying to use shared-variable updates to carry state from one scan iteration to the next. That is what recurrent outputs and outputs_info are for.

Another mistake is ignoring the updates returned by scan, especially when random streams are involved. If you do not pass those updates into theano.function, the symbolic graph does not maintain the expected state.

Developers also sometimes expect updates to happen immediately at graph-construction time. Theano does not work that way. The updates are applied only when the compiled function runs.

Finally, do not assume updates will be non-empty for every scan. Many scans use pure symbolic recurrence and return no meaningful shared-variable updates at all.

Summary

  • 'scan returns both outputs and an updates mapping.'
  • Use outputs_info for state that flows from one iteration to the next.
  • Use updates for shared-variable changes that should occur when the compiled function executes.
  • Random streams are a common reason scan returns important updates.
  • Pass the returned updates into theano.function when they matter.

Related reading
Free course
Beginner
7 lessons
2 hours
Tackling System Design Interview Problems

A short course that equips you with the skills to approach system design interviews methodically.

Start the free course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

ML System Design practice on Codemia

Design recommenders, ranking systems and training pipelines the way ML interviews actually ask for them, with worked solutions.

Practice ML system design

All Rights Reserved.