[Python-Dev] PEP 567 pre v3

Yury Selivanov yselivanov.ml at gmail.com
Mon Jan 8 14:34:57 EST 2018


Hi,

Thanks to everybody participating in the PEP 567 discussion!  I want
to summarize a few topics to make sure that we are all on the same
page (and maybe provoke more discussion).


1. Proposal: ContextVar has default set to None.

>From the typing point of view that would mean that if a context
variable is declared without an explicit default, its type would be
Optional.  E.g. say we have a hypothetical web framework that allows
to access the current request object through a context variable:

  request_var: ContextVar[Optional[Request]] = \
      ContextVar('current_request')

When we need to get the current request object, we would write:

  request: Optional[Request] = request_var.get()

And we'd also need to explicitly handle when 'request' is set to None.
Of course we could create request_var with its default set to some
"InvalidRequest" object, but that would complicate things.  It would
be easier to just state that the framework always sets the current
request and it's a bug if it's not set.

Therefore, in my opinion, it's better to keep the current behaviour:
if a context variable was created without a default value,
ContextVar.get() can raise a LookupError.


2. Context.__contains__, Context.__getitem__ and ContexVar.default

So if we keep the current PEP 567 behaviour w.r.t. defaults,
ContextVar.get() might return a different value from Context.get():

    v = ContextVar('v', default=42)
    ctx = contextvars.copy_context()

    ctx.get(v)   # returns None
    v.get()   # returns 42
    v in ctx  # returns False

I think this discrepancy is OK.  Context is a mapping-like object and
it reflects the contents of the underlying _ContextData mapping
object.

ContextVar.default is meant to be used only by ContextVar.get().
Context objects should not use it.

Maybe we can rename ContextVar.get() to ContextVar.lookup()?  This
would help to avoid potential confusion between Context.get() and
ContextVar.get().


3. Proposal: Context.get() and __getitem__() should always return up
to date values.

The issue with the current PEP 567 design is that PyThreadState points
to a _ContextData object, and not to the current Context.  The
following code illustrates how this manifests in Python code:

    v = ContextVar('v')

    def foo():
        v.set(42)
        print(v.get(), ctx.get(v, 'missing'))

    ctx = Context()
    ctx.run(foo)

The above code will print "42 missing", because 'ctx' points to an
outdated _ContextData.

This is easily fixable if we make PyThreadState to point to the
current Context object (instead of it pointing to a _ContextData).
This change will also make "contextvars.copy_context()" easier to
understand--it will actually return a copy of the current context that
the thread state points to.

Adding a private Context._in_use attribute would allow us to make sure
that Context.run() cannot be simultaneously called in two OS threads.
As Nathaniel points out, this will also simplify cache implementation
in ContextVar.get().  So let's do this.


4. Add Context.copy().

I was actually going to suggest this addition myself.  With the
current PEP 567 design, Context.copy() can be implemented with
"ctx.run(contextvars.copy_context)", but this is very cumbersome.

An example of when a copy() method could be useful is capturing the
current context and executing a few functions with it using
ThreadPoolExecutor.map().  Copying the Context object will ensure that
every mapped function executes in its own context copy (i.e.
isolated).  So I'm +1 for this one.


5. PEP language.

I agree that PEP is vague about some details and is incorrect in some
places (like calling Context objects immutable, which is not really
true, because .run() can modify them). I'll fix the language in v3
once I'm back home.


Yury


More information about the Python-Dev mailing list