diff --git a/examples/moving-geometry.py b/examples/moving-geometry.py
index 7337d1d617b34fb5577a35774bc7685ec49a86fe..f60e65ed781232fe87fd46845281738ed3698c95 100644
--- a/examples/moving-geometry.py
+++ b/examples/moving-geometry.py
@@ -78,6 +78,8 @@ def reconstruct_discr_from_nodes(actx, discr, x):
                            discr_nodes,
                            tagged=(FirstAxisIsElementsTag(),))
 
+    from dataclasses import replace
+
     megs = []
     for igrp, grp in enumerate(discr.groups):
         nodes = np.stack([
@@ -85,10 +87,7 @@ def reconstruct_discr_from_nodes(actx, discr, x):
             for iaxis in range(discr.ambient_dim)
             ])
 
-        meg = grp.mesh_el_group.copy(
-                vertex_indices=None,
-                nodes=nodes,
-                )
+        meg = replace(grp.mesh_el_group, vertex_indices=None, nodes=nodes)
         megs.append(meg)
 
     mesh = discr.mesh.copy(groups=megs, vertices=None)
diff --git a/meshmode/mesh/__init__.py b/meshmode/mesh/__init__.py
index 3f9fb914706bf038a5d2833db8ae628f8cb03e89..24eb2d7b432d1458cee861255c22749d587b3c3e 100644
--- a/meshmode/mesh/__init__.py
+++ b/meshmode/mesh/__init__.py
@@ -21,8 +21,9 @@ THE SOFTWARE.
 """
 
 from abc import ABC, abstractmethod
-from dataclasses import dataclass, replace, field
-from typing import Any, ClassVar, Hashable, Optional, Tuple, Type, Sequence
+from dataclasses import dataclass, field, replace
+from typing import Any, ClassVar, Hashable, Optional, Sequence, Tuple, Type
+from warnings import warn
 
 import numpy as np
 import numpy.linalg as la
@@ -32,6 +33,7 @@ from pytools import Record, memoize_method
 
 from meshmode.mesh.tools import AffineMap
 
+
 __doc__ = """
 
 .. autoclass:: MeshElementGroup
@@ -117,23 +119,8 @@ class BTAG_PARTITION(BTAG_NO_BOUNDARY):  # noqa: N801
 
     .. versionadded:: 2017.1
     """
-    def __init__(self, part_id: PartID, part_nr=None):
-        if part_nr is not None:
-            from warnings import warn
-            warn("part_nr is deprecated and will stop working in March 2023. "
-                 "Use part_id instead.",
-                 DeprecationWarning, stacklevel=2)
-            self.part_id = int(part_nr)
-        else:
-            self.part_id = part_id
-
-    @property
-    def part_nr(self):
-        from warnings import warn
-        warn("part_nr is deprecated and will stop working in March 2023. "
-             "Use part_id instead.",
-             DeprecationWarning, stacklevel=2)
-        return self.part_id
+    def __init__(self, part_id: PartID) -> None:
+        self.part_id = part_id
 
     def __hash__(self):
         return hash((type(self), self.part_id))
@@ -216,7 +203,8 @@ class MeshElementGroup(ABC):
     .. attribute:: vertex_indices
 
         An array of shape ``(nelements, nvertices)`` of (mesh-wide)
-        vertex indices.
+        vertex indices. This can also be *None* to support the case where the
+        associated mesh does not have any :attr:`~Mesh.vertices`.
 
     .. attribute:: nodes
 
@@ -251,43 +239,9 @@ class MeshElementGroup(ABC):
     """
 
     order: int
-
-    # NOTE: the mesh supports not having vertices if no facial or nodal
-    # adjacency is required, so we can mark this as optional
     vertex_indices: Optional[np.ndarray]
     nodes: np.ndarray
-
-    # TODO: Remove ` = None` when everything is constructed through the factory
-    unit_nodes: np.ndarray = None
-
-    # FIXME: these should be removed!
-    # https://github.com/inducer/meshmode/issues/224
-    element_nr_base: Optional[int] = None
-    node_nr_base: Optional[int] = None
-
-    # TODO: remove when everything has been constructed through the factory
-    _factory_constructed: bool = False
-
-    def __post_init__(self):
-        if not self._factory_constructed:
-            from warnings import warn
-            warn(f"Calling the constructor of '{type(self).__name__}' is "
-                 "deprecated and will stop working in July 2022. "
-                 f"Use '{type(self).__name__}.make_group' instead",
-                 DeprecationWarning, stacklevel=2)
-
-    def __getattribute__(self, name):
-        if name in ("element_nr_base", "node_nr_base"):
-            new_name = ("base_element_nrs"
-                        if name == "element_nr_base" else
-                        "base_node_nrs")
-
-            from warnings import warn
-            warn(f"'{type(self).__name__}.{name}' is deprecated and will be "
-                 f"removed in July 2022. Use 'Mesh.{new_name}' instead",
-                 DeprecationWarning, stacklevel=2)
-
-        return super().__getattribute__(name)
+    unit_nodes: np.ndarray
 
     @property
     def dim(self):
@@ -313,21 +267,6 @@ class MeshElementGroup(ABC):
     def nnodes(self):
         return self.nelements * self.unit_nodes.shape[-1]
 
-    def copy(self, **kwargs: Any) -> "MeshElementGroup":
-        from warnings import warn
-        warn(f"{type(self).__name__}.copy is deprecated and will be removed in "
-                f"July 2022. {type(self).__name__} is now a dataclass, so "
-                "standard functions such as dataclasses.replace should be used "
-                "instead.",
-                DeprecationWarning, stacklevel=2)
-
-        if "element_nr_base" not in kwargs:
-            kwargs["element_nr_base"] = None
-        if "node_nr_base" not in kwargs:
-            kwargs["node_nr_base"] = None
-
-        return replace(self, **kwargs)
-
     def __eq__(self, other):
         return (
                 type(self) is type(other)
@@ -384,62 +323,10 @@ class _ModepyElementGroup(MeshElementGroup):
     .. attribute:: _modepy_space
     """
 
-    # TODO: remove once `make_group` is used everywhere
-    dim: Optional[int] = None
-
     _modepy_shape_cls: ClassVar[Type[mp.Shape]] = mp.Shape
     _modepy_shape: mp.Shape = field(default=None, repr=False)
     _modepy_space: mp.FunctionSpace = field(default=None, repr=False)
 
-    def __post_init__(self):
-        super().__post_init__()
-        if self._factory_constructed:
-            return
-
-        # {{{ duplicates make_group below, keep in sync
-
-        if self.unit_nodes is None:
-            if self.dim is None:
-                raise TypeError("either 'dim' or 'unit_nodes' must be provided")
-        else:
-            if self.dim is None:
-                object.__setattr__(self, "dim", self.unit_nodes.shape[0])
-
-            if self.unit_nodes.shape[0] != self.dim:
-                raise ValueError("'dim' does not match 'unit_nodes' dimension")
-
-        # dim is now usable
-        assert self._modepy_shape_cls is not mp.Shape
-        object.__setattr__(self, "_modepy_shape",
-                # pylint: disable=abstract-class-instantiated
-                self._modepy_shape_cls(self.dim))
-        object.__setattr__(self, "_modepy_space",
-                mp.space_for_shape(self._modepy_shape, self.order))
-
-        if self.unit_nodes is None:
-            unit_nodes = mp.edge_clustered_nodes_for_space(
-                    self._modepy_space, self._modepy_shape)
-            object.__setattr__(self, "unit_nodes", unit_nodes)
-
-        if self.nodes is not None:
-            if self.unit_nodes.shape[-1] != self.nodes.shape[-1]:
-                raise ValueError(
-                        "'nodes' has wrong number of unit nodes per element."
-                        f" expected {self.unit_nodes.shape[-1]}, "
-                        f" but got {self.nodes.shape[-1]}.")
-
-        if self.vertex_indices is not None:
-            if not issubclass(self.vertex_indices.dtype.type, np.integer):
-                raise TypeError("'vertex_indices' must be integral")
-
-            if self.vertex_indices.shape[-1] != self.nvertices:
-                raise ValueError(
-                        "'vertex_indices' has wrong number of vertices per element."
-                        f" expected {self.nvertices},"
-                        f" got {self.vertex_indices.shape[-1]}")
-
-        # }}}
-
     @property
     def nvertices(self):
         return self._modepy_shape.nvertices     # pylint: disable=no-member
@@ -486,11 +373,12 @@ class _ModepyElementGroup(MeshElementGroup):
             raise ValueError("'unit_nodes' size does not match the dimension "
                              f"of a '{type(space).__name__}' space of order {order}")
 
-        return cls(order=order, vertex_indices=vertex_indices, nodes=nodes,
-                   dim=dim,
+        return cls(order=order,
+                   vertex_indices=vertex_indices,
+                   nodes=nodes,
                    unit_nodes=unit_nodes,
-                   _modepy_shape=shape, _modepy_space=space,
-                   _factory_constructed=True)
+                   _modepy_shape=shape,
+                   _modepy_space=space)
 
         # }}}
 
@@ -500,6 +388,7 @@ class _ModepyElementGroup(MeshElementGroup):
 @dataclass(frozen=True, eq=False)
 class SimplexElementGroup(_ModepyElementGroup):
     r"""Inherits from :class:`MeshElementGroup`."""
+
     _modepy_shape_cls: ClassVar[Type[mp.Shape]] = mp.Simplex
 
     @property
@@ -511,6 +400,7 @@ class SimplexElementGroup(_ModepyElementGroup):
 @dataclass(frozen=True, eq=False)
 class TensorProductElementGroup(_ModepyElementGroup):
     r"""Inherits from :class:`MeshElementGroup`."""
+
     _modepy_shape_cls: ClassVar[Type[mp.Shape]] = mp.Hypercube
 
     def is_affine(self):
@@ -549,16 +439,6 @@ class NodalAdjacency:
     neighbors_starts: np.ndarray
     neighbors: np.ndarray
 
-    def copy(self, **kwargs: Any) -> "NodalAdjacency":
-        from warnings import warn
-        warn(f"{type(self).__name__}.copy is deprecated and will be removed in "
-                f"July 2022. {type(self).__name__} is now a dataclass, so "
-                "standard functions such as dataclasses.replace should be used "
-                "instead.",
-                DeprecationWarning, stacklevel=2)
-
-        return replace(self, **kwargs)
-
     def __eq__(self, other):
         return (
                 type(self) is type(other)
@@ -605,16 +485,6 @@ class FacialAdjacencyGroup:
 
     igroup: int
 
-    def copy(self, **kwargs: Any) -> "FacialAdjacencyGroup":
-        from warnings import warn
-        warn(f"{type(self).__name__}.copy is deprecated and will be removed in "
-                f"July 2022. {type(self).__name__} is now a dataclass, so "
-                "standard functions such as dataclasses.replace should be used "
-                "instead.",
-                DeprecationWarning, stacklevel=2)
-
-        return replace(self, **kwargs)
-
     def __eq__(self, other):
         return (
                 type(self) is type(other)
@@ -1000,16 +870,6 @@ class Mesh(Record):
             :attr:`facial_adjacency_groups` may be passed.
         """
 
-        el_nr = 0
-        node_nr = 0
-
-        new_groups = []
-        for g in groups:
-            ng = replace(g, element_nr_base=el_nr, node_nr_base=node_nr)
-            new_groups.append(ng)
-            el_nr += ng.nelements
-            node_nr += ng.nnodes
-
         if vertices is None:
             is_conforming = None
 
@@ -1038,7 +898,7 @@ class Mesh(Record):
                 self.face_id_dtype)
 
         Record.__init__(
-                self, vertices=vertices, groups=new_groups,
+                self, vertices=vertices, groups=groups,
                 _nodal_adjacency=nodal_adjacency,
                 _facial_adjacency_groups=facial_adjacency_groups,
                 vertex_id_dtype=np.dtype(vertex_id_dtype),
@@ -1076,8 +936,8 @@ class Mesh(Record):
                             assert fagrp.neighbor_faces.dtype == self.face_id_dtype
                             assert fagrp.neighbor_faces.shape == (nfagrp_elements,)
 
-            from meshmode.mesh.processing import \
-                    test_volume_mesh_element_orientations
+            from meshmode.mesh.processing import (
+                test_volume_mesh_element_orientations)
 
             if self.dim == self.ambient_dim and not skip_element_orientation_test:
                 # only for volume meshes, for now
@@ -1093,9 +953,8 @@ class Mesh(Record):
 
         set_if_not_present("vertices")
         if "groups" not in kwargs:
-            kwargs["groups"] = [
-                replace(group, element_nr_base=None, node_nr_base=None)
-                for group in self.groups]
+            kwargs["groups"] = self.groups
+
         set_if_not_present("nodal_adjacency", "_nodal_adjacency")
         set_if_not_present("facial_adjacency_groups", "_facial_adjacency_groups")
         set_if_not_present("vertex_id_dtype")
@@ -1269,9 +1128,9 @@ def _test_node_vertex_consistency(mesh, tol):
         if isinstance(mgrp, _ModepyElementGroup):
             assert _test_node_vertex_consistency_resampling(mesh, igrp, tol)
         else:
-            from warnings import warn
-            warn("not implemented: node-vertex consistency check for '%s'"
-                    % type(mgrp).__name__)
+            warn("Not implemented: node-vertex consistency check for "
+                 f"groups of type '{type(mgrp).__name__}'.",
+                 stacklevel=3)
 
     return True
 
@@ -1657,7 +1516,7 @@ def as_python(mesh, function_name="make_mesh"):
     recreate the mesh given as an input parameter.
     """
 
-    from pytools.py_codegen import PythonCodeGenerator, Indentation
+    from pytools.py_codegen import Indentation, PythonCodeGenerator
     cg = PythonCodeGenerator()
     cg("""
         # generated by meshmode.mesh.as_python
diff --git a/meshmode/mesh/generation.py b/meshmode/mesh/generation.py
index 16f83c120fcfc059e77d38e3d65779628d6723fa..cc1a331d2b2dd0c3bab4bb4ba6e91f3f32d37dec 100644
--- a/meshmode/mesh/generation.py
+++ b/meshmode/mesh/generation.py
@@ -672,9 +672,10 @@ def generate_sphere(r: float, order: int, *,
     from dataclasses import replace
     vertices = mesh.vertices * r / np.sqrt(np.sum(mesh.vertices**2, axis=0))
     grp, = mesh.groups
-    grp = replace(grp,
-            nodes=grp.nodes * r / np.sqrt(np.sum(grp.nodes**2, axis=0)),
-            element_nr_base=None, node_nr_base=None)
+    grp = replace(
+        grp,
+        nodes=grp.nodes * r / np.sqrt(np.sum(grp.nodes**2, axis=0))
+        )
 
     from meshmode.mesh import Mesh
     return Mesh(
@@ -748,8 +749,7 @@ def generate_surface_of_revolution(
     from dataclasses import replace
     vertices = ensure_radius(mesh.vertices)
     grp, = mesh.groups
-    grp = replace(grp, nodes=ensure_radius(grp.nodes),
-                  element_nr_base=None, node_nr_base=None)
+    grp = replace(grp, nodes=ensure_radius(grp.nodes))
 
     from meshmode.mesh import Mesh
     return Mesh(
@@ -854,8 +854,7 @@ def generate_torus_and_cycle_vertices(
     # }}}
 
     from dataclasses import replace
-    grp = replace(grp, vertex_indices=vertex_indices, nodes=nodes,
-                  element_nr_base=None, node_nr_base=None)
+    grp = replace(grp, vertex_indices=vertex_indices, nodes=nodes)
 
     from meshmode.mesh import Mesh
     return (
@@ -1002,8 +1001,7 @@ def refine_mesh_and_get_urchin_warper(
     def warp_mesh(mesh: Mesh) -> Mesh:
         from dataclasses import replace
         groups = [
-            replace(grp, nodes=map_coords(grp.nodes),
-                    element_nr_base=None, node_nr_base=None)
+            replace(grp, nodes=map_coords(grp.nodes))
             for grp in mesh.groups]
 
         from meshmode.mesh import Mesh
diff --git a/meshmode/mesh/processing.py b/meshmode/mesh/processing.py
index a5013647dd93921c252c9f81b954b7a5fcdefea7..e1d387a201d991089740d5661d0e0abe8e21c74b 100644
--- a/meshmode/mesh/processing.py
+++ b/meshmode/mesh/processing.py
@@ -22,12 +22,10 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 THE SOFTWARE.
 """
 
+from dataclasses import dataclass, replace
 from functools import reduce
 from typing import (
-    Callable, Dict, Optional, Union, Tuple, Mapping, List, Set, Sequence,
-    )
-
-from dataclasses import dataclass
+    Callable, Dict, List, Mapping, Optional, Sequence, Set, Tuple, Union)
 
 import numpy as np
 import numpy.linalg as la
@@ -35,17 +33,9 @@ import numpy.linalg as la
 import modepy as mp
 
 from meshmode.mesh import (
-    MeshElementGroup,
-    Mesh,
-    BTAG_PARTITION,
-    PartID,
-    FacialAdjacencyGroup,
-    InteriorAdjacencyGroup,
-    BoundaryAdjacencyGroup,
-    InterPartAdjacencyGroup
-)
-
-from meshmode.mesh import _FaceIDs
+    BTAG_PARTITION, BoundaryAdjacencyGroup, FacialAdjacencyGroup,
+    InteriorAdjacencyGroup, InterPartAdjacencyGroup, Mesh, MeshElementGroup, PartID,
+    _FaceIDs)
 from meshmode.mesh.tools import AffineMap
 
 
@@ -179,12 +169,10 @@ def _filter_mesh_groups(
 
     # }}}
 
-    from dataclasses import replace
     new_groups = [
             replace(grp,
                 vertex_indices=new_vertex_indices[igrp],
-                nodes=grp.nodes[:, filtered_group_elements[igrp], :].copy(),
-                element_nr_base=None, node_nr_base=None)
+                nodes=grp.nodes[:, filtered_group_elements[igrp], :].copy())
             for igrp, grp in enumerate(mesh.groups)]
 
     return new_groups, required_vertex_indices
@@ -742,7 +730,6 @@ def flip_simplex_element_group(
             "ij,dej->dei",
             flip_matrix, grp.nodes[:, grp_flip_flags])
 
-    from dataclasses import replace
     return replace(grp, vertex_indices=new_vertex_indices, nodes=new_nodes)
 
 
@@ -764,11 +751,9 @@ def perform_flips(
         grp_flip_flags = flip_flags[base_element_nr:base_element_nr + grp.nelements]
 
         if grp_flip_flags.any():
-            new_grp = flip_simplex_element_group(
-                    mesh.vertices, grp, grp_flip_flags)
+            new_grp = flip_simplex_element_group(mesh.vertices, grp, grp_flip_flags)
         else:
-            from dataclasses import replace
-            new_grp = replace(grp, element_nr_base=None, node_nr_base=None)
+            new_grp = replace(grp)
 
         new_groups.append(new_grp)
 
@@ -842,7 +827,6 @@ def merge_disjoint_meshes(
     if any(mesh._facial_adjacency_groups is not None for mesh in meshes):
         facial_adjacency_groups = False
 
-    from dataclasses import replace
     if single_group:
         from pytools import single_valued
         ref_group = single_valued(
@@ -876,8 +860,7 @@ def merge_disjoint_meshes(
             for group in mesh.groups:
                 assert group.vertex_indices is not None
                 new_vertex_indices = group.vertex_indices + vert_base
-                new_group = replace(group, vertex_indices=new_vertex_indices,
-                                    element_nr_base=None, node_nr_base=None)
+                new_group = replace(group, vertex_indices=new_vertex_indices)
 
                 new_groups.append(new_group)
 
@@ -926,7 +909,6 @@ def split_mesh_groups(
     new_groups: List[MeshElementGroup] = []
     subgroup_to_group_map = {}
 
-    from dataclasses import replace
     for igrp, (base_element_nr, grp) in enumerate(
             zip(mesh.base_element_nrs, mesh.groups)
             ):
@@ -942,7 +924,6 @@ def split_mesh_groups(
             new_groups.append(replace(grp,
                 vertex_indices=grp.vertex_indices[mask, :].copy(),
                 nodes=grp.nodes[:, mask, :].copy(),
-                element_nr_base=None, node_nr_base=None,
                 ))
 
     mesh = Mesh(
@@ -1332,17 +1313,15 @@ def map_mesh(mesh: Mesh, f: Callable[[np.ndarray], np.ndarray]) -> Mesh:
 
     # {{{ assemble new groups list
 
-    from dataclasses import replace
     new_groups = []
-
     for group in mesh.groups:
         mapped_nodes = f(group.nodes.reshape(mesh.ambient_dim, -1))
         if not mapped_nodes.flags.c_contiguous:
             mapped_nodes = np.copy(mapped_nodes, order="C")
 
-        new_groups.append(replace(group,
-            nodes=mapped_nodes.reshape(*group.nodes.shape),
-            element_nr_base=None, node_nr_base=None))
+        new_groups.append(
+            replace(group, nodes=mapped_nodes.reshape(*group.nodes.shape))
+            )
 
     # }}}
 
@@ -1384,17 +1363,15 @@ def affine_map(
 
     # {{{ assemble new groups list
 
-    from dataclasses import replace
     new_groups = []
-
     for group in mesh.groups:
         mapped_nodes = f(group.nodes.reshape(mesh.ambient_dim, -1))
         if not mapped_nodes.flags.c_contiguous:
             mapped_nodes = np.copy(mapped_nodes, order="C")
 
-        new_groups.append(replace(group,
-            nodes=mapped_nodes.reshape(*group.nodes.shape),
-            element_nr_base=None, node_nr_base=None))
+        new_groups.append(
+            replace(group, nodes=mapped_nodes.reshape(*group.nodes.shape))
+            )
 
     # }}}
 
@@ -1424,7 +1401,6 @@ def affine_map(
 
             return AffineMap(matrix, offset)
 
-        from dataclasses import replace
         facial_adjacency_groups = []
         for old_fagrp_list in mesh.facial_adjacency_groups:
             fagrp_list = []
diff --git a/test/test_mesh.py b/test/test_mesh.py
index 6d5014ecf9cd92f73c4cde457ea8b786c45694cc..c3dadef4f4f5bff90630c7faf6a09669ea326954 100644
--- a/test/test_mesh.py
+++ b/test/test_mesh.py
@@ -1104,38 +1104,6 @@ def test_tensor_torus(actx_factory, order, visualize=False):
 # }}}
 
 
-# {{{ test_mesh_element_group_constructor
-
-@pytest.mark.parametrize(("shape_cls", "group_cls"), [
-    (mp.Simplex, SimplexElementGroup),
-    (mp.Hypercube, TensorProductElementGroup)])
-def test_mesh_element_group_constructor(shape_cls, group_cls):
-    order = 7
-    dim = 2
-
-    shape = shape_cls(dim)
-    space = mp.space_for_shape(shape, order)
-
-    unit_nodes = mp.edge_clustered_nodes_for_space(space, shape)
-    nodes = np.stack([unit_nodes] * shape.dim)
-
-    # from unit_nodes
-    meg_from_constructor = group_cls(
-        order, vertex_indices=None, nodes=nodes, unit_nodes=unit_nodes)
-    meg_from_factory = group_cls.make_group(
-        order, vertex_indices=None, nodes=nodes, unit_nodes=unit_nodes)
-    assert meg_from_constructor == meg_from_factory
-
-    # from dim (with default unit nodes)
-    meg_from_constructor = group_cls(
-        order, vertex_indices=None, nodes=nodes, dim=dim)
-    meg_from_factory = group_cls.make_group(
-        order, vertex_indices=None, nodes=nodes, dim=dim)
-    assert meg_from_constructor == meg_from_factory
-
-# }}}
-
-
 # {{{ test_node_vertex_consistency_check
 
 def test_node_vertex_consistency_check(actx_factory):
diff --git a/test/test_meshmode.py b/test/test_meshmode.py
index b9b93b25da1cdac527d3b8997869726687ff4323..c2981866ad958b92c3567eed8dc19ae480221a81 100644
--- a/test/test_meshmode.py
+++ b/test/test_meshmode.py
@@ -876,8 +876,7 @@ def test_mesh_without_vertices(actx_factory):
     from dataclasses import replace
     grp, = mesh.groups
     groups = [
-        replace(grp, nodes=grp.nodes, vertex_indices=None,
-                element_nr_base=None, node_nr_base=None)
+        replace(grp, nodes=grp.nodes, vertex_indices=None)
         for grp in mesh.groups]
     mesh = Mesh(None, groups, is_conforming=False)