Context variables in Python

Jundiaius picture Jundiaius · Jun 14, 2018 · Viewed 10.1k times · Source

Suppose that I have a function in my Python application that define some kind of context - a user_id for example. This function call other functions that do not take this context as a function argument. For example:

def f1(user, operation):
    user_id = user.id
    # somehow define user_id as a global/context variable for any function call inside this scope
    f2(operation)

def f2(operation):
    # do something, not important, and then call another function
    f3(operation)

def f3(operation):
    # get user_id if there is a variable user_id in the context, get `None` otherwise
    user_id = getcontext("user_id")
    # do something with user_id and operation

My questions are:

  • Can the Context Variables of Python 3.7 be used for this? How?
  • Is this what these Context Variables are intended for?
  • How to do this with Python v3.6 or earlier?

EDIT

For multiple reasons (architectural legacy, libraries, etc) I can't/won't change the signature of intermediary functions like f2, so I can't just pass user_id as arguments, neither place all those functions inside the same class.

Answer

Blckknght picture Blckknght · Jun 14, 2018

You can use contextvars in Python 3.7 for what you're asking about. It's usually really easy:

import contextvars

user_id = contextvars.ContextVar("user_id")

def f1(user, operation):
    user_id.set(user.id)
    f2()

def f2():
    f3()

def f3():
    print(user_id.get(default=None))  # gets the user_id value, or None if no value is set

The set method on the ContextVar returns a Token instance, which you can use to reset the variable to the value it had before the set operation took place. So if you wanted f1 to restore things the way they were (not really useful for a user_id context variable, but more relevant for something like setting the precision in the decimal module), you can do:

token = some_context_var.set(value)
try:
    do_stuff()    # can use value from some_context_var with some_context_var.get()
finally:
    some_context_var.reset(token)

There's more to the contextvars module than this, but you almost certainly don't need to deal with the other stuff. You probably only need to be creating your own contexts and running code in other contexts if you're writing your own asynchronous framework from scratch.

If you're just using an existing framework (or writing a library that you want to play nice with asynchronous code), you don't need to deal with that stuff. Just create a global ContextVar (or look up one already defined by your framework) and get and set values on it as shown above, and you should be good to go.

A lot of contextvars use is probably going to be in the background, as an implementation detail of various libraries that want to have a "global" state that doesn't leak changes between threads or between separate asynchronous tasks within a single thread. The example above might make more sense in this kind of situation: f1 and f3 are part of the same library, and f2 is a user-supplied callback passed into the library somewhere else.