You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Adding an unstable C API for unique references #133140
In #132070, there was a race brought up in free-threading through use of Py_REFCNT(op) == 1. The fix is to use _PyObject_IsUniquelyReferenced.
There were a couple additional comments about how we should handle this for the public API (for example, @godlygeek suggested implementing Py_REFCNT as _PyObject_IsUniquelyReferenced(op) ? 1 : INT_MAX). I think the best approach is to just expose an unstable API for this and point to it in Py_REFCNT's documentation.
@ngoldbaum can you please open an issue for that? I'll mark it as a deferred blocker so it lands before 3.14.
Sam told me it's temporary code so it's fine for now. Just pointing out that the _PyStackRef API is really not something that can be relied on. It will very likely break often till we settle on something that is nice internally for us.
Regarding the NumPy code, it can't use _PyObject_IsUniquelyReferenced (that won't fix the underlying problem). I agree with @Fidget-Spinner, we should open a new issue for it.
Actually, is there any use case for exposing _PyObject_IsUniquelyReferenced on its own if we're going to expose PyUnstable_Object_IsUniqueTemporary anyway? I'm not totally sure we need this one.
Yeah, cases like enum_next_long where you want to re-use (or modify) an object that you have a unique reference to. PyUnstable_Object_IsUniqueTemporary isn't useful here because the object isn't a temporary on the interpreter stack. _PyObject_IsUniquelyReferenced is appropriate because we already know that en->en_result holds a strong (heap) reference to the relevant object:
IMO, that's generally OK for unstable API. Don't forget docs & tests though.
But if this was sent to the WG I'd ask to avoid documenting it in terms of reference counting, which is an implementation detail you shouldn't rely on (as recent developments like this and immortality show).
Would it be correct to say that if this returns true, “immutable” objects can be safely mutated -- that is, to "other" code it will appear as if the object was destroyed and a new one with the same id was recreated? (And if so, what's "other" code and how long does this apply?)
Does this mean that the Py_REFCNT docs are again wrong in a backwards-incompatible way?
Yes, but unless we want to revert PEP 703, I don't think there's any good way around that.
Would it be correct to say that if this returns true, “immutable” objects can be safely mutated -- that is, to "other" code it will appear as if the object was destroyed and a new one with the same id was recreated?
I think in-place updates would generally be checked by PyUnstable_Object_IsUniqueTemporary (gh-133170), not PyUnstable_Object_IsUniquelyReferenced. If I understand it correctly, the cases are:
If you keep the strong reference in C code (e.g., owning some sub-data structure in your object, such as a dict to dict keys), then you want to check IsUniquelyReferenced. You just want to make sure there's no other threads messing with your object at the time. (Interpreter borrows don't matter here, because if the interpreter has a reference, then the refcnt will above > 1 anyway.)
If you give the strong reference away to the interpreter (typically by returning it), then you need IsUniqueTemporary to check if the only reference is on the interpreter's stack in addition to being the only reference.
But anyways, documenting this one in terms of reference counting would just make the API unclear. It acts as solely a replacement for Py_REFCNT(op) == 1, and since Py_REFCNT is already about reference counting, I think it's fine to document this one as such. The more important thing to me seems like documenting when to use both APIs.
If you keep the strong reference in C code ... then you want to check IsUniquelyReferenced
Yeah, that's right. I think a enum_next_longabove is a good example. The iterator holds a strong reference via en->en_result, so it's sufficient to call PyUnstable_Object_IsUniquelyReferenced. From context, you know that nobody else can be holding a borrowed reference to that object. In psuedo code:
staticPyObject*iter_next(PyObject*self)
{
MyIterObject*iter= (MyIterObject*)self;
if (PyUnstable_Object_IsUniquelyReferenced(iter->it_result) {
// optimization: reuse iter->it_result// update iter->it_result
...
returnPy_NewRef(iter->it_result);
}
else {
// create a new result valuereturn ...
}
}
You need PyUnstable_Object_IsUniqueReferencedTemporary (recently renamed from PyUnstable_Object_IsUniqueTemporary) if you don't hold the strong reference. This is a stricter check than PyUnstable_Object_IsUniquelyReferenced. The typical use cases involve arguments to C functions, because the callee doesn't own the arguments. In pseudo-code:
staticPyObject*my_nb_add(PyObject*a, PyObject*b)
{
if (PyUnstable_Object_IsUniqueReferencedTemporary(a)) {
// optimization: modify a in-place and return it
}
else {
// add `a` and `b` and return the result as a new object
}
}
PyUnstable_Object_IsUniqueReferencedTemporary(op) implies PyUnstable_Object_IsUniquelyReferenced(op), but the reverse is not true.
Bikeshedding, but could the names be tightened up by making them about the reference rather than about the object being referenced? Something like:
PyUnstable_Ref_IsUnique: this reference is the sole reference to the object. It's up to the calling code to ensure the reference can't end up borrowed on the interpreter stack (that is, the ref count must be incremented when making the reference available to the interpreter stack).
PyUnstable_Ref_IsUniqueTemporary: this reference is the sole reference to the object and it's solely stored on the interpreter stack (and not borrowed). Always false for references that aren't owned by the interpreter stack (even if they're unique).
That would be introducing a PyRefgroup prefix (with Unstable_ spliced in, in this case).
That's some prime real estate, and one I'd like to use for a possible PEP-sized feature.
IMO, Object_ is fine.
I don't think this new feature needs to go in 3.14. Having said that, I'd encourage the C API experts above to review the PR and we can include it if approved before tomorrow.
I merged the PR to land the function in Python 3.14. It's an unstable API, it can still change until Python 3.14 final, but I don't expect changes in its API, more on its documentation. I close the issue since the function is added.
Activity
ngoldbaum commentedon Apr 29, 2025
I'm not sure if it's tracked elsewhere, but I'd also like to see an API function so NumPy can replace this code:
https://github.com/numpy/numpy/blob/d692fbccd98cb880812b32936e5f94fcfe55053f/numpy/_core/src/multiarray/temp_elide.c#L119-L152
We had to add it to fix NumPy on 3.14. See numpy/numpy#28681.
Fidget-Spinner commentedon Apr 29, 2025
@ngoldbaum can you please open an issue for that? I'll mark it as a deferred blocker so it lands before 3.14.
Sam told me it's temporary code so it's fine for now. Just pointing out that the
_PyStackRefAPI is really not something that can be relied on. It will very likely break often till we settle on something that is nice internally for us.colesbury commentedon Apr 29, 2025
Regarding the NumPy code, it can't use
_PyObject_IsUniquelyReferenced(that won't fix the underlying problem). I agree with @Fidget-Spinner, we should open a new issue for it.ngoldbaum commentedon Apr 29, 2025
see #133164
ZeroIntensity commentedon Apr 29, 2025
Actually, is there any use case for exposing
_PyObject_IsUniquelyReferencedon its own if we're going to exposePyUnstable_Object_IsUniqueTemporaryanyway? I'm not totally sure we need this one.colesbury commentedon Apr 29, 2025
Yeah, cases like
enum_next_longwhere you want to re-use (or modify) an object that you have a unique reference to.PyUnstable_Object_IsUniqueTemporaryisn't useful here because the object isn't a temporary on the interpreter stack._PyObject_IsUniquelyReferencedis appropriate because we already know thaten->en_resultholds a strong (heap) reference to the relevant object:cpython/Objects/enumobject.c
Lines 197 to 236 in c46635a
encukou commentedon Apr 30, 2025
Does this mean that the
Py_REFCNTdocs are again wrong in a backwards-incompatible way?@ZeroIntensity
IMO, that's generally OK for unstable API. Don't forget docs & tests though.
But if this was sent to the WG I'd ask to avoid documenting it in terms of reference counting, which is an implementation detail you shouldn't rely on (as recent developments like this and immortality show).
Would it be correct to say that if this returns true, “immutable” objects can be safely mutated -- that is, to "other" code it will appear as if the object was destroyed and a new one with the same
idwas recreated? (And if so, what's "other" code and how long does this apply?)ZeroIntensity commentedon Apr 30, 2025
Yes, but unless we want to revert PEP 703, I don't think there's any good way around that.
I think in-place updates would generally be checked by
PyUnstable_Object_IsUniqueTemporary(gh-133170), notPyUnstable_Object_IsUniquelyReferenced. If I understand it correctly, the cases are:IsUniquelyReferenced. You just want to make sure there's no other threads messing with your object at the time. (Interpreter borrows don't matter here, because if the interpreter has a reference, then the refcnt will above > 1 anyway.)IsUniqueTemporaryto check if the only reference is on the interpreter's stack in addition to being the only reference.(@colesbury Fact check, please!)
But anyways, documenting this one in terms of reference counting would just make the API unclear. It acts as solely a replacement for
Py_REFCNT(op) == 1, and sincePy_REFCNTis already about reference counting, I think it's fine to document this one as such. The more important thing to me seems like documenting when to use both APIs.colesbury commentedon Apr 30, 2025
Yeah, that's right. I think a
enum_next_longabove is a good example. The iterator holds a strong reference viaen->en_result, so it's sufficient to callPyUnstable_Object_IsUniquelyReferenced. From context, you know that nobody else can be holding a borrowed reference to that object. In psuedo code:You need
PyUnstable_Object_IsUniqueReferencedTemporary(recently renamed fromPyUnstable_Object_IsUniqueTemporary) if you don't hold the strong reference. This is a stricter check thanPyUnstable_Object_IsUniquelyReferenced. The typical use cases involve arguments to C functions, because the callee doesn't own the arguments. In pseudo-code:PyUnstable_Object_IsUniqueReferencedTemporary(op)impliesPyUnstable_Object_IsUniquelyReferenced(op), but the reverse is not true.ncoghlan commentedon May 2, 2025
Bikeshedding, but could the names be tightened up by making them about the reference rather than about the object being referenced? Something like:
PyUnstable_Ref_IsUnique: this reference is the sole reference to the object. It's up to the calling code to ensure the reference can't end up borrowed on the interpreter stack (that is, the ref count must be incremented when making the reference available to the interpreter stack).PyUnstable_Ref_IsUniqueTemporary: this reference is the sole reference to the object and it's solely stored on the interpreter stack (and not borrowed). Always false for references that aren't owned by the interpreter stack (even if they're unique).encukou commentedon May 2, 2025
That would be introducing a
PyRefgroup prefix (withUnstable_spliced in, in this case).That's some prime real estate, and one I'd like to use for a possible PEP-sized feature.
IMO,
Object_is fine.ronaldoussoren commentedon May 5, 2025
Will this be in 3.14?
And is there a way to get the same behaviour in 3.13 where
_PyObject_IsUniquelyReferencedis not exposed in headers?ZeroIntensity commentedon May 5, 2025
Ideally, this one will make it into 3.14 too. I don't know if it's right to mark this as a blocker. cc @hugovk as the RM--do you want this in 3.14?
hugovk commentedon May 5, 2025
I don't think this new feature needs to go in 3.14. Having said that, I'd encourage the C API experts above to review the PR and we can include it if approved before tomorrow.
gh-133140: Add `PyUnstable_Object_IsUniquelyReferenced` for free-thre…
vstinner commentedon May 5, 2025
I merged the PR to land the function in Python 3.14. It's an unstable API, it can still change until Python 3.14 final, but I don't expect changes in its API, more on its documentation. I close the issue since the function is added.
pythongh-133140: Add `PyUnstable_Object_IsUniquelyReferenced` for fre…