Skip to content

gh-150708: Make functools.cache (unbound) and cached_property strictly idempotent - #154069

Open
seberg wants to merge 6 commits into
python:mainfrom
seberg:functools-thread-idempotent
Open

gh-150708: Make functools.cache (unbound) and cached_property strictly idempotent#154069
seberg wants to merge 6 commits into
python:mainfrom
seberg:functools-thread-idempotent

Conversation

@seberg

@seberg seberg commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

When running multiple threads, we don't want to guarantee call-once style behavior for functools.cache or cached_property. That could be convenient but is not strictly needed.

However, we should use dict.setdefault() to ensure that:
obj.cached_property is obj.cached_property
and:
cached_fuction(arg) is cached_function(arg)

Both may be run twice (and for statistics purposes we may return a cached value but still have a miss).

I confirmed tests were failing locally before the fixes. I added it to the free-threaded tests, because it seemed similar ones, since we release the GIL this behavior is not limited to free-threaded Python though.

(I have used an agent mostly to get the dict changes started, but it looks fine to me -- and more minimal than I had hoped for. Created at EuroPython sprints.)

When running multiple threads, we don't want to guarantee call-once
style behavior for functools.cache or cached_property.
That could be convenient but is not strictly needed.

However, we should use `dict.setdefault()` to ensure that:
    obj.cached_property is obj.cached_property
and:
    cached_fuction(arg) is cached_function(arg)

Both may be run twice (and for statistics purposes we may return
a cached value but still have a miss).
@read-the-docs-community

read-the-docs-community Bot commented Jul 19, 2026

Copy link
Copy Markdown

Comment thread Lib/functools.py Outdated
val = self.func(instance)
try:
cache[self.attrname] = val
val = cache.setdefault(self.attrname, val)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I talked with Petr briefly at the sprint about this. It isn't clear to me if __dict__ could be something weird that might succeed with __setattr__ but not have setdefault (or even different).

It seems possible in principle, but especially in combination with cached_property maybe/hopefully non-existing in practice. But if this is a concern, I guess this could check for dict or add another level of try/except...

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That level of try/except should only cover the operation that's expected to fail -- cache.setdefault, not the call itself.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, and the other comments). In case anyone wonders, splitting the attribute makes it 22ns -> 36ns for me; but this is the slow path, so I assume it's OK and the setitem was also a bit similarly a bit quicker.

Comment thread Lib/functools.py Outdated
val = self.func(instance)
try:
cache[self.attrname] = val
val = cache.setdefault(self.attrname, val)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That level of try/except should only cover the operation that's expected to fail -- cache.setdefault, not the call itself.

Comment thread Lib/functools.py Outdated
except AttributeError:
msg = (
f"The '__dict__' attribute on {type(instance).__name__!r} instance "
f"does not support item assignment for caching {self.attrname!r} property."

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is now setdefault rather than item assignment.

Comment thread Misc/NEWS.d/next/Core_and_Builtins/2026-07-19-09-01-02.gh-issue-150708.AESsFo.rst Outdated
Comment thread Include/internal/pycore_dict.h Outdated
PyAPI_FUNC(int) _PyDict_SetItem_KnownHash(PyObject *mp, PyObject *key,
PyObject *item, Py_hash_t hash);
// Same return codes as PyDict_SetDefaultRef (0 inserted, 1 present, -1 error).
extern int _PyDict_SetDefaultRef_KnownHash(PyObject *mp, PyObject *key,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be PyAPI_FUNC(int) so it's exported properly.

seberg and others added 2 commits July 29, 2026 20:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants