A "typing friendly" variant of threading.local

This is an idiom for code which needs to use a non-threadsafe persistent object (e.g. a connection pool) in a threaded application:

store = threading.local()
def get_foo():
    if not hasattr(store, "foo"):
        store.foo = Foo()
    return store.foo

When we type annotate this, we get this:

store = threading.local()
def get_foo() -> Foo:
    if not hasattr(store, "foo"):
        store.foo = Foo()
    return typing.cast(Foo, store.foo)

Which is fine, but a bit verbose. Is this something which can be generalized into something like TypedDict? I’d love to see

class MyLocalStore(threading.TypedLocal):
    # perhaps with semantics similar to dataclasses?
    foo = threading.local_field(default_factory=Foo)
    bar = threading.local_field(default_factory=_make_bar_callable)

store = MyLocalStore()

store.foo  # type: Foo

Is there a better way of doing this already?

Would using a ContextVar (from the contextvars module) work better? (They’re basically thread-locals, but with extra magic to support async)

ContextVars have much nicer annotation support, but I find them harder to use.

They introduce questions which I’m not used to thinking about with threadlocals – e.g. What parts of my application should create new Contexts? Maybe it’s good to think these things through, and I should experiment more with contextvars so that I can become more knowledgeable about the feature.

But I don’t know that they fully replace threadlocal usage. Threadlocals have the advantage of being really easy to understand (my mental model is “it’s a threadsafe dict with thread IDs as keys”).

You probably don’t want to ever create new Contexts. Every thread gets its own Context automatically, and the Context API is mostly intended for libraries like asyncio that create their own things that are like-threads-but-not-actually-threads.

Think of each ContextVar as a threadsafe dict with thread IDs as keys, and the value being whatever each thread sticks in there.

That’s super-useful to learn, thank you for sharing that!
The last time I read the contextvars docs, I was confused. I thought that was for me as an end user. :sweat_smile:

Oh, you mean it’s like a threadlocal! :stuck_out_tongue_closed_eyes:

So, it seems like my original question can be answered with: Yes, there is a better way. Use contextvars. Hearing from a core dev that contextvars fit this niche definitely re-motivates me to learn the new (I know, since py3.7…) tooling!

Looking for anything useful left in this thread for others / the community, I’m not sure I see any viable salvage. Thanks for engaging on this.