diff --git a/glue/core/coordinates.py b/glue/core/coordinates.py
index 68b26e20..38001ce4 100644
--- a/glue/core/coordinates.py
+++ b/glue/core/coordinates.py
@@ -139,8 +139,8 @@ class Coordinates(object):
 
     def world_axis(self, data, axis):
         """
-        Find the world coordinates along a given dimension, and which for now we
-        center on the pixel origin.
+        Find the world coordinates along a given dimension, and which for now
+        we center on the pixel origin.
 
         Parameters
         ----------
@@ -152,10 +152,10 @@ class Coordinates(object):
 
         Notes
         -----
-        This method computes the axis values using pixel positions at the center
-        of the data along all other axes. This will therefore only give the
-        correct result for non-dependent axes (which can be checked using the
-        ``dependent_axes`` method)
+        This method computes the axis values using pixel positions at the
+        center of the data along all other axes. This will therefore only give
+        the correct result for non-dependent axes (which can be checked using
+        the ``dependent_axes`` method).
         """
         pixel = []
         for i, s in enumerate(data.shape):
@@ -163,7 +163,8 @@ class Coordinates(object):
                 pixel.append(np.arange(data.shape[axis]))
             else:
                 pixel.append(np.repeat((s - 1) / 2, data.shape[axis]))
-        return self.pixel2world_single_axis(*pixel[::-1], axis=data.ndim - 1 - axis)
+        return self.pixel2world_single_axis(*pixel[::-1],
+                                            axis=data.ndim - 1 - axis)
 
     def world_axis_unit(self, axis):
         """
@@ -173,7 +174,7 @@ class Coordinates(object):
         return ''
 
     def axis_label(self, axis):
-        return "World %i" % axis
+        return "World {}".format(axis)
 
     def dependent_axes(self, axis):
         """Return a tuple of which world-axes are non-indepndent
@@ -199,18 +200,31 @@ class WCSCoordinates(Coordinates):
     standard.  This class does not take into account
     distortions.
 
+    Parameters
+    ----------
+    header : :class:`astropy.io.fits.Header`
+        FITS header (derived from WCS if not given)
+    wcs : :class:`astropy.wcs.WCS`
+        WCS object to use, if different from header
+
     References
     ----------
-      * Greisen & Calabretta (2002), Astronomy and Astrophysics, 395, 1061
-      * Calabretta & Greisen (2002), Astronomy and Astrophysics, 395, 1077
-      * Greisen, Calabretta, Valdes & Allen (2006), Astronomy and
-        Astrophysics, 446, 747
+    * Greisen & Calabretta (2002), Astronomy and Astrophysics, 395, 1061
+    * Calabretta & Greisen (2002), Astronomy and Astrophysics, 395, 1077
+    * Greisen, Calabretta, Valdes & Allen (2006), Astronomy and
+      Astrophysics, 446, 747
     '''
 
-    def __init__(self, header, wcs=None):
+    def __init__(self, header=None, wcs=None):
         super(WCSCoordinates, self).__init__()
         from astropy.wcs import WCS
 
+        if header is None and wcs is None:
+            raise ValueError('Must provide either FITS header or WCS or both')
+
+        if header is None:
+            header = wcs.to_header()
+
         self._header = header
 
         try:
@@ -322,12 +336,17 @@ class WCSCoordinates(Coordinates):
 
 
 def coordinates_from_header(header):
-    """ Convert a FITS header into a glue Coordinates object
+    """
+    Convert a FITS header into a glue Coordinates object.
 
-    :param header: Header to convert
-    :type header: :class:`astropy.io.fits.Header`
+    Parameters
+    ----------
+    header : :class:`astropy.io.fits.Header`
+        Header to convert
 
-    :rtype: :class:`~glue.core.coordinates.Coordinates`
+    Returns
+    -------
+    coordinates : :class:`~glue.core.coordinates.Coordinates`
     """
 
     # We check whether the header contains at least CRVAL1 - if not, we would
@@ -340,12 +359,12 @@ def coordinates_from_header(header):
         try:
             return WCSCoordinates(header)
         except Exception as e:
-            logging.getLogger(__name__).warn("\n\n*******************************\n"
-                                             "Encounted an error during WCS parsing. "
-                                             "Discarding world coordinates! "
-                                             "\n%s\n"
-                                             "*******************************\n\n" % e
-                                             )
+            logging.getLogger(__name__).warn(
+                "\n\n*******************************\n"
+                "Encounted an error during WCS parsing. "
+                "Discarding world coordinates! "
+                "\n{}\n"
+                "*******************************\n\n".format(str(e)))
     return Coordinates()
 
 
@@ -358,10 +377,17 @@ def _get_ndim(header):
 
 
 def coordinates_from_wcs(wcs):
-    """Convert a wcs object into a glue Coordinates object
+    """
+    Convert an Astropy WCS object into a glue Coordinates object.
+
+    Parameters
+    ----------
+    wcs : :class:`astropy.wcs.WCS`
+        The WCS object to use
 
-    :param wcs: The WCS object to use
-    :rtype: :class:`~glue.core.coordinates.Coordinates`
+    Returns
+    -------
+    coordinates : :class:`~glue.core.coordinates.Coordinates`
     """
     from astropy.io import fits
     hdr_str = wcs.wcs.to_header()
@@ -375,7 +401,7 @@ def coordinates_from_wcs(wcs):
 
 def header_from_string(string):
     """
-    Convert a string to a FITS header
+    Convert a string to a FITS header.
     """
     from astropy.io import fits
     return fits.Header.fromstring(string, sep='\n')

Allow WCSCoordinates to be initialized with only a WCS object
At the moment, WCSCoordinates **requires** a header, but we should change this so that a WCS object alone should be enough.
diff --git a/glue/core/tests/test_coordinates.py b/glue/core/tests/test_coordinates.py
index 1e51f16d..232869df 100644
--- a/glue/core/tests/test_coordinates.py
+++ b/glue/core/tests/test_coordinates.py
@@ -4,7 +4,6 @@ from __future__ import absolute_import, division, print_function
 
 import pytest
 import numpy as np
-from mock import patch
 from numpy.testing import assert_allclose
 
 from glue.tests.helpers import requires_astropy
@@ -134,16 +133,12 @@ class TestWcsCoordinates(object):
         assert_allclose(result[1], expected[1])
 
     def test_pixel2world_invalid_input(self):
-        coord = WCSCoordinates(None)
-        x, y = {}, {}
-        with pytest.raises(TypeError) as exc:
-            coord.pixel2world(x, y)
+        with pytest.raises(ValueError):
+            WCSCoordinates(None)
 
     def test_world2pixel_invalid_input(self):
-        coord = WCSCoordinates(None)
-        x, y = {}, {}
-        with pytest.raises(TypeError) as exc:
-            coord.world2pixel(x, y)
+        with pytest.raises(ValueError):
+            WCSCoordinates(None)
 
     def test_axis_label(self):
         hdr = self.default_header()

pip install -e .[dev]
parse_log_pytest
requirements.txt
pytest mock
apt-get update apt-get install -y gcc
3.9
requirements/base.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
astropy==6.0.1
astropy-iers-data==0.2025.3.31.0.36.18
contourpy==1.3.0
cycler==0.12.1
exceptiongroup==1.2.2
fonttools==4.56.0
-e git+https://github.com/glue-viz/glue.git@a036b8c270af8e3892ac88748f12955a1b2c1ff9#egg=glueviz
importlib_resources==6.5.2
iniconfig==2.1.0
kiwisolver==1.4.7
matplotlib==3.9.4
mock==5.2.0
numpy==1.26.4
packaging==24.2
pandas==2.2.3
pillow==11.1.0
pluggy==1.5.0
pyerfa==2.0.1.5
pyparsing==3.2.3
pytest==8.3.5
python-dateutil==2.9.0.post0
pytz==2025.2
PyYAML==6.0.2
QtPy==2.4.3
six==1.17.0
tomli==2.2.1
tzdata==2025.2
zipp==3.21.0

name: glue
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - pip=25.0=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - astropy==6.0.1
      - astropy-iers-data==0.2025.3.31.0.36.18
      - contourpy==1.3.0
      - cycler==0.12.1
      - exceptiongroup==1.2.2
      - fonttools==4.56.0
      - importlib-resources==6.5.2
      - iniconfig==2.1.0
      - kiwisolver==1.4.7
      - matplotlib==3.9.4
      - mock==5.2.0
      - numpy==1.26.4
      - packaging==24.2
      - pandas==2.2.3
      - pillow==11.1.0
      - pluggy==1.5.0
      - pyerfa==2.0.1.5
      - pyparsing==3.2.3
      - pytest==8.3.5
      - python-dateutil==2.9.0.post0
      - pytz==2025.2
      - pyyaml==6.0.2
      - qtpy==2.4.3
      - six==1.17.0
      - tomli==2.2.1
      - tzdata==2025.2
      - zipp==3.21.0
prefix: /opt/conda/envs/glue

glue/core/tests/test_coordinates.py::TestWcsCoordinates::test_pixel2world_invalid_input glue/core/tests/test_coordinates.py::TestWcsCoordinates::test_world2pixel_invalid_input
glue/core/tests/test_coordinates.py::test_world_axis_wcs
glue/core/tests/test_coordinates.py::TestWcsCoordinates::test_pixel2world_scalar glue/core/tests/test_coordinates.py::TestWcsCoordinates::test_pixel2world_different_input_types glue/core/tests/test_coordinates.py::TestWcsCoordinates::test_pixel2world_list glue/core/tests/test_coordinates.py::TestWcsCoordinates::test_pixel2world_numpy glue/core/tests/test_coordinates.py::TestWcsCoordinates::test_world2pixel_numpy glue/core/tests/test_coordinates.py::TestWcsCoordinates::test_world2pixel_list glue/core/tests/test_coordinates.py::TestWcsCoordinates::test_world2pixel_scalar glue/core/tests/test_coordinates.py::TestWcsCoordinates::test_world2pixel_mismatched_input glue/core/tests/test_coordinates.py::TestWcsCoordinates::test_pixel2world_mismatched_input glue/core/tests/test_coordinates.py::TestWcsCoordinates::test_axis_label glue/core/tests/test_coordinates.py::TestCoordinatesFromHeader::test_2d_nowcs glue/core/tests/test_coordinates.py::TestCoordinatesFromHeader::test_2d glue/core/tests/test_coordinates.py::TestCoordinatesFromHeader::test_3d_nowcs glue/core/tests/test_coordinates.py::TestCoordinatesFromHeader::test_3d glue/core/tests/test_coordinates.py::TestCoordinatesFromHeader::test_nod glue/core/tests/test_coordinates.py::test_coords_preserve_shape_2d glue/core/tests/test_coordinates.py::test_coords_preserve_shape_3d glue/core/tests/test_coordinates.py::test_world_axis_unit
glue/core/coordinates.py
glue/core/coordinates.py