@@ -134,10 +134,11 @@ def _rollback_registration(cls: type, type_info: Any) -> None:
134134# ---------------------------------------------------------------------------
135135
136136
137- def _collect_own_fields (
137+ def _collect_own_fields ( # noqa: PLR0912
138138 cls : type ,
139139 hints : dict [str , Any ],
140140 decorator_kw_only : bool ,
141+ decorator_frozen : bool ,
141142) -> list [Field ]:
142143 """Parse own annotations into :class:`Field` objects.
143144
@@ -194,6 +195,10 @@ def _collect_own_fields(
194195 if f .kw_only is None :
195196 f .kw_only = kw_only_active
196197
198+ # Apply class-level frozen when the field doesn't explicitly set it
199+ if decorator_frozen and not f .frozen :
200+ f .frozen = True
201+
197202 # Resolve hash=None → follow compare (native dataclass semantics)
198203 if f .hash is None :
199204 f .hash = f .compare
@@ -248,7 +253,7 @@ def _register_fields_into_type(
248253 except (NameError , AttributeError ):
249254 return False
250255
251- own_fields = _collect_own_fields (cls , hints , params ["kw_only" ])
256+ own_fields = _collect_own_fields (cls , hints , params ["kw_only" ], params [ "frozen" ] )
252257 py_methods = _collect_py_methods (cls )
253258
254259 # Register fields and type-level structural eq/hash kind with the C layer.
@@ -414,11 +419,12 @@ def _install_deferred_init(
414419 order_default = False ,
415420 field_specifiers = (field , Field ),
416421)
417- def py_class (
422+ def py_class ( # noqa: PLR0913
418423 cls_or_type_key : type | str | None = None ,
419424 / ,
420425 * ,
421426 type_key : str | None = None ,
427+ frozen : bool = False ,
422428 init : bool = True ,
423429 repr : bool = True ,
424430 eq : bool = False ,
@@ -465,6 +471,11 @@ class MyNode(Object):
465471 type_key
466472 Explicit FFI type key. Auto-generated from
467473 ``{module}.{qualname}`` when omitted.
474+ frozen
475+ If True, all fields are read-only after ``__init__`` by default.
476+ Individual fields can still be marked ``field(frozen=True)`` on a
477+ non-frozen class. Use ``type(obj).field_name.set(obj, value)``
478+ as an escape hatch when mutation is necessary.
468479 init
469480 If True (default), generate ``__init__`` from field annotations.
470481 repr
@@ -514,6 +525,7 @@ class MyNode(Object):
514525
515526 effective_type_key = type_key
516527 params : dict [str , Any ] = {
528+ "frozen" : frozen ,
517529 "init" : init ,
518530 "repr" : repr ,
519531 "eq" : eq ,
0 commit comments