From 241ae35be79c5967ae25153955c17160d2a73bd0 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sat, 16 May 2026 08:28:22 -0700 Subject: [PATCH 01/12] init --- peps/pep-0834.rst | 191 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 191 insertions(+) create mode 100644 peps/pep-0834.rst diff --git a/peps/pep-0834.rst b/peps/pep-0834.rst new file mode 100644 index 00000000000..149880429bd --- /dev/null +++ b/peps/pep-0834.rst @@ -0,0 +1,191 @@ +PEP: 834 +Title: Class Builders +Author: Jelle Zijlstra +Discussions-To: Pending +Status: Draft +Type: Standards Track +Created: 16-May-2026 +Python-Version: 3.16 +Post-History: Pending + + +Abstract +======== + +This PEP proposes a new syntax for declaring class-like constructs:: + + builder C: + ... + +In this example, the name ``builder`` is looked up in the current scope and its ``__build_class__`` +attribute is used to build an arbitrary objects. + +Intended use cases include enums, dataclasses, and several typing constructs:: + + from dataclasses import dataclass + from enum import enum + from typing import namedtuple, protocol, typed_dict + + dataclass InventoryItem(frozen=True): + name: str + amount: int + + enum Color: + red = 1 + green = 2 + blue = 3 + + typed_dict Movie(closed=True): + name: str + year: int + + protocol HasClose: + def supports_close(self) -> None: ... + + namedtuple Employee: + name: str + title: str + +This provides more intuitive syntax for beginners and +improved flexibility in the implementation. + + +Motivation +========== + +The Python standard library contains several constructs that create what may be called +a "class with benefits": something that is created through a class statement, but that +has some special behavior that is unlike a normal class. The :py:func:`dataclasses.dataclass` +decorator injects various methods into the class; the :py:class:`enum.Enum` base class +transforms a class into an enum; and the :py:mod:`typing` module provides mechanisms to +build protocols, named tuples, and typed dictionaries. + +The current syntax is verbose and not intuitive for beginners. It requires users to +understand that certain base classes or decorators radically change what a class statement +does, instead of putting the special behavior of these constructs front and center in the +syntax. + +This PEP proposes a flexible new syntax to declare these constructs using *class builders*: + + builder Simple: + ... + + builder Complex[T](Base, key="value"): + ... + +In this syntax, the name ``builder`` is looked up in the current scope and its ``__build_class__`` +attribute is called with the name, bases, body, and constructor keyword arguments used in the definition. +This function may return an arbitrary object, which is then stored at the name provided in the definition. +The standard library will be changed to provide builders for dataclasses, enums, :py:class:`typing.Protocol`, +:py:class:`typing.NamedTuple`, and :py:class:`typing.TypedDict`. + +In addition to improved concision, the new syntax allows more flexibility. The ``slots=True`` version +of dataclasses is currently implemented by creating a new wrapper class replacing the original class, +which causes various problems [TODO: link to issues]. The dataclass class builder can bypass this problem +by injecting the slots definition directly into the class body. + + +Specification +============= + +[Describe the syntax and semantics of any new language feature.] + + +Standard library changes +------------------------ + +The following class builders will be added to the standard library: + +* ``enum.enum``: creates an enum class. +* ``dataclasses.dataclass``: creates a dataclass. +* ``typing.protocol``: creates a protocol. +* ``typing.namedtuple``: creates a typed named tuple. +* ``typing.typed_dict``: creates a typed dictionary. + + +Type checker behavior +--------------------- + +The flexibility of class builders means that it is difficult to check them in full generality. + +Python type checkers should recognize the standard library builders and treat them similarly to the +existing syntax. In other cases, type checkers should look up the ``__build_class__`` attribute on +the builder and type check the call. + +A ``__build_class__`` callable may be decorated with the :py:func:`typing.dataclass_transform` decorator, +indicating that the builder behaves similarly to a dataclass. + + +Rationale +========= + +[Describe why particular design decisions were made.] + + +Backwards Compatibility +======================= + +[Describe potential impact and severity on pre-existing code.] + + +Security Implications +===================== + +[How could a malicious user take advantage of this new feature?] + + +How to Teach This +================= + +[How to teach users, new and experienced, how to apply the PEP to their work.] + + +Reference Implementation +======================== + +[Link to any existing implementation and details about its state, e.g. proof-of-concept.] + + +Rejected Ideas +============== + +[Why certain ideas that were brought while discussing this PEP were not ultimately pursued.] + + +Open Issues +=========== + +* What should the shape of the AST be? A new field on ``ClassDef``, or a new ``BuilderDef`` node? +* Should we allow builders to be something other than bare names? ``dataclasses.dataclass C:``? +* Should there be a ``__prepare__`` hook? +* Should the ``dataclass_transform`` behavior have any enhancements? Some way to inject a base class or metaclass? +* For enums, do we need more variants for flags and intenums etc.? + + +Acknowledgements +================ + +[Thank anyone who has helped with the PEP.] + + +Footnotes +========= + +[A collection of footnotes cited in the PEP, and a place to list non-inline hyperlink targets.] + + +Change History +============== + +[A summary of major changes the PEP has undergone. Whenever you update the +``Post-History``, add a new bullet item in newest-first (i.e. reverse +chronological) order, using the same ``DD-MMM-YYYY`` format, with sub-bullets +summarizing the changes. You can use the same link for the date bullet as you +do in the ``Post-History`` addition.] + + +Copyright +========= + +This document is placed in the public domain or under the +CC0-1.0-Universal license, whichever is more permissive. From c7f257a658a55fc04b68ad8af21d15c05706d192 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sat, 16 May 2026 08:38:35 -0700 Subject: [PATCH 02/12] spec --- peps/pep-0834.rst | 272 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 271 insertions(+), 1 deletion(-) diff --git a/peps/pep-0834.rst b/peps/pep-0834.rst index 149880429bd..6fc2328285f 100644 --- a/peps/pep-0834.rst +++ b/peps/pep-0834.rst @@ -88,7 +88,277 @@ by injecting the slots definition directly into the class body. Specification ============= -[Describe the syntax and semantics of any new language feature.] +Syntax +------ + +The grammar for class definitions is extended to allow a name before the +class name:: + + class_def_raw: + | 'class' NAME type_params? ['(' arguments? ')'] ':' block + | NAME NAME type_params? ['(' arguments? ')'] ':' block + +The first ``NAME`` in the second alternative is the *class builder*. The +second ``NAME`` is the name bound by the statement. The type parameter list, +parenthesized arguments, and block have the same syntax as in an ordinary +class definition. + +For example, all of the following are syntactically valid class builder +definitions:: + + builder C: + ... + + builder C[T](Base[T], key=value): + ... + + type C: + ... + +The builder name is parsed as a normal name token, not as a new keyword. In +particular, existing and future soft keywords may be used as builder names +when they appear in this syntactic position. Hard keywords may not be used as +builder names. + +The ``match`` soft keyword remains special only to the extent already required +by the grammar for ``match`` statements. A statement of the form:: + + match subject: + case pattern: + ... + +continues to be parsed as a ``match`` statement. A statement such as:: + + match C: + ... + +where the indented block is not a sequence of ``case`` clauses, is parsed as a +class builder definition using the builder named ``match``. + +Decorator syntax is supported in the same way as for ordinary class +definitions:: + + @decorator + builder C: + ... + +The builder itself must be a bare name. Attribute references and arbitrary +expressions are not part of this proposal; for example, +``dataclasses.dataclass C:`` is a syntax error. + + +Runtime semantics +----------------- + +A class builder definition evaluates the builder name in the surrounding +scope, retrieves its ``__build_class__`` attribute, and calls that attribute +using a calling convention modeled on :py:func:`builtins.__build_class__`. + +The statement:: + + builder C(Base, key=value): + body + +is approximately equivalent to:: + + _build = builder.__build_class__ + + def C(): + __module__ = __name__ + __qualname__ = "C" + body + + C = _build(C, "C", Base, key=value) + +This pseudocode is explanatory only. As with ordinary class definitions, the +body is compiled as a class body, not as an ordinary Python function body, and +the exact handling of ``__module__``, ``__qualname__``, ``__classcell__``, +annotations, static attributes, and related implementation details follows the +existing class-definition machinery. + +The builder's ``__build_class__`` attribute is called with these arguments: + +* the class body function; +* the name being bound, as a string; +* all positional arguments supplied in parentheses after the name; +* all keyword arguments supplied in parentheses after the name. + +The return value of this call is the value bound to the class name, after +applying any decorators. The returned object need not be a class. + +For example, this builder delegates directly to the normal class creation +machinery:: + + import builtins + + class Builder: + def __build_class__(self, func, name, *bases, **kwds): + return builtins.__build_class__(func, name, *bases, **kwds) + + builder = Builder() + + builder C: + x = 1 + +After executing this code, ``C`` is an ordinary class. + +If the builder object has no ``__build_class__`` attribute, the statement +raises :py:exc:`AttributeError` at runtime. Exceptions raised while evaluating +the builder name, retrieving ``__build_class__``, evaluating bases or keyword +arguments, executing the body, or applying decorators propagate normally. + + +Order of evaluation +------------------- + +Class builder definitions follow the same broad evaluation order as ordinary +class definitions, with the builder lookup replacing the lookup of +:py:func:`builtins.__build_class__`. + +For a non-generic builder definition, the order is: + +1. Evaluate decorators, if any, from top to bottom. +2. Evaluate the builder name and retrieve its ``__build_class__`` attribute. +3. Create the class body function. +4. Evaluate the bases and keyword arguments from left to right. +5. Call the builder's ``__build_class__`` attribute. +6. Apply decorators, from bottom to top. +7. Bind the resulting object to the class name in the current scope. + +As with ordinary class definitions, the exact interleaving of creating the +class body function and evaluating base expressions is an implementation +detail, except where it is observable through the ordering above. + + +Generic class builder definitions +--------------------------------- + +Class builder definitions may use the type parameter syntax introduced by +:pep:`695`:: + + builder C[T](Base[T]): + item: T + +The type parameters are created using the same runtime machinery as for +ordinary generic classes. The class body receives ``__type_params__`` in its +namespace, as it does for an ordinary generic class. + +The builder expression is evaluated in the surrounding scope, not in the +synthetic type-parameter scope. This matters when the builder is a local +variable:: + + def make_class(): + builder = get_builder() + + builder C[T]: + item: T + + return C + +In this example, ``builder`` is resolved in the local scope of +``make_class``. The type parameter ``T`` is visible to base expressions and +to the class body, but not to the builder name lookup itself. + +For explanatory purposes, the runtime behavior is similar to:: + + _build = builder.__build_class__ + + def _generic_parameters_of_C(_build): + T = TypeVar("T") + _type_params = (T,) + + def C(): + __type_params__ = _type_params + item: T + + return _build(C, "C", Generic[T]) + + C = _generic_parameters_of_C(_build) + +Again, this pseudocode is not an exact source transformation. In particular, +the actual implementation does not expose the temporary names shown here. + +If the builder returns a class-like object by delegating to +:py:func:`builtins.__build_class__`, ``__type_params__`` will normally become +an attribute on the resulting class because it was present in the class +namespace. If the builder returns some other object, Python does not add +``__type_params__`` to that object after the builder returns. A builder that +returns a non-class object is responsible for preserving any information from +the body that it wants to expose. + + +Interaction with metaclasses +---------------------------- + +Class builder definitions do not directly invoke the normal metaclass +selection algorithm. Instead, metaclass behavior is determined by the builder. +A builder that delegates to :py:func:`builtins.__build_class__` receives the +same metaclass behavior as an ordinary class definition with the same bases +and keywords. + +For example:: + + class Builder: + def __build_class__(self, func, name, *bases, **kwds): + return builtins.__build_class__(func, name, *bases, **kwds) + + builder C(Base, metaclass=Meta): + ... + +In this case ``Meta`` is handled by :py:func:`builtins.__build_class__` in the +usual way. A builder may instead interpret ``metaclass`` or other keywords +itself, pass them through, reject them, or ignore them. + + +Decorators +---------- + +Decorators on class builder definitions behave like decorators on ordinary +class definitions. They are evaluated before the builder call and applied to +the object returned by the builder. + +The statement:: + + @decorator1 + @decorator2 + builder C: + ... + +is approximately equivalent to:: + + C = decorator1(decorator2(builder.__build_class__(...))) + +The decorators operate on the builder's return value. If the builder returns +a non-class object, the decorators receive that non-class object. + + +AST +--- + +The :py:class:`ast.ClassDef` node gains a new field, ``builder``. The field +is either ``None`` for ordinary class definitions or an :py:class:`ast.Name` +node in load context for class builder definitions. + +For example, parsing:: + + dataclass C: + pass + +produces an ``ast.ClassDef`` node with ``name == "C"`` and +``builder == ast.Name(id="dataclass", ctx=ast.Load())``. + +The order of fields on :py:class:`ast.ClassDef` becomes:: + + name + bases + keywords + body + decorator_list + type_params + builder + +The :py:func:`compile` function rejects an AST whose ``builder`` field is +neither ``None`` nor an expression valid in load context. Standard library changes From bd5553ce1ecdc49f23e89ba23e09a13351d33545 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sun, 17 May 2026 07:21:28 -0700 Subject: [PATCH 03/12] additions --- peps/pep-0834.rst | 119 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) diff --git a/peps/pep-0834.rst b/peps/pep-0834.rst index 6fc2328285f..b41fd1680a2 100644 --- a/peps/pep-0834.rst +++ b/peps/pep-0834.rst @@ -361,6 +361,123 @@ The :py:func:`compile` function rejects an AST whose ``builder`` field is neither ``None`` nor an expression valid in load context. +Executing class bodies +---------------------- + +The :py:mod:`types` module gains a helper function +``exec_class_body(func, ns)``. The first argument is a class body function +such as the one passed to a builder's ``__build_class__`` method. The second +argument is the namespace mapping into which the body should be executed. + +The helper executes the body using the function's globals and closure, and +uses ``ns`` as the class namespace. It performs the part of +:py:func:`builtins.__build_class__` that runs the body, but does not select a +metaclass or create the final class object. + +For example:: + + def exec_body(ns): + types.exec_class_body(func, ns) + + cls = types.new_class(name, bases, kwds, exec_body) + +The helper is intended for builders that want to inspect or modify the class +namespace before creating an object. In particular, it avoids requiring +builder authors to reconstruct a function with custom globals or to duplicate +the details of how compiler-generated class body functions handle +``__classcell__``, ``__classdictcell__``, and annotation-related state. + +``exec_class_body`` returns ``None``. Information produced by executing the +body is communicated by mutating the namespace mapping. + + +Writing class builders +---------------------- + +A simple class builder that behaves exactly like an ordinary class statement +can delegate to :py:func:`builtins.__build_class__`:: + + import builtins + + class PlainBuilder: + def __build_class__(self, func, name, *bases, **kwds): + return builtins.__build_class__(func, name, *bases, **kwds) + + plain = PlainBuilder() + + plain C: + ... + +Builders that need access to the namespace should generally use +``types.exec_class_body`` together with :py:func:`types.new_class`:: + + import types + + class RecordingBuilder: + def __build_class__(self, func, name, *bases, **kwds): + captured = {} + + def exec_body(ns): + types.exec_class_body(func, ns) + captured.update(ns) + + cls = types.new_class(name, bases, kwds, exec_body) + cls.captured_namespace = captured + return cls + +This pattern preserves the normal metaclass protocol. ``types.new_class`` +resolves ``__mro_entries__``, calls the appropriate ``__prepare__`` method, +passes the prepared namespace to ``exec_body``, and then invokes the +metaclass. + +Builders that return non-class objects may execute the body into an ordinary +mapping and construct any object they choose from the resulting namespace:: + + class SchemaBuilder: + def __build_class__(self, func, name, *bases, **kwds): + ns = {} + types.exec_class_body(func, ns) + return Schema(name, ns.get("__annotate_func__"), ns, **kwds) + + schema = SchemaBuilder() + + schema Movie: + title: str + year: int + +Such a builder is responsible for deciding which entries in the namespace are +meaningful. Python does not add class-specific attributes such as +``__type_params__`` to the returned object after the builder returns. + +Some builders need to mutate the namespace after the body has executed but +before the class object is created. For example, a dataclass builder with +``slots=True`` can compute the field names from the executed namespace, insert +``__slots__``, and remove field defaults before calling the metaclass:: + + def dataclass_build_class(func, name, *bases, **kwds): + dataclass_kwds, class_kwds = split_dataclass_keywords(kwds) + resolved_bases = types.resolve_bases(bases) + + def exec_body(ns): + types.exec_class_body(func, ns) + if dataclass_kwds.get("slots"): + slots = compute_dataclass_slots(ns, resolved_bases) + save_field_defaults(ns, slots) + remove_field_defaults(ns, slots) + ns["__slots__"] = slots + if resolved_bases is not bases: + ns["__orig_bases__"] = bases + + cls = types.new_class(name, resolved_bases, class_kwds, exec_body) + return finish_dataclass(cls, dataclass_kwds) + +This avoids the current implementation strategy used by +``@dataclass(slots=True)``, which creates a class and then replaces it with a +new slotted class. With class builders, the slots can be present in the +namespace seen by the metaclass and by descriptors' ``__set_name__`` methods +from the start. + + Standard library changes ------------------------ @@ -372,6 +489,8 @@ The following class builders will be added to the standard library: * ``typing.namedtuple``: creates a typed named tuple. * ``typing.typed_dict``: creates a typed dictionary. +In addition, ``types.exec_class_body`` will be added as described above. + Type checker behavior --------------------- From 909c219210550723237461da82fa389e741258f3 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sun, 17 May 2026 08:21:39 -0700 Subject: [PATCH 04/12] add --- peps/pep-0834.rst | 53 +++++++++++++++++++++++++++++++---------------- 1 file changed, 35 insertions(+), 18 deletions(-) diff --git a/peps/pep-0834.rst b/peps/pep-0834.rst index b41fd1680a2..24bc235115f 100644 --- a/peps/pep-0834.rst +++ b/peps/pep-0834.rst @@ -215,20 +215,28 @@ Class builder definitions follow the same broad evaluation order as ordinary class definitions, with the builder lookup replacing the lookup of :py:func:`builtins.__build_class__`. -For a non-generic builder definition, the order is: +For a builder definition, the order is: 1. Evaluate decorators, if any, from top to bottom. 2. Evaluate the builder name and retrieve its ``__build_class__`` attribute. -3. Create the class body function. -4. Evaluate the bases and keyword arguments from left to right. -5. Call the builder's ``__build_class__`` attribute. -6. Apply decorators, from bottom to top. -7. Bind the resulting object to the class name in the current scope. +3. If the definition has type parameters, enter the synthetic type-parameter + scope and create the type parameter objects. +4. Create the class body function. +5. Evaluate the bases and keyword arguments from left to right. For a + generic builder definition, these expressions are evaluated in the + type-parameter scope, so they may refer to the type parameters. +6. Call the builder's ``__build_class__`` attribute. +7. Apply decorators, from bottom to top. +8. Bind the resulting object to the class name in the current scope. As with ordinary class definitions, the exact interleaving of creating the class body function and evaluating base expressions is an implementation detail, except where it is observable through the ordering above. +The builder lookup in step 2 is deliberately outside the type-parameter +scope. Type parameters are therefore not visible to the builder name lookup, +but they are visible to base expressions and to the class body. + Generic class builder definitions --------------------------------- @@ -243,21 +251,24 @@ The type parameters are created using the same runtime machinery as for ordinary generic classes. The class body receives ``__type_params__`` in its namespace, as it does for an ordinary generic class. -The builder expression is evaluated in the surrounding scope, not in the -synthetic type-parameter scope. This matters when the builder is a local -variable:: +The builder expression is evaluated before entering the synthetic +type-parameter scope. This means that type parameters are not visible to the +builder lookup, even though they are visible to base expressions and to the +class body. - def make_class(): - builder = get_builder() +This rule prevents a type parameter from shadowing the builder. For example:: - builder C[T]: - item: T + dataclass C[dataclass]: + value: dataclass - return C +In this example, the builder is the ``dataclass`` object from the surrounding +scope. The type parameter also named ``dataclass`` is visible in the class +body and may be used as an annotation, but it does not affect which builder is +called. -In this example, ``builder`` is resolved in the local scope of -``make_class``. The type parameter ``T`` is visible to base expressions and -to the class body, but not to the builder name lookup itself. +This is consistent with the role of the builder name: it selects the mechanism +used to construct the definition. Type parameters parameterize the definition +being constructed; they do not participate in selecting the builder. For explanatory purposes, the runtime behavior is similar to:: @@ -433,11 +444,14 @@ metaclass. Builders that return non-class objects may execute the body into an ordinary mapping and construct any object they choose from the resulting namespace:: + import annotationlib + class SchemaBuilder: def __build_class__(self, func, name, *bases, **kwds): ns = {} types.exec_class_body(func, ns) - return Schema(name, ns.get("__annotate_func__"), ns, **kwds) + annotate = annotationlib.get_annotate_from_class_namespace(ns) + return Schema(name, annotate, ns, **kwds) schema = SchemaBuilder() @@ -448,6 +462,9 @@ mapping and construct any object they choose from the resulting namespace:: Such a builder is responsible for deciding which entries in the namespace are meaningful. Python does not add class-specific attributes such as ``__type_params__`` to the returned object after the builder returns. +Builders that need to preserve annotations from a class namespace should use +``annotationlib.get_annotate_from_class_namespace`` rather than accessing +implementation-specific namespace entries directly. Some builders need to mutate the namespace after the body has executed but before the class object is created. For example, a dataclass builder with From 37703a951b52db27aa5b0312c007276bab2a264c Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sun, 17 May 2026 08:22:05 -0700 Subject: [PATCH 05/12] amend --- peps/pep-0834.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/peps/pep-0834.rst b/peps/pep-0834.rst index 24bc235115f..c2ecc4210fe 100644 --- a/peps/pep-0834.rst +++ b/peps/pep-0834.rst @@ -287,7 +287,8 @@ For explanatory purposes, the runtime behavior is similar to:: C = _generic_parameters_of_C(_build) Again, this pseudocode is not an exact source transformation. In particular, -the actual implementation does not expose the temporary names shown here. +the actual implementation does not expose the temporary names shown here, +and the scoping rules are slightly different, as specified in :pep:`695`. If the builder returns a class-like object by delegating to :py:func:`builtins.__build_class__`, ``__type_params__`` will normally become @@ -302,7 +303,8 @@ Interaction with metaclasses ---------------------------- Class builder definitions do not directly invoke the normal metaclass -selection algorithm. Instead, metaclass behavior is determined by the builder. +selection algorithm. The ``metaclass`` keyword argument in the definition +is not special; it is passed to the builder as an ordinary keyword argument. A builder that delegates to :py:func:`builtins.__build_class__` receives the same metaclass behavior as an ordinary class definition with the same bases and keywords. From 064ef4d3e968826d2dccdf0fca3d8cef54d2d33f Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sun, 17 May 2026 08:53:45 -0700 Subject: [PATCH 06/12] . --- peps/pep-0834.rst | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/peps/pep-0834.rst b/peps/pep-0834.rst index c2ecc4210fe..99f271e5159 100644 --- a/peps/pep-0834.rst +++ b/peps/pep-0834.rst @@ -407,7 +407,21 @@ body is communicated by mutating the namespace mapping. Writing class builders ---------------------- -A simple class builder that behaves exactly like an ordinary class statement +Builders can return arbitrary objects. A sample builder could simply return +its arguments:: + + class EchoBuilder: + def __build_class__(self, func, name, *bases, **kwds): + return (func, name, bases, kwds) + + echo = EchoBuilder() + + echo C("Base", key="value"): + ... + + print(C) # (, 'C', ('Base',), {'key': 'value'}) + +A class builder that behaves exactly like an ordinary class statement can delegate to :py:func:`builtins.__build_class__`:: import builtins @@ -421,8 +435,13 @@ can delegate to :py:func:`builtins.__build_class__`:: plain C: ... -Builders that need access to the namespace should generally use -``types.exec_class_body`` together with :py:func:`types.new_class`:: +Some builders need to work with the class namespace before the class object is created. +To make this easier, this PEP adds a new function, ``types.exec_class_body``, that executes +a class body function into a provided namespace mapping. This function takes two arguments: +the class body function passed to the builder's ``__build_class__`` method, and a mapping +to use as the class namespace. The mapping is mutated in place, and the function returns ``None``. + +The following builder captures the raw class namespace during execution:: import types @@ -448,6 +467,8 @@ mapping and construct any object they choose from the resulting namespace:: import annotationlib + class Schema: ... + class SchemaBuilder: def __build_class__(self, func, name, *bases, **kwds): ns = {} From f0838bb0093a55dd7ebebadae1d76c3c7872ae63 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sun, 17 May 2026 09:55:45 -0700 Subject: [PATCH 07/12] more --- peps/pep-0834.rst | 61 ++++++++++++++++++++++++++++------------------- 1 file changed, 36 insertions(+), 25 deletions(-) diff --git a/peps/pep-0834.rst b/peps/pep-0834.rst index 99f271e5159..448f8fe947e 100644 --- a/peps/pep-0834.rst +++ b/peps/pep-0834.rst @@ -490,32 +490,34 @@ Builders that need to preserve annotations from a class namespace should use implementation-specific namespace entries directly. Some builders need to mutate the namespace after the body has executed but -before the class object is created. For example, a dataclass builder with -``slots=True`` can compute the field names from the executed namespace, insert -``__slots__``, and remove field defaults before calling the metaclass:: +before the class object is created. The following builder creates slotted +classes by reading the annotations written by the class body and inserting a +``__slots__`` tuple before calling the metaclass:: - def dataclass_build_class(func, name, *bases, **kwds): - dataclass_kwds, class_kwds = split_dataclass_keywords(kwds) - resolved_bases = types.resolve_bases(bases) + import annotationlib + import types - def exec_body(ns): - types.exec_class_body(func, ns) - if dataclass_kwds.get("slots"): - slots = compute_dataclass_slots(ns, resolved_bases) - save_field_defaults(ns, slots) - remove_field_defaults(ns, slots) - ns["__slots__"] = slots - if resolved_bases is not bases: - ns["__orig_bases__"] = bases + class SlottedBuilder: + def __build_class__(self, func, name, *bases, **kwds): + def exec_body(ns): + types.exec_class_body(func, ns) + annotate = annotationlib.get_annotate_from_class_namespace(ns) + annotations = annotationlib.call_annotate_function( + annotate, annotationlib.Format.STRING) + ns["__slots__"] = tuple(annotations) + + return types.new_class(name, bases, kwds, exec_body) + + slotted = SlottedBuilder() - cls = types.new_class(name, resolved_bases, class_kwds, exec_body) - return finish_dataclass(cls, dataclass_kwds) + slotted C: + a: int -This avoids the current implementation strategy used by -``@dataclass(slots=True)``, which creates a class and then replaces it with a -new slotted class. With class builders, the slots can be present in the -namespace seen by the metaclass and by descriptors' ``__set_name__`` methods -from the start. + print(C.__slots__) # ("a",) + +This example deliberately omits details that a production-quality slotted +dataclass builder would need, such as inherited slots and class-level field +defaults. Standard library changes @@ -554,19 +556,28 @@ Rationale Backwards Compatibility ======================= -[Describe potential impact and severity on pre-existing code.] +This proposal adds new syntax that is currently invalid, so it does not break +existing code. Security Implications ===================== -[How could a malicious user take advantage of this new feature?] +This feature does not introduce a new attack surface. Class builders can execute +arbitrary code, but this is already true for pre-existing class definitions: an +attacker-controlled metaclass, base class, or decorator can also execute arbitrary code. How to Teach This ================= -[How to teach users, new and experienced, how to apply the PEP to their work.] +I recommend that teachers introduce concepts such as dataclasses, enums, +and protocols using the new class builder syntax. This allows students to +learn the new concepts without needing to understand the more complex machinery +of decorators and metaclasses. + +The general class builder concept is a more advanced topic that can be introduced +along with other metaprogramming techniques such as metaclasses. Reference Implementation From e95e9f47a17306302d69372083c9e556e9b9c458 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 18 May 2026 09:06:04 -0700 Subject: [PATCH 08/12] changes --- peps/pep-0834.rst | 186 +++++++++++++++++++++------------------------- 1 file changed, 84 insertions(+), 102 deletions(-) diff --git a/peps/pep-0834.rst b/peps/pep-0834.rst index 448f8fe947e..3d86b4079b1 100644 --- a/peps/pep-0834.rst +++ b/peps/pep-0834.rst @@ -17,8 +17,8 @@ This PEP proposes a new syntax for declaring class-like constructs:: builder C: ... -In this example, the name ``builder`` is looked up in the current scope and its ``__build_class__`` -attribute is used to build an arbitrary objects. +When this syntax is used, the name ``builder`` is looked up in the +current scope and its ``__build_class__`` attribute is used to build an arbitrary object. Intended use cases include enums, dataclasses, and several typing constructs:: @@ -26,7 +26,7 @@ Intended use cases include enums, dataclasses, and several typing constructs:: from enum import enum from typing import namedtuple, protocol, typed_dict - dataclass InventoryItem(frozen=True): + dataclass InventoryItem(slots=True): name: str amount: int @@ -80,9 +80,10 @@ The standard library will be changed to provide builders for dataclasses, enums, :py:class:`typing.NamedTuple`, and :py:class:`typing.TypedDict`. In addition to improved concision, the new syntax allows more flexibility. The ``slots=True`` version -of dataclasses is currently implemented by creating a new wrapper class replacing the original class, -which causes various problems [TODO: link to issues]. The dataclass class builder can bypass this problem -by injecting the slots definition directly into the class body. +of dataclasses is currently implemented by creating a new wrapper class replacing the original class. +This can cause subtle differences between the class initially created by the class statement and the +class eventually bound to the class name. The dataclass class builder can bypass this problem by +injecting the slots definition directly into the class body. Specification @@ -374,8 +375,16 @@ The :py:func:`compile` function rejects an AST whose ``builder`` field is neither ``None`` nor an expression valid in load context. -Executing class bodies ----------------------- +Standard library changes +------------------------ + +The following class builders will be added to the standard library: + +* ``enum.enum``: creates an enum class. +* ``dataclasses.dataclass``: creates a dataclass. +* ``typing.protocol``: creates a protocol. +* ``typing.namedtuple``: creates a typed named tuple. +* ``typing.typed_dict``: creates a typed dictionary. The :py:mod:`types` module gains a helper function ``exec_class_body(func, ns)``. The first argument is a class body function @@ -385,63 +394,43 @@ argument is the namespace mapping into which the body should be executed. The helper executes the body using the function's globals and closure, and uses ``ns`` as the class namespace. It performs the part of :py:func:`builtins.__build_class__` that runs the body, but does not select a -metaclass or create the final class object. +metaclass or create the final class object. It returns ``None``; information +produced by executing the body is communicated by mutating ``ns``. -For example:: - - def exec_body(ns): - types.exec_class_body(func, ns) - - cls = types.new_class(name, bases, kwds, exec_body) - -The helper is intended for builders that want to inspect or modify the class -namespace before creating an object. In particular, it avoids requiring -builder authors to reconstruct a function with custom globals or to duplicate -the details of how compiler-generated class body functions handle -``__classcell__``, ``__classdictcell__``, and annotation-related state. - -``exec_class_body`` returns ``None``. Information produced by executing the -body is communicated by mutating the namespace mapping. +Type checker behavior +--------------------- -Writing class builders ----------------------- - -Builders can return arbitrary objects. A sample builder could simply return -its arguments:: +The flexibility of class builders means that it is difficult to check them in full generality. - class EchoBuilder: - def __build_class__(self, func, name, *bases, **kwds): - return (func, name, bases, kwds) +Python type checkers should recognize the standard library builders and treat them similarly to the +existing syntax. In other cases, type checkers should look up the ``__build_class__`` attribute on +the builder and type check the call. - echo = EchoBuilder() +A ``__build_class__`` callable may be decorated with the :py:func:`typing.dataclass_transform` decorator, +indicating that the builder behaves similarly to a dataclass. - echo C("Base", key="value"): - ... - print(C) # (, 'C', ('Base',), {'key': 'value'}) +Examples +======== -A class builder that behaves exactly like an ordinary class statement -can delegate to :py:func:`builtins.__build_class__`:: +A class builder that behaves exactly like an ordinary class statement can +delegate to :py:func:`builtins.__build_class__`:: import builtins class PlainBuilder: def __build_class__(self, func, name, *bases, **kwds): + print("Creating class", name) return builtins.__build_class__(func, name, *bases, **kwds) plain = PlainBuilder() - plain C: - ... - -Some builders need to work with the class namespace before the class object is created. -To make this easier, this PEP adds a new function, ``types.exec_class_body``, that executes -a class body function into a provided namespace mapping. This function takes two arguments: -the class body function passed to the builder's ``__build_class__`` method, and a mapping -to use as the class namespace. The mapping is mutated in place, and the function returns ``None``. + plain C: # prints "Creating class C" + x = 1 -The following builder captures the raw class namespace during execution:: +Builders that need to inspect the namespace should use +``types.exec_class_body`` together with :py:func:`types.new_class`:: import types @@ -457,15 +446,18 @@ The following builder captures the raw class namespace during execution:: cls.captured_namespace = captured return cls -This pattern preserves the normal metaclass protocol. ``types.new_class`` -resolves ``__mro_entries__``, calls the appropriate ``__prepare__`` method, -passes the prepared namespace to ``exec_body``, and then invokes the -metaclass. + recording = RecordingBuilder() + + recording C: + x = 1 + + print(C.captured_namespace) # {"__module__": "__main__", "__qualname__": "C", ...} Builders that return non-class objects may execute the body into an ordinary mapping and construct any object they choose from the resulting namespace:: import annotationlib + import types class Schema: ... @@ -482,9 +474,6 @@ mapping and construct any object they choose from the resulting namespace:: title: str year: int -Such a builder is responsible for deciding which entries in the namespace are -meaningful. Python does not add class-specific attributes such as -``__type_params__`` to the returned object after the builder returns. Builders that need to preserve annotations from a class namespace should use ``annotationlib.get_annotate_from_class_namespace`` rather than accessing implementation-specific namespace entries directly. @@ -520,37 +509,22 @@ dataclass builder would need, such as inherited slots and class-level field defaults. -Standard library changes ------------------------- - -The following class builders will be added to the standard library: - -* ``enum.enum``: creates an enum class. -* ``dataclasses.dataclass``: creates a dataclass. -* ``typing.protocol``: creates a protocol. -* ``typing.namedtuple``: creates a typed named tuple. -* ``typing.typed_dict``: creates a typed dictionary. - -In addition, ``types.exec_class_body`` will be added as described above. - - -Type checker behavior ---------------------- - -The flexibility of class builders means that it is difficult to check them in full generality. - -Python type checkers should recognize the standard library builders and treat them similarly to the -existing syntax. In other cases, type checkers should look up the ``__build_class__`` attribute on -the builder and type check the call. - -A ``__build_class__`` callable may be decorated with the :py:func:`typing.dataclass_transform` decorator, -indicating that the builder behaves similarly to a dataclass. - - Rationale ========= -[Describe why particular design decisions were made.] +Class builders make the class-transforming nature of dataclasses, enums, and +typing constructs visible at the start of the statement. This is easier to +read and teach than a decorator or base class that changes class construction +after the reader has already parsed the statement as an ordinary class. + +The builder protocol deliberately reuses class-body machinery instead of +introducing a general macro or block system. This keeps the feature close to +existing class semantics while still allowing builders to return non-class +objects when appropriate. + +Only bare names are accepted as builders. This keeps the syntax compact and +avoids ambiguous statement prefixes such as ``pkg.builder C:``. Users can +bind a local name when they want to use a builder imported from another module. Backwards Compatibility @@ -563,7 +537,7 @@ existing code. Security Implications ===================== -This feature does not introduce a new attack surface. Class builders can execute +This feature does not introduce any new attack surface. Class builders can execute arbitrary code, but this is already true for pre-existing class definitions: an attacker-controlled metaclass, base class, or decorator can also execute arbitrary code. @@ -583,13 +557,35 @@ along with other metaprogramming techniques such as metaclasses. Reference Implementation ======================== -[Link to any existing implementation and details about its state, e.g. proof-of-concept.] +A prototype implementation exists in a `draft PR `__. Rejected Ideas ============== -[Why certain ideas that were brought while discussing this PEP were not ultimately pursued.] +Allowing arbitrary builder expressions +-------------------------------------- + +The PEP does not allow syntax such as ``dataclasses.dataclass C:`` or +``factory() C:``. Such forms are more flexible, but they make the grammar and +visual shape of the feature less clear. A module attribute or computed +builder can be assigned to a local name before use. + + +Adding a separate AST node +-------------------------- + +The PEP uses a new ``builder`` field on :py:class:`ast.ClassDef` rather than a +new AST node. Builder definitions share the same name, bases, keywords, body, +decorators, and type-parameter structure as ordinary class definitions. + + +Adding a ``__prepare__`` hook to builders +----------------------------------------- + +The PEP does not add a separate builder-level ``__prepare__`` hook. Builders +that need to control the namespace can use :py:func:`types.new_class` and +``types.exec_class_body``. Open Issues @@ -600,28 +596,14 @@ Open Issues * Should there be a ``__prepare__`` hook? * Should the ``dataclass_transform`` behavior have any enhancements? Some way to inject a base class or metaclass? * For enums, do we need more variants for flags and intenums etc.? +* Could we usefully make protocol and typeddict definitions using builders lazy, where it does not evaluate the + class body until we need it at runtime? Acknowledgements ================ -[Thank anyone who has helped with the PEP.] - - -Footnotes -========= - -[A collection of footnotes cited in the PEP, and a place to list non-inline hyperlink targets.] - - -Change History -============== - -[A summary of major changes the PEP has undergone. Whenever you update the -``Post-History``, add a new bullet item in newest-first (i.e. reverse -chronological) order, using the same ``DD-MMM-YYYY`` format, with sub-bullets -summarizing the changes. You can use the same link for the date bullet as you -do in the ``Post-History`` addition.] +I thank all the people at PyCon US who humored me when I started talking about this idea. Copyright From b6dd2d38892bf0d34e667058e3d526d7af65bc4d Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 18 May 2026 11:23:10 -0700 Subject: [PATCH 09/12] rewrite rationale --- peps/pep-0834.rst | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/peps/pep-0834.rst b/peps/pep-0834.rst index 3d86b4079b1..cbb6ad97376 100644 --- a/peps/pep-0834.rst +++ b/peps/pep-0834.rst @@ -512,19 +512,18 @@ defaults. Rationale ========= -Class builders make the class-transforming nature of dataclasses, enums, and -typing constructs visible at the start of the statement. This is easier to -read and teach than a decorator or base class that changes class construction -after the reader has already parsed the statement as an ordinary class. - -The builder protocol deliberately reuses class-body machinery instead of -introducing a general macro or block system. This keeps the feature close to -existing class semantics while still allowing builders to return non-class -objects when appropriate. - -Only bare names are accepted as builders. This keeps the syntax compact and -avoids ambiguous statement prefixes such as ``pkg.builder C:``. Users can -bind a local name when they want to use a builder imported from another module. +Python has various constructs that are somewhat like classes, but behave +subtly (or not so subtly!) differently. This PEP proposes a generic, flexible +mechanism for defining such constructs. + +An alternative could be to add specific syntax for some or all of the constructs +for which this PEP proposes to use class builders. For example, ``protocol`` could +be made a soft keyword, allowing protocols to be written as proposed in this PEP, +but without an import and without a more powerful new language feature. + +However, this would unduly privilege the standard library. There are use +cases in third-party frameworks that could be helped by builder syntax, including +alternative dataclass-like frameworks, ORMs, or DSLs. Backwards Compatibility From e36a310352cf99dafc9594e4209701994cc2e1d8 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 18 May 2026 11:27:31 -0700 Subject: [PATCH 10/12] . --- peps/pep-0834.rst | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/peps/pep-0834.rst b/peps/pep-0834.rst index cbb6ad97376..0533a9357e1 100644 --- a/peps/pep-0834.rst +++ b/peps/pep-0834.rst @@ -153,7 +153,7 @@ Runtime semantics A class builder definition evaluates the builder name in the surrounding scope, retrieves its ``__build_class__`` attribute, and calls that attribute -using a calling convention modeled on :py:func:`builtins.__build_class__`. +using a calling convention modeled on :py:func:`!builtins.__build_class__`. The statement:: @@ -214,7 +214,7 @@ Order of evaluation Class builder definitions follow the same broad evaluation order as ordinary class definitions, with the builder lookup replacing the lookup of -:py:func:`builtins.__build_class__`. +:py:func:`!builtins.__build_class__`. For a builder definition, the order is: @@ -292,7 +292,7 @@ the actual implementation does not expose the temporary names shown here, and the scoping rules are slightly different, as specified in :pep:`695`. If the builder returns a class-like object by delegating to -:py:func:`builtins.__build_class__`, ``__type_params__`` will normally become +:py:func:`!builtins.__build_class__`, ``__type_params__`` will normally become an attribute on the resulting class because it was present in the class namespace. If the builder returns some other object, Python does not add ``__type_params__`` to that object after the builder returns. A builder that @@ -306,7 +306,7 @@ Interaction with metaclasses Class builder definitions do not directly invoke the normal metaclass selection algorithm. The ``metaclass`` keyword argument in the definition is not special; it is passed to the builder as an ordinary keyword argument. -A builder that delegates to :py:func:`builtins.__build_class__` receives the +A builder that delegates to :py:func:`!builtins.__build_class__` receives the same metaclass behavior as an ordinary class definition with the same bases and keywords. @@ -319,7 +319,7 @@ For example:: builder C(Base, metaclass=Meta): ... -In this case ``Meta`` is handled by :py:func:`builtins.__build_class__` in the +In this case ``Meta`` is handled by :py:func:`!builtins.__build_class__` in the usual way. A builder may instead interpret ``metaclass`` or other keywords itself, pass them through, reject them, or ignore them. @@ -393,7 +393,7 @@ argument is the namespace mapping into which the body should be executed. The helper executes the body using the function's globals and closure, and uses ``ns`` as the class namespace. It performs the part of -:py:func:`builtins.__build_class__` that runs the body, but does not select a +:py:func:`!builtins.__build_class__` that runs the body, but does not select a metaclass or create the final class object. It returns ``None``; information produced by executing the body is communicated by mutating ``ns``. @@ -415,7 +415,7 @@ Examples ======== A class builder that behaves exactly like an ordinary class statement can -delegate to :py:func:`builtins.__build_class__`:: +delegate to :py:func:`!builtins.__build_class__`:: import builtins From ca9d990d4fa85edfccd3b9061cc5f75655bf6042 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 19 May 2026 05:30:40 -0700 Subject: [PATCH 11/12] Apply suggestions from code review Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- peps/pep-0834.rst | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/peps/pep-0834.rst b/peps/pep-0834.rst index 0533a9357e1..e7a48db4925 100644 --- a/peps/pep-0834.rst +++ b/peps/pep-0834.rst @@ -65,7 +65,7 @@ understand that certain base classes or decorators radically change what a class does, instead of putting the special behavior of these constructs front and center in the syntax. -This PEP proposes a flexible new syntax to declare these constructs using *class builders*: +This PEP proposes a flexible new syntax to declare these constructs using *class builders*:: builder Simple: ... @@ -93,7 +93,10 @@ Syntax ------ The grammar for class definitions is extended to allow a name before the -class name:: +class name: + +.. code-block:: peg + class_def_raw: | 'class' NAME type_params? ['(' arguments? ')'] ':' block From 02904d753fe4e75d6e3b77805bbd16dcc3c49023 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 19 May 2026 14:09:52 -0700 Subject: [PATCH 12/12] makers --- peps/pep-0834.rst | 266 ++++++++++++++++++++++------------------------ 1 file changed, 128 insertions(+), 138 deletions(-) diff --git a/peps/pep-0834.rst b/peps/pep-0834.rst index e7a48db4925..1f150b47a9c 100644 --- a/peps/pep-0834.rst +++ b/peps/pep-0834.rst @@ -1,5 +1,5 @@ PEP: 834 -Title: Class Builders +Title: Maker syntax Author: Jelle Zijlstra Discussions-To: Pending Status: Draft @@ -14,35 +14,35 @@ Abstract This PEP proposes a new syntax for declaring class-like constructs:: - builder C: + make maker C: ... -When this syntax is used, the name ``builder`` is looked up in the +When this syntax is used, the name ``maker`` is looked up in the current scope and its ``__build_class__`` attribute is used to build an arbitrary object. Intended use cases include enums, dataclasses, and several typing constructs:: from dataclasses import dataclass from enum import enum - from typing import namedtuple, protocol, typed_dict + from typing import namedtuple, protocol, typeddict - dataclass InventoryItem(slots=True): + make dataclass InventoryItem(slots=True): name: str amount: int - enum Color: + make enum Color: red = 1 green = 2 blue = 3 - typed_dict Movie(closed=True): + make typeddict Movie(closed=True): name: str year: int - protocol HasClose: + make protocol HasClose: def supports_close(self) -> None: ... - namedtuple Employee: + make namedtuple Employee: name: str title: str @@ -65,24 +65,24 @@ understand that certain base classes or decorators radically change what a class does, instead of putting the special behavior of these constructs front and center in the syntax. -This PEP proposes a flexible new syntax to declare these constructs using *class builders*:: +This PEP proposes a flexible new syntax to declare these constructs using *makers*:: - builder Simple: + make maker Simple: ... - builder Complex[T](Base, key="value"): + make maker Complex[T](Base, key="value"): ... -In this syntax, the name ``builder`` is looked up in the current scope and its ``__build_class__`` +In this syntax, the name ``maker`` is looked up in the current scope and its ``__build_class__`` attribute is called with the name, bases, body, and constructor keyword arguments used in the definition. This function may return an arbitrary object, which is then stored at the name provided in the definition. -The standard library will be changed to provide builders for dataclasses, enums, :py:class:`typing.Protocol`, +The standard library will be changed to provide makers for dataclasses, enums, :py:class:`typing.Protocol`, :py:class:`typing.NamedTuple`, and :py:class:`typing.TypedDict`. In addition to improved concision, the new syntax allows more flexibility. The ``slots=True`` version of dataclasses is currently implemented by creating a new wrapper class replacing the original class. This can cause subtle differences between the class initially created by the class statement and the -class eventually bound to the class name. The dataclass class builder can bypass this problem by +class eventually bound to the class name. The dataclass maker can bypass this problem by injecting the slots definition directly into the class body. @@ -100,72 +100,61 @@ class name: class_def_raw: | 'class' NAME type_params? ['(' arguments? ')'] ':' block - | NAME NAME type_params? ['(' arguments? ')'] ':' block + | 'make' NAME NAME type_params? ['(' arguments? ')'] ':' block -The first ``NAME`` in the second alternative is the *class builder*. The +The first ``NAME`` in the second alternative is the *maker*. The second ``NAME`` is the name bound by the statement. The type parameter list, parenthesized arguments, and block have the same syntax as in an ordinary class definition. -For example, all of the following are syntactically valid class builder +For example, all of the following are syntactically valid maker definitions:: - builder C: + make maker C: ... - builder C[T](Base[T], key=value): + make maker C[T](Base[T], key=value): ... - type C: + make type C: ... -The builder name is parsed as a normal name token, not as a new keyword. In -particular, existing and future soft keywords may be used as builder names -when they appear in this syntactic position. Hard keywords may not be used as -builder names. +The initial ``make`` token is a soft keyword in this position. The maker +name itself is parsed as a normal name token after ``make``. Existing and +future soft keywords may therefore be used as maker names in this position; +hard keywords may not be used as maker names. -The ``match`` soft keyword remains special only to the extent already required -by the grammar for ``match`` statements. A statement of the form:: - - match subject: - case pattern: - ... - -continues to be parsed as a ``match`` statement. A statement such as:: - - match C: - ... - -where the indented block is not a sequence of ``case`` clauses, is parsed as a -class builder definition using the builder named ``match``. +Requiring the leading ``make`` token keeps maker definitions visually distinct +from existing compound statements such as ``match`` and preserves room for +future dedicated soft-keyword syntax such as ``dataclass C:``. Decorator syntax is supported in the same way as for ordinary class definitions:: @decorator - builder C: + make maker C: ... -The builder itself must be a bare name. Attribute references and arbitrary +The maker itself must be a bare name. Attribute references and arbitrary expressions are not part of this proposal; for example, -``dataclasses.dataclass C:`` is a syntax error. +``make dataclasses.dataclass C:`` is a syntax error. Runtime semantics ----------------- -A class builder definition evaluates the builder name in the surrounding +A maker definition evaluates the maker name in the surrounding scope, retrieves its ``__build_class__`` attribute, and calls that attribute using a calling convention modeled on :py:func:`!builtins.__build_class__`. The statement:: - builder C(Base, key=value): + make maker C(Base, key=value): body is approximately equivalent to:: - _build = builder.__build_class__ + _build = maker.__build_class__ def C(): __module__ = __name__ @@ -180,7 +169,7 @@ the exact handling of ``__module__``, ``__qualname__``, ``__classcell__``, annotations, static attributes, and related implementation details follows the existing class-definition machinery. -The builder's ``__build_class__`` attribute is called with these arguments: +The maker's ``__build_class__`` attribute is called with these arguments: * the class body function; * the name being bound, as a string; @@ -190,46 +179,46 @@ The builder's ``__build_class__`` attribute is called with these arguments: The return value of this call is the value bound to the class name, after applying any decorators. The returned object need not be a class. -For example, this builder delegates directly to the normal class creation +For example, this maker delegates directly to the normal class creation machinery:: import builtins - class Builder: + class Maker: def __build_class__(self, func, name, *bases, **kwds): return builtins.__build_class__(func, name, *bases, **kwds) - builder = Builder() + maker = Maker() - builder C: + make maker C: x = 1 After executing this code, ``C`` is an ordinary class. -If the builder object has no ``__build_class__`` attribute, the statement +If the maker object has no ``__build_class__`` attribute, the statement raises :py:exc:`AttributeError` at runtime. Exceptions raised while evaluating -the builder name, retrieving ``__build_class__``, evaluating bases or keyword +the maker name, retrieving ``__build_class__``, evaluating bases or keyword arguments, executing the body, or applying decorators propagate normally. Order of evaluation ------------------- -Class builder definitions follow the same broad evaluation order as ordinary -class definitions, with the builder lookup replacing the lookup of +Maker definitions follow the same broad evaluation order as ordinary +class definitions, with the maker lookup replacing the lookup of :py:func:`!builtins.__build_class__`. -For a builder definition, the order is: +For a maker definition, the order is: 1. Evaluate decorators, if any, from top to bottom. -2. Evaluate the builder name and retrieve its ``__build_class__`` attribute. +2. Evaluate the maker name and retrieve its ``__build_class__`` attribute. 3. If the definition has type parameters, enter the synthetic type-parameter scope and create the type parameter objects. 4. Create the class body function. 5. Evaluate the bases and keyword arguments from left to right. For a - generic builder definition, these expressions are evaluated in the + generic maker definition, these expressions are evaluated in the type-parameter scope, so they may refer to the type parameters. -6. Call the builder's ``__build_class__`` attribute. +6. Call the maker's ``__build_class__`` attribute. 7. Apply decorators, from bottom to top. 8. Bind the resulting object to the class name in the current scope. @@ -237,46 +226,46 @@ As with ordinary class definitions, the exact interleaving of creating the class body function and evaluating base expressions is an implementation detail, except where it is observable through the ordering above. -The builder lookup in step 2 is deliberately outside the type-parameter -scope. Type parameters are therefore not visible to the builder name lookup, +The maker lookup in step 2 is deliberately outside the type-parameter +scope. Type parameters are therefore not visible to the maker name lookup, but they are visible to base expressions and to the class body. -Generic class builder definitions +Generic maker definitions --------------------------------- -Class builder definitions may use the type parameter syntax introduced by +Maker definitions may use the type parameter syntax introduced by :pep:`695`:: - builder C[T](Base[T]): + make maker C[T](Base[T]): item: T The type parameters are created using the same runtime machinery as for ordinary generic classes. The class body receives ``__type_params__`` in its namespace, as it does for an ordinary generic class. -The builder expression is evaluated before entering the synthetic +The maker expression is evaluated before entering the synthetic type-parameter scope. This means that type parameters are not visible to the -builder lookup, even though they are visible to base expressions and to the +maker lookup, even though they are visible to base expressions and to the class body. -This rule prevents a type parameter from shadowing the builder. For example:: +This rule prevents a type parameter from shadowing the maker. For example:: - dataclass C[dataclass]: + make dataclass C[dataclass]: value: dataclass -In this example, the builder is the ``dataclass`` object from the surrounding +In this example, the maker is the ``dataclass`` object from the surrounding scope. The type parameter also named ``dataclass`` is visible in the class -body and may be used as an annotation, but it does not affect which builder is +body and may be used as an annotation, but it does not affect which maker is called. -This is consistent with the role of the builder name: it selects the mechanism +This is consistent with the role of the maker name: it selects the mechanism used to construct the definition. Type parameters parameterize the definition -being constructed; they do not participate in selecting the builder. +being constructed; they do not participate in selecting the maker. For explanatory purposes, the runtime behavior is similar to:: - _build = builder.__build_class__ + _build = maker.__build_class__ def _generic_parameters_of_C(_build): T = TypeVar("T") @@ -294,11 +283,11 @@ Again, this pseudocode is not an exact source transformation. In particular, the actual implementation does not expose the temporary names shown here, and the scoping rules are slightly different, as specified in :pep:`695`. -If the builder returns a class-like object by delegating to +If the maker returns a class-like object by delegating to :py:func:`!builtins.__build_class__`, ``__type_params__`` will normally become an attribute on the resulting class because it was present in the class -namespace. If the builder returns some other object, Python does not add -``__type_params__`` to that object after the builder returns. A builder that +namespace. If the maker returns some other object, Python does not add +``__type_params__`` to that object after the maker returns. A maker that returns a non-class object is responsible for preserving any information from the body that it wants to expose. @@ -306,63 +295,63 @@ the body that it wants to expose. Interaction with metaclasses ---------------------------- -Class builder definitions do not directly invoke the normal metaclass +Maker definitions do not directly invoke the normal metaclass selection algorithm. The ``metaclass`` keyword argument in the definition -is not special; it is passed to the builder as an ordinary keyword argument. -A builder that delegates to :py:func:`!builtins.__build_class__` receives the +is not special; it is passed to the maker as an ordinary keyword argument. +A maker that delegates to :py:func:`!builtins.__build_class__` receives the same metaclass behavior as an ordinary class definition with the same bases and keywords. For example:: - class Builder: + class Maker: def __build_class__(self, func, name, *bases, **kwds): return builtins.__build_class__(func, name, *bases, **kwds) - builder C(Base, metaclass=Meta): + make maker C(Base, metaclass=Meta): ... In this case ``Meta`` is handled by :py:func:`!builtins.__build_class__` in the -usual way. A builder may instead interpret ``metaclass`` or other keywords +usual way. A maker may instead interpret ``metaclass`` or other keywords itself, pass them through, reject them, or ignore them. Decorators ---------- -Decorators on class builder definitions behave like decorators on ordinary -class definitions. They are evaluated before the builder call and applied to -the object returned by the builder. +Decorators on maker definitions behave like decorators on ordinary +class definitions. They are evaluated before the maker call and applied to +the object returned by the maker. The statement:: @decorator1 @decorator2 - builder C: + make maker C: ... is approximately equivalent to:: - C = decorator1(decorator2(builder.__build_class__(...))) + C = decorator1(decorator2(maker.__build_class__(...))) -The decorators operate on the builder's return value. If the builder returns +The decorators operate on the maker's return value. If the maker returns a non-class object, the decorators receive that non-class object. AST --- -The :py:class:`ast.ClassDef` node gains a new field, ``builder``. The field +The :py:class:`ast.ClassDef` node gains a new field, ``maker``. The field is either ``None`` for ordinary class definitions or an :py:class:`ast.Name` -node in load context for class builder definitions. +node in load context for maker definitions. For example, parsing:: - dataclass C: + make dataclass C: pass produces an ``ast.ClassDef`` node with ``name == "C"`` and -``builder == ast.Name(id="dataclass", ctx=ast.Load())``. +``maker == ast.Name(id="dataclass", ctx=ast.Load())``. The order of fields on :py:class:`ast.ClassDef` becomes:: @@ -372,26 +361,26 @@ The order of fields on :py:class:`ast.ClassDef` becomes:: body decorator_list type_params - builder + maker -The :py:func:`compile` function rejects an AST whose ``builder`` field is +The :py:func:`compile` function rejects an AST whose ``maker`` field is neither ``None`` nor an expression valid in load context. Standard library changes ------------------------ -The following class builders will be added to the standard library: +The following makers will be added to the standard library: * ``enum.enum``: creates an enum class. * ``dataclasses.dataclass``: creates a dataclass. * ``typing.protocol``: creates a protocol. * ``typing.namedtuple``: creates a typed named tuple. -* ``typing.typed_dict``: creates a typed dictionary. +* ``typing.typeddict``: creates a typed dictionary. The :py:mod:`types` module gains a helper function ``exec_class_body(func, ns)``. The first argument is a class body function -such as the one passed to a builder's ``__build_class__`` method. The second +such as the one passed to a maker's ``__build_class__`` method. The second argument is the namespace mapping into which the body should be executed. The helper executes the body using the function's globals and closure, and @@ -404,40 +393,40 @@ produced by executing the body is communicated by mutating ``ns``. Type checker behavior --------------------- -The flexibility of class builders means that it is difficult to check them in full generality. +The flexibility of makers means that it is difficult to check them in full generality. -Python type checkers should recognize the standard library builders and treat them similarly to the +Python type checkers should recognize the standard library makers and treat them similarly to the existing syntax. In other cases, type checkers should look up the ``__build_class__`` attribute on -the builder and type check the call. +the maker and type check the call. A ``__build_class__`` callable may be decorated with the :py:func:`typing.dataclass_transform` decorator, -indicating that the builder behaves similarly to a dataclass. +indicating that the maker behaves similarly to a dataclass. Examples ======== -A class builder that behaves exactly like an ordinary class statement can +A maker that behaves exactly like an ordinary class statement can delegate to :py:func:`!builtins.__build_class__`:: import builtins - class PlainBuilder: + class PlainMaker: def __build_class__(self, func, name, *bases, **kwds): print("Creating class", name) return builtins.__build_class__(func, name, *bases, **kwds) - plain = PlainBuilder() + plain = PlainMaker() - plain C: # prints "Creating class C" + make plain C: # prints "Creating class C" x = 1 -Builders that need to inspect the namespace should use +Makers that need to inspect the namespace should use ``types.exec_class_body`` together with :py:func:`types.new_class`:: import types - class RecordingBuilder: + class RecordingMaker: def __build_class__(self, func, name, *bases, **kwds): captured = {} @@ -449,14 +438,14 @@ Builders that need to inspect the namespace should use cls.captured_namespace = captured return cls - recording = RecordingBuilder() + recording = RecordingMaker() - recording C: + make recording C: x = 1 print(C.captured_namespace) # {"__module__": "__main__", "__qualname__": "C", ...} -Builders that return non-class objects may execute the body into an ordinary +Makers that return non-class objects may execute the body into an ordinary mapping and construct any object they choose from the resulting namespace:: import annotationlib @@ -464,32 +453,32 @@ mapping and construct any object they choose from the resulting namespace:: class Schema: ... - class SchemaBuilder: + class SchemaMaker: def __build_class__(self, func, name, *bases, **kwds): ns = {} types.exec_class_body(func, ns) annotate = annotationlib.get_annotate_from_class_namespace(ns) return Schema(name, annotate, ns, **kwds) - schema = SchemaBuilder() + schema = SchemaMaker() - schema Movie: + make schema Movie: title: str year: int -Builders that need to preserve annotations from a class namespace should use +Makers that need to preserve annotations from a class namespace should use ``annotationlib.get_annotate_from_class_namespace`` rather than accessing implementation-specific namespace entries directly. -Some builders need to mutate the namespace after the body has executed but -before the class object is created. The following builder creates slotted +Some makers need to mutate the namespace after the body has executed but +before the class object is created. The following maker creates slotted classes by reading the annotations written by the class body and inserting a ``__slots__`` tuple before calling the metaclass:: import annotationlib import types - class SlottedBuilder: + class SlottedMaker: def __build_class__(self, func, name, *bases, **kwds): def exec_body(ns): types.exec_class_body(func, ns) @@ -500,15 +489,15 @@ classes by reading the annotations written by the class body and inserting a return types.new_class(name, bases, kwds, exec_body) - slotted = SlottedBuilder() + slotted = SlottedMaker() - slotted C: + make slotted C: a: int print(C.__slots__) # ("a",) This example deliberately omits details that a production-quality slotted -dataclass builder would need, such as inherited slots and class-level field +dataclass maker would need, such as inherited slots and class-level field defaults. @@ -520,26 +509,27 @@ subtly (or not so subtly!) differently. This PEP proposes a generic, flexible mechanism for defining such constructs. An alternative could be to add specific syntax for some or all of the constructs -for which this PEP proposes to use class builders. For example, ``protocol`` could +for which this PEP proposes to use makers. For example, ``protocol`` could be made a soft keyword, allowing protocols to be written as proposed in this PEP, but without an import and without a more powerful new language feature. However, this would unduly privilege the standard library. There are use -cases in third-party frameworks that could be helped by builder syntax, including +cases in third-party frameworks that could be helped by maker syntax, including alternative dataclass-like frameworks, ORMs, or DSLs. Backwards Compatibility ======================= -This proposal adds new syntax that is currently invalid, so it does not break -existing code. +This proposal adds a new soft keyword use of ``make`` before a pair of names. +Code using ``make`` as an ordinary name remains valid outside this syntactic +position. Security Implications ===================== -This feature does not introduce any new attack surface. Class builders can execute +This feature does not introduce any new attack surface. Makers can execute arbitrary code, but this is already true for pre-existing class definitions: an attacker-controlled metaclass, base class, or decorator can also execute arbitrary code. @@ -548,11 +538,11 @@ How to Teach This ================= I recommend that teachers introduce concepts such as dataclasses, enums, -and protocols using the new class builder syntax. This allows students to +and protocols using the new maker syntax. This allows students to learn the new concepts without needing to understand the more complex machinery of decorators and metaclasses. -The general class builder concept is a more advanced topic that can be introduced +The general maker concept is a more advanced topic that can be introduced along with other metaprogramming techniques such as metaclasses. @@ -565,27 +555,27 @@ A prototype implementation exists in a `draft PR