Skip to content

Add set_freezable() for per-object freeze policy#77

Merged
mjp41 merged 8 commits intoimmutable-mainfrom
immutable-set_freezable
Mar 6, 2026
Merged

Add set_freezable() for per-object freeze policy#77
mjp41 merged 8 commits intoimmutable-mainfrom
immutable-set_freezable

Conversation

@mjp41
Copy link
Collaborator

@mjp41 mjp41 commented Mar 6, 2026

Add set_freezable(obj, status) to control whether individual objects
can be frozen. The status is one of four values:

FREEZABLE_YES - always freezable
FREEZABLE_NO - never freezable
FREEZABLE_EXPLICIT - freezable only when freeze() is called directly
on the object, not when reached as a child
FREEZABLE_PROXY - reserved for future module proxy use

Storage uses a two-tier strategy: first tries setting a __freezable__
attribute on the object (works for any object with a __dict__), falling
back to a weakref-keyed dictionary in the interpreter state (works for
__slots__ objects with __weakref__ support). If neither is possible,
raises TypeError.

During freeze, check_freezable() queries the status by checking the
__freezable__ attribute first, then the weakref dictionary. For
EXPLICIT, the freeze root object is tracked in FreezeState so that
direct vs child freezing can be distinguished.

The old standalone __freezable__ = False check in traverse_freeze is
removed; all __freezable__ handling is now unified through
check_freezable() and _PyImmutability_GetFreezable().

Changes:

  • Include/internal/pycore_immutability.h: add _Py_freezable_status
    enum, freezable_objects dict, and destroy_objects_cb to state
  • Include/cpython/immutability.h: declare SetFreezable/GetFreezable
  • Python/immutability.c: implement SetFreezable (attr-first, weakref
    fallback), GetFreezable (attr-first, weakref fallback),
    _destroy_dict callback, FreezeState.root, updated check_freezable
  • Modules/_immutablemodule.c: expose set_freezable() and constants
  • Lib/immutable.py: export set_freezable and FREEZABLE_* constants
  • Python/pystate.c: cleanup new state fields in interpreter_clear()
  • Lib/test/test_freeze/test_set_freezable.py: 18 tests covering all
    status values, storage strategies, edge cases, and constants
  • Migrate _blocking_on and _module_locks to use this mechanism.

mjp41 added 2 commits March 6, 2026 09:42
Add set_freezable(obj, status) to control whether individual objects
can be frozen. The status is one of four values:

  FREEZABLE_YES      - always freezable
  FREEZABLE_NO       - never freezable
  FREEZABLE_EXPLICIT - freezable only when freeze() is called directly
                       on the object, not when reached as a child
  FREEZABLE_PROXY    - reserved for future module proxy use

Storage uses a two-tier strategy: first tries setting a __freezable__
attribute on the object (works for any object with a __dict__), falling
back to a weakref-keyed dictionary in the interpreter state (works for
__slots__ objects with __weakref__ support). If neither is possible,
raises TypeError.

During freeze, check_freezable() queries the status by checking the
__freezable__ attribute first, then the weakref dictionary. For
EXPLICIT, the freeze root object is tracked in FreezeState so that
direct vs child freezing can be distinguished.

The old standalone __freezable__ = False check in traverse_freeze is
removed; all __freezable__ handling is now unified through
check_freezable() and _PyImmutability_GetFreezable().

Changes:
- Include/internal/pycore_immutability.h: add _Py_freezable_status
  enum, freezable_objects dict, and destroy_objects_cb to state
- Include/cpython/immutability.h: declare SetFreezable/GetFreezable
- Python/immutability.c: implement SetFreezable (attr-first, weakref
  fallback), GetFreezable (attr-first, weakref fallback),
  _destroy_dict callback, FreezeState.root, updated check_freezable
- Modules/_immutablemodule.c: expose set_freezable() and constants
- Lib/immutable.py: export set_freezable and FREEZABLE_* constants
- Python/pystate.c: cleanup new state fields in interpreter_clear()
- Lib/test/test_freeze/test_set_freezable.py: 18 tests covering all
  status values, storage strategies, edge cases, and constants
@mjp41 mjp41 requested a review from xFrednet March 6, 2026 09:58
Copy link
Collaborator

@xFrednet xFrednet left a comment

Choose a reason for hiding this comment

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

Some high level nits, but the rest LGTM. Nice work :D

def test_object_without_weakref_support_raises(self):
# Built-in ints don't support weak references or attribute setting.
with self.assertRaises(TypeError):
set_freezable(42, FREEZABLE_NO)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Should we allow setting freezability for shallow immutable objects in the first place?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Good question, I don't know. There are a bunch of defaults I am not sure on the right choices for.


// Check is object is subclass of NotFreezable
// TODO: Would be nice for this to be faster.
if (PyObject_IsInstance(obj, (PyObject *)&_PyNotFreezable_Type) == 1){
Copy link
Collaborator

Choose a reason for hiding this comment

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

Now we should be able to remove the _PyNotFreezable_Type (Can be done in a follow up.)

@mjp41 mjp41 merged commit e1c6249 into immutable-main Mar 6, 2026
24 of 32 checks passed
@xFrednet xFrednet deleted the immutable-set_freezable branch March 6, 2026 15:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants