Repository: SciTools/iris (language: python)

Issue:
Granular categorisation of Iris warnings
Introduce subclasses of `IrisWarning` to precisely categorise all the types of warning relating to Iris behaviour. This will enable easy suppression of specific types of warning if a user knows this does not matter for their workflow.

Worldwide recognised warning categories - e.g. `DeprecationWarning` - should keep these categorisations and not be brought into the `IrisWarning` hierarchy.

Illustrative examples

- `IrisWarning`
  - `CFWarning`
    - `NotStdNameWarning`
    - `MissingCoordWarning`
  - `DemotedCoordWarning`



Discussion / hints:
## Initial thoughts

### Top class name

The annoying volume of warnings are primarily [`UserWarning`](https://docs.python.org/3/library/exceptions.html#UserWarning)s - those that relate to something the user is doing e.g. loading malformed data, plotting non-bounded coordinates etc. These create the noise so they are the ones we want to subcategorise. Top class should therefore by `IrisUserWarning(UserWarning)`, to help clarify that filtering these will not filter other, rarer warnings such as `DeprecationWarning`.

### Warning location

To ease user filtering, all `IrisUserWarning` subclasses should all be available from a single module. We already have an established precedent: `iris.exceptions`. This is ideal since `Warning` is a subclass of `Exception`.

The precedent of `iris.exceptions` is that this is the primary place the exceptions are defined, then they are imported by the modules that use them. This creates an unfortunate element of separation for future developers, however I think it is better than the inverse (creating in the location-of-use, which requires the creator to remember to import into `iris.exceptions`, and also potentially risks circular imports).

### Existing warning classes

Iris currently has 3 subcategories of warning, spread throughout the code.

- https://github.com/SciTools/iris/blob/c526ce0e5e9f1715fc81c29c9526bfda0a78deca/lib/iris/fileformats/nimrod_load_rules.py#L31
- https://github.com/SciTools/iris/blob/c526ce0e5e9f1715fc81c29c9526bfda0a78deca/lib/iris/fileformats/_nc_load_rules/helpers.py#L285
- https://github.com/SciTools/iris/blob/c526ce0e5e9f1715fc81c29c9526bfda0a78deca/lib/iris/fileformats/netcdf/saver.py#L311

These will be brought into the new hierarchy. Making `TranslationWarning` and `UnkownCellMethodWarning` subclasses of `UserWarning` has a minor potential to cause a breaking change in existing user filtering, but this is thought to be a niche case.

They will also be moved into `iris.exceptions`, then simply imported into their previous locations. There will be no breaking changes from this migration.

Fix patch:
diff --git a/lib/iris/__init__.py b/lib/iris/__init__.py
index 0e6670533f..2a3bd8a753 100644
--- a/lib/iris/__init__.py
+++ b/lib/iris/__init__.py
@@ -175,6 +175,8 @@ def __init__(self, datum_support=False, pandas_ndim=False):
         # self.__dict__['example_future_flag'] = example_future_flag
         self.__dict__["datum_support"] = datum_support
         self.__dict__["pandas_ndim"] = pandas_ndim
+        # TODO: next major release: set IrisDeprecation to subclass
+        #  DeprecationWarning instead of UserWarning.
 
     def __repr__(self):
         # msg = ('Future(example_future_flag={})')
diff --git a/lib/iris/_concatenate.py b/lib/iris/_concatenate.py
index c6d58b1622..837afd73f3 100644
--- a/lib/iris/_concatenate.py
+++ b/lib/iris/_concatenate.py
@@ -16,6 +16,7 @@
 
 import iris.coords
 import iris.cube
+import iris.exceptions
 from iris.util import array_equal, guess_coord_axis
 
 #
@@ -998,7 +999,7 @@ def register(
                 raise iris.exceptions.ConcatenateError([msg])
             elif not match:
                 msg = f"Found cubes with overlap on concatenate axis {candidate_axis}, skipping concatenation for these cubes"
-                warnings.warn(msg)
+                warnings.warn(msg, category=iris.exceptions.IrisUserWarning)
 
         # Check for compatible AuxCoords.
         if match:
diff --git a/lib/iris/_deprecation.py b/lib/iris/_deprecation.py
index 73fcedcd82..8ad762a558 100644
--- a/lib/iris/_deprecation.py
+++ b/lib/iris/_deprecation.py
@@ -12,7 +12,13 @@
 
 
 class IrisDeprecation(UserWarning):
-    """An Iris deprecation warning."""
+    """
+    An Iris deprecation warning.
+
+    Note this subclasses UserWarning for backwards compatibility with Iris'
+    original deprection warnings. Should subclass DeprecationWarning at the
+    next major release.
+    """
 
     pass
 
@@ -44,7 +50,7 @@ def warn_deprecated(msg, stacklevel=2):
         >>>
 
     """
-    warnings.warn(msg, IrisDeprecation, stacklevel=stacklevel)
+    warnings.warn(msg, category=IrisDeprecation, stacklevel=stacklevel)
 
 
 # A Mixin for a wrapper class that copies the docstring of the wrapped class
diff --git a/lib/iris/analysis/_regrid.py b/lib/iris/analysis/_regrid.py
index 4592a0ede7..65679cd968 100644
--- a/lib/iris/analysis/_regrid.py
+++ b/lib/iris/analysis/_regrid.py
@@ -20,6 +20,7 @@
     snapshot_grid,
 )
 from iris.analysis._scipy_interpolate import _RegularGridInterpolator
+from iris.exceptions import IrisImpossibleUpdateWarning
 from iris.util import _meshgrid, guess_coord_axis
 
 
@@ -1136,6 +1137,6 @@ def regrid_reference_surface(
                 "Cannot update aux_factory {!r} because of dropped"
                 " coordinates.".format(factory.name())
             )
-            warnings.warn(msg)
+            warnings.warn(msg, category=IrisImpossibleUpdateWarning)
 
     return result
diff --git a/lib/iris/analysis/calculus.py b/lib/iris/analysis/calculus.py
index 75b7d86406..44b1adc580 100644
--- a/lib/iris/analysis/calculus.py
+++ b/lib/iris/analysis/calculus.py
@@ -24,6 +24,7 @@
 import iris.analysis.maths
 import iris.coord_systems
 import iris.coords
+from iris.exceptions import IrisUserWarning
 from iris.util import delta
 
 __all__ = ["cube_delta", "curl", "differentiate"]
@@ -85,7 +86,10 @@ def _construct_midpoint_coord(coord, circular=None):
             "Construction coordinate midpoints for the '{}' coordinate, "
             "though it has the attribute 'circular'={}."
         )
-        warnings.warn(msg.format(circular, coord.circular, coord.name()))
+        warnings.warn(
+            msg.format(circular, coord.circular, coord.name()),
+            category=IrisUserWarning,
+        )
 
     if coord.ndim != 1:
         raise iris.exceptions.CoordinateMultiDimError(coord)
diff --git a/lib/iris/analysis/cartography.py b/lib/iris/analysis/cartography.py
index 0d17f0b38a..0fae5bc499 100644
--- a/lib/iris/analysis/cartography.py
+++ b/lib/iris/analysis/cartography.py
@@ -401,16 +401,25 @@ def area_weights(cube, normalize=False):
     cs = cube.coord_system("CoordSystem")
     if isinstance(cs, iris.coord_systems.GeogCS):
         if cs.inverse_flattening != 0.0:
-            warnings.warn("Assuming spherical earth from ellipsoid.")
+            warnings.warn(
+                "Assuming spherical earth from ellipsoid.",
+                category=iris.exceptions.IrisDefaultingWarning,
+            )
         radius_of_earth = cs.semi_major_axis
     elif isinstance(cs, iris.coord_systems.RotatedGeogCS) and (
         cs.ellipsoid is not None
     ):
         if cs.ellipsoid.inverse_flattening != 0.0:
-            warnings.warn("Assuming spherical earth from ellipsoid.")
+            warnings.warn(
+                "Assuming spherical earth from ellipsoid.",
+                category=iris.exceptions.IrisDefaultingWarning,
+            )
         radius_of_earth = cs.ellipsoid.semi_major_axis
     else:
-        warnings.warn("Using DEFAULT_SPHERICAL_EARTH_RADIUS.")
+        warnings.warn(
+            "Using DEFAULT_SPHERICAL_EARTH_RADIUS.",
+            category=iris.exceptions.IrisDefaultingWarning,
+        )
         radius_of_earth = DEFAULT_SPHERICAL_EARTH_RADIUS
 
     # Get the lon and lat coords and axes
@@ -551,7 +560,7 @@ def cosine_latitude_weights(cube):
         warnings.warn(
             "Out of range latitude values will be "
             "clipped to the valid range.",
-            UserWarning,
+            category=iris.exceptions.IrisDefaultingWarning,
         )
     points = lat.points
     l_weights = np.cos(points).clip(0.0, 1.0)
@@ -665,7 +674,8 @@ def project(cube, target_proj, nx=None, ny=None):
         # Assume WGS84 latlon if unspecified
         warnings.warn(
             "Coordinate system of latitude and longitude "
-            "coordinates is not specified. Assuming WGS84 Geodetic."
+            "coordinates is not specified. Assuming WGS84 Geodetic.",
+            category=iris.exceptions.IrisDefaultingWarning,
         )
         orig_cs = iris.coord_systems.GeogCS(
             semi_major_axis=6378137.0, inverse_flattening=298.257223563
@@ -857,7 +867,8 @@ def project(cube, target_proj, nx=None, ny=None):
                 lat_coord.name(),
                 lon_coord.name(),
                 [coord.name() for coord in discarded_coords],
-            )
+            ),
+            category=iris.exceptions.IrisIgnoringWarning,
         )
 
     # TODO handle derived coords/aux_factories
diff --git a/lib/iris/analysis/geometry.py b/lib/iris/analysis/geometry.py
index b246b518d4..9898f4e974 100644
--- a/lib/iris/analysis/geometry.py
+++ b/lib/iris/analysis/geometry.py
@@ -74,7 +74,7 @@ def _extract_relevant_cube_slice(cube, geometry):
     except ValueError:
         warnings.warn(
             "The geometry exceeds the cube's x dimension at the " "lower end.",
-            UserWarning,
+            category=iris.exceptions.IrisGeometryExceedWarning,
         )
         x_min_ix = 0 if x_ascending else x_coord.points.size - 1
 
@@ -84,7 +84,7 @@ def _extract_relevant_cube_slice(cube, geometry):
     except ValueError:
         warnings.warn(
             "The geometry exceeds the cube's x dimension at the " "upper end.",
-            UserWarning,
+            category=iris.exceptions.IrisGeometryExceedWarning,
         )
         x_max_ix = x_coord.points.size - 1 if x_ascending else 0
 
@@ -94,7 +94,7 @@ def _extract_relevant_cube_slice(cube, geometry):
     except ValueError:
         warnings.warn(
             "The geometry exceeds the cube's y dimension at the " "lower end.",
-            UserWarning,
+            category=iris.exceptions.IrisGeometryExceedWarning,
         )
         y_min_ix = 0 if y_ascending else y_coord.points.size - 1
 
@@ -104,7 +104,7 @@ def _extract_relevant_cube_slice(cube, geometry):
     except ValueError:
         warnings.warn(
             "The geometry exceeds the cube's y dimension at the " "upper end.",
-            UserWarning,
+            category=iris.exceptions.IrisGeometryExceedWarning,
         )
         y_max_ix = y_coord.points.size - 1 if y_ascending else 0
 
diff --git a/lib/iris/analysis/maths.py b/lib/iris/analysis/maths.py
index b77c6cd80f..5e180c6ee2 100644
--- a/lib/iris/analysis/maths.py
+++ b/lib/iris/analysis/maths.py
@@ -988,7 +988,8 @@ def _broadcast_cube_coord_data(cube, other, operation_name, dim=None):
     if other.has_bounds():
         warnings.warn(
             "Using {!r} with a bounded coordinate is not well "
-            "defined; ignoring bounds.".format(operation_name)
+            "defined; ignoring bounds.".format(operation_name),
+            category=iris.exceptions.IrisIgnoringBoundsWarning,
         )
 
     points = other.points
diff --git a/lib/iris/aux_factory.py b/lib/iris/aux_factory.py
index f49de62b3f..323c89e3fb 100644
--- a/lib/iris/aux_factory.py
+++ b/lib/iris/aux_factory.py
@@ -21,6 +21,7 @@
     metadata_manager_factory,
 )
 import iris.coords
+from iris.exceptions import IrisIgnoringBoundsWarning
 
 
 class AuxCoordFactory(CFVariableMixin, metaclass=ABCMeta):
@@ -441,7 +442,9 @@ def _check_dependencies(pressure_at_top, sigma, surface_air_pressure):
                     f"Coordinate '{coord.name()}' has bounds. These will "
                     "be disregarded"
                 )
-                warnings.warn(msg, UserWarning, stacklevel=2)
+                warnings.warn(
+                    msg, category=IrisIgnoringBoundsWarning, stacklevel=2
+                )
 
         # Check units
         if sigma.units.is_unknown():
@@ -522,7 +525,8 @@ def make_coord(self, coord_dims_func):
             if pressure_at_top.shape[-1:] not in [(), (1,)]:
                 warnings.warn(
                     "Pressure at top coordinate has bounds. These are being "
-                    "disregarded"
+                    "disregarded",
+                    category=IrisIgnoringBoundsWarning,
                 )
                 pressure_at_top_pts = nd_points_by_key["pressure_at_top"]
                 bds_shape = list(pressure_at_top_pts.shape) + [1]
@@ -530,7 +534,8 @@ def make_coord(self, coord_dims_func):
             if surface_air_pressure.shape[-1:] not in [(), (1,)]:
                 warnings.warn(
                     "Surface pressure coordinate has bounds. These are being "
-                    "disregarded"
+                    "disregarded",
+                    category=IrisIgnoringBoundsWarning,
                 )
                 surface_air_pressure_pts = nd_points_by_key[
                     "surface_air_pressure"
@@ -595,7 +600,9 @@ def __init__(self, delta=None, sigma=None, orography=None):
                 "Orography coordinate {!r} has bounds."
                 " These will be disregarded.".format(orography.name())
             )
-            warnings.warn(msg, UserWarning, stacklevel=2)
+            warnings.warn(
+                msg, category=IrisIgnoringBoundsWarning, stacklevel=2
+            )
 
         self.delta = delta
         self.sigma = sigma
@@ -684,7 +691,7 @@ def make_coord(self, coord_dims_func):
                 warnings.warn(
                     "Orography coordinate has bounds. "
                     "These are being disregarded.",
-                    UserWarning,
+                    category=IrisIgnoringBoundsWarning,
                     stacklevel=2,
                 )
                 orography_pts = nd_points_by_key["orography"]
@@ -739,7 +746,9 @@ def update(self, old_coord, new_coord=None):
                     "Orography coordinate {!r} has bounds."
                     " These will be disregarded.".format(new_coord.name())
                 )
-                warnings.warn(msg, UserWarning, stacklevel=2)
+                warnings.warn(
+                    msg, category=IrisIgnoringBoundsWarning, stacklevel=2
+                )
             self.orography = new_coord
 
 
@@ -806,7 +815,9 @@ def _check_dependencies(delta, sigma, surface_air_pressure):
                 "Surface pressure coordinate {!r} has bounds. These will"
                 " be disregarded.".format(surface_air_pressure.name())
             )
-            warnings.warn(msg, UserWarning, stacklevel=2)
+            warnings.warn(
+                msg, category=IrisIgnoringBoundsWarning, stacklevel=2
+            )
 
         # Check units.
         if sigma is not None and sigma.units.is_unknown():
@@ -898,7 +909,8 @@ def make_coord(self, coord_dims_func):
             if surface_air_pressure.shape[-1:] not in [(), (1,)]:
                 warnings.warn(
                     "Surface pressure coordinate has bounds. "
-                    "These are being disregarded."
+                    "These are being disregarded.",
+                    category=IrisIgnoringBoundsWarning,
                 )
                 surface_air_pressure_pts = nd_points_by_key[
                     "surface_air_pressure"
@@ -1012,7 +1024,9 @@ def _check_dependencies(sigma, eta, depth, depth_c, nsigma, zlev):
                     "The {} coordinate {!r} has bounds. "
                     "These are being disregarded.".format(term, coord.name())
                 )
-                warnings.warn(msg, UserWarning, stacklevel=2)
+                warnings.warn(
+                    msg, category=IrisIgnoringBoundsWarning, stacklevel=2
+                )
 
         for coord, term in ((depth_c, "depth_c"), (nsigma, "nsigma")):
             if coord is not None and coord.shape != (1,):
@@ -1187,7 +1201,9 @@ def make_coord(self, coord_dims_func):
                         "The {} coordinate {!r} has bounds. "
                         "These are being disregarded.".format(key, name)
                     )
-                    warnings.warn(msg, UserWarning, stacklevel=2)
+                    warnings.warn(
+                        msg, category=IrisIgnoringBoundsWarning, stacklevel=2
+                    )
                     # Swap bounds with points.
                     bds_shape = list(nd_points_by_key[key].shape) + [1]
                     bounds = nd_points_by_key[key].reshape(bds_shape)
@@ -1268,7 +1284,9 @@ def _check_dependencies(sigma, eta, depth):
                     "The {} coordinate {!r} has bounds. "
                     "These are being disregarded.".format(term, coord.name())
                 )
-                warnings.warn(msg, UserWarning, stacklevel=2)
+                warnings.warn(
+                    msg, category=IrisIgnoringBoundsWarning, stacklevel=2
+                )
 
         # Check units.
         if sigma is not None and sigma.units.is_unknown():
@@ -1349,7 +1367,9 @@ def make_coord(self, coord_dims_func):
                         "The {} coordinate {!r} has bounds. "
                         "These are being disregarded.".format(key, name)
                     )
-                    warnings.warn(msg, UserWarning, stacklevel=2)
+                    warnings.warn(
+                        msg, category=IrisIgnoringBoundsWarning, stacklevel=2
+                    )
                     # Swap bounds with points.
                     bds_shape = list(nd_points_by_key[key].shape) + [1]
                     bounds = nd_points_by_key[key].reshape(bds_shape)
@@ -1444,7 +1464,9 @@ def _check_dependencies(s, c, eta, depth, depth_c):
                     "The {} coordinate {!r} has bounds. "
                     "These are being disregarded.".format(term, coord.name())
                 )
-                warnings.warn(msg, UserWarning, stacklevel=2)
+                warnings.warn(
+                    msg, category=IrisIgnoringBoundsWarning, stacklevel=2
+                )
 
         if depth_c is not None and depth_c.shape != (1,):
             msg = (
@@ -1543,7 +1565,9 @@ def make_coord(self, coord_dims_func):
                         "The {} coordinate {!r} has bounds. "
                         "These are being disregarded.".format(key, name)
                     )
-                    warnings.warn(msg, UserWarning, stacklevel=2)
+                    warnings.warn(
+                        msg, category=IrisIgnoringBoundsWarning, stacklevel=2
+                    )
                     # Swap bounds with points.
                     bds_shape = list(nd_points_by_key[key].shape) + [1]
                     bounds = nd_points_by_key[key].reshape(bds_shape)
@@ -1637,7 +1661,9 @@ def _check_dependencies(s, eta, depth, a, b, depth_c):
                     "The {} coordinate {!r} has bounds. "
                     "These are being disregarded.".format(term, coord.name())
                 )
-                warnings.warn(msg, UserWarning, stacklevel=2)
+                warnings.warn(
+                    msg, category=IrisIgnoringBoundsWarning, stacklevel=2
+                )
 
         coords = ((a, "a"), (b, "b"), (depth_c, "depth_c"))
         for coord, term in coords:
@@ -1740,7 +1766,9 @@ def make_coord(self, coord_dims_func):
                         "The {} coordinate {!r} has bounds. "
                         "These are being disregarded.".format(key, name)
                     )
-                    warnings.warn(msg, UserWarning, stacklevel=2)
+                    warnings.warn(
+                        msg, category=IrisIgnoringBoundsWarning, stacklevel=2
+                    )
                     # Swap bounds with points.
                     bds_shape = list(nd_points_by_key[key].shape) + [1]
                     bounds = nd_points_by_key[key].reshape(bds_shape)
@@ -1839,7 +1867,9 @@ def _check_dependencies(s, c, eta, depth, depth_c):
                     "The {} coordinate {!r} has bounds. "
                     "These are being disregarded.".format(term, coord.name())
                 )
-                warnings.warn(msg, UserWarning, stacklevel=2)
+                warnings.warn(
+                    msg, category=IrisIgnoringBoundsWarning, stacklevel=2
+                )
 
         if depth_c is not None and depth_c.shape != (1,):
             msg = (
@@ -1938,7 +1968,9 @@ def make_coord(self, coord_dims_func):
                         "The {} coordinate {!r} has bounds. "
                         "These are being disregarded.".format(key, name)
                     )
-                    warnings.warn(msg, UserWarning, stacklevel=2)
+                    warnings.warn(
+                        msg, category=IrisIgnoringBoundsWarning, stacklevel=2
+                    )
                     # Swap bounds with points.
                     bds_shape = list(nd_points_by_key[key].shape) + [1]
                     bounds = nd_points_by_key[key].reshape(bds_shape)
diff --git a/lib/iris/config.py b/lib/iris/config.py
index 79d141e53f..03d3d363a6 100644
--- a/lib/iris/config.py
+++ b/lib/iris/config.py
@@ -36,6 +36,8 @@
 import os.path
 import warnings
 
+import iris.exceptions
+
 
 def get_logger(
     name, datefmt=None, fmt=None, level=None, propagate=None, handler=True
@@ -145,7 +147,10 @@ def get_dir_option(section, option, default=None):
                 "Ignoring config item {!r}:{!r} (section:option) as {!r}"
                 " is not a valid directory path."
             )
-            warnings.warn(msg.format(section, option, c_path))
+            warnings.warn(
+                msg.format(section, option, c_path),
+                category=iris.exceptions.IrisIgnoringWarning,
+            )
     return path
 
 
@@ -251,7 +256,10 @@ def __setattr__(self, name, value):
                     "Attempting to set invalid value {!r} for "
                     "attribute {!r}. Defaulting to {!r}."
                 )
-                warnings.warn(wmsg.format(value, name, good_value))
+                warnings.warn(
+                    wmsg.format(value, name, good_value),
+                    category=iris.exceptions.IrisDefaultingWarning,
+                )
                 value = good_value
         self.__dict__[name] = value
 
diff --git a/lib/iris/coord_systems.py b/lib/iris/coord_systems.py
index edf0c1871b..e2003d1286 100644
--- a/lib/iris/coord_systems.py
+++ b/lib/iris/coord_systems.py
@@ -15,6 +15,8 @@
 import cartopy.crs as ccrs
 import numpy as np
 
+import iris.exceptions
+
 
 def _arg_default(value, default, cast_as=float):
     """Apply a default value and type for an optional kwarg."""
@@ -449,7 +451,7 @@ def inverse_flattening(self, value):
             "the GeogCS object. To change other properties set them explicitly"
             " or create a new GeogCS instance."
         )
-        warnings.warn(wmsg, UserWarning)
+        warnings.warn(wmsg, category=iris.exceptions.IrisUserWarning)
         value = float(value)
         self._inverse_flattening = value
 
@@ -818,7 +820,8 @@ def as_cartopy_crs(self):
 
         warnings.warn(
             "Discarding false_easting and false_northing that are "
-            "not used by Cartopy."
+            "not used by Cartopy.",
+            category=iris.exceptions.IrisDefaultingWarning,
         )
 
         return ccrs.Orthographic(
diff --git a/lib/iris/coords.py b/lib/iris/coords.py
index 1a6e8d4e6a..3ff9bc8e5e 100644
--- a/lib/iris/coords.py
+++ b/lib/iris/coords.py
@@ -2057,7 +2057,8 @@ def contiguous_bounds(self):
             if self.ndim == 1:
                 warnings.warn(
                     "Coordinate {!r} is not bounded, guessing "
-                    "contiguous bounds.".format(self.name())
+                    "contiguous bounds.".format(self.name()),
+                    category=iris.exceptions.IrisGuessBoundsWarning,
                 )
                 bounds = self._guess_bounds()
             elif self.ndim == 2:
@@ -2224,7 +2225,10 @@ def serialize(x):
                     "Collapsing a multi-dimensional coordinate. "
                     "Metadata may not be fully descriptive for {!r}."
                 )
-                warnings.warn(msg.format(self.name()))
+                warnings.warn(
+                    msg.format(self.name()),
+                    category=iris.exceptions.IrisVagueMetadataWarning,
+                )
             else:
                 try:
                     self._sanity_check_bounds()
@@ -2234,7 +2238,10 @@ def serialize(x):
                         "Metadata may not be fully descriptive for {!r}. "
                         "Ignoring bounds."
                     )
-                    warnings.warn(msg.format(str(exc), self.name()))
+                    warnings.warn(
+                        msg.format(str(exc), self.name()),
+                        category=iris.exceptions.IrisVagueMetadataWarning,
+                    )
                     self.bounds = None
                 else:
                     if not self.is_contiguous():
@@ -2242,7 +2249,10 @@ def serialize(x):
                             "Collapsing a non-contiguous coordinate. "
                             "Metadata may not be fully descriptive for {!r}."
                         )
-                        warnings.warn(msg.format(self.name()))
+                        warnings.warn(
+                            msg.format(self.name()),
+                            category=iris.exceptions.IrisVagueMetadataWarning,
+                        )
 
             if self.has_bounds():
                 item = self.core_bounds()
diff --git a/lib/iris/cube.py b/lib/iris/cube.py
index aec80dce47..60fdbc9c94 100644
--- a/lib/iris/cube.py
+++ b/lib/iris/cube.py
@@ -3857,7 +3857,10 @@ def collapsed(self, coords, aggregator, **kwargs):
             ]
             if lat_match:
                 for coord in lat_match:
-                    warnings.warn(msg.format(coord.name()))
+                    warnings.warn(
+                        msg.format(coord.name()),
+                        category=iris.exceptions.IrisUserWarning,
+                    )
 
         # Determine the dimensions we need to collapse (and those we don't)
         if aggregator.cell_method == "peak":
@@ -4444,7 +4447,8 @@ def rolling_window(self, coord, aggregator, window, **kwargs):
             if coord_.has_bounds():
                 warnings.warn(
                     "The bounds of coordinate %r were ignored in "
-                    "the rolling window operation." % coord_.name()
+                    "the rolling window operation." % coord_.name(),
+                    category=iris.exceptions.IrisIgnoringBoundsWarning,
                 )
 
             if coord_.ndim != 1:
diff --git a/lib/iris/exceptions.py b/lib/iris/exceptions.py
index 5d3da3349e..919917a01d 100644
--- a/lib/iris/exceptions.py
+++ b/lib/iris/exceptions.py
@@ -180,3 +180,212 @@ class CannotAddError(ValueError):
     """Raised when an object (e.g. coord) cannot be added to a :class:`~iris.cube.Cube`."""
 
     pass
+
+
+###############################################################################
+# WARNINGS
+# Please namespace all warning objects (i.e. prefix with Iris...).
+
+
+class IrisUserWarning(UserWarning):
+    """
+    Base class for :class:`UserWarning`\\ s generated by Iris.
+    """
+
+    pass
+
+
+class IrisLoadWarning(IrisUserWarning):
+    """Any warning relating to loading."""
+
+    pass
+
+
+class IrisSaveWarning(IrisUserWarning):
+    """Any warning relating to saving."""
+
+    pass
+
+
+class IrisCfWarning(IrisUserWarning):
+    """Any warning relating to :term:`CF Conventions` ."""
+
+    pass
+
+
+class IrisIgnoringWarning(IrisUserWarning):
+    """
+    Any warning that involves an Iris operation not using some information.
+
+    E.g. :class:`~iris.aux_factory.AuxCoordFactory` generation disregarding
+    bounds.
+    """
+
+    pass
+
+
+class IrisDefaultingWarning(IrisUserWarning):
+    """
+    Any warning that involves Iris changing invalid/missing information.
+
+    E.g. creating a :class:`~iris.coords.AuxCoord` from an invalid
+    :class:`~iris.coords.DimCoord` definition.
+    """
+
+    pass
+
+
+class IrisVagueMetadataWarning(IrisUserWarning):
+    """Warnings where object metadata may not be fully descriptive."""
+
+    pass
+
+
+class IrisUnsupportedPlottingWarning(IrisUserWarning):
+    """Warnings where support for a plotting module/function is not guaranteed."""
+
+    pass
+
+
+class IrisImpossibleUpdateWarning(IrisUserWarning):
+    """
+    Warnings where it is not possible to update an object.
+
+    Mainly generated during regridding where the necessary information for
+    updating an :class:`~iris.aux_factory.AuxCoordFactory` is no longer
+    present.
+    """
+
+    pass
+
+
+class IrisGeometryExceedWarning(IrisUserWarning):
+    """:mod:`iris.analysis.geometry` warnings about geometry exceeding dimensions."""
+
+    pass
+
+
+class IrisMaskValueMatchWarning(IrisUserWarning):
+    """Warnings where the value representing masked data is actually present in data."""
+
+    pass
+
+
+########
+
+
+class IrisCfLoadWarning(IrisCfWarning, IrisLoadWarning):
+    """Any warning relating to both loading and :term:`CF Conventions` ."""
+
+    pass
+
+
+class IrisCfSaveWarning(IrisCfWarning, IrisSaveWarning):
+    """Any warning relating to both saving and :term:`CF Conventions` ."""
+
+    pass
+
+
+class IrisCfInvalidCoordParamWarning(IrisCfLoadWarning):
+    """
+    Warnings where incorrect information for CF coord construction is in a file.
+    """
+
+    pass
+
+
+class IrisCfMissingVarWarning(IrisCfLoadWarning):
+    """
+    Warnings where a CF variable references another variable that is not in the file.
+    """
+
+    pass
+
+
+class IrisCfLabelVarWarning(IrisCfLoadWarning, IrisIgnoringWarning):
+    """
+    Warnings where a CF string/label variable is being used inappropriately.
+    """
+
+    pass
+
+
+class IrisCfNonSpanningVarWarning(IrisCfLoadWarning, IrisIgnoringWarning):
+    """
+    Warnings where a CF variable is ignored because it does not span the required dimension.
+    """
+
+    pass
+
+
+########
+
+
+class IrisIgnoringBoundsWarning(IrisIgnoringWarning):
+    """
+    Warnings where bounds information has not been used by an Iris operation.
+    """
+
+    pass
+
+
+class IrisCannotAddWarning(IrisIgnoringWarning):
+    """
+    Warnings where a member object cannot be added to a :class:`~iris.cube.Cube` .
+    """
+
+    pass
+
+
+class IrisGuessBoundsWarning(IrisDefaultingWarning):
+    """
+    Warnings where Iris has filled absent bounds information with a best estimate.
+    """
+
+    pass
+
+
+class IrisPpClimModifiedWarning(IrisSaveWarning, IrisDefaultingWarning):
+    """
+    Warnings where a climatology has been modified while saving :term:`Post Processing (PP) Format` .
+    """
+
+    pass
+
+
+class IrisFactoryCoordNotFoundWarning(IrisLoadWarning):
+    """
+    Warnings where a referenced factory coord can not be found when loading a variable in :term:`NetCDF Format`.
+    """
+
+    pass
+
+
+class IrisNimrodTranslationWarning(IrisLoadWarning):
+    """
+    For unsupported vertical coord types in :mod:`iris.file_formats.nimrod_load_rules`.
+
+    (Pre-dates the full categorisation of Iris UserWarnings).
+    """
+
+    pass
+
+
+class IrisUnknownCellMethodWarning(IrisCfLoadWarning):
+    """
+    If a loaded :class:`~iris.coords.CellMethod` is not one the method names known to Iris.
+
+    (Pre-dates the full categorisation of Iris UserWarnings).
+    """
+
+    pass
+
+
+class IrisSaverFillValueWarning(IrisMaskValueMatchWarning, IrisSaveWarning):
+    """
+    For fill value complications during Iris file saving :term:`NetCDF Format`.
+
+    (Pre-dates the full categorisation of Iris UserWarnings).
+    """
+
+    pass
diff --git a/lib/iris/experimental/regrid.py b/lib/iris/experimental/regrid.py
index 76c6002d2b..d5fa7c6f72 100644
--- a/lib/iris/experimental/regrid.py
+++ b/lib/iris/experimental/regrid.py
@@ -43,6 +43,7 @@
 import iris.analysis.cartography
 import iris.coord_systems
 import iris.cube
+from iris.exceptions import IrisImpossibleUpdateWarning
 from iris.util import _meshgrid
 
 wmsg = (
@@ -538,7 +539,7 @@ def regrid_reference_surface(
                     "Cannot update aux_factory {!r} because of dropped"
                     " coordinates.".format(factory.name())
                 )
-                warnings.warn(msg)
+                warnings.warn(msg, category=IrisImpossibleUpdateWarning)
         return result
 
     def __call__(self, src_cube):
diff --git a/lib/iris/experimental/ugrid/cf.py b/lib/iris/experimental/ugrid/cf.py
index 86b76c7a75..42c1cfd0a3 100644
--- a/lib/iris/experimental/ugrid/cf.py
+++ b/lib/iris/experimental/ugrid/cf.py
@@ -12,6 +12,7 @@
 """
 import warnings
 
+from ...exceptions import IrisCfLabelVarWarning, IrisCfMissingVarWarning
 from ...fileformats import cf
 from .mesh import Connectivity
 
@@ -65,7 +66,9 @@ def identify(cls, variables, ignore=None, target=None, warn=True):
                                 f"{nc_var_name}"
                             )
                             if warn:
-                                warnings.warn(message)
+                                warnings.warn(
+                                    message, category=IrisCfMissingVarWarning
+                                )
                         else:
                             # Restrict to non-string type i.e. not a
                             # CFLabelVariable.
@@ -80,7 +83,9 @@ def identify(cls, variables, ignore=None, target=None, warn=True):
                                     f"CF-netCDF label variable."
                                 )
                                 if warn:
-                                    warnings.warn(message)
+                                    warnings.warn(
+                                        message, category=IrisCfLabelVarWarning
+                                    )
 
         return result
 
@@ -136,7 +141,10 @@ def identify(cls, variables, ignore=None, target=None, warn=True):
                                     f"variable {nc_var_name}"
                                 )
                                 if warn:
-                                    warnings.warn(message)
+                                    warnings.warn(
+                                        message,
+                                        category=IrisCfMissingVarWarning,
+                                    )
                             else:
                                 # Restrict to non-string type i.e. not a
                                 # CFLabelVariable.
@@ -154,7 +162,10 @@ def identify(cls, variables, ignore=None, target=None, warn=True):
                                         f"CF-netCDF label variable."
                                     )
                                     if warn:
-                                        warnings.warn(message)
+                                        warnings.warn(
+                                            message,
+                                            category=IrisCfLabelVarWarning,
+                                        )
 
         return result
 
@@ -211,7 +222,9 @@ def identify(cls, variables, ignore=None, target=None, warn=True):
                             f"referenced by netCDF variable {nc_var_name}"
                         )
                         if warn:
-                            warnings.warn(message)
+                            warnings.warn(
+                                message, category=IrisCfMissingVarWarning
+                            )
                     else:
                         # Restrict to non-string type i.e. not a
                         # CFLabelVariable.
@@ -226,7 +239,9 @@ def identify(cls, variables, ignore=None, target=None, warn=True):
                                 f"variable."
                             )
                             if warn:
-                                warnings.warn(message)
+                                warnings.warn(
+                                    message, category=IrisCfLabelVarWarning
+                                )
 
         return result
 
diff --git a/lib/iris/experimental/ugrid/load.py b/lib/iris/experimental/ugrid/load.py
index d2670ac690..67d1491930 100644
--- a/lib/iris/experimental/ugrid/load.py
+++ b/lib/iris/experimental/ugrid/load.py
@@ -19,6 +19,11 @@
 
 from ...config import get_logger
 from ...coords import AuxCoord
+from ...exceptions import (
+    IrisCfWarning,
+    IrisDefaultingWarning,
+    IrisIgnoringWarning,
+)
 from ...fileformats._nc_load_rules.helpers import get_attr_units, get_names
 from ...fileformats.netcdf import loader as nc_loader
 from ...io import decode_uri, expand_filespecs
@@ -35,6 +40,20 @@
 logger = get_logger(__name__, propagate=True, handler=False)
 
 
+class _WarnComboCfDefaulting(IrisCfWarning, IrisDefaultingWarning):
+    """One-off combination of warning classes - enhances user filtering."""
+
+    pass
+
+
+class _WarnComboCfDefaultingIgnoring(
+    _WarnComboCfDefaulting, IrisIgnoringWarning
+):
+    """One-off combination of warning classes - enhances user filtering."""
+
+    pass
+
+
 class ParseUGridOnLoad(threading.local):
     def __init__(self):
         """
@@ -351,7 +370,10 @@ def _build_mesh(cf, mesh_var, file_path):
         )
     if cf_role_message:
         cf_role_message += " Correcting to 'mesh_topology'."
-        warnings.warn(cf_role_message)
+        warnings.warn(
+            cf_role_message,
+            category=_WarnComboCfDefaulting,
+        )
 
     if hasattr(mesh_var, "volume_node_connectivity"):
         topology_dimension = 3
@@ -369,7 +391,7 @@ def _build_mesh(cf, mesh_var, file_path):
             f" : *Assuming* topology_dimension={topology_dimension}"
             ", consistent with the attached connectivities."
         )
-        warnings.warn(msg)
+        warnings.warn(msg, category=_WarnComboCfDefaulting)
     else:
         quoted_topology_dimension = mesh_var.topology_dimension
         if quoted_topology_dimension != topology_dimension:
@@ -381,7 +403,10 @@ def _build_mesh(cf, mesh_var, file_path):
                 f"{quoted_topology_dimension}"
                 " -- ignoring this as it is inconsistent."
             )
-            warnings.warn(msg)
+            warnings.warn(
+                msg,
+                category=_WarnComboCfDefaultingIgnoring,
+            )
 
     node_dimension = None
     edge_dimension = getattr(mesh_var, "edge_dimension", None)
diff --git a/lib/iris/fileformats/_ff.py b/lib/iris/fileformats/_ff.py
index 2545bc39ae..5121b47976 100644
--- a/lib/iris/fileformats/_ff.py
+++ b/lib/iris/fileformats/_ff.py
@@ -13,7 +13,11 @@
 
 import numpy as np
 
-from iris.exceptions import NotYetImplementedError
+from iris.exceptions import (
+    IrisDefaultingWarning,
+    IrisLoadWarning,
+    NotYetImplementedError,
+)
 from iris.fileformats._ff_cross_references import STASH_TRANS
 
 from . import pp
@@ -118,6 +122,12 @@
 REAL_POLE_LON = 5
 
 
+class _WarnComboLoadingDefaulting(IrisDefaultingWarning, IrisLoadWarning):
+    """One-off combination of warning classes - enhances user filtering."""
+
+    pass
+
+
 class Grid:
     """
     An abstract class representing the default/file-level grid
@@ -431,7 +441,8 @@ def grid(self):
             grid_class = NewDynamics
             warnings.warn(
                 "Staggered grid type: {} not currently interpreted, assuming "
-                "standard C-grid".format(self.grid_staggering)
+                "standard C-grid".format(self.grid_staggering),
+                category=_WarnComboLoadingDefaulting,
             )
         grid = grid_class(
             self.column_dependent_constants,
@@ -554,7 +565,7 @@ def range_order(range1, range2, resolution):
                 "may be incorrect, not having taken into account the "
                 "boundary size."
             )
-            warnings.warn(msg)
+            warnings.warn(msg, category=IrisLoadWarning)
         else:
             range2 = field_dim[0] - res_low
             range1 = field_dim[0] - halo_dim * res_low
@@ -628,7 +639,8 @@ def _adjust_field_for_lbc(self, field):
                     "The LBC has a bdy less than 0. No "
                     "case has previously been seen of "
                     "this, and the decompression may be "
-                    "erroneous."
+                    "erroneous.",
+                    category=IrisLoadWarning,
                 )
             field.bzx -= field.bdx * boundary_packing.x_halo
             field.bzy -= field.bdy * boundary_packing.y_halo
@@ -741,7 +753,8 @@ def _extract_field(self):
                                 "which has not been explicitly "
                                 "handled by the fieldsfile loader."
                                 " Assuming the data is on a P grid"
-                                ".".format(stash, subgrid)
+                                ".".format(stash, subgrid),
+                                category=_WarnComboLoadingDefaulting,
                             )
 
                     field.x, field.y = grid.vectors(subgrid)
@@ -757,14 +770,18 @@ def _extract_field(self):
                                 "STASH to grid type mapping. Picking the P "
                                 "position as the cell type".format(stash)
                             )
-                            warnings.warn(msg)
+                            warnings.warn(
+                                msg,
+                                category=_WarnComboLoadingDefaulting,
+                            )
                         field.bzx, field.bdx = grid.regular_x(subgrid)
                         field.bzy, field.bdy = grid.regular_y(subgrid)
                         field.bplat = grid.pole_lat
                         field.bplon = grid.pole_lon
                     elif no_x or no_y:
                         warnings.warn(
-                            "Partially missing X or Y coordinate values."
+                            "Partially missing X or Y coordinate values.",
+                            category=IrisLoadWarning,
                         )
 
                     # Check for LBC fields.
@@ -810,7 +827,9 @@ def _extract_field(self):
                         "Input field skipped as PPField creation failed :"
                         " error = {!r}"
                     )
-                    warnings.warn(msg.format(str(valerr)))
+                    warnings.warn(
+                        msg.format(str(valerr)), category=IrisLoadWarning
+                    )
 
     def __iter__(self):
         return pp._interpret_fields(self._extract_field())
diff --git a/lib/iris/fileformats/_nc_load_rules/actions.py b/lib/iris/fileformats/_nc_load_rules/actions.py
index 09237d3f11..be84b65132 100644
--- a/lib/iris/fileformats/_nc_load_rules/actions.py
+++ b/lib/iris/fileformats/_nc_load_rules/actions.py
@@ -44,6 +44,7 @@
 import warnings
 
 from iris.config import get_logger
+import iris.exceptions
 import iris.fileformats.cf
 import iris.fileformats.pp as pp
 
@@ -53,6 +54,24 @@
 logger = get_logger(__name__, fmt="[%(funcName)s]")
 
 
+class _WarnComboCfLoadIgnoring(
+    iris.exceptions.IrisCfLoadWarning,
+    iris.exceptions.IrisIgnoringWarning,
+):
+    """One-off combination of warning classes - enhances user filtering."""
+
+    pass
+
+
+class _WarnComboLoadIgnoring(
+    iris.exceptions.IrisLoadWarning,
+    iris.exceptions.IrisIgnoringWarning,
+):
+    """One-off combination of warning classes - enhances user filtering."""
+
+    pass
+
+
 def _default_rulenamesfunc(func_name):
     # A simple default function to deduce the rules-name from an action-name.
     funcname_prefix = "action_"
@@ -471,7 +490,10 @@ def action_formula_type(engine, formula_root_fact):
         succeed = False
         rule_name += f"(FAILED - unrecognised formula type = {formula_type!r})"
         msg = f"Ignored formula of unrecognised type: {formula_type!r}."
-        warnings.warn(msg)
+        warnings.warn(
+            msg,
+            category=_WarnComboCfLoadIgnoring,
+        )
     if succeed:
         # Check we don't already have one.
         existing_type = engine.requires.get("formula_type")
@@ -486,7 +508,10 @@ def action_formula_type(engine, formula_root_fact):
                 f"Formula of type ={formula_type!r} "
                 f"overrides another of type ={existing_type!r}.)"
             )
-            warnings.warn(msg)
+            warnings.warn(
+                msg,
+                category=_WarnComboLoadIgnoring,
+            )
         rule_name += f"_{formula_type}"
         # Set 'requires' info for iris.fileformats.netcdf._load_aux_factory.
         engine.requires["formula_type"] = formula_type
diff --git a/lib/iris/fileformats/_nc_load_rules/helpers.py b/lib/iris/fileformats/_nc_load_rules/helpers.py
index bbf9c660c5..19a9cd18ca 100644
--- a/lib/iris/fileformats/_nc_load_rules/helpers.py
+++ b/lib/iris/fileformats/_nc_load_rules/helpers.py
@@ -219,6 +219,42 @@
 ]
 
 
+class _WarnComboIgnoringLoad(
+    iris.exceptions.IrisIgnoringWarning,
+    iris.exceptions.IrisLoadWarning,
+):
+    """One-off combination of warning classes - enhances user filtering."""
+
+    pass
+
+
+class _WarnComboDefaultingLoad(
+    iris.exceptions.IrisDefaultingWarning,
+    iris.exceptions.IrisLoadWarning,
+):
+    """One-off combination of warning classes - enhances user filtering."""
+
+    pass
+
+
+class _WarnComboDefaultingCfLoad(
+    iris.exceptions.IrisCfLoadWarning,
+    iris.exceptions.IrisDefaultingWarning,
+):
+    """One-off combination of warning classes - enhances user filtering."""
+
+    pass
+
+
+class _WarnComboIgnoringCfLoad(
+    iris.exceptions.IrisIgnoringWarning,
+    iris.exceptions.IrisCfLoadWarning,
+):
+    """One-off combination of warning classes - enhances user filtering."""
+
+    pass
+
+
 def _split_cell_methods(nc_cell_methods: str) -> List[re.Match]:
     """
     Split a CF cell_methods attribute string into a list of zero or more cell
@@ -256,7 +292,11 @@ def _split_cell_methods(nc_cell_methods: str) -> List[re.Match]:
                     "Cell methods may be incorrectly parsed due to mismatched "
                     "brackets"
                 )
-                warnings.warn(msg, UserWarning, stacklevel=2)
+                warnings.warn(
+                    msg,
+                    category=iris.exceptions.IrisCfLoadWarning,
+                    stacklevel=2,
+                )
         if bracket_depth > 0 and ind in name_start_inds:
             name_start_inds.remove(ind)
 
@@ -275,14 +315,21 @@ def _split_cell_methods(nc_cell_methods: str) -> List[re.Match]:
             msg = (
                 f"Failed to fully parse cell method string: {nc_cell_methods}"
             )
-            warnings.warn(msg, UserWarning, stacklevel=2)
+            warnings.warn(
+                msg, category=iris.exceptions.IrisCfLoadWarning, stacklevel=2
+            )
             continue
         nc_cell_methods_matches.append(nc_cell_method_match)
 
     return nc_cell_methods_matches
 
 
-class UnknownCellMethodWarning(Warning):
+class UnknownCellMethodWarning(iris.exceptions.IrisUnknownCellMethodWarning):
+    """
+    Backwards compatible form of :class:`iris.exceptions.IrisUnknownCellMethodWarning`.
+    """
+
+    # TODO: remove at the next major release.
     pass
 
 
@@ -320,7 +367,7 @@ def parse_cell_methods(nc_cell_methods):
                 msg = "NetCDF variable contains unknown cell method {!r}"
                 warnings.warn(
                     msg.format("{}".format(method_words[0])),
-                    UnknownCellMethodWarning,
+                    category=UnknownCellMethodWarning,
                 )
             d[_CM_METHOD] = method
             name = d[_CM_NAME]
@@ -389,7 +436,6 @@ def parse_cell_methods(nc_cell_methods):
 ################################################################################
 def build_cube_metadata(engine):
     """Add the standard meta data to the cube."""
-
     cf_var = engine.cf_var
     cube = engine.cube
 
@@ -436,7 +482,10 @@ def build_cube_metadata(engine):
             cube.attributes[str(attr_name)] = attr_value
         except ValueError as e:
             msg = "Skipping global attribute {!r}: {}"
-            warnings.warn(msg.format(attr_name, str(e)))
+            warnings.warn(
+                msg.format(attr_name, str(e)),
+                category=_WarnComboIgnoringLoad,
+            )
 
 
 ################################################################################
@@ -479,7 +528,7 @@ def _get_ellipsoid(cf_grid_var):
             "applied. To apply the datum when loading, use the "
             "iris.FUTURE.datum_support flag."
         )
-        warnings.warn(wmsg, FutureWarning, stacklevel=14)
+        warnings.warn(wmsg, category=FutureWarning, stacklevel=14)
         datum = None
 
     if datum is not None:
@@ -512,7 +561,10 @@ def build_rotated_coordinate_system(engine, cf_grid_var):
         cf_grid_var, CF_ATTR_GRID_NORTH_POLE_LON, 0.0
     )
     if north_pole_latitude is None or north_pole_longitude is None:
-        warnings.warn("Rotated pole position is not fully specified")
+        warnings.warn(
+            "Rotated pole position is not fully specified",
+            category=iris.exceptions.IrisCfLoadWarning,
+        )
 
     north_pole_grid_lon = getattr(
         cf_grid_var, CF_ATTR_GRID_NORTH_POLE_GRID_LON, 0.0
@@ -859,7 +911,10 @@ def get_attr_units(cf_var, attributes):
         msg = "Ignoring netCDF variable {!r} invalid units {!r}".format(
             cf_var.cf_name, attr_units
         )
-        warnings.warn(msg)
+        warnings.warn(
+            msg,
+            category=_WarnComboIgnoringCfLoad,
+        )
         attributes["invalid_units"] = attr_units
         attr_units = UNKNOWN_UNIT_STRING
 
@@ -948,7 +1003,8 @@ def get_cf_bounds_var(cf_coord_var):
     if attr_bounds is not None and attr_climatology is not None:
         warnings.warn(
             "Ignoring climatology in favour of bounds attribute "
-            "on NetCDF variable {!r}.".format(cf_coord_var.cf_name)
+            "on NetCDF variable {!r}.".format(cf_coord_var.cf_name),
+            category=_WarnComboIgnoringCfLoad,
         )
 
     return cf_bounds_var, climatological
@@ -1007,7 +1063,10 @@ def build_dimension_coordinate(
     if ma.is_masked(points_data):
         points_data = ma.filled(points_data)
         msg = "Gracefully filling {!r} dimension coordinate masked points"
-        warnings.warn(msg.format(str(cf_coord_var.cf_name)))
+        warnings.warn(
+            msg.format(str(cf_coord_var.cf_name)),
+            category=_WarnComboDefaultingLoad,
+        )
 
     # Get any coordinate bounds.
     cf_bounds_var, climatological = get_cf_bounds_var(cf_coord_var)
@@ -1017,7 +1076,10 @@ def build_dimension_coordinate(
         if ma.is_masked(bounds_data):
             bounds_data = ma.filled(bounds_data)
             msg = "Gracefully filling {!r} dimension coordinate masked bounds"
-            warnings.warn(msg.format(str(cf_coord_var.cf_name)))
+            warnings.warn(
+                msg.format(str(cf_coord_var.cf_name)),
+                category=_WarnComboDefaultingLoad,
+            )
         # Handle transposed bounds where the vertex dimension is not
         # the last one. Test based on shape to support different
         # dimension names.
@@ -1082,7 +1144,10 @@ def build_dimension_coordinate(
             "Failed to create {name!r} dimension coordinate: {error}\n"
             "Gracefully creating {name!r} auxiliary coordinate instead."
         )
-        warnings.warn(msg.format(name=str(cf_coord_var.cf_name), error=e_msg))
+        warnings.warn(
+            msg.format(name=str(cf_coord_var.cf_name), error=e_msg),
+            category=_WarnComboDefaultingCfLoad,
+        )
         coord = iris.coords.AuxCoord(
             points_data,
             standard_name=standard_name,
@@ -1097,7 +1162,10 @@ def build_dimension_coordinate(
         try:
             cube.add_aux_coord(coord, data_dims)
         except iris.exceptions.CannotAddError as e_msg:
-            warnings.warn(coord_skipped_msg.format(error=e_msg))
+            warnings.warn(
+                coord_skipped_msg.format(error=e_msg),
+                category=iris.exceptions.IrisCannotAddWarning,
+            )
             coord_skipped = True
     else:
         # Add the dimension coordinate to the cube.
@@ -1108,7 +1176,10 @@ def build_dimension_coordinate(
                 # Scalar coords are placed in the aux_coords container.
                 cube.add_aux_coord(coord, data_dims)
         except iris.exceptions.CannotAddError as e_msg:
-            warnings.warn(coord_skipped_msg.format(error=e_msg))
+            warnings.warn(
+                coord_skipped_msg.format(error=e_msg),
+                category=iris.exceptions.IrisCannotAddWarning,
+            )
             coord_skipped = True
 
     if not coord_skipped:
@@ -1186,7 +1257,10 @@ def build_auxiliary_coordinate(
         cube.add_aux_coord(coord, data_dims)
     except iris.exceptions.CannotAddError as e_msg:
         msg = "{name!r} coordinate not added to Cube: {error}"
-        warnings.warn(msg.format(name=str(cf_coord_var.cf_name), error=e_msg))
+        warnings.warn(
+            msg.format(name=str(cf_coord_var.cf_name), error=e_msg),
+            category=iris.exceptions.IrisCannotAddWarning,
+        )
     else:
         # Make a list with names, stored on the engine, so we can find them all later.
         engine.cube_parts["coordinates"].append((coord, cf_coord_var.cf_name))
@@ -1237,7 +1311,10 @@ def build_cell_measures(engine, cf_cm_var):
         cube.add_cell_measure(cell_measure, data_dims)
     except iris.exceptions.CannotAddError as e_msg:
         msg = "{name!r} cell measure not added to Cube: {error}"
-        warnings.warn(msg.format(name=str(cf_cm_var.cf_name), error=e_msg))
+        warnings.warn(
+            msg.format(name=str(cf_cm_var.cf_name), error=e_msg),
+            category=iris.exceptions.IrisCannotAddWarning,
+        )
     else:
         # Make a list with names, stored on the engine, so we can find them all later.
         engine.cube_parts["cell_measures"].append(
@@ -1286,7 +1363,10 @@ def build_ancil_var(engine, cf_av_var):
         cube.add_ancillary_variable(av, data_dims)
     except iris.exceptions.CannotAddError as e_msg:
         msg = "{name!r} ancillary variable not added to Cube: {error}"
-        warnings.warn(msg.format(name=str(cf_av_var.cf_name), error=e_msg))
+        warnings.warn(
+            msg.format(name=str(cf_av_var.cf_name), error=e_msg),
+            category=iris.exceptions.IrisCannotAddWarning,
+        )
     else:
         # Make a list with names, stored on the engine, so we can find them all later.
         engine.cube_parts["ancillary_variables"].append(
@@ -1503,7 +1583,8 @@ def has_supported_mercator_parameters(engine, cf_name):
     ):
         warnings.warn(
             "It does not make sense to provide both "
-            '"scale_factor_at_projection_origin" and "standard_parallel".'
+            '"scale_factor_at_projection_origin" and "standard_parallel".',
+            category=iris.exceptions.IrisCfInvalidCoordParamWarning,
         )
         is_valid = False
 
@@ -1533,7 +1614,10 @@ def has_supported_polar_stereographic_parameters(engine, cf_name):
         latitude_of_projection_origin != 90
         and latitude_of_projection_origin != -90
     ):
-        warnings.warn('"latitude_of_projection_origin" must be +90 or -90.')
+        warnings.warn(
+            '"latitude_of_projection_origin" must be +90 or -90.',
+            category=iris.exceptions.IrisCfInvalidCoordParamWarning,
+        )
         is_valid = False
 
     if (
@@ -1542,14 +1626,16 @@ def has_supported_polar_stereographic_parameters(engine, cf_name):
     ):
         warnings.warn(
             "It does not make sense to provide both "
-            '"scale_factor_at_projection_origin" and "standard_parallel".'
+            '"scale_factor_at_projection_origin" and "standard_parallel".',
+            category=iris.exceptions.IrisCfInvalidCoordParamWarning,
         )
         is_valid = False
 
     if scale_factor_at_projection_origin is None and standard_parallel is None:
         warnings.warn(
             'One of "scale_factor_at_projection_origin" and '
-            '"standard_parallel" is required.'
+            '"standard_parallel" is required.',
+            category=iris.exceptions.IrisCfInvalidCoordParamWarning,
         )
         is_valid = False
 
diff --git a/lib/iris/fileformats/cf.py b/lib/iris/fileformats/cf.py
index 2ed01846bd..f412955adb 100644
--- a/lib/iris/fileformats/cf.py
+++ b/lib/iris/fileformats/cf.py
@@ -23,6 +23,7 @@
 import numpy as np
 import numpy.ma as ma
 
+import iris.exceptions
 from iris.fileformats.netcdf import _thread_safe_nc
 import iris.util
 
@@ -280,7 +281,10 @@ def identify(cls, variables, ignore=None, target=None, warn=True):
                         if name not in variables:
                             if warn:
                                 message = "Missing CF-netCDF ancillary data variable %r, referenced by netCDF variable %r"
-                                warnings.warn(message % (name, nc_var_name))
+                                warnings.warn(
+                                    message % (name, nc_var_name),
+                                    category=iris.exceptions.IrisCfMissingVarWarning,
+                                )
                         else:
                             result[name] = CFAncillaryDataVariable(
                                 name, variables[name]
@@ -323,7 +327,10 @@ def identify(cls, variables, ignore=None, target=None, warn=True):
                         if name not in variables:
                             if warn:
                                 message = "Missing CF-netCDF auxiliary coordinate variable %r, referenced by netCDF variable %r"
-                                warnings.warn(message % (name, nc_var_name))
+                                warnings.warn(
+                                    message % (name, nc_var_name),
+                                    category=iris.exceptions.IrisCfMissingVarWarning,
+                                )
                         else:
                             # Restrict to non-string type i.e. not a CFLabelVariable.
                             if not _is_str_dtype(variables[name]):
@@ -369,7 +376,10 @@ def identify(cls, variables, ignore=None, target=None, warn=True):
                     if name not in variables:
                         if warn:
                             message = "Missing CF-netCDF boundary variable %r, referenced by netCDF variable %r"
-                            warnings.warn(message % (name, nc_var_name))
+                            warnings.warn(
+                                message % (name, nc_var_name),
+                                category=iris.exceptions.IrisCfMissingVarWarning,
+                            )
                     else:
                         result[name] = CFBoundaryVariable(
                             name, variables[name]
@@ -441,7 +451,10 @@ def identify(cls, variables, ignore=None, target=None, warn=True):
                     if name not in variables:
                         if warn:
                             message = "Missing CF-netCDF climatology variable %r, referenced by netCDF variable %r"
-                            warnings.warn(message % (name, nc_var_name))
+                            warnings.warn(
+                                message % (name, nc_var_name),
+                                category=iris.exceptions.IrisCfMissingVarWarning,
+                            )
                     else:
                         result[name] = CFClimatologyVariable(
                             name, variables[name]
@@ -582,7 +595,8 @@ def identify(cls, variables, ignore=None, target=None, warn=True):
                             if warn:
                                 message = "Missing CF-netCDF formula term variable %r, referenced by netCDF variable %r"
                                 warnings.warn(
-                                    message % (variable_name, nc_var_name)
+                                    message % (variable_name, nc_var_name),
+                                    category=iris.exceptions.IrisCfMissingVarWarning,
                                 )
                         else:
                             if variable_name not in result:
@@ -646,7 +660,10 @@ def identify(cls, variables, ignore=None, target=None, warn=True):
                     if name not in variables:
                         if warn:
                             message = "Missing CF-netCDF grid mapping variable %r, referenced by netCDF variable %r"
-                            warnings.warn(message % (name, nc_var_name))
+                            warnings.warn(
+                                message % (name, nc_var_name),
+                                category=iris.exceptions.IrisCfMissingVarWarning,
+                            )
                     else:
                         result[name] = CFGridMappingVariable(
                             name, variables[name]
@@ -685,7 +702,10 @@ def identify(cls, variables, ignore=None, target=None, warn=True):
                         if name not in variables:
                             if warn:
                                 message = "Missing CF-netCDF label variable %r, referenced by netCDF variable %r"
-                                warnings.warn(message % (name, nc_var_name))
+                                warnings.warn(
+                                    message % (name, nc_var_name),
+                                    category=iris.exceptions.IrisCfMissingVarWarning,
+                                )
                         else:
                             # Register variable, but only allow string type.
                             var = variables[name]
@@ -857,7 +877,8 @@ def identify(cls, variables, ignore=None, target=None, warn=True):
                             if warn:
                                 message = "Missing CF-netCDF measure variable %r, referenced by netCDF variable %r"
                                 warnings.warn(
-                                    message % (variable_name, nc_var_name)
+                                    message % (variable_name, nc_var_name),
+                                    category=iris.exceptions.IrisCfMissingVarWarning,
                                 )
                         else:
                             result[variable_name] = CFMeasureVariable(
@@ -1069,7 +1090,8 @@ def __init__(self, file_source, warn=False, monotonic=False):
         ]:
             warnings.warn(
                 "Optimise CF-netCDF loading by converting data from NetCDF3 "
-                'to NetCDF4 file format using the "nccopy" command.'
+                'to NetCDF4 file format using the "nccopy" command.',
+                category=iris.exceptions.IrisLoadWarning,
             )
 
         self._check_monotonic = monotonic
@@ -1210,7 +1232,10 @@ def _build(cf_variable):
                                 cf_variable.dimensions,
                             )
                         )
-                        warnings.warn(msg)
+                        warnings.warn(
+                            msg,
+                            category=iris.exceptions.IrisCfNonSpanningVarWarning,
+                        )
 
             # Build CF data variable relationships.
             if isinstance(cf_variable, CFDataVariable):
@@ -1261,7 +1286,10 @@ def _build(cf_variable):
                                         cf_variable.dimensions,
                                     )
                                 )
-                                warnings.warn(msg)
+                                warnings.warn(
+                                    msg,
+                                    category=iris.exceptions.IrisCfNonSpanningVarWarning,
+                                )
 
             # Add the CF group to the variable.
             cf_variable.cf_group = cf_group
diff --git a/lib/iris/fileformats/name_loaders.py b/lib/iris/fileformats/name_loaders.py
index 0189a8806f..cb8867b6ea 100644
--- a/lib/iris/fileformats/name_loaders.py
+++ b/lib/iris/fileformats/name_loaders.py
@@ -17,7 +17,7 @@
 import iris.coord_systems
 from iris.coords import AuxCoord, CellMethod, DimCoord
 import iris.cube
-from iris.exceptions import TranslationError
+from iris.exceptions import IrisLoadWarning, TranslationError
 import iris.util
 
 EARTH_RADIUS = 6371229.0
@@ -273,7 +273,9 @@ def _parse_units(units):
     try:
         units = cf_units.Unit(units)
     except ValueError:
-        warnings.warn("Unknown units: {!r}".format(units))
+        warnings.warn(
+            "Unknown units: {!r}".format(units), category=IrisLoadWarning
+        )
         units = cf_units.Unit(None)
 
     return units
@@ -611,7 +613,9 @@ def _build_cell_methods(av_or_ints, coord):
         else:
             cell_method = None
             msg = "Unknown {} statistic: {!r}. Unable to create cell method."
-            warnings.warn(msg.format(coord, av_or_int))
+            warnings.warn(
+                msg.format(coord, av_or_int), category=IrisLoadWarning
+            )
         cell_methods.append(cell_method)  # NOTE: this can be a None
     return cell_methods
 
diff --git a/lib/iris/fileformats/netcdf/loader.py b/lib/iris/fileformats/netcdf/loader.py
index 20d255ea44..29202af89e 100644
--- a/lib/iris/fileformats/netcdf/loader.py
+++ b/lib/iris/fileformats/netcdf/loader.py
@@ -50,6 +50,15 @@
 NetCDFDataProxy = _thread_safe_nc.NetCDFDataProxy
 
 
+class _WarnComboIgnoringBoundsLoad(
+    iris.exceptions.IrisIgnoringBoundsWarning,
+    iris.exceptions.IrisLoadWarning,
+):
+    """One-off combination of warning classes - enhances user filtering."""
+
+    pass
+
+
 def _actions_engine():
     # Return an 'actions engine', which provides a pyke-rules-like interface to
     # the core cf translation code.
@@ -352,7 +361,8 @@ def coord_from_term(term):
                         return coord
                 warnings.warn(
                     "Unable to find coordinate for variable "
-                    "{!r}".format(name)
+                    "{!r}".format(name),
+                    category=iris.exceptions.IrisFactoryCoordNotFoundWarning,
                 )
 
         if formula_type == "atmosphere_sigma_coordinate":
@@ -393,7 +403,10 @@ def coord_from_term(term):
                                 coord_p0.name()
                             )
                         )
-                        warnings.warn(msg)
+                        warnings.warn(
+                            msg,
+                            category=_WarnComboIgnoringBoundsLoad,
+                        )
                     coord_a = coord_from_term("a")
                     if coord_a is not None:
                         if coord_a.units.is_unknown():
@@ -584,7 +597,10 @@ def load_cubes(file_sources, callback=None, constraints=None):
                 try:
                     _load_aux_factory(engine, cube)
                 except ValueError as e:
-                    warnings.warn("{}".format(e))
+                    warnings.warn(
+                        "{}".format(e),
+                        category=iris.exceptions.IrisLoadWarning,
+                    )
 
                 # Perform any user registered callback function.
                 cube = run_callback(callback, cube, cf_var, file_source)
diff --git a/lib/iris/fileformats/netcdf/saver.py b/lib/iris/fileformats/netcdf/saver.py
index c0cfd3d10b..1ff69df1f7 100644
--- a/lib/iris/fileformats/netcdf/saver.py
+++ b/lib/iris/fileformats/netcdf/saver.py
@@ -157,6 +157,15 @@
 }
 
 
+class _WarnComboMaskSave(
+    iris.exceptions.IrisMaskValueMatchWarning,
+    iris.exceptions.IrisSaveWarning,
+):
+    """One-off combination of warning classes - enhances user filtering."""
+
+    pass
+
+
 class CFNameCoordMap:
     """Provide a simple CF name to CF coordinate mapping."""
 
@@ -308,7 +317,12 @@ def _data_fillvalue_check(arraylib, data, check_value):
     return is_masked, contains_value
 
 
-class SaverFillValueWarning(UserWarning):
+class SaverFillValueWarning(iris.exceptions.IrisSaverFillValueWarning):
+    """
+    Backwards compatible form of :class:`iris.exceptions.IrisSaverFillValueWarning`.
+    """
+
+    # TODO: remove at the next major release.
     pass
 
 
@@ -359,7 +373,10 @@ def _fillvalue_report(fill_info, is_masked, contains_fill_value, warn=False):
         )
 
     if warn and result is not None:
-        warnings.warn(result)
+        warnings.warn(
+            result,
+            category=_WarnComboMaskSave,
+        )
     return result
 
 
@@ -733,7 +750,7 @@ def write(
                 msg = "cf_profile is available but no {} defined.".format(
                     "cf_patch"
                 )
-                warnings.warn(msg)
+                warnings.warn(msg, category=iris.exceptions.IrisCfSaveWarning)
 
     @staticmethod
     def check_attribute_compliance(container, data_dtype):
@@ -1144,7 +1161,7 @@ def _add_aux_factories(self, cube, cf_var_cube, dimension_names):
                     "Unable to determine formula terms "
                     "for AuxFactory: {!r}".format(factory)
                 )
-                warnings.warn(msg)
+                warnings.warn(msg, category=iris.exceptions.IrisSaveWarning)
             else:
                 # Override `standard_name`, `long_name`, and `axis` of the
                 # primary coord that signals the presence of a dimensionless
@@ -2126,7 +2143,10 @@ def add_ellipsoid(ellipsoid):
 
                 # osgb (a specific tmerc)
                 elif isinstance(cs, iris.coord_systems.OSGB):
-                    warnings.warn("OSGB coordinate system not yet handled")
+                    warnings.warn(
+                        "OSGB coordinate system not yet handled",
+                        category=iris.exceptions.IrisSaveWarning,
+                    )
 
                 # lambert azimuthal equal area
                 elif isinstance(
@@ -2195,7 +2215,8 @@ def add_ellipsoid(ellipsoid):
                     warnings.warn(
                         "Unable to represent the horizontal "
                         "coordinate system. The coordinate system "
-                        "type %r is not yet implemented." % type(cs)
+                        "type %r is not yet implemented." % type(cs),
+                        category=iris.exceptions.IrisSaveWarning,
                     )
 
                 self._coord_systems.append(cs)
@@ -2359,7 +2380,7 @@ def set_packing_ncattrs(cfvar):
                     "attribute, but {attr_name!r} should only be a CF "
                     "global attribute.".format(attr_name=attr_name)
                 )
-                warnings.warn(msg)
+                warnings.warn(msg, category=iris.exceptions.IrisCfSaveWarning)
 
             _setncattr(cf_var, attr_name, value)
 
@@ -2593,7 +2614,9 @@ def complete(self, issue_warnings=True) -> List[Warning]:
         if issue_warnings:
             # Issue any delayed warnings from the compute.
             for delayed_warning in result_warnings:
-                warnings.warn(delayed_warning)
+                warnings.warn(
+                    delayed_warning, category=iris.exceptions.IrisSaveWarning
+                )
 
         return result_warnings
 
@@ -2911,7 +2934,7 @@ def is_valid_packspec(p):
                 msg = "cf_profile is available but no {} defined.".format(
                     "cf_patch_conventions"
                 )
-                warnings.warn(msg)
+                warnings.warn(msg, category=iris.exceptions.IrisCfSaveWarning)
 
         # Add conventions attribute.
         sman.update_global_attributes(Conventions=conventions)
diff --git a/lib/iris/fileformats/nimrod_load_rules.py b/lib/iris/fileformats/nimrod_load_rules.py
index fd1ccb0e95..17db0644ee 100644
--- a/lib/iris/fileformats/nimrod_load_rules.py
+++ b/lib/iris/fileformats/nimrod_load_rules.py
@@ -16,7 +16,11 @@
 import iris
 import iris.coord_systems
 from iris.coords import DimCoord
-from iris.exceptions import CoordinateNotFoundError, TranslationError
+from iris.exceptions import (
+    CoordinateNotFoundError,
+    IrisNimrodTranslationWarning,
+    TranslationError,
+)
 
 __all__ = ["run"]
 
@@ -28,7 +32,12 @@
 )
 
 
-class TranslationWarning(Warning):
+class TranslationWarning(IrisNimrodTranslationWarning):
+    """
+    Backwards compatible form of :class:`iris.exceptions.IrisNimrodTranslationWarning`.
+    """
+
+    # TODO: remove at the next major release.
     pass
 
 
@@ -181,7 +190,8 @@ def units(cube, field):
         warnings.warn(
             "Unhandled units '{0}' recorded in cube attributes.".format(
                 field_units
-            )
+            ),
+            category=IrisNimrodTranslationWarning,
         )
         cube.attributes["invalid_units"] = field_units
 
@@ -417,7 +427,8 @@ def coord_system(field, handle_metadata_errors):
         if any([is_missing(field, v) for v in crs_args]):
             warnings.warn(
                 "Coordinate Reference System is not completely defined. "
-                "Plotting and reprojection may be impaired."
+                "Plotting and reprojection may be impaired.",
+                category=IrisNimrodTranslationWarning,
             )
         coord_sys = iris.coord_systems.TransverseMercator(
             *crs_args, iris.coord_systems.GeogCS(**ellipsoid)
@@ -539,7 +550,7 @@ def vertical_coord(cube, field):
             f"{field.vertical_coord_type} != {field.reference_vertical_coord_type}. "
             f"Assuming {field.vertical_coord_type}"
         )
-        warnings.warn(msg)
+        warnings.warn(msg, category=IrisNimrodTranslationWarning)
 
     coord_point = field.vertical_coord
     if coord_point == 8888.0:
@@ -586,7 +597,7 @@ def vertical_coord(cube, field):
     warnings.warn(
         "Vertical coord {!r} not yet handled"
         "".format(field.vertical_coord_type),
-        TranslationWarning,
+        category=TranslationWarning,
     )
 
 
@@ -831,7 +842,8 @@ def probability_coord(cube, field, handle_metadata_errors):
             )
             warnings.warn(
                 f"No default units for {coord_name} coord of {cube.name()}. "
-                "Meta-data may be incomplete."
+                "Meta-data may be incomplete.",
+                category=IrisNimrodTranslationWarning,
             )
         new_coord = iris.coords.AuxCoord(
             np.array(coord_val, dtype=np.float32), bounds=bounds, **coord_keys
diff --git a/lib/iris/fileformats/pp.py b/lib/iris/fileformats/pp.py
index 65e0e16d72..e19ba3adff 100644
--- a/lib/iris/fileformats/pp.py
+++ b/lib/iris/fileformats/pp.py
@@ -27,6 +27,7 @@
 from iris._lazy_data import as_concrete_data, as_lazy_data, is_lazy_data
 import iris.config
 import iris.coord_systems
+import iris.exceptions
 
 # NOTE: this is for backwards-compatitibility *ONLY*
 # We could simply remove it for v2.0 ?
@@ -220,6 +221,33 @@
 }
 
 
+class _WarnComboLoadingMask(
+    iris.exceptions.IrisLoadWarning,
+    iris.exceptions.IrisMaskValueMatchWarning,
+):
+    """One-off combination of warning classes - enhances user filtering."""
+
+    pass
+
+
+class _WarnComboLoadingDefaulting(
+    iris.exceptions.IrisDefaultingWarning,
+    iris.exceptions.IrisLoadWarning,
+):
+    """One-off combination of warning classes - enhances user filtering."""
+
+    pass
+
+
+class _WarnComboIgnoringLoad(
+    iris.exceptions.IrisIgnoringWarning,
+    iris.exceptions.IrisLoadWarning,
+):
+    """One-off combination of warning classes - enhances user filtering."""
+
+    pass
+
+
 class STASH(collections.namedtuple("STASH", "model section item")):
     """
     A class to hold a single STASH code.
@@ -1165,7 +1193,10 @@ def save(self, file_handle):
                 "missing data. To save these as normal values, please "
                 "set the field BMDI not equal to any valid data points."
             )
-            warnings.warn(msg.format(mdi))
+            warnings.warn(
+                msg.format(mdi),
+                category=_WarnComboLoadingMask,
+            )
         if isinstance(data, ma.MaskedArray):
             if ma.is_masked(data):
                 data = data.filled(fill_value=mdi)
@@ -1290,7 +1321,8 @@ def save(self, file_handle):
             warnings.warn(
                 "Downcasting array precision from float64 to float32"
                 " for save.If float64 precision is required then"
-                " please save in a different format"
+                " please save in a different format",
+                category=_WarnComboLoadingDefaulting,
             )
             data = data.astype(">f4")
             lb[self.HEADER_DICT["lbuser"][0]] = 1
@@ -1732,7 +1764,8 @@ def _interpret_fields(fields):
             warnings.warn(
                 "Landmask compressed fields existed without a "
                 "landmask to decompress with. The data will have "
-                "a shape of (0, 0) and will not read."
+                "a shape of (0, 0) and will not read.",
+                category=iris.exceptions.IrisLoadWarning,
             )
             mask_shape = (0, 0)
         else:
@@ -1901,7 +1934,10 @@ def _field_gen(filename, read_data_bytes, little_ended=False):
                     "Unable to interpret field {}. {}. Skipping "
                     "the remainder of the file.".format(field_count, str(e))
                 )
-                warnings.warn(msg)
+                warnings.warn(
+                    msg,
+                    category=_WarnComboIgnoringLoad,
+                )
                 break
 
             # Skip the trailing 4-byte word containing the header length
@@ -1921,7 +1957,8 @@ def _field_gen(filename, read_data_bytes, little_ended=False):
                 warnings.warn(
                     wmsg.format(
                         pp_field.lblrec * PP_WORD_DEPTH, len_of_data_plus_extra
-                    )
+                    ),
+                    category=_WarnComboIgnoringLoad,
                 )
                 break
 
diff --git a/lib/iris/fileformats/pp_save_rules.py b/lib/iris/fileformats/pp_save_rules.py
index 0369fc9fd0..998255ff2b 100644
--- a/lib/iris/fileformats/pp_save_rules.py
+++ b/lib/iris/fileformats/pp_save_rules.py
@@ -10,6 +10,7 @@
 
 import iris
 from iris.aux_factory import HybridHeightFactory, HybridPressureFactory
+from iris.exceptions import IrisPpClimModifiedWarning
 from iris.fileformats._ff_cross_references import STASH_TRANS
 from iris.fileformats._pp_lbproc_pairs import LBPROC_MAP
 from iris.fileformats.rules import (
@@ -890,4 +891,4 @@ def verify(cube, field):
 
 def _conditional_warning(condition, warning):
     if condition:
-        warnings.warn(warning)
+        warnings.warn(warning, category=IrisPpClimModifiedWarning)
diff --git a/lib/iris/fileformats/rules.py b/lib/iris/fileformats/rules.py
index 707fd58757..d5a4b9c823 100644
--- a/lib/iris/fileformats/rules.py
+++ b/lib/iris/fileformats/rules.py
@@ -47,7 +47,8 @@ def as_cube(self):
                 src_cubes = src_cubes.merge(unique=False)
                 if len(src_cubes) > 1:
                     warnings.warn(
-                        "Multiple reference cubes for {}".format(self.name)
+                        "Multiple reference cubes for {}".format(self.name),
+                        category=iris.exceptions.IrisUserWarning,
                     )
             src_cube = src_cubes[-1]
 
@@ -329,7 +330,7 @@ def _make_cube(field, converter):
             cube.units = metadata.units
         except ValueError:
             msg = "Ignoring PP invalid units {!r}".format(metadata.units)
-            warnings.warn(msg)
+            warnings.warn(msg, category=iris.exceptions.IrisIgnoringWarning)
             cube.attributes["invalid_units"] = metadata.units
             cube.units = cf_units._UNKNOWN_UNIT_STRING
 
@@ -350,7 +351,10 @@ def _resolve_factory_references(
         except _ReferenceError as e:
             msg = "Unable to create instance of {factory}. " + str(e)
             factory_name = factory.factory_class.__name__
-            warnings.warn(msg.format(factory=factory_name))
+            warnings.warn(
+                msg.format(factory=factory_name),
+                category=iris.exceptions.IrisUserWarning,
+            )
         else:
             aux_factory = factory.factory_class(*args)
             cube.add_aux_factory(aux_factory)
diff --git a/lib/iris/iterate.py b/lib/iris/iterate.py
index cf16c9cbe6..cc82433e85 100644
--- a/lib/iris/iterate.py
+++ b/lib/iris/iterate.py
@@ -14,6 +14,8 @@
 
 import numpy as np
 
+from iris.exceptions import IrisUserWarning
+
 __all__ = ["izip"]
 
 
@@ -164,7 +166,8 @@ def izip(*cubes, **kwargs):
                 warnings.warn(
                     "Iterating over coordinate '%s' in step whose "
                     "definitions match but whose values "
-                    "differ." % coord_a.name()
+                    "differ." % coord_a.name(),
+                    category=IrisUserWarning,
                 )
 
     return _ZipSlicesIterator(
diff --git a/lib/iris/pandas.py b/lib/iris/pandas.py
index 4d6681e94e..9c54a0514b 100644
--- a/lib/iris/pandas.py
+++ b/lib/iris/pandas.py
@@ -29,6 +29,7 @@
 from iris._deprecation import warn_deprecated
 from iris.coords import AncillaryVariable, AuxCoord, CellMeasure, DimCoord
 from iris.cube import Cube, CubeList
+from iris.exceptions import IrisIgnoringWarning
 
 
 def _get_dimensional_metadata(name, values, calendar=None, dm_class=None):
@@ -446,7 +447,7 @@ def format_dimensional_metadata(dm_class_, values_, name_, dimensions_):
         if columns_ignored:
             ignored_args = ", ".join([t[2] for t in class_arg_mapping])
             message = f"The input pandas_structure is a Series; ignoring arguments: {ignored_args} ."
-            warnings.warn(message)
+            warnings.warn(message, category=IrisIgnoringWarning)
         class_arg_mapping = []
 
     non_data_names = []
@@ -896,7 +897,7 @@ def merge_metadata(meta_var_list):
             "'iris.FUTURE.pandas_ndim = True'. More info is in the "
             "documentation."
         )
-        warnings.warn(message, FutureWarning)
+        warnings.warn(message, category=FutureWarning)
 
         # The legacy behaviour.
         data = cube.data
diff --git a/lib/iris/plot.py b/lib/iris/plot.py
index ebcb5c3bcb..28b458f715 100644
--- a/lib/iris/plot.py
+++ b/lib/iris/plot.py
@@ -34,7 +34,7 @@
 import iris.coord_systems
 import iris.coords
 import iris.cube
-from iris.exceptions import IrisError
+from iris.exceptions import IrisError, IrisUnsupportedPlottingWarning
 
 # Importing iris.palette to register the brewer palettes.
 import iris.palette
@@ -2023,7 +2023,7 @@ def update_animation_iris(i, cubes, vmin, vmax, coords):
             "use: {}."
         )
         msg = msg.format(plot_func.__module__, supported)
-        warnings.warn(msg, UserWarning)
+        warnings.warn(msg, category=IrisUnsupportedPlottingWarning)
 
     supported = ["contour", "contourf", "pcolor", "pcolormesh"]
     if plot_func.__name__ not in supported:
@@ -2032,7 +2032,7 @@ def update_animation_iris(i, cubes, vmin, vmax, coords):
             "use: {}."
         )
         msg = msg.format(plot_func.__name__, supported)
-        warnings.warn(msg, UserWarning)
+        warnings.warn(msg, category=IrisUnsupportedPlottingWarning)
 
     # Determine plot range.
     vmin = kwargs.pop("vmin", min([cc.data.min() for cc in cubes]))


Test patch:
diff --git a/docs/src/whatsnew/latest.rst b/docs/src/whatsnew/latest.rst
index 35b6121c41..a3abf6c5ad 100644
--- a/docs/src/whatsnew/latest.rst
+++ b/docs/src/whatsnew/latest.rst
@@ -30,7 +30,9 @@ This document explains the changes made to Iris for this release
 ✨ Features
 ===========
 
-#. N/A
+#. `@trexfeathers`_ and `@HGWright`_ (reviewer) sub-categorised all Iris'
+   :class:`UserWarning`\s for richer filtering. The full index of
+   sub-categories can be seen here: :mod:`iris.exceptions` . (:pull:`5498`)
 
 
 🐛 Bugs Fixed
@@ -38,7 +40,7 @@ This document explains the changes made to Iris for this release
 
 #. `@scottrobinson02`_ fixed the output units when dividing a coordinate by a
    cube. (:issue:`5305`, :pull:`5331`)
-   
+
 #. `@ESadek-MO`_ has updated :mod:`iris.tests.graphics.idiff` to stop duplicated file names
    preventing acceptance. (:issue:`5098`, :pull:`5482`)
 
@@ -84,7 +86,7 @@ This document explains the changes made to Iris for this release
 
 #. `@trexfeathers`_ replaced all uses of the ``logging.WARNING`` level, in
    favour of using Python warnings, following team agreement. (:pull:`5488`)
-   
+
 #. `@trexfeathers`_ adapted benchmarking to work with ASV ``>=v0.6`` by no
    longer using the ``--strict`` argument. (:pull:`5496`)
 
diff --git a/lib/iris/tests/graphics/idiff.py b/lib/iris/tests/graphics/idiff.py
index 62e72f4e0e..4af7f4726d 100755
--- a/lib/iris/tests/graphics/idiff.py
+++ b/lib/iris/tests/graphics/idiff.py
@@ -28,6 +28,7 @@
 from matplotlib.testing.exceptions import ImageComparisonFailure  # noqa
 import matplotlib.widgets as mwidget  # noqa
 
+from iris.exceptions import IrisIgnoringWarning  # noqa
 import iris.tests  # noqa
 import iris.tests.graphics as graphics  # noqa
 
@@ -151,7 +152,7 @@ def step_over_diffs(result_dir, display=True):
             distance = graphics.get_phash(reference_image_path) - phash
         except FileNotFoundError:
             wmsg = "Ignoring unregistered test result {!r}."
-            warnings.warn(wmsg.format(test_key))
+            warnings.warn(wmsg.format(test_key), category=IrisIgnoringWarning)
             continue
 
         processed = True
diff --git a/lib/iris/tests/integration/experimental/test_ugrid_load.py b/lib/iris/tests/integration/experimental/test_ugrid_load.py
index b0b60ee506..d94e85d2f5 100644
--- a/lib/iris/tests/integration/experimental/test_ugrid_load.py
+++ b/lib/iris/tests/integration/experimental/test_ugrid_load.py
@@ -19,6 +19,7 @@
 import pytest
 
 from iris import Constraint, load
+from iris.exceptions import IrisCfWarning
 from iris.experimental.ugrid.load import (
     PARSE_UGRID_ON_LOAD,
     load_mesh,
@@ -170,7 +171,7 @@ def create_synthetic_file(self, **create_kwargs):
     def test_mesh_bad_topology_dimension(self):
         # Check that the load generates a suitable warning.
         warn_regex = r"topology_dimension.* ignoring"
-        with pytest.warns(UserWarning, match=warn_regex):
+        with pytest.warns(IrisCfWarning, match=warn_regex):
             template = "minimal_bad_topology_dim"
             dim_line = "mesh_var:topology_dimension = 1 ;"  # which is wrong !
             cube = self.create_synthetic_test_cube(
@@ -183,7 +184,7 @@ def test_mesh_bad_topology_dimension(self):
     def test_mesh_no_topology_dimension(self):
         # Check that the load generates a suitable warning.
         warn_regex = r"Mesh variable.* has no 'topology_dimension'"
-        with pytest.warns(UserWarning, match=warn_regex):
+        with pytest.warns(IrisCfWarning, match=warn_regex):
             template = "minimal_bad_topology_dim"
             dim_line = ""  # don't create ANY topology_dimension property
             cube = self.create_synthetic_test_cube(
@@ -196,7 +197,7 @@ def test_mesh_no_topology_dimension(self):
     def test_mesh_bad_cf_role(self):
         # Check that the load generates a suitable warning.
         warn_regex = r"inappropriate cf_role"
-        with pytest.warns(UserWarning, match=warn_regex):
+        with pytest.warns(IrisCfWarning, match=warn_regex):
             template = "minimal_bad_mesh_cf_role"
             dim_line = 'mesh_var:cf_role = "foo" ;'
             _ = self.create_synthetic_test_cube(
@@ -206,7 +207,7 @@ def test_mesh_bad_cf_role(self):
     def test_mesh_no_cf_role(self):
         # Check that the load generates a suitable warning.
         warn_regex = r"no cf_role attribute"
-        with pytest.warns(UserWarning, match=warn_regex):
+        with pytest.warns(IrisCfWarning, match=warn_regex):
             template = "minimal_bad_mesh_cf_role"
             dim_line = ""
             _ = self.create_synthetic_test_cube(
diff --git a/lib/iris/tests/integration/netcdf/test_delayed_save.py b/lib/iris/tests/integration/netcdf/test_delayed_save.py
index 616feb3b0e..09f6235aab 100644
--- a/lib/iris/tests/integration/netcdf/test_delayed_save.py
+++ b/lib/iris/tests/integration/netcdf/test_delayed_save.py
@@ -17,8 +17,8 @@
 import pytest
 
 import iris
+from iris.exceptions import IrisSaverFillValueWarning
 from iris.fileformats.netcdf._thread_safe_nc import default_fillvals
-from iris.fileformats.netcdf.saver import SaverFillValueWarning
 import iris.tests
 from iris.tests.stock import realistic_4d
 
@@ -311,7 +311,7 @@ def test_fill_warnings(self, warning_type, output_path, save_is_delayed):
         result_warnings = [
             log.message
             for log in logged_warnings
-            if isinstance(log.message, SaverFillValueWarning)
+            if isinstance(log.message, IrisSaverFillValueWarning)
         ]
 
         if save_is_delayed:
@@ -320,7 +320,9 @@ def test_fill_warnings(self, warning_type, output_path, save_is_delayed):
             # Complete the operation now
             with warnings.catch_warnings():
                 # NOTE: warnings should *not* be issued here, instead they are returned.
-                warnings.simplefilter("error", category=SaverFillValueWarning)
+                warnings.simplefilter(
+                    "error", category=IrisSaverFillValueWarning
+                )
                 result_warnings = result.compute()
 
         # Either way, we should now have 2 similar warnings.
diff --git a/lib/iris/tests/integration/netcdf/test_general.py b/lib/iris/tests/integration/netcdf/test_general.py
index dc0c29455f..6214f09e7e 100644
--- a/lib/iris/tests/integration/netcdf/test_general.py
+++ b/lib/iris/tests/integration/netcdf/test_general.py
@@ -25,7 +25,7 @@
 from iris.coords import CellMethod
 from iris.cube import Cube, CubeList
 import iris.exceptions
-from iris.fileformats.netcdf import Saver, UnknownCellMethodWarning
+from iris.fileformats.netcdf import Saver
 
 # Get the netCDF4 module, but in a sneaky way that avoids triggering the "do not import
 # netCDF4" check in "iris.tests.test_coding_standards.test_netcdf4_import()".
@@ -141,7 +141,9 @@ def test_unknown_method(self):
             warning_messages = [
                 warn
                 for warn in warning_messages
-                if isinstance(warn, UnknownCellMethodWarning)
+                if isinstance(
+                    warn, iris.exceptions.IrisUnknownCellMethodWarning
+                )
             ]
             self.assertEqual(len(warning_messages), 1)
             message = warning_messages[0].args[0]
diff --git a/lib/iris/tests/integration/netcdf/test_self_referencing.py b/lib/iris/tests/integration/netcdf/test_self_referencing.py
index 3395296e11..554fabb4fc 100644
--- a/lib/iris/tests/integration/netcdf/test_self_referencing.py
+++ b/lib/iris/tests/integration/netcdf/test_self_referencing.py
@@ -16,6 +16,7 @@
 import numpy as np
 
 import iris
+from iris.exceptions import IrisCfMissingVarWarning
 from iris.fileformats.netcdf import _thread_safe_nc
 
 
@@ -46,7 +47,9 @@ def test_cmip6_volcello_load_issue_3367(self):
         with mock.patch("warnings.warn") as warn:
             # ensure file loads without failure
             cube = iris.load_cube(self.fname)
-            warn.assert_has_calls([mock.call(expected_msg)])
+            warn.assert_has_calls(
+                [mock.call(expected_msg, category=IrisCfMissingVarWarning)]
+            )
 
         # extra check to ensure correct variable was found
         assert cube.standard_name == "ocean_volume"
@@ -113,7 +116,9 @@ def test_self_referencing_load_issue_3367(self):
         with mock.patch("warnings.warn") as warn:
             # ensure file loads without failure
             cube = iris.load_cube(self.temp_dir_path)
-            warn.assert_called_with(expected_msg)
+            warn.assert_called_with(
+                expected_msg, category=IrisCfMissingVarWarning
+            )
 
         # extra check to ensure correct variable was found
         assert cube.standard_name == "ocean_volume"
diff --git a/lib/iris/tests/integration/test_pp.py b/lib/iris/tests/integration/test_pp.py
index e654694aa7..026bdae58a 100644
--- a/lib/iris/tests/integration/test_pp.py
+++ b/lib/iris/tests/integration/test_pp.py
@@ -18,7 +18,7 @@
 from iris.aux_factory import HybridHeightFactory, HybridPressureFactory
 from iris.coords import AuxCoord, CellMethod, DimCoord
 from iris.cube import Cube
-from iris.exceptions import IgnoreCubeException
+from iris.exceptions import IgnoreCubeException, IrisUserWarning
 import iris.fileformats.pp
 from iris.fileformats.pp import load_pairs_from_fields
 import iris.fileformats.pp_load_rules
@@ -290,7 +290,7 @@ def test_hybrid_pressure_with_duplicate_references(self):
             "iris.fileformats.pp.load", new=load
         ) as load, mock.patch("warnings.warn") as warn:
             _, _, _ = iris.fileformats.pp.load_cubes("DUMMY")
-            warn.assert_called_with(msg)
+            warn.assert_called_with(msg, category=IrisUserWarning)
 
     def test_hybrid_height_with_non_standard_coords(self):
         # Check the save rules are using the AuxFactory to find the
@@ -415,7 +415,7 @@ def test_hybrid_height_round_trip_no_reference(self):
             "Unable to create instance of HybridHeightFactory. "
             "The source data contains no field(s) for 'orography'."
         )
-        warn.assert_called_with(msg)
+        warn.assert_called_with(msg, category=IrisUserWarning)
 
         # Check the data cube is set up to use hybrid height.
         self._test_coord(
diff --git a/lib/iris/tests/test_coding_standards.py b/lib/iris/tests/test_coding_standards.py
index 6cea9dc001..54309e3906 100644
--- a/lib/iris/tests/test_coding_standards.py
+++ b/lib/iris/tests/test_coding_standards.py
@@ -8,6 +8,7 @@
 # importing anything else
 import iris.tests as tests  # isort:skip
 
+import ast
 from datetime import datetime
 from fnmatch import fnmatch
 from glob import glob
@@ -133,6 +134,66 @@ def test_python_versions():
         assert search in path.read_text()
 
 
+def test_categorised_warnings():
+    """
+    To ensure that all UserWarnings raised by Iris are categorised, for ease of use.
+
+    No obvious category? Use the parent:
+    :class:`iris.exceptions.IrisUserWarning`.
+
+    Warning matches multiple categories? Create a one-off combo class. For
+    example:
+
+    .. code-block:: python
+
+        class _WarnComboCfDefaulting(IrisCfWarning, IrisDefaultingWarning):
+            \"""
+            One-off combination of warning classes - enhances user filtering.
+            \"""
+            pass
+
+    """
+    warns_without_category = []
+    warns_with_user_warning = []
+    tmp_list = []
+
+    for file_path in Path(IRIS_DIR).rglob("*.py"):
+        file_text = file_path.read_text()
+        parsed = ast.parse(source=file_text)
+        calls = filter(lambda node: hasattr(node, "func"), ast.walk(parsed))
+        warn_calls = filter(
+            lambda c: getattr(c.func, "attr", None) == "warn", calls
+        )
+
+        warn_call: ast.Call
+        for warn_call in warn_calls:
+            warn_ref = f"{file_path}:{warn_call.lineno}"
+            tmp_list.append(warn_ref)
+
+            category_kwargs = filter(
+                lambda k: k.arg == "category", warn_call.keywords
+            )
+            category_kwarg: ast.keyword = next(category_kwargs, None)
+
+            if category_kwarg is None:
+                warns_without_category.append(warn_ref)
+            # Work with Attribute or Name instances.
+            elif (
+                getattr(category_kwarg.value, "attr", None)
+                or getattr(category_kwarg.value, "id", None)
+            ) == "UserWarning":
+                warns_with_user_warning.append(warn_ref)
+
+    # This avoids UserWarnings being raised by unwritten default behaviour.
+    assert (
+        warns_without_category == []
+    ), "All warnings raised by Iris must be raised with the category kwarg."
+
+    assert (
+        warns_with_user_warning == []
+    ), "No warnings raised by Iris can be the base UserWarning class."
+
+
 class TestLicenseHeaders(tests.IrisTest):
     @staticmethod
     def whatchanged_parse(whatchanged_output):
diff --git a/lib/iris/tests/test_concatenate.py b/lib/iris/tests/test_concatenate.py
index 9287a79fda..ec92838466 100644
--- a/lib/iris/tests/test_concatenate.py
+++ b/lib/iris/tests/test_concatenate.py
@@ -20,6 +20,7 @@
 from iris.aux_factory import HybridHeightFactory
 from iris.coords import AncillaryVariable, AuxCoord, CellMeasure, DimCoord
 import iris.cube
+from iris.exceptions import IrisUserWarning
 import iris.tests.stock as stock
 
 
@@ -340,7 +341,8 @@ def test_points_overlap_increasing(self):
         cubes.append(_make_cube((0, 2), y, 1))
         cubes.append(_make_cube((1, 3), y, 2))
         with pytest.warns(
-            UserWarning, match="Found cubes with overlap on concatenate axis"
+            IrisUserWarning,
+            match="Found cubes with overlap on concatenate axis",
         ):
             result = concatenate(cubes)
         self.assertEqual(len(result), 2)
@@ -351,7 +353,8 @@ def test_points_overlap_decreasing(self):
         cubes.append(_make_cube(x, (3, 0, -1), 1))
         cubes.append(_make_cube(x, (1, -1, -1), 2))
         with pytest.warns(
-            UserWarning, match="Found cubes with overlap on concatenate axis"
+            IrisUserWarning,
+            match="Found cubes with overlap on concatenate axis",
         ):
             result = concatenate(cubes)
         self.assertEqual(len(result), 2)
@@ -366,7 +369,8 @@ def test_bounds_overlap_increasing(self):
         )
         cubes.append(cube)
         with pytest.warns(
-            UserWarning, match="Found cubes with overlap on concatenate axis"
+            IrisUserWarning,
+            match="Found cubes with overlap on concatenate axis",
         ):
             result = concatenate(cubes)
         self.assertEqual(len(result), 2)
@@ -381,7 +385,8 @@ def test_bounds_overlap_decreasing(self):
         )
         cubes.append(cube)
         with pytest.warns(
-            UserWarning, match="Found cubes with overlap on concatenate axis"
+            IrisUserWarning,
+            match="Found cubes with overlap on concatenate axis",
         ):
             result = concatenate(cubes)
         self.assertEqual(len(result), 2)
diff --git a/lib/iris/tests/test_coordsystem.py b/lib/iris/tests/test_coordsystem.py
index 7cd15297cc..2e5aef249c 100644
--- a/lib/iris/tests/test_coordsystem.py
+++ b/lib/iris/tests/test_coordsystem.py
@@ -18,6 +18,7 @@
 )
 import iris.coords
 import iris.cube
+from iris.exceptions import IrisUserWarning
 import iris.tests.stock
 
 
@@ -341,7 +342,7 @@ def test_inverse_flattening_change(self):
         cs = GeogCS(6543210, 6500000)
         initial_crs = cs.as_cartopy_crs()
         with self.assertWarnsRegex(
-            UserWarning,
+            IrisUserWarning,
             "Setting inverse_flattening does not affect other properties of the GeogCS object.",
         ):
             cs.inverse_flattening = cs.inverse_flattening + 1
diff --git a/lib/iris/tests/test_hybrid.py b/lib/iris/tests/test_hybrid.py
index 76fc971a08..b070f36a7a 100644
--- a/lib/iris/tests/test_hybrid.py
+++ b/lib/iris/tests/test_hybrid.py
@@ -18,6 +18,7 @@
 
 import iris
 from iris.aux_factory import HybridHeightFactory, HybridPressureFactory
+from iris.exceptions import IrisIgnoringBoundsWarning
 import iris.tests.stock
 
 
@@ -136,7 +137,7 @@ def test_invalid_dependencies(self):
         with warnings.catch_warnings():
             # Cause all warnings to raise Exceptions
             warnings.simplefilter("error")
-            with self.assertRaises(UserWarning):
+            with self.assertRaises(IrisIgnoringBoundsWarning):
                 _ = HybridHeightFactory(orography=sigma)
 
     def test_bounded_orography(self):
@@ -154,7 +155,7 @@ def test_bounded_orography(self):
         with warnings.catch_warnings():
             # Cause all warnings to raise Exceptions
             warnings.simplefilter("error")
-            with self.assertRaisesRegex(UserWarning, msg):
+            with self.assertRaisesRegex(IrisIgnoringBoundsWarning, msg):
                 self.cube.coord("altitude")
 
 
@@ -215,7 +216,7 @@ def test_invalid_dependencies(self):
         with warnings.catch_warnings():
             # Cause all warnings to raise Exceptions
             warnings.simplefilter("error")
-            with self.assertRaises(UserWarning):
+            with self.assertRaises(IrisIgnoringBoundsWarning):
                 _ = HybridPressureFactory(
                     sigma=sigma, surface_air_pressure=sigma
                 )
@@ -235,7 +236,7 @@ def test_bounded_surface_pressure(self):
         with warnings.catch_warnings():
             # Cause all warnings to raise Exceptions
             warnings.simplefilter("error")
-            with self.assertRaisesRegex(UserWarning, msg):
+            with self.assertRaisesRegex(IrisIgnoringBoundsWarning, msg):
                 self.cube.coord("air_pressure")
 
 
diff --git a/lib/iris/tests/test_iterate.py b/lib/iris/tests/test_iterate.py
index ec86d2f69d..6317ef32b5 100644
--- a/lib/iris/tests/test_iterate.py
+++ b/lib/iris/tests/test_iterate.py
@@ -22,6 +22,7 @@
 
 import iris
 import iris.analysis
+from iris.exceptions import IrisUserWarning
 import iris.iterate
 import iris.tests.stock
 
@@ -365,12 +366,12 @@ def test_izip_different_valued_coords(self):
             warnings.simplefilter(
                 "error"
             )  # Cause all warnings to raise Exceptions
-            with self.assertRaises(UserWarning):
+            with self.assertRaises(IrisUserWarning):
                 iris.iterate.izip(
                     self.cube_a, self.cube_b, coords=self.coord_names
                 )
             # Call with coordinates, rather than names
-            with self.assertRaises(UserWarning):
+            with self.assertRaises(IrisUserWarning):
                 iris.iterate.izip(
                     self.cube_a, self.cube_b, coords=[latitude, longitude]
                 )
diff --git a/lib/iris/tests/test_netcdf.py b/lib/iris/tests/test_netcdf.py
index 6438140ed9..2e389942bf 100644
--- a/lib/iris/tests/test_netcdf.py
+++ b/lib/iris/tests/test_netcdf.py
@@ -26,6 +26,7 @@
 from iris._lazy_data import is_lazy_data
 import iris.analysis.trajectory
 import iris.coord_systems as icoord_systems
+from iris.exceptions import IrisCfSaveWarning
 from iris.fileformats._nc_load_rules import helpers as ncload_helpers
 import iris.fileformats.netcdf
 from iris.fileformats.netcdf import _thread_safe_nc
@@ -1099,7 +1100,9 @@ def test_conflicting_global_attributes(self):
         with self.temp_filename(suffix=".nc") as filename:
             with mock.patch("warnings.warn") as warn:
                 iris.save([self.cube, self.cube2], filename)
-                warn.assert_called_with(expected_msg)
+                warn.assert_called_with(
+                    expected_msg, category=IrisCfSaveWarning
+                )
                 self.assertCDL(
                     filename, ("netcdf", "netcdf_save_confl_global_attr.cdl")
                 )
diff --git a/lib/iris/tests/unit/analysis/cartography/test_project.py b/lib/iris/tests/unit/analysis/cartography/test_project.py
index 8649cc55ea..c00830aacc 100644
--- a/lib/iris/tests/unit/analysis/cartography/test_project.py
+++ b/lib/iris/tests/unit/analysis/cartography/test_project.py
@@ -16,6 +16,7 @@
 import iris.coord_systems
 import iris.coords
 import iris.cube
+from iris.exceptions import IrisDefaultingWarning
 import iris.tests
 import iris.tests.stock
 
@@ -161,7 +162,8 @@ def test_no_coord_system(self):
         warn.assert_called_once_with(
             "Coordinate system of latitude and "
             "longitude coordinates is not specified. "
-            "Assuming WGS84 Geodetic."
+            "Assuming WGS84 Geodetic.",
+            category=IrisDefaultingWarning,
         )
 
 
diff --git a/lib/iris/tests/unit/analysis/geometry/test_geometry_area_weights.py b/lib/iris/tests/unit/analysis/geometry/test_geometry_area_weights.py
index 49e03a1174..62ab1ae283 100644
--- a/lib/iris/tests/unit/analysis/geometry/test_geometry_area_weights.py
+++ b/lib/iris/tests/unit/analysis/geometry/test_geometry_area_weights.py
@@ -21,6 +21,7 @@
 from iris.analysis.geometry import geometry_area_weights
 from iris.coords import DimCoord
 from iris.cube import Cube
+from iris.exceptions import IrisGeometryExceedWarning
 import iris.tests.stock as stock
 
 
@@ -148,7 +149,9 @@ def test_distinct_xy_bounds_pole(self):
                 "The geometry exceeds the "
                 "cube's y dimension at the upper end.",
             )
-            self.assertTrue(issubclass(w[-1].category, UserWarning))
+            self.assertTrue(
+                issubclass(w[-1].category, IrisGeometryExceedWarning)
+            )
         target = np.array(
             [
                 [0, top_cell_half, top_cell_half, 0],
diff --git a/lib/iris/tests/unit/coords/test_Coord.py b/lib/iris/tests/unit/coords/test_Coord.py
index 69b6b70c96..c548d017f2 100644
--- a/lib/iris/tests/unit/coords/test_Coord.py
+++ b/lib/iris/tests/unit/coords/test_Coord.py
@@ -19,7 +19,7 @@
 import iris
 from iris.coords import AuxCoord, Coord, DimCoord
 from iris.cube import Cube
-from iris.exceptions import UnitConversionError
+from iris.exceptions import IrisVagueMetadataWarning, UnitConversionError
 from iris.tests.unit.coords import CoordTestMixin
 
 Pair = collections.namedtuple("Pair", "points bounds")
@@ -482,7 +482,7 @@ def test_numeric_nd_multidim_bounds_warning(self):
             "Collapsing a multi-dimensional coordinate. "
             "Metadata may not be fully descriptive for 'y'."
         )
-        with self.assertWarnsRegex(UserWarning, msg):
+        with self.assertWarnsRegex(IrisVagueMetadataWarning, msg):
             coord.collapsed()
 
     def test_lazy_nd_multidim_bounds_warning(self):
@@ -493,7 +493,7 @@ def test_lazy_nd_multidim_bounds_warning(self):
             "Collapsing a multi-dimensional coordinate. "
             "Metadata may not be fully descriptive for 'y'."
         )
-        with self.assertWarnsRegex(UserWarning, msg):
+        with self.assertWarnsRegex(IrisVagueMetadataWarning, msg):
             coord.collapsed()
 
     def test_numeric_nd_noncontiguous_bounds_warning(self):
@@ -504,7 +504,7 @@ def test_numeric_nd_noncontiguous_bounds_warning(self):
             "Collapsing a non-contiguous coordinate. "
             "Metadata may not be fully descriptive for 'y'."
         )
-        with self.assertWarnsRegex(UserWarning, msg):
+        with self.assertWarnsRegex(IrisVagueMetadataWarning, msg):
             coord.collapsed()
 
     def test_lazy_nd_noncontiguous_bounds_warning(self):
@@ -515,7 +515,7 @@ def test_lazy_nd_noncontiguous_bounds_warning(self):
             "Collapsing a non-contiguous coordinate. "
             "Metadata may not be fully descriptive for 'y'."
         )
-        with self.assertWarnsRegex(UserWarning, msg):
+        with self.assertWarnsRegex(IrisVagueMetadataWarning, msg):
             coord.collapsed()
 
     def test_numeric_3_bounds(self):
@@ -530,7 +530,7 @@ def test_numeric_3_bounds(self):
             r"1D coordinates with 2 bounds. Metadata may not be fully "
             r"descriptive for 'x'. Ignoring bounds."
         )
-        with self.assertWarnsRegex(UserWarning, msg):
+        with self.assertWarnsRegex(IrisVagueMetadataWarning, msg):
             collapsed_coord = coord.collapsed()
 
         self.assertFalse(collapsed_coord.has_lazy_points())
@@ -553,7 +553,7 @@ def test_lazy_3_bounds(self):
             r"1D coordinates with 2 bounds. Metadata may not be fully "
             r"descriptive for 'x'. Ignoring bounds."
         )
-        with self.assertWarnsRegex(UserWarning, msg):
+        with self.assertWarnsRegex(IrisVagueMetadataWarning, msg):
             collapsed_coord = coord.collapsed()
 
         self.assertTrue(collapsed_coord.has_lazy_points())
diff --git a/lib/iris/tests/unit/cube/test_Cube.py b/lib/iris/tests/unit/cube/test_Cube.py
index a733665df8..443c9db546 100644
--- a/lib/iris/tests/unit/cube/test_Cube.py
+++ b/lib/iris/tests/unit/cube/test_Cube.py
@@ -40,6 +40,8 @@
     AncillaryVariableNotFoundError,
     CellMeasureNotFoundError,
     CoordinateNotFoundError,
+    IrisUserWarning,
+    IrisVagueMetadataWarning,
     UnitConversionError,
 )
 import iris.tests.stock as stock
@@ -676,7 +678,10 @@ def _assert_warn_collapse_without_weight(self, coords, warn):
         # Ensure that warning is raised.
         msg = "Collapsing spatial coordinate {!r} without weighting"
         for coord in coords:
-            self.assertIn(mock.call(msg.format(coord)), warn.call_args_list)
+            self.assertIn(
+                mock.call(msg.format(coord), category=IrisUserWarning),
+                warn.call_args_list,
+            )
 
     def _assert_nowarn_collapse_without_weight(self, coords, warn):
         # Ensure that warning is not raised.
@@ -765,7 +770,10 @@ def _assert_warn_cannot_check_contiguity(self, warn):
                 f"bounds. Metadata may not be fully descriptive for "
                 f"'{coord}'. Ignoring bounds."
             )
-            self.assertIn(mock.call(msg), warn.call_args_list)
+            self.assertIn(
+                mock.call(msg, category=IrisVagueMetadataWarning),
+                warn.call_args_list,
+            )
 
     def _assert_cube_as_expected(self, cube):
         """Ensure that cube data and coordinates are as expected."""
diff --git a/lib/iris/tests/unit/experimental/ugrid/cf/test_CFUGridAuxiliaryCoordinateVariable.py b/lib/iris/tests/unit/experimental/ugrid/cf/test_CFUGridAuxiliaryCoordinateVariable.py
index a4e0e05a08..641b6b7b44 100644
--- a/lib/iris/tests/unit/experimental/ugrid/cf/test_CFUGridAuxiliaryCoordinateVariable.py
+++ b/lib/iris/tests/unit/experimental/ugrid/cf/test_CFUGridAuxiliaryCoordinateVariable.py
@@ -20,6 +20,7 @@
 import numpy as np
 import pytest
 
+import iris.exceptions
 from iris.experimental.ugrid.cf import CFUGridAuxiliaryCoordinateVariable
 from iris.tests.unit.experimental.ugrid.cf.test_CFUGridReader import (
     netcdf_ugrid_variable,
@@ -215,7 +216,10 @@ def test_warn(self):
         }
 
         def operation(warn: bool):
-            warnings.warn("emit at least 1 warning")
+            warnings.warn(
+                "emit at least 1 warning",
+                category=iris.exceptions.IrisUserWarning,
+            )
             result = CFUGridAuxiliaryCoordinateVariable.identify(
                 vars_all, warn=warn
             )
@@ -223,7 +227,9 @@ def operation(warn: bool):
 
         # Missing warning.
         warn_regex = rf"Missing CF-netCDF auxiliary coordinate variable {subject_name}.*"
-        with pytest.warns(UserWarning, match=warn_regex):
+        with pytest.warns(
+            iris.exceptions.IrisCfMissingVarWarning, match=warn_regex
+        ):
             operation(warn=True)
         with pytest.warns() as record:
             operation(warn=False)
@@ -235,7 +241,9 @@ def operation(warn: bool):
         vars_all[subject_name] = netcdf_ugrid_variable(
             subject_name, "", np.bytes_
         )
-        with pytest.warns(UserWarning, match=warn_regex):
+        with pytest.warns(
+            iris.exceptions.IrisCfLabelVarWarning, match=warn_regex
+        ):
             operation(warn=True)
         with pytest.warns() as record:
             operation(warn=False)
diff --git a/lib/iris/tests/unit/experimental/ugrid/cf/test_CFUGridConnectivityVariable.py b/lib/iris/tests/unit/experimental/ugrid/cf/test_CFUGridConnectivityVariable.py
index 27d5c1db90..5a68a8c03f 100644
--- a/lib/iris/tests/unit/experimental/ugrid/cf/test_CFUGridConnectivityVariable.py
+++ b/lib/iris/tests/unit/experimental/ugrid/cf/test_CFUGridConnectivityVariable.py
@@ -20,6 +20,7 @@
 import numpy as np
 import pytest
 
+import iris.exceptions
 from iris.experimental.ugrid.cf import CFUGridConnectivityVariable
 from iris.experimental.ugrid.mesh import Connectivity
 from iris.tests.unit.experimental.ugrid.cf.test_CFUGridReader import (
@@ -204,7 +205,10 @@ def test_warn(self):
         }
 
         def operation(warn: bool):
-            warnings.warn("emit at least 1 warning")
+            warnings.warn(
+                "emit at least 1 warning",
+                category=iris.exceptions.IrisUserWarning,
+            )
             result = CFUGridConnectivityVariable.identify(vars_all, warn=warn)
             self.assertDictEqual({}, result)
 
@@ -212,7 +216,9 @@ def operation(warn: bool):
         warn_regex = (
             rf"Missing CF-UGRID connectivity variable {subject_name}.*"
         )
-        with pytest.warns(UserWarning, match=warn_regex):
+        with pytest.warns(
+            iris.exceptions.IrisCfMissingVarWarning, match=warn_regex
+        ):
             operation(warn=True)
         with pytest.warns() as record:
             operation(warn=False)
@@ -224,7 +230,9 @@ def operation(warn: bool):
         vars_all[subject_name] = netcdf_ugrid_variable(
             subject_name, "", np.bytes_
         )
-        with pytest.warns(UserWarning, match=warn_regex):
+        with pytest.warns(
+            iris.exceptions.IrisCfLabelVarWarning, match=warn_regex
+        ):
             operation(warn=True)
         with pytest.warns() as record:
             operation(warn=False)
diff --git a/lib/iris/tests/unit/experimental/ugrid/cf/test_CFUGridMeshVariable.py b/lib/iris/tests/unit/experimental/ugrid/cf/test_CFUGridMeshVariable.py
index 6b278cf1b1..8302c30177 100644
--- a/lib/iris/tests/unit/experimental/ugrid/cf/test_CFUGridMeshVariable.py
+++ b/lib/iris/tests/unit/experimental/ugrid/cf/test_CFUGridMeshVariable.py
@@ -20,6 +20,7 @@
 import numpy as np
 import pytest
 
+import iris.exceptions
 from iris.experimental.ugrid.cf import CFUGridMeshVariable
 from iris.tests.unit.experimental.ugrid.cf.test_CFUGridReader import (
     netcdf_ugrid_variable,
@@ -247,13 +248,18 @@ def test_warn(self):
         }
 
         def operation(warn: bool):
-            warnings.warn("emit at least 1 warning")
+            warnings.warn(
+                "emit at least 1 warning",
+                category=iris.exceptions.IrisUserWarning,
+            )
             result = CFUGridMeshVariable.identify(vars_all, warn=warn)
             self.assertDictEqual({}, result)
 
         # Missing warning.
         warn_regex = rf"Missing CF-UGRID mesh variable {subject_name}.*"
-        with pytest.warns(UserWarning, match=warn_regex):
+        with pytest.warns(
+            iris.exceptions.IrisCfMissingVarWarning, match=warn_regex
+        ):
             operation(warn=True)
         with pytest.warns() as record:
             operation(warn=False)
@@ -265,7 +271,9 @@ def operation(warn: bool):
         vars_all[subject_name] = netcdf_ugrid_variable(
             subject_name, "", np.bytes_
         )
-        with pytest.warns(UserWarning, match=warn_regex):
+        with pytest.warns(
+            iris.exceptions.IrisCfLabelVarWarning, match=warn_regex
+        ):
             operation(warn=True)
         with pytest.warns() as record:
             operation(warn=False)
diff --git a/lib/iris/tests/unit/fileformats/ff/test_FF2PP.py b/lib/iris/tests/unit/fileformats/ff/test_FF2PP.py
index cec4f53bc3..16943c0c15 100644
--- a/lib/iris/tests/unit/fileformats/ff/test_FF2PP.py
+++ b/lib/iris/tests/unit/fileformats/ff/test_FF2PP.py
@@ -15,7 +15,7 @@
 
 import numpy as np
 
-from iris.exceptions import NotYetImplementedError
+from iris.exceptions import IrisLoadWarning, NotYetImplementedError
 import iris.fileformats._ff as ff
 from iris.fileformats._ff import FF2PP
 import iris.fileformats.pp as pp
@@ -467,7 +467,7 @@ def test_unequal_spacing_eitherside(self):
 
         with mock.patch("warnings.warn") as warn:
             result = ff2pp._det_border(field_x, None)
-        warn.assert_called_with(msg)
+        warn.assert_called_with(msg, category=IrisLoadWarning)
         self.assertIs(result, field_x)
 
     def test_increasing_field_values(self):
diff --git a/lib/iris/tests/unit/fileformats/ff/test_FFHeader.py b/lib/iris/tests/unit/fileformats/ff/test_FFHeader.py
index 6a65397086..72d522ec85 100644
--- a/lib/iris/tests/unit/fileformats/ff/test_FFHeader.py
+++ b/lib/iris/tests/unit/fileformats/ff/test_FFHeader.py
@@ -14,7 +14,7 @@
 
 import numpy as np
 
-from iris.fileformats._ff import FFHeader
+from iris.fileformats._ff import FFHeader, _WarnComboLoadingDefaulting
 
 MyGrid = collections.namedtuple("MyGrid", "column row real horiz_grid_type")
 
@@ -60,7 +60,8 @@ def test_unknown(self):
                 grid = header.grid()
         warn.assert_called_with(
             "Staggered grid type: 0 not currently"
-            " interpreted, assuming standard C-grid"
+            " interpreted, assuming standard C-grid",
+            category=_WarnComboLoadingDefaulting,
         )
         self.assertIs(grid, mock.sentinel.grid)
 
diff --git a/lib/iris/tests/unit/fileformats/name_loaders/test__build_cell_methods.py b/lib/iris/tests/unit/fileformats/name_loaders/test__build_cell_methods.py
index ded635984c..624837c19d 100644
--- a/lib/iris/tests/unit/fileformats/name_loaders/test__build_cell_methods.py
+++ b/lib/iris/tests/unit/fileformats/name_loaders/test__build_cell_methods.py
@@ -15,6 +15,7 @@
 from unittest import mock
 
 import iris.coords
+from iris.exceptions import IrisLoadWarning
 from iris.fileformats.name_loaders import _build_cell_methods
 
 
@@ -104,7 +105,7 @@ def test_unrecognised(self):
             "Unknown {} statistic: {!r}. Unable to "
             "create cell method.".format(coord_name, unrecognised_heading)
         )
-        warn.assert_called_with(expected_msg)
+        warn.assert_called_with(expected_msg, category=IrisLoadWarning)
 
     def test_unrecognised_similar_to_no_averaging(self):
         unrecognised_headings = [
@@ -129,7 +130,7 @@ def test_unrecognised_similar_to_no_averaging(self):
                 "Unknown {} statistic: {!r}. Unable to "
                 "create cell method.".format(coord_name, unrecognised_heading)
             )
-            warn.assert_called_with(expected_msg)
+            warn.assert_called_with(expected_msg, category=IrisLoadWarning)
 
 
 if __name__ == "__main__":
diff --git a/lib/iris/tests/unit/fileformats/nc_load_rules/actions/__init__.py b/lib/iris/tests/unit/fileformats/nc_load_rules/actions/__init__.py
index 399a987f11..38882810d2 100644
--- a/lib/iris/tests/unit/fileformats/nc_load_rules/actions/__init__.py
+++ b/lib/iris/tests/unit/fileformats/nc_load_rules/actions/__init__.py
@@ -12,6 +12,7 @@
 import tempfile
 import warnings
 
+from iris.exceptions import IrisLoadWarning
 import iris.fileformats._nc_load_rules.engine
 from iris.fileformats.cf import CFReader
 import iris.fileformats.netcdf
@@ -138,7 +139,7 @@ def run_testcase(self, warning_regex=None, **testcase_kwargs):
         if warning_regex is None:
             context = self.assertNoWarningsRegexp()
         else:
-            context = self.assertWarnsRegex(UserWarning, warning_regex)
+            context = self.assertWarnsRegex(IrisLoadWarning, warning_regex)
         with context:
             cube = self.load_cube_from_cdl(cdl_string, cdl_path, nc_path)
 
diff --git a/lib/iris/tests/unit/fileformats/nc_load_rules/helpers/test_parse_cell_methods.py b/lib/iris/tests/unit/fileformats/nc_load_rules/helpers/test_parse_cell_methods.py
index 729a2d8b14..9935a6e5ae 100644
--- a/lib/iris/tests/unit/fileformats/nc_load_rules/helpers/test_parse_cell_methods.py
+++ b/lib/iris/tests/unit/fileformats/nc_load_rules/helpers/test_parse_cell_methods.py
@@ -15,6 +15,7 @@
 from unittest import mock
 
 from iris.coords import CellMethod
+from iris.exceptions import IrisCfLoadWarning
 from iris.fileformats._nc_load_rules.helpers import parse_cell_methods
 
 
@@ -123,7 +124,7 @@ def test_comment_bracket_mismatch_warning(self):
         ]
         for cell_method_str in cell_method_strings:
             with self.assertWarns(
-                UserWarning,
+                IrisCfLoadWarning,
                 msg="Cell methods may be incorrectly parsed due to mismatched brackets",
             ):
                 _ = parse_cell_methods(cell_method_str)
@@ -139,7 +140,7 @@ def test_badly_formatted_warning(self):
         ]
         for cell_method_str in cell_method_strings:
             with self.assertWarns(
-                UserWarning,
+                IrisCfLoadWarning,
                 msg=f"Failed to fully parse cell method string: {cell_method_str}",
             ):
                 _ = parse_cell_methods(cell_method_str)
diff --git a/lib/iris/tests/unit/fileformats/netcdf/loader/test__load_aux_factory.py b/lib/iris/tests/unit/fileformats/netcdf/loader/test__load_aux_factory.py
index 841935cc81..c15c8737fd 100644
--- a/lib/iris/tests/unit/fileformats/netcdf/loader/test__load_aux_factory.py
+++ b/lib/iris/tests/unit/fileformats/netcdf/loader/test__load_aux_factory.py
@@ -16,6 +16,7 @@
 
 from iris.coords import DimCoord
 from iris.cube import Cube
+from iris.exceptions import IrisFactoryCoordNotFoundWarning
 from iris.fileformats.netcdf.loader import _load_aux_factory
 
 
@@ -165,7 +166,8 @@ def test_formula_terms_ap_missing_coords(self):
         with mock.patch("warnings.warn") as warn:
             _load_aux_factory(self.engine, self.cube)
         warn.assert_called_once_with(
-            "Unable to find coordinate for variable " "'ap'"
+            "Unable to find coordinate for variable " "'ap'",
+            category=IrisFactoryCoordNotFoundWarning,
         )
         self._check_no_delta()
 
diff --git a/lib/iris/tests/unit/fileformats/netcdf/saver/test_Saver.py b/lib/iris/tests/unit/fileformats/netcdf/saver/test_Saver.py
index 12af318c01..af0d7bcd30 100644
--- a/lib/iris/tests/unit/fileformats/netcdf/saver/test_Saver.py
+++ b/lib/iris/tests/unit/fileformats/netcdf/saver/test_Saver.py
@@ -31,6 +31,7 @@
 )
 from iris.coords import AuxCoord, DimCoord
 from iris.cube import Cube
+from iris.exceptions import IrisMaskValueMatchWarning
 from iris.fileformats.netcdf import Saver, _thread_safe_nc
 import iris.tests.stock as stock
 
@@ -555,7 +556,7 @@ def test_contains_fill_value_passed(self):
         cube = self._make_cube(">f4")
         fill_value = 1
         with self.assertWarnsRegex(
-            UserWarning,
+            IrisMaskValueMatchWarning,
             "contains unmasked data points equal to the fill-value",
         ):
             with self._netCDF_var(cube, fill_value=fill_value):
@@ -567,7 +568,7 @@ def test_contains_fill_value_byte(self):
         cube = self._make_cube(">i1")
         fill_value = 1
         with self.assertWarnsRegex(
-            UserWarning,
+            IrisMaskValueMatchWarning,
             "contains unmasked data points equal to the fill-value",
         ):
             with self._netCDF_var(cube, fill_value=fill_value):
@@ -579,7 +580,7 @@ def test_contains_default_fill_value(self):
         cube = self._make_cube(">f4")
         cube.data[0, 0] = _thread_safe_nc.default_fillvals["f4"]
         with self.assertWarnsRegex(
-            UserWarning,
+            IrisMaskValueMatchWarning,
             "contains unmasked data points equal to the fill-value",
         ):
             with self._netCDF_var(cube):
diff --git a/lib/iris/tests/unit/fileformats/netcdf/saver/test_Saver__lazy_stream_data.py b/lib/iris/tests/unit/fileformats/netcdf/saver/test_Saver__lazy_stream_data.py
index 9686c88abf..da5f2d88fa 100644
--- a/lib/iris/tests/unit/fileformats/netcdf/saver/test_Saver__lazy_stream_data.py
+++ b/lib/iris/tests/unit/fileformats/netcdf/saver/test_Saver__lazy_stream_data.py
@@ -18,6 +18,7 @@
 import numpy as np
 import pytest
 
+from iris.exceptions import IrisMaskValueMatchWarning
 import iris.fileformats.netcdf._thread_safe_nc as threadsafe_nc
 from iris.fileformats.netcdf.saver import Saver, _FillvalueCheckInfo
 
@@ -183,5 +184,5 @@ def test_warnings(self, compute, data_form):
         if n_expected_warnings > 0:
             warning = issued_warnings[0]
             msg = "contains unmasked data points equal to the fill-value, 2.0"
-            assert isinstance(warning, UserWarning)
+            assert isinstance(warning, IrisMaskValueMatchWarning)
             assert msg in warning.args[0]
diff --git a/lib/iris/tests/unit/fileformats/netcdf/saver/test__fillvalue_report.py b/lib/iris/tests/unit/fileformats/netcdf/saver/test__fillvalue_report.py
index b2e4b63e3a..317f75bb8c 100644
--- a/lib/iris/tests/unit/fileformats/netcdf/saver/test__fillvalue_report.py
+++ b/lib/iris/tests/unit/fileformats/netcdf/saver/test__fillvalue_report.py
@@ -11,9 +11,9 @@
 import numpy as np
 import pytest
 
+from iris.exceptions import IrisSaverFillValueWarning
 from iris.fileformats.netcdf._thread_safe_nc import default_fillvals
 from iris.fileformats.netcdf.saver import (
-    SaverFillValueWarning,
     _fillvalue_report,
     _FillvalueCheckInfo,
 )
@@ -93,12 +93,14 @@ def test_warn(self, has_collision):
             expected_msg = "'<testvar>' contains unmasked data points equal to the fill-value"
             # Enter a warnings context that checks for the error.
             warning_context = pytest.warns(
-                SaverFillValueWarning, match=expected_msg
+                IrisSaverFillValueWarning, match=expected_msg
             )
             warning_context.__enter__()
         else:
             # Check that we get NO warning of the expected type.
-            warnings.filterwarnings("error", category=SaverFillValueWarning)
+            warnings.filterwarnings(
+                "error", category=IrisSaverFillValueWarning
+            )
 
         # Do call: it should raise AND return a warning, ONLY IF there was a collision.
         result = _fillvalue_report(
diff --git a/lib/iris/tests/unit/fileformats/nimrod_load_rules/test_vertical_coord.py b/lib/iris/tests/unit/fileformats/nimrod_load_rules/test_vertical_coord.py
index 44dcf8ac48..2279bcffc3 100644
--- a/lib/iris/tests/unit/fileformats/nimrod_load_rules/test_vertical_coord.py
+++ b/lib/iris/tests/unit/fileformats/nimrod_load_rules/test_vertical_coord.py
@@ -63,7 +63,7 @@ def test_unhandled(self):
                 vertical_coord_val=1.0, vertical_coord_type=-1
             )
         warn.assert_called_once_with(
-            "Vertical coord -1 not yet handled", TranslationWarning
+            "Vertical coord -1 not yet handled", category=TranslationWarning
         )
 
     def test_null(self):
diff --git a/lib/iris/tests/unit/fileformats/pp/test_PPField.py b/lib/iris/tests/unit/fileformats/pp/test_PPField.py
index 316894ded1..f2bbf97a80 100644
--- a/lib/iris/tests/unit/fileformats/pp/test_PPField.py
+++ b/lib/iris/tests/unit/fileformats/pp/test_PPField.py
@@ -13,6 +13,7 @@
 
 import numpy as np
 
+from iris.exceptions import IrisDefaultingWarning, IrisMaskValueMatchWarning
 import iris.fileformats.pp as pp
 from iris.fileformats.pp import PPField, SplittableInt
 
@@ -91,7 +92,7 @@ def field_checksum(data):
         data_64 = np.linspace(0, 1, num=10, endpoint=False).reshape(2, 5)
         checksum_32 = field_checksum(data_64.astype(">f4"))
         msg = "Downcasting array precision from float64 to float32 for save."
-        with self.assertWarnsRegex(UserWarning, msg):
+        with self.assertWarnsRegex(IrisDefaultingWarning, msg):
             checksum_64 = field_checksum(data_64.astype(">f8"))
         self.assertEqual(checksum_32, checksum_64)
 
@@ -104,7 +105,7 @@ def test_masked_mdi_value_warning(self):
             [1.0, field.bmdi, 3.0], dtype=np.float32
         )
         msg = "PPField data contains unmasked points"
-        with self.assertWarnsRegex(UserWarning, msg):
+        with self.assertWarnsRegex(IrisMaskValueMatchWarning, msg):
             with self.temp_filename(".pp") as temp_filename:
                 with open(temp_filename, "wb") as pp_file:
                     field.save(pp_file)
@@ -116,7 +117,7 @@ def test_unmasked_mdi_value_warning(self):
         # Make float32 data, as float64 default produces an extra warning.
         field.data = np.array([1.0, field.bmdi, 3.0], dtype=np.float32)
         msg = "PPField data contains unmasked points"
-        with self.assertWarnsRegex(UserWarning, msg):
+        with self.assertWarnsRegex(IrisMaskValueMatchWarning, msg):
             with self.temp_filename(".pp") as temp_filename:
                 with open(temp_filename, "wb") as pp_file:
                     field.save(pp_file)