Repository: Quansight-Labs/pyyaml
Pull request #2: Mark extension as free-threading compatible & resolve Python-level issues
Author: login: lysnikolaou
Created: 2025-04-03T19:12:15Z | Merged: 2025-04-18T21:08:26Z

- After a few Cython and CPython fixes, and since the API does not allow for sharing `Dumper` / `Loader` classes, just marking the Cython extension as free-threading compatible is enough.
- For the Python-level issues the solution is the following:
	- For each `Constructor`, `Representer` or `Resolver` class, we introduce a thread-local registry that's a class attribute in earch class, and which holds all items that constructor/representer/resolver supports.
	- Every subclass automatically gets an uninitialized registry by using a metaclass.
	- Upon calling `add_constructor` (and friends) or `load/dump` (and friends) for the first time, the registry is populated with the default items.
	- In order to get the items in the registry we do the following:
		- If the registry has been initialized, we return the thread-local data.
		- If not, we go up the mro until we find a parent class with an initialized registry and return their items.


Diff:
diff --git a/lib/yaml/constructor.py b/lib/yaml/constructor.py
index 619acd307..da746a5ca 100644
--- a/lib/yaml/constructor.py
+++ b/lib/yaml/constructor.py
@@ -10,16 +10,85 @@
 
 from .error import *
 from .nodes import *
+from .registry import RegistryMeta, ConstructorRegistry
 
+import threading
 import collections.abc, datetime, base64, binascii, re, sys, types
 
 class ConstructorError(MarkedYAMLError):
     pass
 
-class BaseConstructor:
 
-    yaml_constructors = {}
-    yaml_multi_constructors = {}
+class ConstructorSetup(threading.local):
+    def __init__(self):
+        self.initialized = False
+
+    def ensure_initialized(self):
+        if not self.initialized:
+            self.initialized = True
+            setup_constructors()
+
+
+class BaseConstructor(metaclass=RegistryMeta):
+    """
+    Base class for all constructors.
+
+    Some information on the `setup` and `registry` class attributes:
+    - Each class in the class hierarchy should have its own registry.
+      If it's not initialized, it should inherit the parent's registry.
+      As soon as it gets initialized, it should copy over all of the
+      parent's registry items and start growing independently.
+    - `setup` is initialized in the base class and is not
+      overwritten in subclasses. Its aim is to initialize the registry
+      with the default constructors *once* per thread for all the classes
+      in the hierarchy.
+    - `registry` is initialized in the base class and is
+      overwritten in subclasses (done in `RegistryMeta`). Its aim is
+      to have a thread-local registry for constructors. `yaml_constructors`
+      is a property that checks if the registry is initialized for the
+      current thread and returns it if it is. If it's not, it walks up the
+      MRO until it finds a class that has an initialized registry (remember
+      that the base class always starts out initialized so the recursion
+      will certainly stop there).
+
+    The following diagram shows the inheritance structure of the constructor
+    and resolver classes (the same exact thing applies to representers and
+    reolvers).
+
+    ┌────────────────────────────────┐    ┌────────────────────────────┐
+    │       Base Constructor         │    │       Base Resolver        │
+    │                                │    │                            │
+    │ constructor_setup:             │    │ resolver_setup:            │
+    │  Starts uninitialized. Does    │    │  Same properties with      │
+    │  not get overwritten by        │    │  constructor_setup.        │
+    │  subclasses.                   │    │                            │
+    │                                │    │                            │
+    │ constructor_registry:          │    │ resolver_registry:         │
+    │  Starts initialized for Base   │    │  Same properties with      │
+    │  and uninitialized for all     │    │  constructor_registry.     │
+    │  subclasses. Gets overwritten  │    │                            │
+    │  in subclasses.                │    │                            │
+    │                                │    │                            │
+    └───────────────┬────────────────┘    └─────────────┬──────────────┘
+                    │                                   │               
+                    ▼                                   │               
+    ┌────────────────────────────────┐                  │               
+    │       Safe Constructor         │                  │               
+    └───────────────┬────────────────┘                  │               
+                    │                                   │               
+                    ▼                                   ▼               
+    ┌────────────────────────────────┐    ┌────────────────────────────┐
+    │       Constructor              │    │       Resolver             │
+    └───────────────┬────────────────┘    └─────────────┬──────────────┘
+                    │                                   │               
+                    │                                   │               
+                    │       ┌───────────────────┐       │               
+                    └──────►│    Loader         │◄──────┘               
+                            └───────────────────┘                       
+    """
+
+    constructor_setup = ConstructorSetup() 
+    constructor_registry = ConstructorRegistry(yaml_constructors_initialized=True, yaml_multi_constructors_initialized=True)
 
     def __init__(self):
         self.constructed_objects = {}
@@ -76,20 +145,22 @@ def construct_object(self, node, deep=False):
         self.recursive_objects[node] = None
         constructor = None
         tag_suffix = None
-        if node.tag in self.yaml_constructors:
-            constructor = self.yaml_constructors[node.tag]
+        yaml_constructors = self.yaml_constructors()
+        yaml_multi_constructors = self.yaml_multi_constructors()
+        if node.tag in yaml_constructors:
+            constructor = yaml_constructors[node.tag]
         else:
-            for tag_prefix in self.yaml_multi_constructors:
+            for tag_prefix in yaml_multi_constructors:
                 if tag_prefix is not None and node.tag.startswith(tag_prefix):
                     tag_suffix = node.tag[len(tag_prefix):]
-                    constructor = self.yaml_multi_constructors[tag_prefix]
+                    constructor = yaml_multi_constructors[tag_prefix]
                     break
             else:
-                if None in self.yaml_multi_constructors:
+                if None in yaml_multi_constructors:
                     tag_suffix = node.tag
-                    constructor = self.yaml_multi_constructors[None]
-                elif None in self.yaml_constructors:
-                    constructor = self.yaml_constructors[None]
+                    constructor = yaml_multi_constructors[None]
+                elif None in yaml_constructors:
+                    constructor = yaml_constructors[None]
                 elif isinstance(node, ScalarNode):
                     constructor = self.__class__.construct_scalar
                 elif isinstance(node, SequenceNode):
@@ -156,17 +227,47 @@ def construct_pairs(self, node, deep=False):
             pairs.append((key, value))
         return pairs
 
+    @classmethod
+    def yaml_constructors(cls):
+        cls.constructor_setup.ensure_initialized()
+
+        if cls.constructor_registry.yaml_constructors_initialized:
+            return cls.constructor_registry.yaml_constructors
+
+        constructor_cls = next(c for c in cls.mro() if hasattr(c, 'yaml_constructors') and c is not cls)
+        return constructor_cls.yaml_constructors()
+
+    @classmethod
+    def yaml_multi_constructors(cls):
+        cls.constructor_setup.ensure_initialized()
+
+        if cls.constructor_registry.yaml_multi_constructors_initialized:
+            return cls.constructor_registry.yaml_multi_constructors
+
+        constructor_cls = next(c for c in cls.mro() if hasattr(c, 'yaml_multi_constructors') and c is not cls)
+        return constructor_cls.yaml_multi_constructors()
+
+    @classmethod
+    def _ensure_yaml_constructors_initialized(cls):
+        if not cls.constructor_registry.yaml_constructors_initialized:
+            cls.constructor_registry.yaml_constructors = cls.yaml_constructors().copy()
+            cls.constructor_registry.yaml_constructors_initialized = True
+
+    @classmethod
+    def _ensure_yaml_multi_constructors_initialized(cls):
+        if not cls.constructor_registry.yaml_multi_constructors_initialized:
+            cls.constructor_registry.yaml_multi_constructors = cls.yaml_multi_constructors().copy()
+            cls.constructor_registry.yaml_multi_constructors_initialized = True
+
     @classmethod
     def add_constructor(cls, tag, constructor):
-        if not 'yaml_constructors' in cls.__dict__:
-            cls.yaml_constructors = cls.yaml_constructors.copy()
-        cls.yaml_constructors[tag] = constructor
+        cls._ensure_yaml_constructors_initialized()
+        cls.constructor_registry.yaml_constructors[tag] = constructor
 
     @classmethod
     def add_multi_constructor(cls, tag_prefix, multi_constructor):
-        if not 'yaml_multi_constructors' in cls.__dict__:
-            cls.yaml_multi_constructors = cls.yaml_multi_constructors.copy()
-        cls.yaml_multi_constructors[tag_prefix] = multi_constructor
+        cls._ensure_yaml_multi_constructors_initialized()
+        cls.constructor_registry.yaml_multi_constructors[tag_prefix] = multi_constructor
 
 class SafeConstructor(BaseConstructor):
 
@@ -428,56 +529,6 @@ def construct_undefined(self, node):
                 "could not determine a constructor for the tag %r" % node.tag,
                 node.start_mark)
 
-SafeConstructor.add_constructor(
-        'tag:yaml.org,2002:null',
-        SafeConstructor.construct_yaml_null)
-
-SafeConstructor.add_constructor(
-        'tag:yaml.org,2002:bool',
-        SafeConstructor.construct_yaml_bool)
-
-SafeConstructor.add_constructor(
-        'tag:yaml.org,2002:int',
-        SafeConstructor.construct_yaml_int)
-
-SafeConstructor.add_constructor(
-        'tag:yaml.org,2002:float',
-        SafeConstructor.construct_yaml_float)
-
-SafeConstructor.add_constructor(
-        'tag:yaml.org,2002:binary',
-        SafeConstructor.construct_yaml_binary)
-
-SafeConstructor.add_constructor(
-        'tag:yaml.org,2002:timestamp',
-        SafeConstructor.construct_yaml_timestamp)
-
-SafeConstructor.add_constructor(
-        'tag:yaml.org,2002:omap',
-        SafeConstructor.construct_yaml_omap)
-
-SafeConstructor.add_constructor(
-        'tag:yaml.org,2002:pairs',
-        SafeConstructor.construct_yaml_pairs)
-
-SafeConstructor.add_constructor(
-        'tag:yaml.org,2002:set',
-        SafeConstructor.construct_yaml_set)
-
-SafeConstructor.add_constructor(
-        'tag:yaml.org,2002:str',
-        SafeConstructor.construct_yaml_str)
-
-SafeConstructor.add_constructor(
-        'tag:yaml.org,2002:seq',
-        SafeConstructor.construct_yaml_seq)
-
-SafeConstructor.add_constructor(
-        'tag:yaml.org,2002:map',
-        SafeConstructor.construct_yaml_map)
-
-SafeConstructor.add_constructor(None,
-        SafeConstructor.construct_undefined)
 
 class FullConstructor(SafeConstructor):
     # 'extend' is blacklisted because it is used by
@@ -658,57 +709,6 @@ def construct_python_object_apply(self, suffix, node, newobj=False):
     def construct_python_object_new(self, suffix, node):
         return self.construct_python_object_apply(suffix, node, newobj=True)
 
-FullConstructor.add_constructor(
-    'tag:yaml.org,2002:python/none',
-    FullConstructor.construct_yaml_null)
-
-FullConstructor.add_constructor(
-    'tag:yaml.org,2002:python/bool',
-    FullConstructor.construct_yaml_bool)
-
-FullConstructor.add_constructor(
-    'tag:yaml.org,2002:python/str',
-    FullConstructor.construct_python_str)
-
-FullConstructor.add_constructor(
-    'tag:yaml.org,2002:python/unicode',
-    FullConstructor.construct_python_unicode)
-
-FullConstructor.add_constructor(
-    'tag:yaml.org,2002:python/bytes',
-    FullConstructor.construct_python_bytes)
-
-FullConstructor.add_constructor(
-    'tag:yaml.org,2002:python/int',
-    FullConstructor.construct_yaml_int)
-
-FullConstructor.add_constructor(
-    'tag:yaml.org,2002:python/long',
-    FullConstructor.construct_python_long)
-
-FullConstructor.add_constructor(
-    'tag:yaml.org,2002:python/float',
-    FullConstructor.construct_yaml_float)
-
-FullConstructor.add_constructor(
-    'tag:yaml.org,2002:python/complex',
-    FullConstructor.construct_python_complex)
-
-FullConstructor.add_constructor(
-    'tag:yaml.org,2002:python/list',
-    FullConstructor.construct_yaml_seq)
-
-FullConstructor.add_constructor(
-    'tag:yaml.org,2002:python/tuple',
-    FullConstructor.construct_python_tuple)
-
-FullConstructor.add_constructor(
-    'tag:yaml.org,2002:python/dict',
-    FullConstructor.construct_yaml_map)
-
-FullConstructor.add_multi_constructor(
-    'tag:yaml.org,2002:python/name:',
-    FullConstructor.construct_python_name)
 
 class UnsafeConstructor(FullConstructor):
 
@@ -726,23 +726,130 @@ def set_python_instance_state(self, instance, state):
         return super(UnsafeConstructor, self).set_python_instance_state(
             instance, state, unsafe=True)
 
-UnsafeConstructor.add_multi_constructor(
-    'tag:yaml.org,2002:python/module:',
-    UnsafeConstructor.construct_python_module)
-
-UnsafeConstructor.add_multi_constructor(
-    'tag:yaml.org,2002:python/object:',
-    UnsafeConstructor.construct_python_object)
-
-UnsafeConstructor.add_multi_constructor(
-    'tag:yaml.org,2002:python/object/new:',
-    UnsafeConstructor.construct_python_object_new)
-
-UnsafeConstructor.add_multi_constructor(
-    'tag:yaml.org,2002:python/object/apply:',
-    UnsafeConstructor.construct_python_object_apply)
 
 # Constructor is same as UnsafeConstructor. Need to leave this in place in case
 # people have extended it directly.
 class Constructor(UnsafeConstructor):
     pass
+
+
+def setup_constructors():
+    SafeConstructor.add_constructor(
+        'tag:yaml.org,2002:null',
+        SafeConstructor.construct_yaml_null)
+
+    SafeConstructor.add_constructor(
+        'tag:yaml.org,2002:bool',
+        SafeConstructor.construct_yaml_bool)
+
+    SafeConstructor.add_constructor(
+        'tag:yaml.org,2002:int',
+        SafeConstructor.construct_yaml_int)
+
+    SafeConstructor.add_constructor(
+        'tag:yaml.org,2002:float',
+        SafeConstructor.construct_yaml_float)
+
+    SafeConstructor.add_constructor(
+        'tag:yaml.org,2002:binary',
+        SafeConstructor.construct_yaml_binary)
+
+    SafeConstructor.add_constructor(
+        'tag:yaml.org,2002:timestamp',
+        SafeConstructor.construct_yaml_timestamp)
+
+    SafeConstructor.add_constructor(
+        'tag:yaml.org,2002:omap',
+        SafeConstructor.construct_yaml_omap)
+
+    SafeConstructor.add_constructor(
+        'tag:yaml.org,2002:pairs',
+        SafeConstructor.construct_yaml_pairs)
+
+    SafeConstructor.add_constructor(
+        'tag:yaml.org,2002:set',
+        SafeConstructor.construct_yaml_set)
+
+    SafeConstructor.add_constructor(
+        'tag:yaml.org,2002:str',
+        SafeConstructor.construct_yaml_str)
+
+    SafeConstructor.add_constructor(
+        'tag:yaml.org,2002:seq',
+        SafeConstructor.construct_yaml_seq)
+
+    SafeConstructor.add_constructor(
+        'tag:yaml.org,2002:map',
+        SafeConstructor.construct_yaml_map)
+
+    SafeConstructor.add_constructor(
+        None,
+        SafeConstructor.construct_undefined)
+
+    FullConstructor.add_constructor(
+        'tag:yaml.org,2002:python/none',
+        FullConstructor.construct_yaml_null)
+
+    FullConstructor.add_constructor(
+        'tag:yaml.org,2002:python/bool',
+        FullConstructor.construct_yaml_bool)
+
+    FullConstructor.add_constructor(
+        'tag:yaml.org,2002:python/str',
+        FullConstructor.construct_python_str)
+
+    FullConstructor.add_constructor(
+        'tag:yaml.org,2002:python/unicode',
+        FullConstructor.construct_python_unicode)
+
+    FullConstructor.add_constructor(
+        'tag:yaml.org,2002:python/bytes',
+        FullConstructor.construct_python_bytes)
+
+    FullConstructor.add_constructor(
+        'tag:yaml.org,2002:python/int',
+        FullConstructor.construct_yaml_int)
+
+    FullConstructor.add_constructor(
+        'tag:yaml.org,2002:python/long',
+        FullConstructor.construct_python_long)
+
+    FullConstructor.add_constructor(
+        'tag:yaml.org,2002:python/float',
+        FullConstructor.construct_yaml_float)
+
+    FullConstructor.add_constructor(
+        'tag:yaml.org,2002:python/complex',
+        FullConstructor.construct_python_complex)
+
+    FullConstructor.add_constructor(
+        'tag:yaml.org,2002:python/list',
+        FullConstructor.construct_yaml_seq)
+
+    FullConstructor.add_constructor(
+        'tag:yaml.org,2002:python/tuple',
+        FullConstructor.construct_python_tuple)
+
+    FullConstructor.add_constructor(
+        'tag:yaml.org,2002:python/dict',
+        FullConstructor.construct_yaml_map)
+
+    FullConstructor.add_multi_constructor(
+        'tag:yaml.org,2002:python/name:',
+        FullConstructor.construct_python_name)
+
+    UnsafeConstructor.add_multi_constructor(
+        'tag:yaml.org,2002:python/module:',
+        UnsafeConstructor.construct_python_module)
+
+    UnsafeConstructor.add_multi_constructor(
+        'tag:yaml.org,2002:python/object:',
+        UnsafeConstructor.construct_python_object)
+
+    UnsafeConstructor.add_multi_constructor(
+        'tag:yaml.org,2002:python/object/new:',
+        UnsafeConstructor.construct_python_object_new)
+
+    UnsafeConstructor.add_multi_constructor(
+        'tag:yaml.org,2002:python/object/apply:',
+        UnsafeConstructor.construct_python_object_apply)
diff --git a/lib/yaml/registry.py b/lib/yaml/registry.py
new file mode 100644
index 000000000..4090dbe23
--- /dev/null
+++ b/lib/yaml/registry.py
@@ -0,0 +1,39 @@
+import threading
+
+
+class ConstructorRegistry(threading.local):
+    def __init__(self, yaml_constructors_initialized=False, yaml_multi_constructors_initialized=False):
+        self.yaml_constructors = {}
+        self.yaml_multi_constructors = {}
+        self.yaml_constructors_initialized = yaml_constructors_initialized
+        self.yaml_multi_constructors_initialized = yaml_multi_constructors_initialized
+
+
+class RepresenterRegistry(threading.local):
+    def __init__(self, yaml_representers_initialized=False, yaml_multi_representers_initialized=False):
+        self.yaml_representers = {}
+        self.yaml_multi_representers = {}
+        self.yaml_representers_initialized = yaml_representers_initialized
+        self.yaml_multi_representers_initialized = yaml_multi_representers_initialized
+
+
+class ResolverRegistry(threading.local):
+    def __init__(self, yaml_implicit_resolvers_initialized=False, yaml_path_resolvers_initialized=False):
+        self.yaml_implicit_resolvers = {}
+        self.yaml_path_resolvers = {}
+        self.yaml_implicit_resolvers_initialized = yaml_implicit_resolvers_initialized
+        self.yaml_path_resolvers_initialized = yaml_path_resolvers_initialized
+
+
+class RegistryMeta(type):
+    """Metaclass to handle registry inheritance"""
+    
+    def __new__(cls, name, bases, attrs):
+        cls = super().__new__(cls, name, bases, attrs)
+        if 'constructor_registry' not in cls.__dict__:
+            cls.constructor_registry = ConstructorRegistry()
+        if 'representer_registry' not in cls.__dict__:
+            cls.representer_registry = RepresenterRegistry()
+        if 'resolver_registry' not in cls.__dict__:
+            cls.resolver_registry = ResolverRegistry()
+        return cls
diff --git a/lib/yaml/representer.py b/lib/yaml/representer.py
index 808ca06df..a172cb141 100644
--- a/lib/yaml/representer.py
+++ b/lib/yaml/representer.py
@@ -4,16 +4,35 @@
 
 from .error import *
 from .nodes import *
+from .registry import RegistryMeta, RepresenterRegistry
 
+import threading
 import datetime, copyreg, types, base64, collections
 
 class RepresenterError(YAMLError):
     pass
 
-class BaseRepresenter:
 
-    yaml_representers = {}
-    yaml_multi_representers = {}
+class RepresenterSetup(threading.local):
+    def __init__(self):
+        self.initialized = False
+
+    def ensure_initialized(self):
+        if not self.initialized:
+            self.initialized = True
+            setup_representers()
+
+
+class BaseRepresenter(metaclass=RegistryMeta):
+    """
+    Base class for representers.
+
+    More information on `setup` and `registry` below can be found in the `BaseConstructor`
+    docstring.
+    """
+
+    representer_setup = RepresenterSetup()
+    representer_registry = RepresenterRegistry(yaml_representers_initialized=True, yaml_multi_representers_initialized=True)
 
     def __init__(self, default_style=None, default_flow_style=False, sort_keys=True):
         self.default_style = default_style
@@ -44,35 +63,67 @@ def represent_data(self, data):
             #self.represented_objects[alias_key] = None
             self.object_keeper.append(data)
         data_types = type(data).__mro__
-        if data_types[0] in self.yaml_representers:
-            node = self.yaml_representers[data_types[0]](self, data)
+        yaml_representers = self.yaml_representers()
+        yaml_multi_representers = self.yaml_multi_representers()
+        if data_types[0] in yaml_representers:
+            node = yaml_representers[data_types[0]](self, data)
         else:
             for data_type in data_types:
-                if data_type in self.yaml_multi_representers:
-                    node = self.yaml_multi_representers[data_type](self, data)
+                if data_type in yaml_multi_representers:
+                    node = yaml_multi_representers[data_type](self, data)
                     break
             else:
-                if None in self.yaml_multi_representers:
-                    node = self.yaml_multi_representers[None](self, data)
-                elif None in self.yaml_representers:
-                    node = self.yaml_representers[None](self, data)
+                if None in yaml_multi_representers:
+                    node = yaml_multi_representers[None](self, data)
+                elif None in yaml_representers:
+                    node = yaml_representers[None](self, data)
                 else:
                     node = ScalarNode(None, str(data))
         #if alias_key is not None:
         #    self.represented_objects[alias_key] = node
         return node
 
+    @classmethod
+    def yaml_representers(cls):
+        cls.representer_setup.ensure_initialized()
+
+        if cls.representer_registry.yaml_representers_initialized:
+            return cls.representer_registry.yaml_representers
+
+        constructor_cls = next(c for c in cls.mro() if hasattr(c, 'yaml_representers') and c is not cls)
+        return constructor_cls.yaml_representers()
+
+    @classmethod
+    def yaml_multi_representers(cls):
+        cls.representer_setup.ensure_initialized()
+
+        if cls.representer_registry.yaml_multi_representers_initialized:
+            return cls.representer_registry.yaml_multi_representers
+
+        constructor_cls = next(c for c in cls.mro() if hasattr(c, 'yaml_multi_representers') and c is not cls)
+        return constructor_cls.yaml_multi_representers()
+
+    @classmethod
+    def _ensure_yaml_representers_initialized(cls):
+        if not cls.representer_registry.yaml_representers_initialized:
+            cls.representer_registry.yaml_representers = cls.yaml_representers().copy()
+            cls.representer_registry.yaml_representers_initialized = True
+
+    @classmethod
+    def _ensure_yaml_multi_representers_initialized(cls):
+        if not cls.representer_registry.yaml_multi_representers_initialized:
+            cls.representer_registry.yaml_multi_representers = cls.yaml_multi_representers().copy()
+            cls.representer_registry.yaml_multi_representers_initialized = True
+
     @classmethod
     def add_representer(cls, data_type, representer):
-        if not 'yaml_representers' in cls.__dict__:
-            cls.yaml_representers = cls.yaml_representers.copy()
-        cls.yaml_representers[data_type] = representer
+        cls._ensure_yaml_representers_initialized()
+        cls.representer_registry.yaml_representers[data_type] = representer
 
     @classmethod
-    def add_multi_representer(cls, data_type, representer):
-        if not 'yaml_multi_representers' in cls.__dict__:
-            cls.yaml_multi_representers = cls.yaml_multi_representers.copy()
-        cls.yaml_multi_representers[data_type] = representer
+    def add_multi_representer(cls, data_type, multi_representer):
+        cls._ensure_yaml_multi_representers_initialized()
+        cls.representer_registry.yaml_multi_representers[data_type] = multi_representer
 
     def represent_scalar(self, tag, value, style=None):
         if style is None:
@@ -230,44 +281,6 @@ def represent_yaml_object(self, tag, data, cls, flow_style=None):
     def represent_undefined(self, data):
         raise RepresenterError("cannot represent an object", data)
 
-SafeRepresenter.add_representer(type(None),
-        SafeRepresenter.represent_none)
-
-SafeRepresenter.add_representer(str,
-        SafeRepresenter.represent_str)
-
-SafeRepresenter.add_representer(bytes,
-        SafeRepresenter.represent_binary)
-
-SafeRepresenter.add_representer(bool,
-        SafeRepresenter.represent_bool)
-
-SafeRepresenter.add_representer(int,
-        SafeRepresenter.represent_int)
-
-SafeRepresenter.add_representer(float,
-        SafeRepresenter.represent_float)
-
-SafeRepresenter.add_representer(list,
-        SafeRepresenter.represent_list)
-
-SafeRepresenter.add_representer(tuple,
-        SafeRepresenter.represent_list)
-
-SafeRepresenter.add_representer(dict,
-        SafeRepresenter.represent_dict)
-
-SafeRepresenter.add_representer(set,
-        SafeRepresenter.represent_set)
-
-SafeRepresenter.add_representer(datetime.date,
-        SafeRepresenter.represent_date)
-
-SafeRepresenter.add_representer(datetime.datetime,
-        SafeRepresenter.represent_datetime)
-
-SafeRepresenter.add_representer(None,
-        SafeRepresenter.represent_undefined)
 
 class Representer(SafeRepresenter):
 
@@ -363,27 +376,68 @@ def represent_ordered_dict(self, data):
         items = [[key, value] for key, value in data.items()]
         return self.represent_sequence(tag, [items])
 
-Representer.add_representer(complex,
+
+def setup_representers():
+    SafeRepresenter.add_representer(type(None),
+        SafeRepresenter.represent_none)
+
+    SafeRepresenter.add_representer(str,
+        SafeRepresenter.represent_str)
+
+    SafeRepresenter.add_representer(bytes,
+        SafeRepresenter.represent_binary)
+
+    SafeRepresenter.add_representer(bool,
+        SafeRepresenter.represent_bool)
+
+    SafeRepresenter.add_representer(int,
+        SafeRepresenter.represent_int)
+
+    SafeRepresenter.add_representer(float,
+        SafeRepresenter.represent_float)
+
+    SafeRepresenter.add_representer(list,
+        SafeRepresenter.represent_list)
+
+    SafeRepresenter.add_representer(tuple,
+        SafeRepresenter.represent_list)
+
+    SafeRepresenter.add_representer(dict,
+        SafeRepresenter.represent_dict)
+
+    SafeRepresenter.add_representer(set,
+        SafeRepresenter.represent_set)
+
+    SafeRepresenter.add_representer(datetime.date,
+        SafeRepresenter.represent_date)
+
+    SafeRepresenter.add_representer(datetime.datetime,
+        SafeRepresenter.represent_datetime)
+
+    SafeRepresenter.add_representer(None,
+        SafeRepresenter.represent_undefined)
+
+    Representer.add_representer(complex,
         Representer.represent_complex)
 
-Representer.add_representer(tuple,
+    Representer.add_representer(tuple,
         Representer.represent_tuple)
 
-Representer.add_multi_representer(type,
+    Representer.add_multi_representer(type,
         Representer.represent_name)
 
-Representer.add_representer(collections.OrderedDict,
+    Representer.add_representer(collections.OrderedDict,
         Representer.represent_ordered_dict)
 
-Representer.add_representer(types.FunctionType,
+    Representer.add_representer(types.FunctionType,
         Representer.represent_name)
 
-Representer.add_representer(types.BuiltinFunctionType,
+    Representer.add_representer(types.BuiltinFunctionType,
         Representer.represent_name)
 
-Representer.add_representer(types.ModuleType,
+    Representer.add_representer(types.ModuleType,
         Representer.represent_module)
 
-Representer.add_multi_representer(object,
+    Representer.add_multi_representer(object,
         Representer.represent_object)
 
diff --git a/lib/yaml/resolver.py b/lib/yaml/resolver.py
index 3522bdaaf..163dcea16 100644
--- a/lib/yaml/resolver.py
+++ b/lib/yaml/resolver.py
@@ -3,36 +3,87 @@
 
 from .error import *
 from .nodes import *
+from .registry import RegistryMeta, ResolverRegistry
 
 import re
+import threading
 
 class ResolverError(YAMLError):
     pass
 
-class BaseResolver:
+
+class ResolverSetup(threading.local):
+    def __init__(self):
+        self.initialized = False
+
+    def ensure_initialized(self):
+        if not self.initialized:
+            self.initialized = True
+            setup_resolvers()
+
+
+class BaseResolver(metaclass=RegistryMeta):
+    """
+    Base class for resolvers.
+
+    More information on `setup` and `registry` below can be found in the `BaseConstructor`
+    docstring.
+    """
 
     DEFAULT_SCALAR_TAG = 'tag:yaml.org,2002:str'
     DEFAULT_SEQUENCE_TAG = 'tag:yaml.org,2002:seq'
     DEFAULT_MAPPING_TAG = 'tag:yaml.org,2002:map'
 
-    yaml_implicit_resolvers = {}
-    yaml_path_resolvers = {}
+    resolver_setup = ResolverSetup()
+    resolver_registry = ResolverRegistry(yaml_implicit_resolvers_initialized=True, yaml_path_resolvers_initialized=True)
 
     def __init__(self):
         self.resolver_exact_paths = []
         self.resolver_prefix_paths = []
 
+    @classmethod
+    def yaml_implicit_resolvers(cls):
+        cls.resolver_setup.ensure_initialized()
+
+        if cls.resolver_registry.yaml_implicit_resolvers_initialized:
+            return cls.resolver_registry.yaml_implicit_resolvers
+
+        constructor_cls = next(c for c in cls.mro() if hasattr(c, 'yaml_implicit_resolvers') and c is not cls)
+        return constructor_cls.yaml_implicit_resolvers()
+
+    @classmethod
+    def yaml_path_resolvers(cls):
+        cls.resolver_setup.ensure_initialized()
+
+        if cls.resolver_registry.yaml_path_resolvers_initialized:
+            return cls.resolver_registry.yaml_path_resolvers
+
+        constructor_cls = next(c for c in cls.mro() if hasattr(c, 'yaml_path_resolvers') and c is not cls)
+        return constructor_cls.yaml_path_resolvers()
+
+    @classmethod
+    def _ensure_yaml_implicit_resolvers_initialized(cls):
+        if not cls.resolver_registry.yaml_implicit_resolvers_initialized:
+            new_yaml_implicit_resolvers = {}
+            old_yaml_implicit_resolvers = cls.yaml_implicit_resolvers()
+            for key in old_yaml_implicit_resolvers:
+                new_yaml_implicit_resolvers[key] = old_yaml_implicit_resolvers[key][:]
+            cls.resolver_registry.yaml_implicit_resolvers = new_yaml_implicit_resolvers
+            cls.resolver_registry.yaml_implicit_resolvers_initialized = True
+
+    @classmethod
+    def _ensure_yaml_path_resolvers_initialized(cls):
+        if not cls.resolver_registry.yaml_path_resolvers_initialized:
+            cls.resolver_registry.yaml_path_resolvers = cls.yaml_path_resolvers().copy()
+            cls.resolver_registry.yaml_path_resolvers_initialized = True
+
     @classmethod
     def add_implicit_resolver(cls, tag, regexp, first):
-        if not 'yaml_implicit_resolvers' in cls.__dict__:
-            implicit_resolvers = {}
-            for key in cls.yaml_implicit_resolvers:
-                implicit_resolvers[key] = cls.yaml_implicit_resolvers[key][:]
-            cls.yaml_implicit_resolvers = implicit_resolvers
+        cls._ensure_yaml_implicit_resolvers_initialized()
         if first is None:
             first = [None]
         for ch in first:
-            cls.yaml_implicit_resolvers.setdefault(ch, []).append((tag, regexp))
+            cls.resolver_registry.yaml_implicit_resolvers.setdefault(ch, []).append((tag, regexp))
 
     @classmethod
     def add_path_resolver(cls, tag, path, kind=None):
@@ -48,8 +99,7 @@ def add_path_resolver(cls, tag, path, kind=None):
         # a mapping value that corresponds to a scalar key which content is
         # equal to the `index_check` value.  An integer `index_check` matches
         # against a sequence value with the index equal to `index_check`.
-        if not 'yaml_path_resolvers' in cls.__dict__:
-            cls.yaml_path_resolvers = cls.yaml_path_resolvers.copy()
+        cls._ensure_yaml_path_resolvers_initialized()
         new_path = []
         for element in path:
             if isinstance(element, (list, tuple)):
@@ -86,10 +136,11 @@ def add_path_resolver(cls, tag, path, kind=None):
         elif kind not in [ScalarNode, SequenceNode, MappingNode]    \
                 and kind is not None:
             raise ResolverError("Invalid node kind: %s" % kind)
-        cls.yaml_path_resolvers[tuple(new_path), kind] = tag
+        cls.resolver_registry.yaml_path_resolvers[tuple(new_path), kind] = tag
 
     def descend_resolver(self, current_node, current_index):
-        if not self.yaml_path_resolvers:
+        yaml_path_resolvers = self.yaml_path_resolvers()
+        if not yaml_path_resolvers:
             return
         exact_paths = {}
         prefix_paths = []
@@ -101,18 +152,18 @@ def descend_resolver(self, current_node, current_index):
                     if len(path) > depth:
                         prefix_paths.append((path, kind))
                     else:
-                        exact_paths[kind] = self.yaml_path_resolvers[path, kind]
+                        exact_paths[kind] = yaml_path_resolvers[path, kind]
         else:
-            for path, kind in self.yaml_path_resolvers:
+            for path, kind in yaml_path_resolvers:
                 if not path:
-                    exact_paths[kind] = self.yaml_path_resolvers[path, kind]
+                    exact_paths[kind] = yaml_path_resolvers[path, kind]
                 else:
                     prefix_paths.append((path, kind))
         self.resolver_exact_paths.append(exact_paths)
         self.resolver_prefix_paths.append(prefix_paths)
 
     def ascend_resolver(self):
-        if not self.yaml_path_resolvers:
+        if not self.yaml_path_resolvers():
             return
         self.resolver_exact_paths.pop()
         self.resolver_prefix_paths.pop()
@@ -141,17 +192,19 @@ def check_resolver_prefix(self, depth, path, kind,
         return True
 
     def resolve(self, kind, value, implicit):
+        yaml_implicit_resolvers = self.yaml_implicit_resolvers()
+        yaml_path_resolvers = self.yaml_path_resolvers()
         if kind is ScalarNode and implicit[0]:
             if value == '':
-                resolvers = self.yaml_implicit_resolvers.get('', [])
+                resolvers = yaml_implicit_resolvers.get('', [])
             else:
-                resolvers = self.yaml_implicit_resolvers.get(value[0], [])
-            wildcard_resolvers = self.yaml_implicit_resolvers.get(None, [])
+                resolvers = yaml_implicit_resolvers.get(value[0], [])
+            wildcard_resolvers = yaml_implicit_resolvers.get(None, [])
             for tag, regexp in resolvers + wildcard_resolvers:
                 if regexp.match(value):
                     return tag
             implicit = implicit[1]
-        if self.yaml_path_resolvers:
+        if yaml_path_resolvers:
             exact_paths = self.resolver_exact_paths[-1]
             if kind in exact_paths:
                 return exact_paths[kind]
@@ -167,61 +220,65 @@ def resolve(self, kind, value, implicit):
 class Resolver(BaseResolver):
     pass
 
-Resolver.add_implicit_resolver(
-        'tag:yaml.org,2002:bool',
-        re.compile(r'''^(?:yes|Yes|YES|no|No|NO
-                    |true|True|TRUE|false|False|FALSE
-                    |on|On|ON|off|Off|OFF)$''', re.X),
-        list('yYnNtTfFoO'))
-
-Resolver.add_implicit_resolver(
-        'tag:yaml.org,2002:float',
-        re.compile(r'''^(?:[-+]?(?:[0-9][0-9_]*)\.[0-9_]*(?:[eE][-+][0-9]+)?
-                    |\.[0-9][0-9_]*(?:[eE][-+][0-9]+)?
-                    |[-+]?[0-9][0-9_]*(?::[0-5]?[0-9])+\.[0-9_]*
-                    |[-+]?\.(?:inf|Inf|INF)
-                    |\.(?:nan|NaN|NAN))$''', re.X),
-        list('-+0123456789.'))
-
-Resolver.add_implicit_resolver(
-        'tag:yaml.org,2002:int',
-        re.compile(r'''^(?:[-+]?0b[0-1_]+
-                    |[-+]?0[0-7_]+
-                    |[-+]?(?:0|[1-9][0-9_]*)
-                    |[-+]?0x[0-9a-fA-F_]+
-                    |[-+]?[1-9][0-9_]*(?::[0-5]?[0-9])+)$''', re.X),
-        list('-+0123456789'))
-
-Resolver.add_implicit_resolver(
-        'tag:yaml.org,2002:merge',
-        re.compile(r'^(?:<<)$'),
-        ['<'])
-
-Resolver.add_implicit_resolver(
-        'tag:yaml.org,2002:null',
-        re.compile(r'''^(?: ~
-                    |null|Null|NULL
-                    | )$''', re.X),
-        ['~', 'n', 'N', ''])
-
-Resolver.add_implicit_resolver(
-        'tag:yaml.org,2002:timestamp',
-        re.compile(r'''^(?:[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]
-                    |[0-9][0-9][0-9][0-9] -[0-9][0-9]? -[0-9][0-9]?
-                     (?:[Tt]|[ \t]+)[0-9][0-9]?
-                     :[0-9][0-9] :[0-9][0-9] (?:\.[0-9]*)?
-                     (?:[ \t]*(?:Z|[-+][0-9][0-9]?(?::[0-9][0-9])?))?)$''', re.X),
-        list('0123456789'))
-
-Resolver.add_implicit_resolver(
-        'tag:yaml.org,2002:value',
-        re.compile(r'^(?:=)$'),
-        ['='])
-
-# The following resolver is only for documentation purposes. It cannot work
-# because plain scalars cannot start with '!', '&', or '*'.
-Resolver.add_implicit_resolver(
-        'tag:yaml.org,2002:yaml',
-        re.compile(r'^(?:!|&|\*)$'),
-        list('!&*'))
 
+def setup_resolvers():
+    Resolver.add_implicit_resolver(
+            'tag:yaml.org,2002:bool',
+            re.compile(r'''^(?:yes|Yes|YES|no|No|NO
+                        |true|True|TRUE|false|False|FALSE
+                        |on|On|ON|off|Off|OFF)$''', re.X),
+            list('yYnNtTfFoO'))
+
+    Resolver.add_implicit_resolver(
+            'tag:yaml.org,2002:float',
+            re.compile(r'''^(?:[-+]?(?:[0-9][0-9_]*)\.[0-9_]*(?:[eE][-+][0-9]+)?
+                        |\.[0-9][0-9_]*(?:[eE][-+][0-9]+)?
+                        |[-+]?[0-9][0-9_]*(?::[0-5]?[0-9])+\.[0-9_]*
+                        |[-+]?\.(?:inf|Inf|INF)
+                        |\.(?:nan|NaN|NAN))$''', re.X),
+            list('-+0123456789.'))
+
+    Resolver.add_implicit_resolver(
+            'tag:yaml.org,2002:int',
+            re.compile(r'''^(?:[-+]?0b[0-1_]+
+                        |[-+]?0[0-7_]+
+                        |[-+]?(?:0|[1-9][0-9_]*)
+                        |[-+]?0x[0-9a-fA-F_]+
+                        |[-+]?[1-9][0-9_]*(?::[0-5]?[0-9])+)$''', re.X),
+            list('-+0123456789'))
+
+    Resolver.add_implicit_resolver(
+            'tag:yaml.org,2002:merge',
+            re.compile(r'^(?:<<)$'),
+            ['<'])
+
+    Resolver.add_implicit_resolver(
+            'tag:yaml.org,2002:null',
+            re.compile(r'''^(?: ~
+                        |null|Null|NULL
+                        | )$''', re.X),
+            ['~', 'n', 'N', ''])
+
+    Resolver.add_implicit_resolver(
+            'tag:yaml.org,2002:timestamp',
+            re.compile(r'''^(?:[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]
+                        |[0-9][0-9][0-9][0-9] -[0-9][0-9]? -[0-9][0-9]?
+                        (?:[Tt]|[ \t]+)[0-9][0-9]?
+                        :[0-9][0-9] :[0-9][0-9] (?:\.[0-9]*)?
+                        (?:[ \t]*(?:Z|[-+][0-9][0-9]?(?::[0-9][0-9])?))?)$''', re.X),
+            list('0123456789'))
+
+    Resolver.add_implicit_resolver(
+            'tag:yaml.org,2002:value',
+            re.compile(r'^(?:=)$'),
+            ['='])
+
+    # The following resolver is only for documentation purposes. It cannot work
+    # because plain scalars cannot start with '!', '&', or '*'.
+    Resolver.add_implicit_resolver(
+            'tag:yaml.org,2002:yaml',
+            re.compile(r'^(?:!|&|\*)$'),
+            list('!&*'))
+
+
+setup_resolvers()
diff --git a/tests/free_threading/README.md b/tests/free_threading/README.md
new file mode 100644
index 000000000..9079c5dc6
--- /dev/null
+++ b/tests/free_threading/README.md
@@ -0,0 +1,16 @@
+# Free-threaded tests
+
+This directory aims to collect some tests around free-threading that are
+to be run with [`pytest-run-parallel`](https://github.com/Quansight-Labs/pytest-run-parallel).
+
+Because `Constructor`, `Representer` and `Resolver` classes have been
+adjusted to include thread-local registries, the respective tests make sure
+that the registries are indeed thread-local and thread-safe when changing
+from multiple threads.
+
+`test_stress` aims to add an integration stress test that tests parsing/dumping
+yaml strings and adds some randomness to the process.
+
+`test_real_world` presents a use-case closer to how PyYAML will be used
+in the real world. It creates a directory with a lot of different YAML files
+that resemble those of Github Actions and tries to parse them all in a thread pool.
diff --git a/tests/free_threading/__init__.py b/tests/free_threading/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/free_threading/test_constructor.py b/tests/free_threading/test_constructor.py
new file mode 100644
index 000000000..bbc58dfb5
--- /dev/null
+++ b/tests/free_threading/test_constructor.py
@@ -0,0 +1,36 @@
+import io
+
+import yaml
+
+from .utils import MyTestClass1, construct1
+
+
+class MyLoader(yaml.CLoader):
+    pass
+
+
+def test_default_constructors_registered():
+    yamlcode = io.StringIO("""\
+- !!python/tuple [hello, world]
+""")
+
+    objs = yaml.load(yamlcode, Loader=yaml.CLoader)
+    assert objs == [("hello", "world")]
+
+
+def test_constructor_registration():
+    yaml.add_constructor("!tag1", construct1, Loader=MyLoader)
+
+    yamlcode = io.StringIO("""\
+- !tag1
+  x: 1
+- !tag1
+  x: 1
+  'y': 2
+  z: 3
+""")
+
+    objs = yaml.load(yamlcode, Loader=MyLoader)
+    assert objs == [MyTestClass1(x=1), MyTestClass1(x=1, y=2, z=3)]
+    # deletions from the registry should not impact other threads
+    del MyLoader.yaml_constructors()["!tag1"]
diff --git a/tests/free_threading/test_real_world.py b/tests/free_threading/test_real_world.py
new file mode 100644
index 000000000..56b2c9e02
--- /dev/null
+++ b/tests/free_threading/test_real_world.py
@@ -0,0 +1,108 @@
+import concurrent.futures
+import random
+import threading
+
+import pytest
+
+import yaml
+
+
+NUM_FILES = 200
+NUM_THREADS = 8
+ERROR_RATE = 0.1
+
+barrier = threading.Barrier(NUM_THREADS)
+
+
+def _generate_random_github_workflow():
+    """Generate a random GitHub Actions workflow-like YAML structure."""
+    workflow = {
+        "name": f"Test Workflow {random.randint(1, 1000)}",
+        "on": ["push", "pull_request"],
+        "jobs": {
+            "build": {
+                "runs-on": "ubuntu-latest",
+                "steps": [
+                    {
+                        "name": "Checkout",
+                        "uses": "actions/checkout@v3"
+                    },
+                    {
+                        "name": "Setup Python",
+                        "uses": "actions/setup-python@v4",
+                        "with": {
+                            "python-version": random.choice(["3.8", "3.9", "3.10", "3.11"])
+                        }
+                    },
+                    {
+                        "name": "Run tests",
+                        "run": "pytest"
+                    }
+                ]
+            }
+        }
+    }
+    
+    # Add some randomness to the structure
+    if random.random() > 0.5:
+        workflow["jobs"]["deploy"] = {
+            "runs-on": "ubuntu-latest",
+            "needs": ["build"],
+            "steps": [
+                {
+                    "name": "Deploy step",
+                    "run": "echo Deploying..."
+                }
+            ]
+        }
+    
+    return workflow
+
+
+def _create_yaml_file(directory, index, valid=True):
+    barrier.wait()
+
+    filepath = directory / f"workflow_{index}.yml"
+    workflow = _generate_random_github_workflow()
+    with open(filepath, 'w') as f:
+        if valid:
+            yaml.dump(workflow, f, Dumper=yaml.CDumper)
+        else:
+            # Introduce an error in the YAML structure
+            f.write("name: Invalid Workflow\non: [push\njobs:")
+    return filepath, workflow, valid
+
+
+def _parse_yaml_file(filepath, original_data, expected_valid):
+    barrier.wait()
+
+    try:
+        with open(filepath, 'r') as f:
+            data = yaml.load(f, Loader=yaml.CLoader)
+        assert expected_valid and data == original_data
+    except yaml.YAMLError:
+        assert not expected_valid
+
+
+@pytest.mark.parallel_threads(1)
+def test_multithreaded_yaml_parsing(tmp_path):
+    # Create test files in threads
+    with concurrent.futures.ThreadPoolExecutor(max_workers=NUM_THREADS) as executor:
+        futures = [
+            executor.submit(_create_yaml_file, tmp_path, i, random.random() > ERROR_RATE)
+            for i in range(NUM_FILES)
+        ]
+        
+        file_info = []
+        for future in concurrent.futures.as_completed(futures):
+            filepath, workflow, valid = future.result()
+            file_info.append((filepath, workflow, valid))
+
+    # Parse files using thread pool
+    with concurrent.futures.ThreadPoolExecutor(max_workers=NUM_THREADS) as executor:
+        future_to_file = [
+            executor.submit(_parse_yaml_file, filepath, workflow, valid)
+            for filepath, workflow, valid in file_info
+        ]
+        for future in concurrent.futures.as_completed(future_to_file):
+            future.result()  # Asserts that this does not raise any assertion errors
diff --git a/tests/free_threading/test_representer.py b/tests/free_threading/test_representer.py
new file mode 100644
index 000000000..003f9ee36
--- /dev/null
+++ b/tests/free_threading/test_representer.py
@@ -0,0 +1,36 @@
+import yaml
+
+from .utils import MyTestClass1, represent1
+
+class MyDumper(yaml.CDumper):
+    pass
+
+
+def test_default_representers_registered():
+    obj = [("hello", "world")]
+
+    yamlcode = yaml.dump(obj, Dumper=yaml.CDumper)
+    assert yamlcode == """\
+- !!python/tuple
+  - hello
+  - world
+"""
+
+
+def test_representer_registration():
+    yaml.add_representer(MyTestClass1, represent1, Dumper=MyDumper)
+
+    obj = [MyTestClass1(x=1), MyTestClass1(x=1, y=2, z=3)]
+
+    yamlcode = yaml.dump(obj, Dumper=MyDumper)
+    assert yamlcode == """\
+- !tag1
+  x: 1
+  y: 0
+  z: 0
+- !tag1
+  x: 1
+  y: 2
+  z: 3
+"""
+    del MyDumper.yaml_representers()[MyTestClass1]
diff --git a/tests/free_threading/test_resolver.py b/tests/free_threading/test_resolver.py
new file mode 100644
index 000000000..2d7039daa
--- /dev/null
+++ b/tests/free_threading/test_resolver.py
@@ -0,0 +1,107 @@
+import re
+import yaml
+
+from .utils import Dice, dice_representer
+
+
+def _convert_node(node):
+    if isinstance(node, yaml.ScalarNode):
+        return (node.tag, node.value)
+    elif isinstance(node, yaml.SequenceNode):
+        value = []
+        for item in node.value:
+            value.append(_convert_node(item))
+        return (node.tag, value)
+    elif isinstance(node, yaml.MappingNode):
+        value = []
+        for key, item in node.value:
+            value.append((_convert_node(key), _convert_node(item)))
+        return (node.tag, value)
+
+
+def test_default_implicit_resolvers_registered():
+    yamlcode = """\
+- [1, 2, 3]
+- 2
+"""
+
+    node = yaml.compose(yamlcode, Loader=yaml.CLoader)
+    assert isinstance(node, yaml.SequenceNode)
+    assert isinstance(node.value[0], yaml.SequenceNode)
+    for scalar in node.value[0].value:
+        assert isinstance(scalar, yaml.ScalarNode)
+        assert scalar.tag == 'tag:yaml.org,2002:int'
+    assert isinstance(node.value[1], yaml.ScalarNode)
+    assert node.value[1].tag == 'tag:yaml.org,2002:int'
+
+
+class ImplicitResolverLoader(yaml.CLoader):
+    pass
+
+
+class ImplicitResolverDumper(yaml.CDumper):
+    pass
+
+
+def test_implicit_resolver_registration():
+    yaml.add_representer(Dice, dice_representer, Dumper=ImplicitResolverDumper)
+    yaml.add_implicit_resolver('!dice', re.compile(r'^\d+d\d+$'), Loader=ImplicitResolverLoader, Dumper=ImplicitResolverDumper)
+
+    yamlcode = """\
+- 1d4
+- 2d6
+- 3d8
+"""
+    node = yaml.compose(yamlcode, Loader=ImplicitResolverLoader)
+    assert isinstance(node, yaml.SequenceNode)
+    for scalar in node.value:
+        assert isinstance(scalar, yaml.ScalarNode)
+        assert scalar.tag == '!dice'
+
+
+class PathResolverLoader(yaml.CLoader):
+    pass
+
+
+class PathResolverDumper(yaml.CDumper):
+    pass
+
+
+def test_path_resolver_loader():
+    yaml.add_path_resolver('!root', [],
+            Loader=PathResolverLoader, Dumper=PathResolverDumper)
+    yaml.add_path_resolver('!root/scalar', [], str,
+            Loader=PathResolverLoader, Dumper=PathResolverDumper)
+    yaml.add_path_resolver('!root/key11/key12/*', ['key11', 'key12'],
+            Loader=PathResolverLoader, Dumper=PathResolverDumper)
+
+    yamlcode = """\
+---
+"this scalar should be selected"
+---
+key11: !foo
+    key12:
+        is: [selected]
+    key22:
+        key13: [not, selected]
+        key23: [not, selected]
+"""
+
+    resolved_yamlcode = """\
+--- !root/scalar
+"this scalar should be selected"
+--- !root
+key11: !foo
+    key12: !root/key11/key12/*
+        is: [selected]
+    key22:
+        key13: [not, selected]
+        key23: [not, selected]
+"""
+
+    nodes1 = list(yaml.compose_all(yamlcode, Loader=PathResolverLoader))
+    nodes2 = list(yaml.compose_all(resolved_yamlcode, Loader=PathResolverLoader))
+    for node1, node2 in zip(nodes1, nodes2):
+        data1 = _convert_node(node1)
+        data2 = _convert_node(node2)
+        assert data1 == data2
diff --git a/tests/free_threading/test_stress.py b/tests/free_threading/test_stress.py
new file mode 100644
index 000000000..a857b3e52
--- /dev/null
+++ b/tests/free_threading/test_stress.py
@@ -0,0 +1,163 @@
+import random
+import re
+import threading
+
+import yaml
+
+from .utils import Dice, dice_constructor
+
+# Different YAML content types for testing
+YAML_LOAD_SAMPLES = [
+    # Simple key-value pairs
+    ("""\
+key1: value1
+key2: value2
+key3: 123
+key4: true
+""", {
+        "key1": "value1",
+        "key2": "value2",
+        "key3": 123,
+        "key4": True
+    }),
+    
+    # Nested structures
+    ("""\
+config:
+  database:
+    host: localhost
+    port: 5432
+    credentials:
+      username: admin
+      password: secret
+  logging:
+    level: INFO
+    file: /var/log/app.log
+""", {
+        "config": {
+            "database": {
+                "host": "localhost",
+                "port": 5432,
+                "credentials": {
+                    "username": "admin",
+                    "password": "secret"
+                }
+            },
+            "logging": {
+                "level": "INFO",
+                "file": "/var/log/app.log"
+            }
+        }
+    }),
+
+    # Lists
+    ("""\
+fruits:
+  - apple
+  - banana
+  - orange
+numbers: [1, 2, 3, 4, 5]
+mixed:
+  - name: John
+    age: 30
+  - name: Alice
+    age: 25
+""", {
+        "fruits": ["apple", "banana", "orange"],
+        "numbers": [1, 2, 3, 4, 5],
+        "mixed": [
+            {"name": "John", "age": 30},
+            {"name": "Alice", "age": 25}
+        ]
+    }),
+    
+    # Complex with references
+    ("""\
+defaults: &defaults
+  adapter: postgresql
+  host: localhost
+
+development:
+  database: myapp_development
+  <<: *defaults
+
+test:
+  database: myapp_test
+  <<: *defaults
+""", {
+        "defaults": {
+            "adapter": "postgresql",
+            "host": "localhost"
+        },
+        "development": {
+            "database": "myapp_development",
+            "adapter": "postgresql",
+            "host": "localhost"
+        },
+        "test": {
+            "database": "myapp_test",
+            "adapter": "postgresql",
+            "host": "localhost"
+        }
+    }),
+
+    # Dice with resolver
+    ("""\
+rolls_resolver:
+  - 1d6
+  - 2d4
+  - 3d1
+""", {
+        "rolls_resolver": [
+            Dice(1, 6),
+            Dice(2, 4),
+            Dice(3, 1)
+        ]
+    }),
+
+    # Dice without resolver
+    ("""\
+rolls_no_resolver:
+  - 1d6
+  - 2d4
+  - 3d1
+""", {
+        "rolls_no_resolver": [
+            "1d6",
+            "2d4",
+            "3d1"
+        ]
+    }),
+]
+
+
+class MyLoader(yaml.CLoader):
+    pass
+
+
+class MyDumper(yaml.CDumper):
+    pass
+
+
+def test_yaml_load_stress():
+    yamlcode, result = random.choice(YAML_LOAD_SAMPLES)
+    thread_id = threading.current_thread().name
+    randint = random.randint(1, 1000)
+    yamlcode += f"\nrandom_value_{thread_id}: {randint}"
+
+
+    constructor, resolver = False, False
+    if yamlcode.startswith("rolls"):
+        yaml.add_constructor("!dice", dice_constructor, Loader=MyLoader)
+        constructor = True
+        if yamlcode.startswith("rolls_resolver"):
+            yaml.add_implicit_resolver('!dice', re.compile(r'^\d+d\d+$'),
+                                        Loader=MyLoader, Dumper=MyDumper)
+            resolver = True
+
+    obj = yaml.load(yamlcode, Loader=MyLoader)
+    assert obj == {**result, f"random_value_{thread_id}": randint}
+    if constructor:
+        del MyLoader.yaml_constructors()["!dice"]
+    if resolver:
+        del MyLoader.yaml_implicit_resolvers()[None]
diff --git a/tests/free_threading/utils.py b/tests/free_threading/utils.py
new file mode 100644
index 000000000..b5f52ab5d
--- /dev/null
+++ b/tests/free_threading/utils.py
@@ -0,0 +1,41 @@
+class MyTestClass1:
+    def __init__(self, x, y=0, z=0):
+        self.x = x
+        self.y = y
+        self.z = z
+
+    def __eq__(self, other):
+        if isinstance(other, MyTestClass1):
+            return self.__class__, self.__dict__ == other.__class__, other.__dict__
+        else:
+            return False
+
+    def __repr__(self):
+        return f"MyTestClass1(x={self.x}, y={self.y}, z={self.z})"
+
+
+def represent1(representer, native):
+    return representer.represent_mapping("!tag1", native.__dict__)
+
+
+def construct1(constructor, node):
+    mapping = constructor.construct_mapping(node)
+    return MyTestClass1(**mapping)
+
+
+class Dice(tuple):
+    def __new__(cls, a, b):
+        return tuple.__new__(cls, (a, b))
+
+    def __repr__(self):
+        return "Dice(%s,%s)" % self
+
+
+def dice_constructor(loader, node):
+    value = loader.construct_scalar(node)
+    a, b = map(int, value.split('d'))
+    return Dice(a, b)
+
+
+def dice_representer(dumper, data):
+    return dumper.represent_scalar("!dice", "%sd%s" % data)
diff --git a/yaml/_yaml.pyx b/yaml/_yaml.pyx
index e3e93e2ce..080daff66 100644
--- a/yaml/_yaml.pyx
+++ b/yaml/_yaml.pyx
@@ -1,3 +1,4 @@
+# cython: freethreading_compatible=True
 
 import yaml
 


Reviews:
user: login: ngoldbaum
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-04T18:57:49Z
user: login: ngoldbaum
body: Maybe it makes sense to define a new helper that handles all the single-initialization details? There's a decent amount of duplicated code around dealing with caches that should only be initialized once.
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-04T19:11:39Z
user: login: ngoldbaum
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-04T19:18:24Z
user: login: lysnikolaou
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-06T12:12:17Z
user: login: lysnikolaou
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-06T12:13:55Z
user: login: ngoldbaum
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-06T15:22:47Z
user: login: lysnikolaou
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-06T16:09:16Z
user: login: ngoldbaum
body: Sorry for taking a few days to share the comments from our discussion.

With the context you shared I think this is close, we just want to make these changes a little more self-explanatory so it's clear why the changes were necessary, that the new tests are useful, and why the thread-local registries are worth the small amount of extra complexity.
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-11T18:50:58Z
user: login: lysnikolaou
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-18T12:29:12Z
user: login: lysnikolaou
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-18T12:29:20Z
user: login: lysnikolaou
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-18T12:29:38Z
user: login: lysnikolaou
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-18T12:30:11Z
user: login: lysnikolaou
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-18T12:30:22Z
user: login: lysnikolaou
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-18T12:31:19Z
user: login: lysnikolaou
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-18T12:31:35Z
user: login: ngoldbaum
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-18T21:01:15Z
user: login: ngoldbaum
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-18T21:02:25Z
user: login: ngoldbaum
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-18T21:03:40Z
user: login: ngoldbaum
body: This is much easier to understand now. Let's merge this so the `main` branch of the repo is thread-safe 🚀 :shipit: 
state: APPROVED
author_association: NONE
submitted_at: 2025-04-18T21:08:20Z

Review comments:
diff_hunk: @@ -0,0 +1,180 @@
+import random
+import re
+import threading
+
+import yaml
+
+
+class Dice(tuple):
+    def __new__(cls, a, b):
+        return tuple.__new__(cls, (a, b))
+
+    def __repr__(self):
+        return "Dice(%s,%s)" % self
+
+
+def dice_constructor(loader, node):
+    value = loader.construct_scalar(node)
+    a, b = map(int, value.split('d'))
+    return Dice(a, b)
+
+
+def dice_representer(dumper, data):
+    return dumper.represent_scalar("!dice", "%sd%s" % data)
+
+
+# Different YAML content types for testing
+YAML_LOAD_SAMPLES = [
+    # Simple key-value pairs
+    ("""\
+key1: value1
+key2: value2
+key3: 123
+key4: true
+""", {
+        "key1": "value1",
+        "key2": "value2",
+        "key3": 123,
+        "key4": True
+    }),
+    
+    # Nested structures
+    ("""\
+config:
+  database:
+    host: localhost
+    port: 5432
+    credentials:
+      username: admin
+      password: secret
+  logging:
+    level: INFO
+    file: /var/log/app.log
+""", {
+        "config": {
+            "database": {
+                "host": "localhost",
+                "port": 5432,
+                "credentials": {
+                    "username": "admin",
+                    "password": "secret"
+                }
+            },
+            "logging": {
+                "level": "INFO",
+                "file": "/var/log/app.log"
+            }
+        }
+    }),
+
+    # Lists
+    ("""\
+fruits:
+  - apple
+  - banana
+  - orange
+numbers: [1, 2, 3, 4, 5]
+mixed:
+  - name: John
+    age: 30
+  - name: Alice
+    age: 25
+""", {
+        "fruits": ["apple", "banana", "orange"],
+        "numbers": [1, 2, 3, 4, 5],
+        "mixed": [
+            {"name": "John", "age": 30},
+            {"name": "Alice", "age": 25}
+        ]
+    }),
+    
+    # Complex with references
+    ("""\
+defaults: &defaults
+  adapter: postgresql
+  host: localhost
+
+development:
+  database: myapp_development
+  <<: *defaults
+
+test:
+  database: myapp_test
+  <<: *defaults
+""", {
+        "defaults": {
+            "adapter": "postgresql",
+            "host": "localhost"
+        },
+        "development": {
+            "database": "myapp_development",
+            "adapter": "postgresql",
+            "host": "localhost"
+        },
+        "test": {
+            "database": "myapp_test",
+            "adapter": "postgresql",
+            "host": "localhost"
+        }
+    }),
+
+    # Dice with resolver
+    ("""\
+rolls_resolver:
+  - 1d6
+  - 2d4
+  - 3d1
+""", {
+        "rolls_resolver": [
+            Dice(1, 6),
+            Dice(2, 4),
+            Dice(3, 1)
+        ]
+    }),
+
+    # Dice without resolver
+    ("""\
+rolls_no_resolver:
+  - 1d6
+  - 2d4
+  - 3d1
+""", {
+        "rolls_no_resolver": [
+            "1d6",
+            "2d4",
+            "3d1"
+        ]
+    }),
+]
+
+
+class MyLoader(yaml.CLoader):
+    pass
+
+
+class MyDumper(yaml.CDumper):
+    pass
+
+
+def test_yaml_load_stress():
+    yamlcode, result = random.choice(YAML_LOAD_SAMPLES)
+    thread_id = threading.current_thread().name
path: tests/free_threading/test_stress.py
user: login: ngoldbaum
body: Just giving this PR an initial once-over.

I would have expected a free-threading stress test to spawn some threads.

Can we get one or more tests that use threads? Ideally the test should be runnable against upstream PyYAML and fail. Bonus points if it fails on the GIL-enabled build, e.g. with the thread switch interval set to 100 nanoseconds.
created_at: 2025-04-04T18:57:49Z
author_association: NONE
diff_hunk: @@ -10,16 +10,29 @@
 
 from .error import *
 from .nodes import *
+from .registry import RegistryMeta, ConstructorRegistry
 
+import threading
 import collections.abc, datetime, base64, binascii, re, sys, types
 
 class ConstructorError(MarkedYAMLError):
     pass
 
-class BaseConstructor:
 
-    yaml_constructors = {}
-    yaml_multi_constructors = {}
+class ConstructorSetup(threading.local):
+    def __init__(self):
+        self.initialized = False
+
+    def ensure_initialized(self):
+        if not self.initialized:
+            self.initialized = True
path: lib/yaml/constructor.py
user: login: ngoldbaum
body: Are these two lines guaranteed to be atomic? I kind of doubt it - you might need to use a mutex too.
created_at: 2025-04-04T19:01:00Z
author_association: NONE
diff_hunk: @@ -4,16 +4,29 @@
 
 from .error import *
 from .nodes import *
+from .registry import RegistryMeta, RepresenterRegistry
 
+import threading
 import datetime, copyreg, types, base64, collections
 
 class RepresenterError(YAMLError):
     pass
 
-class BaseRepresenter:
 
-    yaml_representers = {}
-    yaml_multi_representers = {}
+class RepresenterSetup(threading.local):
+    def __init__(self):
+        self.initialized = False
+
+    def ensure_initialized(self):
+        if not self.initialized:
+            self.initialized = True
path: lib/yaml/representer.py
user: login: ngoldbaum
body: same issue
created_at: 2025-04-04T19:09:13Z
author_association: NONE
diff_hunk: @@ -44,35 +57,67 @@ def represent_data(self, data):
             #self.represented_objects[alias_key] = None
             self.object_keeper.append(data)
         data_types = type(data).__mro__
-        if data_types[0] in self.yaml_representers:
-            node = self.yaml_representers[data_types[0]](self, data)
+        yaml_representers = self.yaml_representers()
+        yaml_multi_representers = self.yaml_multi_representers()
+        if data_types[0] in yaml_representers:
+            node = yaml_representers[data_types[0]](self, data)
         else:
             for data_type in data_types:
-                if data_type in self.yaml_multi_representers:
-                    node = self.yaml_multi_representers[data_type](self, data)
+                if data_type in yaml_multi_representers:
+                    node = yaml_multi_representers[data_type](self, data)
                     break
             else:
-                if None in self.yaml_multi_representers:
-                    node = self.yaml_multi_representers[None](self, data)
-                elif None in self.yaml_representers:
-                    node = self.yaml_representers[None](self, data)
+                if None in yaml_multi_representers:
+                    node = yaml_multi_representers[None](self, data)
+                elif None in yaml_representers:
+                    node = yaml_representers[None](self, data)
                 else:
                     node = ScalarNode(None, str(data))
         #if alias_key is not None:
         #    self.represented_objects[alias_key] = node
         return node
 
+    @classmethod
+    def yaml_representers(cls):
+        cls.representer_setup.ensure_initialized()
+
+        if cls.representer_registry.yaml_representers_initialized:
+            return cls.representer_registry.yaml_representers
+
+        constructor_cls = next(c for c in cls.mro() if hasattr(c, 'yaml_representers') and c is not cls)
+        return constructor_cls.yaml_representers()
+
+    @classmethod
+    def yaml_multi_representers(cls):
+        cls.representer_setup.ensure_initialized()
+
+        if cls.representer_registry.yaml_multi_representers_initialized:
+            return cls.representer_registry.yaml_multi_representers
path: lib/yaml/representer.py
user: login: ngoldbaum
body: I think you should be able to do the quick return if it's initialized *before* calling `ensure_initialized`, also anywhere else this pattern happens in this PR, I didn't search carefully.
created_at: 2025-04-04T19:10:07Z
author_association: NONE
diff_hunk: @@ -10,16 +10,29 @@
 
 from .error import *
 from .nodes import *
+from .registry import RegistryMeta, ConstructorRegistry
 
+import threading
 import collections.abc, datetime, base64, binascii, re, sys, types
 
 class ConstructorError(MarkedYAMLError):
     pass
 
-class BaseConstructor:
 
-    yaml_constructors = {}
-    yaml_multi_constructors = {}
+class ConstructorSetup(threading.local):
+    def __init__(self):
+        self.initialized = False
+
+    def ensure_initialized(self):
+        if not self.initialized:
+            self.initialized = True
+            setup_constructors()
path: lib/yaml/constructor.py
user: login: ngoldbaum
body: also I don't think you can say this is initialized until this function is complete, so I think you need to switch the order here to avoid a thread possibly seeing an "initialized" object before `setup_constructors()` finishes.

(which of course makes the atomicity issue worse)
created_at: 2025-04-04T19:18:24Z
author_association: NONE
diff_hunk: @@ -10,16 +10,29 @@
 
 from .error import *
 from .nodes import *
+from .registry import RegistryMeta, ConstructorRegistry
 
+import threading
 import collections.abc, datetime, base64, binascii, re, sys, types
 
 class ConstructorError(MarkedYAMLError):
     pass
 
-class BaseConstructor:
 
-    yaml_constructors = {}
-    yaml_multi_constructors = {}
+class ConstructorSetup(threading.local):
+    def __init__(self):
+        self.initialized = False
+
+    def ensure_initialized(self):
+        if not self.initialized:
+            self.initialized = True
path: lib/yaml/constructor.py
user: login: lysnikolaou
body: Because `ConstructorSetup` and all of the other `*Setup` and `*Registry` classes inherit from `threading.local`, their instance variables are thread-local and `__init__` is called the first time any one of the instance variables is accessed in a new thread. So atomicity isn't really needed here.
created_at: 2025-04-06T12:12:17Z
author_association: NONE
diff_hunk: @@ -10,16 +10,29 @@
 
 from .error import *
 from .nodes import *
+from .registry import RegistryMeta, ConstructorRegistry
 
+import threading
 import collections.abc, datetime, base64, binascii, re, sys, types
 
 class ConstructorError(MarkedYAMLError):
     pass
 
-class BaseConstructor:
 
-    yaml_constructors = {}
-    yaml_multi_constructors = {}
+class ConstructorSetup(threading.local):
+    def __init__(self):
+        self.initialized = False
+
+    def ensure_initialized(self):
+        if not self.initialized:
+            self.initialized = True
+            setup_constructors()
path: lib/yaml/constructor.py
user: login: lysnikolaou
body: Again, since atomicity isn't an issue here, I'm setting `self.initialized` before calling `setup_constructors` here to avoid an infinite recursion. `setup_constructors` calls `add_constructor` that eventually checks for this `self.initialized` again.
created_at: 2025-04-06T12:13:55Z
author_association: NONE
diff_hunk: @@ -10,16 +10,29 @@
 
 from .error import *
 from .nodes import *
+from .registry import RegistryMeta, ConstructorRegistry
 
+import threading
 import collections.abc, datetime, base64, binascii, re, sys, types
 
 class ConstructorError(MarkedYAMLError):
     pass
 
-class BaseConstructor:
 
-    yaml_constructors = {}
-    yaml_multi_constructors = {}
+class ConstructorSetup(threading.local):
+    def __init__(self):
+        self.initialized = False
+
+    def ensure_initialized(self):
+        if not self.initialized:
+            self.initialized = True
+            setup_constructors()
path: lib/yaml/constructor.py
user: login: ngoldbaum
body: Thanks, clearly I misunderstood the structure. Maybe you can walk me through this PR in detail sometime next week?
created_at: 2025-04-06T15:22:47Z
author_association: NONE
diff_hunk: @@ -10,16 +10,29 @@
 
 from .error import *
 from .nodes import *
+from .registry import RegistryMeta, ConstructorRegistry
 
+import threading
 import collections.abc, datetime, base64, binascii, re, sys, types
 
 class ConstructorError(MarkedYAMLError):
     pass
 
-class BaseConstructor:
 
-    yaml_constructors = {}
-    yaml_multi_constructors = {}
+class ConstructorSetup(threading.local):
+    def __init__(self):
+        self.initialized = False
+
+    def ensure_initialized(self):
+        if not self.initialized:
+            self.initialized = True
+            setup_constructors()
path: lib/yaml/constructor.py
user: login: lysnikolaou
body: Sure!
created_at: 2025-04-06T16:09:16Z
author_association: NONE
diff_hunk: @@ -0,0 +1,39 @@
+import threading
+
+
+class ConstructorRegistry(threading.local):
+    def __init__(self, yaml_constructors_initialized=False, yaml_multi_constructors_initialized=False):
+        self.yaml_constructors = {}
+        self.yaml_multi_constructors = {}
+        self.yaml_constructors_initialized = yaml_constructors_initialized
+        self.yaml_multi_constructors_initialized = yaml_multi_constructors_initialized
+
+
+class RepresenterRegistry(threading.local):
+    def __init__(self, yaml_representers_initialized=False, yaml_multi_representers_initialized=False):
+        self.yaml_representers = {}
+        self.yaml_multi_representers = {}
+        self.yaml_representers_initialized = yaml_representers_initialized
+        self.yaml_multi_representers_initialized = yaml_multi_representers_initialized
+
+
+class ResolverRegistry(threading.local):
+    def __init__(self, yaml_implicit_resolvers_initialized=False, yaml_path_resolvers_initialized=False):
+        self.yaml_implicit_resolvers = {}
+        self.yaml_path_resolvers = {}
+        self.yaml_implicit_resolvers_initialized = yaml_implicit_resolvers_initialized
+        self.yaml_path_resolvers_initialized = yaml_path_resolvers_initialized
+
+
+class RegistryMeta(type):
+    """Metaclass to handle registry inheritance"""
+    
+    def __new__(cls, name, bases, attrs):
+        cls = super().__new__(cls, name, bases, attrs)
+        if 'constructor_registry' not in cls.__dict__:
+            cls.constructor_registry = ConstructorRegistry()
+        if 'representer_registry' not in cls.__dict__:
+            cls.representer_registry = RepresenterRegistry()
+        if 'resolver_registry' not in cls.__dict__:
+            cls.resolver_registry = ResolverRegistry()
+        return cls
path: lib/yaml/registry.py
user: login: ngoldbaum
body: These registry classes share a lot of code, can we refactor so e.g. each registry is an instance of the registry type?
created_at: 2025-04-11T18:38:27Z
author_association: NONE
diff_hunk: @@ -10,16 +10,29 @@
 
 from .error import *
 from .nodes import *
+from .registry import RegistryMeta, ConstructorRegistry
 
+import threading
 import collections.abc, datetime, base64, binascii, re, sys, types
 
 class ConstructorError(MarkedYAMLError):
     pass
 
-class BaseConstructor:
 
-    yaml_constructors = {}
-    yaml_multi_constructors = {}
+class ConstructorSetup(threading.local):
+    def __init__(self):
+        self.initialized = False
+
+    def ensure_initialized(self):
+        if not self.initialized:
+            self.initialized = True
+            setup_constructors()
+
+
+class BaseConstructor(metaclass=RegistryMeta):
path: lib/yaml/constructor.py
user: login: ngoldbaum
body: Can we get an ASCII art diagram that illustrates the inheritance? Like the excalidraw diagram you showed me on our call. asciiflow.com is good for this.
created_at: 2025-04-11T18:39:28Z
author_association: NONE
diff_hunk: @@ -0,0 +1,180 @@
+import random
+import re
+import threading
+
+import yaml
+
+
+class Dice(tuple):
+    def __new__(cls, a, b):
+        return tuple.__new__(cls, (a, b))
+
+    def __repr__(self):
+        return "Dice(%s,%s)" % self
+
+
+def dice_constructor(loader, node):
+    value = loader.construct_scalar(node)
+    a, b = map(int, value.split('d'))
+    return Dice(a, b)
+
+
+def dice_representer(dumper, data):
+    return dumper.represent_scalar("!dice", "%sd%s" % data)
+
+
+# Different YAML content types for testing
+YAML_LOAD_SAMPLES = [
+    # Simple key-value pairs
+    ("""\
+key1: value1
+key2: value2
+key3: 123
+key4: true
+""", {
+        "key1": "value1",
+        "key2": "value2",
+        "key3": 123,
+        "key4": True
+    }),
+    
+    # Nested structures
+    ("""\
+config:
+  database:
+    host: localhost
+    port: 5432
+    credentials:
+      username: admin
+      password: secret
+  logging:
+    level: INFO
+    file: /var/log/app.log
+""", {
+        "config": {
+            "database": {
+                "host": "localhost",
+                "port": 5432,
+                "credentials": {
+                    "username": "admin",
+                    "password": "secret"
+                }
+            },
+            "logging": {
+                "level": "INFO",
+                "file": "/var/log/app.log"
+            }
+        }
+    }),
+
+    # Lists
+    ("""\
+fruits:
+  - apple
+  - banana
+  - orange
+numbers: [1, 2, 3, 4, 5]
+mixed:
+  - name: John
+    age: 30
+  - name: Alice
+    age: 25
+""", {
+        "fruits": ["apple", "banana", "orange"],
+        "numbers": [1, 2, 3, 4, 5],
+        "mixed": [
+            {"name": "John", "age": 30},
+            {"name": "Alice", "age": 25}
+        ]
+    }),
+    
+    # Complex with references
+    ("""\
+defaults: &defaults
+  adapter: postgresql
+  host: localhost
+
+development:
+  database: myapp_development
+  <<: *defaults
+
+test:
+  database: myapp_test
+  <<: *defaults
+""", {
+        "defaults": {
+            "adapter": "postgresql",
+            "host": "localhost"
+        },
+        "development": {
+            "database": "myapp_development",
+            "adapter": "postgresql",
+            "host": "localhost"
+        },
+        "test": {
+            "database": "myapp_test",
+            "adapter": "postgresql",
+            "host": "localhost"
+        }
+    }),
+
+    # Dice with resolver
+    ("""\
+rolls_resolver:
+  - 1d6
+  - 2d4
+  - 3d1
+""", {
+        "rolls_resolver": [
+            Dice(1, 6),
+            Dice(2, 4),
+            Dice(3, 1)
+        ]
+    }),
+
+    # Dice without resolver
+    ("""\
+rolls_no_resolver:
+  - 1d6
+  - 2d4
+  - 3d1
+""", {
+        "rolls_no_resolver": [
+            "1d6",
+            "2d4",
+            "3d1"
+        ]
+    }),
+]
+
+
+class MyLoader(yaml.CLoader):
+    pass
+
+
+class MyDumper(yaml.CDumper):
+    pass
+
+
+def test_yaml_load_stress():
+    yamlcode, result = random.choice(YAML_LOAD_SAMPLES)
+    thread_id = threading.current_thread().name
path: tests/free_threading/test_stress.py
user: login: ngoldbaum
body: We talked about this on our call. There's going to be a follow-up that adds pytest-run-parallel in CI, which makes this a real stress test. Also, we're going to add some explicitly mulithreaded tests of a real-world YAML parsing workflow (e.g. parsing a directory full of YAML files in a thread pool).
created_at: 2025-04-11T18:41:18Z
author_association: NONE
diff_hunk: @@ -0,0 +1,54 @@
+import io
+
+import yaml
+
+
+class MyLoader(yaml.CLoader):
+    pass
+
+
+class MyTestClass1:
+    def __init__(self, x, y=0, z=0):
+        self.x = x
+        self.y = y
+        self.z = z
+
+    def __eq__(self, other):
+        if isinstance(other, MyTestClass1):
+            return self.__class__, self.__dict__ == other.__class__, other.__dict__
+        else:
+            return False
+
+    def __repr__(self):
+        return f"MyTestClass1(x={self.x}, y={self.y}, z={self.z})"
+
+
+def construct1(constructor, node):
+    mapping = constructor.construct_mapping(node)
+    return MyTestClass1(**mapping)
+
+
+def test_default_constructors_registered():
+    yamlcode = io.StringIO("""\
+- !!python/tuple [hello, world]
+""")
+
+    objs = yaml.load(yamlcode, Loader=yaml.CLoader)
+    assert objs == [("hello", "world")]
+
+
+def test_constructor_registration():
+    yaml.add_constructor("!tag1", construct1, Loader=MyLoader)
+
+    yamlcode = io.StringIO("""\
+- !tag1
+  x: 1
+- !tag1
+  x: 1
+  'y': 2
+  z: 3
+""")
+
+    objs = yaml.load(yamlcode, Loader=MyLoader)
+    assert objs == [MyTestClass1(x=1), MyTestClass1(x=1, y=2, z=3)]
+    del MyLoader.yaml_constructors()["!tag1"]
path: tests/free_threading/test_constructor.py
user: login: ngoldbaum
body: What's the point of deleting this here?
created_at: 2025-04-11T18:42:26Z
author_association: NONE
diff_hunk: @@ -0,0 +1,51 @@
+import yaml
+
+
+class MyDumper(yaml.CDumper):
+    pass
+
+
+class MyTestClass1:
+    def __init__(self, x, y=0, z=0):
+        self.x = x
+        self.y = y
+        self.z = z
+    def __eq__(self, other):
+        if isinstance(other, MyTestClass1):
+            return self.__class__, self.__dict__ == other.__class__, other.__dict__
+        else:
+            return False
+
+
+def represent1(representer, native):
+    return representer.represent_mapping("!tag1", native.__dict__)
+
+
+def test_default_representers_registered():
+    obj = [("hello", "world")]
+
+    yamlcode = yaml.dump(obj, Dumper=yaml.CDumper)
+    assert yamlcode == """\
+- !!python/tuple
+  - hello
+  - world
+"""
+
+
+def test_representer_registration():
+    yaml.add_representer(MyTestClass1, represent1, Dumper=MyDumper)
+
+    obj = [MyTestClass1(x=1), MyTestClass1(x=1, y=2, z=3)]
+
+    yamlcode = yaml.dump(obj, Dumper=MyDumper)
+    assert yamlcode == """\
+- !tag1
+  x: 1
+  y: 0
+  z: 0
+- !tag1
+  x: 1
+  y: 2
+  z: 3
+"""
+    del MyDumper.yaml_representers()[MyTestClass1]
path: tests/free_threading/test_representer.py
user: login: ngoldbaum
body: here as well.

The structure of `test_representer` and `test_constructor` are really similar. Does it make sense to combine them maybe?
created_at: 2025-04-11T18:43:52Z
author_association: NONE
diff_hunk: @@ -0,0 +1,180 @@
+import random
+import re
+import threading
+
+import yaml
+
+
+class Dice(tuple):
+    def __new__(cls, a, b):
+        return tuple.__new__(cls, (a, b))
+
+    def __repr__(self):
+        return "Dice(%s,%s)" % self
path: tests/free_threading/test_stress.py
user: login: ngoldbaum
body: This is shared with `test_resolver` - maybe refactor common stuff into e.g. an `utils.py`
created_at: 2025-04-11T18:44:45Z
author_association: NONE
path: tests/free_threading/test_constructor.py
user: login: ngoldbaum
body: Consider adding e.g. a README.md file to this directory that explains these tests are stress tests of the free-threaded build when run under pytest-run-parallel and are designed to trigger races by e.g. registering something at runtime. Without that context it's hard to understand why these tests have anything to do with the multithreading.
created_at: 2025-04-11T18:46:45Z
author_association: NONE
diff_hunk: @@ -0,0 +1,39 @@
+import threading
+
+
+class ConstructorRegistry(threading.local):
+    def __init__(self, yaml_constructors_initialized=False, yaml_multi_constructors_initialized=False):
+        self.yaml_constructors = {}
+        self.yaml_multi_constructors = {}
+        self.yaml_constructors_initialized = yaml_constructors_initialized
+        self.yaml_multi_constructors_initialized = yaml_multi_constructors_initialized
+
+
+class RepresenterRegistry(threading.local):
+    def __init__(self, yaml_representers_initialized=False, yaml_multi_representers_initialized=False):
+        self.yaml_representers = {}
+        self.yaml_multi_representers = {}
+        self.yaml_representers_initialized = yaml_representers_initialized
+        self.yaml_multi_representers_initialized = yaml_multi_representers_initialized
+
+
+class ResolverRegistry(threading.local):
+    def __init__(self, yaml_implicit_resolvers_initialized=False, yaml_path_resolvers_initialized=False):
+        self.yaml_implicit_resolvers = {}
+        self.yaml_path_resolvers = {}
+        self.yaml_implicit_resolvers_initialized = yaml_implicit_resolvers_initialized
+        self.yaml_path_resolvers_initialized = yaml_path_resolvers_initialized
+
+
+class RegistryMeta(type):
+    """Metaclass to handle registry inheritance"""
+    
+    def __new__(cls, name, bases, attrs):
+        cls = super().__new__(cls, name, bases, attrs)
+        if 'constructor_registry' not in cls.__dict__:
+            cls.constructor_registry = ConstructorRegistry()
+        if 'representer_registry' not in cls.__dict__:
+            cls.representer_registry = RepresenterRegistry()
+        if 'resolver_registry' not in cls.__dict__:
+            cls.resolver_registry = ResolverRegistry()
+        return cls
path: lib/yaml/registry.py
user: login: lysnikolaou
body: Hmm, I'm out of ideas on how to do the refactor though and most of what I've tried ends up being even more unclear. The problem is that each class has a different name for their registries (`constructor_registry` vs `resolver_registry`), which makes it a bit hard to share code. Do you have any ideas?
created_at: 2025-04-18T12:29:12Z
author_association: NONE
diff_hunk: @@ -10,16 +10,29 @@
 
 from .error import *
 from .nodes import *
+from .registry import RegistryMeta, ConstructorRegistry
 
+import threading
 import collections.abc, datetime, base64, binascii, re, sys, types
 
 class ConstructorError(MarkedYAMLError):
     pass
 
-class BaseConstructor:
 
-    yaml_constructors = {}
-    yaml_multi_constructors = {}
+class ConstructorSetup(threading.local):
+    def __init__(self):
+        self.initialized = False
+
+    def ensure_initialized(self):
+        if not self.initialized:
+            self.initialized = True
+            setup_constructors()
+
+
+class BaseConstructor(metaclass=RegistryMeta):
path: lib/yaml/constructor.py
user: login: lysnikolaou
body: Done.
created_at: 2025-04-18T12:29:19Z
author_association: NONE
diff_hunk: @@ -0,0 +1,180 @@
+import random
+import re
+import threading
+
+import yaml
+
+
+class Dice(tuple):
+    def __new__(cls, a, b):
+        return tuple.__new__(cls, (a, b))
+
+    def __repr__(self):
+        return "Dice(%s,%s)" % self
+
+
+def dice_constructor(loader, node):
+    value = loader.construct_scalar(node)
+    a, b = map(int, value.split('d'))
+    return Dice(a, b)
+
+
+def dice_representer(dumper, data):
+    return dumper.represent_scalar("!dice", "%sd%s" % data)
+
+
+# Different YAML content types for testing
+YAML_LOAD_SAMPLES = [
+    # Simple key-value pairs
+    ("""\
+key1: value1
+key2: value2
+key3: 123
+key4: true
+""", {
+        "key1": "value1",
+        "key2": "value2",
+        "key3": 123,
+        "key4": True
+    }),
+    
+    # Nested structures
+    ("""\
+config:
+  database:
+    host: localhost
+    port: 5432
+    credentials:
+      username: admin
+      password: secret
+  logging:
+    level: INFO
+    file: /var/log/app.log
+""", {
+        "config": {
+            "database": {
+                "host": "localhost",
+                "port": 5432,
+                "credentials": {
+                    "username": "admin",
+                    "password": "secret"
+                }
+            },
+            "logging": {
+                "level": "INFO",
+                "file": "/var/log/app.log"
+            }
+        }
+    }),
+
+    # Lists
+    ("""\
+fruits:
+  - apple
+  - banana
+  - orange
+numbers: [1, 2, 3, 4, 5]
+mixed:
+  - name: John
+    age: 30
+  - name: Alice
+    age: 25
+""", {
+        "fruits": ["apple", "banana", "orange"],
+        "numbers": [1, 2, 3, 4, 5],
+        "mixed": [
+            {"name": "John", "age": 30},
+            {"name": "Alice", "age": 25}
+        ]
+    }),
+    
+    # Complex with references
+    ("""\
+defaults: &defaults
+  adapter: postgresql
+  host: localhost
+
+development:
+  database: myapp_development
+  <<: *defaults
+
+test:
+  database: myapp_test
+  <<: *defaults
+""", {
+        "defaults": {
+            "adapter": "postgresql",
+            "host": "localhost"
+        },
+        "development": {
+            "database": "myapp_development",
+            "adapter": "postgresql",
+            "host": "localhost"
+        },
+        "test": {
+            "database": "myapp_test",
+            "adapter": "postgresql",
+            "host": "localhost"
+        }
+    }),
+
+    # Dice with resolver
+    ("""\
+rolls_resolver:
+  - 1d6
+  - 2d4
+  - 3d1
+""", {
+        "rolls_resolver": [
+            Dice(1, 6),
+            Dice(2, 4),
+            Dice(3, 1)
+        ]
+    }),
+
+    # Dice without resolver
+    ("""\
+rolls_no_resolver:
+  - 1d6
+  - 2d4
+  - 3d1
+""", {
+        "rolls_no_resolver": [
+            "1d6",
+            "2d4",
+            "3d1"
+        ]
+    }),
+]
+
+
+class MyLoader(yaml.CLoader):
+    pass
+
+
+class MyDumper(yaml.CDumper):
+    pass
+
+
+def test_yaml_load_stress():
+    yamlcode, result = random.choice(YAML_LOAD_SAMPLES)
+    thread_id = threading.current_thread().name
path: tests/free_threading/test_stress.py
user: login: lysnikolaou
body: New test added. Will open the follow-up PR as soon as this one gets merged.
created_at: 2025-04-18T12:29:38Z
author_association: NONE
diff_hunk: @@ -0,0 +1,54 @@
+import io
+
+import yaml
+
+
+class MyLoader(yaml.CLoader):
+    pass
+
+
+class MyTestClass1:
+    def __init__(self, x, y=0, z=0):
+        self.x = x
+        self.y = y
+        self.z = z
+
+    def __eq__(self, other):
+        if isinstance(other, MyTestClass1):
+            return self.__class__, self.__dict__ == other.__class__, other.__dict__
+        else:
+            return False
+
+    def __repr__(self):
+        return f"MyTestClass1(x={self.x}, y={self.y}, z={self.z})"
+
+
+def construct1(constructor, node):
+    mapping = constructor.construct_mapping(node)
+    return MyTestClass1(**mapping)
+
+
+def test_default_constructors_registered():
+    yamlcode = io.StringIO("""\
+- !!python/tuple [hello, world]
+""")
+
+    objs = yaml.load(yamlcode, Loader=yaml.CLoader)
+    assert objs == [("hello", "world")]
+
+
+def test_constructor_registration():
+    yaml.add_constructor("!tag1", construct1, Loader=MyLoader)
+
+    yamlcode = io.StringIO("""\
+- !tag1
+  x: 1
+- !tag1
+  x: 1
+  'y': 2
+  z: 3
+""")
+
+    objs = yaml.load(yamlcode, Loader=MyLoader)
+    assert objs == [MyTestClass1(x=1), MyTestClass1(x=1, y=2, z=3)]
+    del MyLoader.yaml_constructors()["!tag1"]
path: tests/free_threading/test_constructor.py
user: login: lysnikolaou
body: To stress test that everything is indeed thread-local and deletions from the registry do not impact other threads.
created_at: 2025-04-18T12:30:11Z
author_association: NONE
diff_hunk: @@ -0,0 +1,180 @@
+import random
+import re
+import threading
+
+import yaml
+
+
+class Dice(tuple):
+    def __new__(cls, a, b):
+        return tuple.__new__(cls, (a, b))
+
+    def __repr__(self):
+        return "Dice(%s,%s)" % self
path: tests/free_threading/test_stress.py
user: login: lysnikolaou
body: Done.
created_at: 2025-04-18T12:30:22Z
author_association: NONE
diff_hunk: @@ -0,0 +1,51 @@
+import yaml
+
+
+class MyDumper(yaml.CDumper):
+    pass
+
+
+class MyTestClass1:
+    def __init__(self, x, y=0, z=0):
+        self.x = x
+        self.y = y
+        self.z = z
+    def __eq__(self, other):
+        if isinstance(other, MyTestClass1):
+            return self.__class__, self.__dict__ == other.__class__, other.__dict__
+        else:
+            return False
+
+
+def represent1(representer, native):
+    return representer.represent_mapping("!tag1", native.__dict__)
+
+
+def test_default_representers_registered():
+    obj = [("hello", "world")]
+
+    yamlcode = yaml.dump(obj, Dumper=yaml.CDumper)
+    assert yamlcode == """\
+- !!python/tuple
+  - hello
+  - world
+"""
+
+
+def test_representer_registration():
+    yaml.add_representer(MyTestClass1, represent1, Dumper=MyDumper)
+
+    obj = [MyTestClass1(x=1), MyTestClass1(x=1, y=2, z=3)]
+
+    yamlcode = yaml.dump(obj, Dumper=MyDumper)
+    assert yamlcode == """\
+- !tag1
+  x: 1
+  y: 0
+  z: 0
+- !tag1
+  x: 1
+  y: 2
+  z: 3
+"""
+    del MyDumper.yaml_representers()[MyTestClass1]
path: tests/free_threading/test_representer.py
user: login: lysnikolaou
body: They're similar in that they're doing the exact opposite (load vs dump using Loader vs Dumper). My feeling is that with the refactoring to move out common stuff it's okay like it is now.
created_at: 2025-04-18T12:31:19Z
author_association: NONE
path: tests/free_threading/test_constructor.py
user: login: lysnikolaou
body: Great idea! Done.
created_at: 2025-04-18T12:31:35Z
author_association: NONE
diff_hunk: @@ -0,0 +1,39 @@
+import threading
+
+
+class ConstructorRegistry(threading.local):
+    def __init__(self, yaml_constructors_initialized=False, yaml_multi_constructors_initialized=False):
+        self.yaml_constructors = {}
+        self.yaml_multi_constructors = {}
+        self.yaml_constructors_initialized = yaml_constructors_initialized
+        self.yaml_multi_constructors_initialized = yaml_multi_constructors_initialized
+
+
+class RepresenterRegistry(threading.local):
+    def __init__(self, yaml_representers_initialized=False, yaml_multi_representers_initialized=False):
+        self.yaml_representers = {}
+        self.yaml_multi_representers = {}
+        self.yaml_representers_initialized = yaml_representers_initialized
+        self.yaml_multi_representers_initialized = yaml_multi_representers_initialized
+
+
+class ResolverRegistry(threading.local):
+    def __init__(self, yaml_implicit_resolvers_initialized=False, yaml_path_resolvers_initialized=False):
+        self.yaml_implicit_resolvers = {}
+        self.yaml_path_resolvers = {}
+        self.yaml_implicit_resolvers_initialized = yaml_implicit_resolvers_initialized
+        self.yaml_path_resolvers_initialized = yaml_path_resolvers_initialized
+
+
+class RegistryMeta(type):
+    """Metaclass to handle registry inheritance"""
+    
+    def __new__(cls, name, bases, attrs):
+        cls = super().__new__(cls, name, bases, attrs)
+        if 'constructor_registry' not in cls.__dict__:
+            cls.constructor_registry = ConstructorRegistry()
+        if 'representer_registry' not in cls.__dict__:
+            cls.representer_registry = RepresenterRegistry()
+        if 'resolver_registry' not in cls.__dict__:
+            cls.resolver_registry = ResolverRegistry()
+        return cls
path: lib/yaml/registry.py
user: login: ngoldbaum
body: I'll try to take a look at this refactor after we merge this PR.
created_at: 2025-04-18T21:01:15Z
author_association: NONE
diff_hunk: @@ -0,0 +1,54 @@
+import io
+
+import yaml
+
+
+class MyLoader(yaml.CLoader):
+    pass
+
+
+class MyTestClass1:
+    def __init__(self, x, y=0, z=0):
+        self.x = x
+        self.y = y
+        self.z = z
+
+    def __eq__(self, other):
+        if isinstance(other, MyTestClass1):
+            return self.__class__, self.__dict__ == other.__class__, other.__dict__
+        else:
+            return False
+
+    def __repr__(self):
+        return f"MyTestClass1(x={self.x}, y={self.y}, z={self.z})"
+
+
+def construct1(constructor, node):
+    mapping = constructor.construct_mapping(node)
+    return MyTestClass1(**mapping)
+
+
+def test_default_constructors_registered():
+    yamlcode = io.StringIO("""\
+- !!python/tuple [hello, world]
+""")
+
+    objs = yaml.load(yamlcode, Loader=yaml.CLoader)
+    assert objs == [("hello", "world")]
+
+
+def test_constructor_registration():
+    yaml.add_constructor("!tag1", construct1, Loader=MyLoader)
+
+    yamlcode = io.StringIO("""\
+- !tag1
+  x: 1
+- !tag1
+  x: 1
+  'y': 2
+  z: 3
+""")
+
+    objs = yaml.load(yamlcode, Loader=MyLoader)
+    assert objs == [MyTestClass1(x=1), MyTestClass1(x=1, y=2, z=3)]
+    del MyLoader.yaml_constructors()["!tag1"]
path: tests/free_threading/test_constructor.py
user: login: ngoldbaum
body: Cool, I'll add a comment to that effect and merge the PR.
created_at: 2025-04-18T21:02:25Z
author_association: NONE
diff_hunk: @@ -0,0 +1,35 @@
+import io
+
+import yaml
+
+from .utils import MyTestClass1, construct1
+
+
+class MyLoader(yaml.CLoader):
+    pass
+
+
+def test_default_constructors_registered():
+    yamlcode = io.StringIO("""\
+- !!python/tuple [hello, world]
+""")
+
+    objs = yaml.load(yamlcode, Loader=yaml.CLoader)
+    assert objs == [("hello", "world")]
+
+
+def test_constructor_registration():
+    yaml.add_constructor("!tag1", construct1, Loader=MyLoader)
+
+    yamlcode = io.StringIO("""\
+- !tag1
+  x: 1
+- !tag1
+  x: 1
+  'y': 2
+  z: 3
+""")
+
+    objs = yaml.load(yamlcode, Loader=MyLoader)
+    assert objs == [MyTestClass1(x=1), MyTestClass1(x=1, y=2, z=3)]
+    del MyLoader.yaml_constructors()["!tag1"]
path: tests/free_threading/test_constructor.py
user: login: ngoldbaum
body: ```suggestion
    # deletions from the registry should not impact other threads
    del MyLoader.yaml_constructors()["!tag1"]
```
created_at: 2025-04-18T21:03:39Z
author_association: NONE

PR comments:
user: login: ngoldbaum
created_at: 2025-04-03T20:05:58Z
body: I was a little nervous seeing net +600 LoC, but it looks like most of the new code is from new tests, which, good!

I'll try to go over this soon but we'll probably need several of us to do proper code review on a PR this big.
author_association: NONE
user: login: lysnikolaou
created_at: 2025-04-03T20:08:31Z
body: FWIW just having a look at one of `constructor.py`, `representer.py` and `resolver.py` should suffice in terms of understanding what's going on. The changes to the others are equivalent with just naming changes etc, so looking at the rest should be more of a double-check that everything has been adjusted correctly.
author_association: NONE