diff --git a/pyproject.toml b/pyproject.toml
index 451b243029eabfd308441f5161eac2e9d7069a41..f12b50844f60dffea2a78b27441a5710ab3be8a3 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -32,7 +32,8 @@ classifiers = [
 ]
 dependencies = [
     "platformdirs>=2.2",
-    "typing-extensions>=4; python_version<'3.11'",
+    # for dataclass_transform with frozen_default
+    "typing-extensions>=4; python_version<'3.13'",
     "siphash24>=1.6",
 ]
 
diff --git a/pytools/tag.py b/pytools/tag.py
index ed337a6d3cdb71166aca846c013f49a6d66f7df2..3fa32940007a758afefdb5c5e3ae614b6bdc7314 100644
--- a/pytools/tag.py
+++ b/pytools/tag.py
@@ -7,7 +7,6 @@ Tag Interface
 .. autoclass:: Taggable
 .. autoclass:: Tag
 .. autoclass:: UniqueTag
-.. autoclass:: IgnoredForEqualityTag
 
 Supporting Functionality
 ------------------------
@@ -22,14 +21,13 @@ Internal stuff that is only here because the documentation tool wants it
 .. class:: TagT
 
     A type variable with lower bound :class:`Tag`.
-
-.. class:: _Self_Taggable
-
-    A type variable with lower bound :class:`Taggable`.
 """
 
+from __future__ import annotations
+
 from dataclasses import dataclass
 from typing import (
+    TYPE_CHECKING,
     Any,
     FrozenSet,
     Iterable,
@@ -40,6 +38,8 @@ from typing import (
     Union,
 )
 
+from typing_extensions import Self, dataclass_transform
+
 from pytools import memoize, memoize_method
 
 
@@ -100,7 +100,7 @@ class DottedName:
         self.name_parts = name_parts
 
     @classmethod
-    def from_class(cls, argcls: Any) -> "DottedName":
+    def from_class(cls, argcls: Any) -> DottedName:
         name_parts = tuple(
                 [str(part) for part in argcls.__module__.split(".")]
                 + [str(argcls.__name__)])
@@ -124,7 +124,12 @@ class DottedName:
 
 # {{{ tag
 
-tag_dataclass = dataclass(init=True, eq=True, frozen=True, repr=True)
+T = TypeVar("T")
+
+
+@dataclass_transform(eq_default=True, frozen_default=True)
+def tag_dataclass(cls: type[T]) -> type[T]:
+    return dataclass(init=True, frozen=True, eq=True, repr=True)(cls)
 
 
 @tag_dataclass
@@ -155,6 +160,7 @@ class Tag:
 
 # {{{ unique tag
 
+@tag_dataclass
 class UniqueTag(Tag):
     """
     A superclass for tags that are unique on each :class:`Taggable`.
@@ -164,25 +170,22 @@ class UniqueTag(Tag):
     set of `tags`. Multiple `UniqueTag` instances of
     different (immediate) subclasses are allowed.
     """
-    pass
 
 # }}}
 
 
 ToTagSetConvertible = Union[Iterable[Tag], Tag, None]
 TagT = TypeVar("TagT", bound="Tag")
-# FIXME: Replace by Self type
-_Self_Taggable = TypeVar("_Self_Taggable", bound="Taggable")
 
 
 # {{{ UniqueTag rules checking
 
 @memoize
-def _immediate_unique_tag_descendants(cls):
+def _immediate_unique_tag_descendants(cls: type[Tag]) -> FrozenSet[type[Tag]]:
     if UniqueTag in cls.__bases__:
         return frozenset([cls])
     else:
-        result = frozenset()
+        result: FrozenSet[type[Tag]] = frozenset()
         for base in cls.__bases__:
             result = result | _immediate_unique_tag_descendants(base)
         return result
@@ -197,14 +200,14 @@ class NonUniqueTagError(ValueError):
     pass
 
 
-def check_tag_uniqueness(tags: FrozenSet[Tag]):
+def check_tag_uniqueness(tags: FrozenSet[Tag]) -> FrozenSet[Tag]:
     """Ensure that *tags* obeys the rules set forth in :class:`UniqueTag`.
     If not, raise :exc:`NonUniqueTagError`. If any *tags* are not
     subclasses of :class:`Tag`, a :exc:`TypeError` will be raised.
 
     :returns: *tags*
     """
-    unique_tag_descendants: Set[Tag] = set()
+    unique_tag_descendants: Set[type[Tag]] = set()
     for tag in tags:
         if not isinstance(tag, Tag):
             raise TypeError(f"'{tag}' is not an instance of pytools.tag.Tag")
@@ -239,9 +242,7 @@ class Taggable:
     """
     Parent class for objects with a `tags` attribute.
 
-    .. attribute:: tags
-
-        A :class:`frozenset` of :class:`Tag` instances
+    .. autoattribute:: tags
 
     .. automethod:: __init__
 
@@ -257,37 +258,20 @@ class Taggable:
     # ReST references in docstrings must be fully qualified, as docstrings may
     # be inherited and appear in different contexts.
 
-    def __init__(self, tags: FrozenSet[Tag] = frozenset()):
-        """
-        Constructor for all objects that possess a `tags` attribute.
-
-        :arg tags: a :class:`frozenset` of :class:`~pytools.tag.Tag` objects.
-            Tags can be modified via the :meth:`~pytools.tag.Taggable.tagged` and
-            :meth:`~pytools.tag.Taggable.without_tags` routines. Input checking
-            of *tags* should be performed before creating a
-            :class:`~pytools.tag.Taggable` instance, using
-            :func:`~pytools.tag.check_tag_uniqueness`.
-        """
-        self.tags = tags
+    # type-checking only so that self.tags = ... in subclasses still works
+    if TYPE_CHECKING:
+        @property
+        def tags(self) -> FrozenSet[Tag]:
+            ...
 
-    def _with_new_tags(self: _Self_Taggable, tags: FrozenSet[Tag]) -> _Self_Taggable:
+    def _with_new_tags(self, tags: FrozenSet[Tag]) -> Self:
         """
         Returns a copy of *self* with the specified tags. This method
         should be overridden by subclasses.
         """
-        from warnings import warn
-        warn(f"_with_new_tags() for {self.__class__} fell back "
-                "to using copy(). This is deprecated and will stop working in "
-                "July of 2022. Instead, override _with_new_tags to specify "
-                "how tags should be applied to an instance.",
-                DeprecationWarning, stacklevel=2)
-
-        # mypy is right: we're not promising this attribute is defined.
-        # Once this deprecation expires, this will go back to being an
-        # abstract method.
-        return self.copy(tags=tags)  # type: ignore[attr-defined]  # pylint: disable=no-member
+        raise NotImplementedError
 
-    def tagged(self: _Self_Taggable, tags: ToTagSetConvertible) -> _Self_Taggable:
+    def tagged(self, tags: ToTagSetConvertible) -> Self:
         """
         Return a copy of *self* with the specified
         tag or tags added to the set of tags. If the resulting set of
@@ -300,9 +284,9 @@ class Taggable:
         return self._with_new_tags(
                 tags=check_tag_uniqueness(normalize_tags(tags) | self.tags))
 
-    def without_tags(self: _Self_Taggable,
+    def without_tags(self,
                      tags: ToTagSetConvertible, verify_existence: bool = True
-                     ) -> _Self_Taggable:
+                     ) -> Self:
         """
         Return a copy of *self* without the specified tags.
 
@@ -340,32 +324,12 @@ class Taggable:
 
     def __eq__(self, other: object) -> bool:
         if isinstance(other, Taggable):
-            return (self.tags_not_of_type(IgnoredForEqualityTag)
-                    == other.tags_not_of_type(IgnoredForEqualityTag))
+            return self.tags == other.tags
         else:
             return super().__eq__(other)
 
     def __hash__(self) -> int:
-        return hash(self.tags_not_of_type(IgnoredForEqualityTag))
-
-
-# }}}
-
-
-# {{{ IgnoredForEqualityTag
-
-class IgnoredForEqualityTag(Tag):
-    """
-    A superclass for tags that are ignored when testing equality of instances of
-    :class:`Taggable`.
-
-    When testing equality of two instances of :class:`Taggable`, the equality
-    of the ``tags`` of both instances is tested after removing all
-    instances of :class:`IgnoredForEqualityTag`. Instances of
-    :class:`IgnoredForEqualityTag` are removed for hashing instances of
-    :class:`Taggable`.
-    """
-    pass
+        return hash(self.tags)
 
 # }}}
 
@@ -385,7 +349,7 @@ _depr_name_to_replacement_and_obj = {
         }
 
 
-def __getattr__(name):
+def __getattr__(name: str) -> Any:
     replacement_and_obj = _depr_name_to_replacement_and_obj.get(name, None)
     if replacement_and_obj is not None:
         replacement, obj, year = replacement_and_obj
diff --git a/pytools/test/test_persistent_dict.py b/pytools/test/test_persistent_dict.py
index 5239884bf32505116b37bacfc77e5bd9c2ccd20f..151ac0303055ddf748df8fbca70497084d481991 100644
--- a/pytools/test/test_persistent_dict.py
+++ b/pytools/test/test_persistent_dict.py
@@ -82,7 +82,7 @@ def test_persistent_dict_storage_and_lookup() -> None:
 
         keys = [
                 (randrange(2000)-1000, rand_str(), None,
-                 SomeTag(rand_str()),  # type: ignore[call-arg]
+                 SomeTag(rand_str()),
                     frozenset({"abc", 123}))
                 for i in range(20)]
         values = [randrange(2000) for i in range(20)]
@@ -555,7 +555,7 @@ def test_class_hashing() -> None:
     class TagClass3(Tag):
         s: str
 
-    assert (keyb(TagClass3("foo")) == "cf1a33652cc75b9c")  # type: ignore[call-arg]
+    assert (keyb(TagClass3("foo")) == "cf1a33652cc75b9c")
 
 
 def test_dataclass_hashing() -> None:
diff --git a/pytools/test/test_pytools.py b/pytools/test/test_pytools.py
index e6d95d68fe7bb6b8f5fceb6c2937fa26008cbfbf..07b59a9ce1b2996ee94d791630ac8bb04aaacbdf 100644
--- a/pytools/test/test_pytools.py
+++ b/pytools/test/test_pytools.py
@@ -23,11 +23,13 @@ THE SOFTWARE.
 
 import logging
 import sys
+from dataclasses import dataclass
 from typing import FrozenSet
 
 import pytest
 
 from pytools import Record
+from pytools.tag import tag_dataclass
 
 
 logger = logging.getLogger(__name__)
@@ -173,7 +175,6 @@ def test_memoize_keyfunc():
 
 
 def test_memoize_frozen() -> None:
-    from dataclasses import dataclass
 
     from pytools import memoize_method
 
@@ -506,7 +507,9 @@ def test_tag():
     )
 
     # Need a subclass that defines the copy function in order to test.
+    @tag_dataclass
     class TaggableWithCopy(Taggable):
+        tags: FrozenSet[Tag]
 
         def _with_new_tags(self, tags):
             return TaggableWithCopy(tags)
@@ -690,40 +693,6 @@ def test_unique_name_gen_conflicting_ok():
     ung.add_names({"a", "b", "c"}, conflicting_ok=True)
 
 
-def test_ignoredforequalitytag():
-    from pytools.tag import IgnoredForEqualityTag, Tag, Taggable
-
-    # Need a subclass that defines _with_new_tags in order to test.
-    class TaggableWithNewTags(Taggable):
-
-        def _with_new_tags(self, tags: FrozenSet[Tag]):
-            return TaggableWithNewTags(tags)
-
-    class Eq1(IgnoredForEqualityTag):
-        pass
-
-    class Eq2(IgnoredForEqualityTag):
-        pass
-
-    class Eq3(Tag):
-        pass
-
-    eq1 = TaggableWithNewTags(frozenset([Eq1()]))
-    eq2 = TaggableWithNewTags(frozenset([Eq2()]))
-    eq12 = TaggableWithNewTags(frozenset([Eq1(), Eq2()]))
-    eq3 = TaggableWithNewTags(frozenset([Eq1(), Eq3()]))
-
-    assert eq1 == eq2 == eq12
-    assert eq1 != eq3
-
-    assert eq1.without_tags(Eq1())
-    with pytest.raises(ValueError):
-        eq3.without_tags(Eq2())
-
-    assert hash(eq1) == hash(eq2) == hash(eq12)
-    assert hash(eq1) != hash(eq3)
-
-
 def test_strtobool():
     from pytools import strtobool
     assert strtobool("true") is True
diff --git a/run-mypy.sh b/run-mypy.sh
index ae0edc90e6028ad7ac4ae8b7f491ee60b8205c41..244d6cc47f454df60e888a80e216a4dd9bf594dc 100755
--- a/run-mypy.sh
+++ b/run-mypy.sh
@@ -4,4 +4,7 @@ set -ex
 
 mypy --show-error-codes pytools
 
-mypy --strict --follow-imports=silent pytools/datatable.py pytools/persistent_dict.py
+mypy --strict --follow-imports=silent \
+    pytools/tag.py \
+    pytools/datatable.py \
+    pytools/persistent_dict.py