Fang Hung-chien
Fang Hung-chien

Reputation: 206

Context Variables should be created at the top module level and never in closures

In the documentation for the Python standard library module contextvars, it is stated that:

Context Variables should be created at the top module level and never within closures.

However, I am unsure about the meaning of this statement. While many online tutorials mention it, none provide an explanation. Could you please provide an example where a context variable is created within a closure, and explain the potential problems it may cause? Is it related to GC?

Context objects hold strong references to context variables which prevents context variables from being properly garbage collected.

Upvotes: 0

Views: 465

Answers (1)

jsbueno
jsbueno

Reputation: 110591

This means that if a context variable is created in a function, it will persist in existence after the call is gone. And that each time that function is called, a new context variable (with the same name) will be created.

In short lived, run once, scripts that is no problem - but the typical codebase that will make use of context variables are servers, or other long-running tasks - the cumulative creation of these objects implies in a a memory-leak.

See for example these experiments on the REPL:

In [6]: import contextvars

In [7]: def blah():
   ...:     x = contextvars.ContextVar("x")
   ...:     x.set(0)
   ...:

In [8]: _ = [blah() for _ in range(10)]

In [9]: ctx = contextvars.copy_context()

In [10]: list(ctx.keys())
Out[10]:
[<ContextVar name='x' at 0x7f2c74557150>,
 <ContextVar name='x' at 0x7f2c75ae7150>,
 <ContextVar name='x' at 0x7f2c75153fb0>,
 <ContextVar name='x' at 0x7f2c75153790>,
 <ContextVar name='x' at 0x7f2c751537e0>,
 <ContextVar name='x' at 0x7f2c75153e20>,
 <ContextVar name='x' at 0x7f2c75153650>,
 <ContextVar name='x' at 0x7f2c745562a0>,
 <ContextVar name='x' at 0x7f2c75153ec0>,
 <ContextVar name='x' at 0x7f2c75148cc0>]



As you can see, the current context (outside of the function blah), holds one instance of ContextVar (and its respective value) for each time the function was called, even after the function is over, and there is other reference to the contents of the local variable x during its execution.

(Even if no value is set to x inside blah a reference to the contextvar object itself will still be held, even though it is not retrievable outside of the function scope)

Playing around a bit further, I found out that if no value is set on the contextvars inside that scope, they are properly garbage collected. Check how the id for x is reused in this small experiment:


In [1]: import contextvars

In [2]: def blah():
   ...:     x = contextvars.ContextVar("x")
   ...:     ctx_ids.append(id(x))
   ...:

In [4]: ctx_ids = []

In [5]: _ = [blah() for _ in range(10)]

In [6]: ctx_ids
Out[6]:
[140436854037360,
 140436854037360,
 140436854037360,
 140436854037360,
 140436854037360,
 140436854037360,
 140436854037360,
 140436854037360,
 140436854037360,
 140436854037360]

And the behavior when a value is set:


In [7]: ctx_ids = []

In [8]: def blah():
   ...:     x = contextvars.ContextVar("x")
   ...:     ctx_ids.append(id(x))
   ...:     x.set(0)
   ...:

In [9]: _ = [blah() for _ in range(10)]

In [10]: ctx_ids
Out[10]:
[140436853777536,
 140436853778656,
 140436853769456,
 140436853774096,
 140436853778576,
 140436853776976,
 140436864406592,
 140436864402912,
 140436864407152,
 140436863775376]

This suggests that with some careful usage, a contextvar could be created inside a function scope, and used in calls made from there in context-copies, contrary to the docs. The leak will take place if a value is ever assigned to the contextvar in the context the function itself is running, though.

Still "top level" is not the only option for using them - the important thing is that the creation of a contextvar takes place one single place in a process. I've been creating them as class attributes, instead, directly in the body of a class: it really feels more idiomatic, specially if the methods of that class will be the ones making use of that variable.

Upvotes: 2

Related Questions