diff --git a/cellmlmanip/main.py b/cellmlmanip/main.py
index a9fefb7..42764f1 100644
--- a/cellmlmanip/main.py
+++ b/cellmlmanip/main.py
@@ -9,4 +9,3 @@ def load_model(path, unit_store=None):
         underlying registry so that conversions between model units and those from the provided store work.
     """
     return Parser(path).parse(unit_store=unit_store)
-
diff --git a/cellmlmanip/model.py b/cellmlmanip/model.py
index e768403..8a3c651 100644
--- a/cellmlmanip/model.py
+++ b/cellmlmanip/model.py
@@ -194,6 +194,7 @@ class Model(object):
         self._name_to_variable[name] = var = VariableDummy(
             name=name,
             units=units,
+            model=self,
             initial_value=initial_value,
             public_interface=public_interface,
             private_interface=private_interface,
@@ -681,6 +682,15 @@ class Model(object):
         """ Checks if the given ``variable`` is a state variable (i.e. if it's defined by an ODE). """
         return variable in self._ode_definition_map
 
+    def is_constant(self, variable):
+        """Determine whether the given ``variable`` is a constant.
+
+        This is calculated by looking at the RHS of the defining equation and checking it has no
+        variable references.
+        """
+        defn = self._var_definition_map.get(variable)
+        return defn is not None and len(defn.rhs.atoms(VariableDummy)) == 0
+
     def find_variables_and_derivatives(self, expressions):
         """ Returns a set containing all variables and derivatives referenced in a list of expressions.
 
@@ -741,6 +751,7 @@ class Model(object):
         del self._name_to_variable[variable.name]
         if variable._cmeta_id is not None:
             del self._cmeta_id_to_variable[variable._cmeta_id]
+        variable._model = None  # Just in case!
         self._invalidate_cache()
 
     def transfer_cmeta_id(self, source, target):
@@ -1070,11 +1081,13 @@ class VariableDummy(sympy.Dummy):
     def __init__(self,
                  name,
                  units,
+                 model=None,
                  initial_value=None,
                  public_interface=None,
                  private_interface=None,
                  order_added=None,
                  cmeta_id=None):
+        self._model = model
         self.name = name
         self.units = units
         self.initial_value = None if initial_value is None else float(initial_value)
@@ -1102,24 +1115,28 @@ class VariableDummy(sympy.Dummy):
         # TODO: Define allowed types via enum
         self.type = None
 
+    def __str__(self):
+        return self.name
+
+    @property
+    def model(self):
+        """The :class:`Model` this variable is part of."""
+        return self._model
+
+    @property
+    def rdf_identity(self):
+        """The RDF identity for this variable (will be ``None`` unless the variable has a cmeta id)."""
+        return self._rdf_identity
+
     @property
     def cmeta_id(self):
-        """Provides read-only access the the cmeta id."""
+        """Provides read-only access to the cmeta id."""
         return self._cmeta_id
 
-    def __str__(self):
-        return self.name
-
     def _set_cmeta_id(self, cmeta_id):
-        """ Sets this variable's cmeta id. Should only be called by Model, which can verify cmeta id uniqueness. """
+        """Sets this variable's cmeta id. Should only be called by Model, which can verify cmeta id uniqueness."""
         self._cmeta_id = cmeta_id
         if cmeta_id is None:
             self._rdf_identity = None
         else:
             self._rdf_identity = create_rdf_node('#' + self._cmeta_id)
-
-    @property
-    def rdf_identity(self):
-        """The RDF identity for this variable (will be ``None`` unless the variable has a cmeta id)."""
-        return self._rdf_identity
-
diff --git a/cellmlmanip/parser.py b/cellmlmanip/parser.py
index 4a6c818..3699aeb 100644
--- a/cellmlmanip/parser.py
+++ b/cellmlmanip/parser.py
@@ -501,4 +501,3 @@ class Parser(object):
         if not rnc.validate(tree):
             msg = '. '.join([str(x) for x in rnc.error_log])
             raise ValueError('Invalid or unsupported CellML file. ' + msg)
-
diff --git a/cellmlmanip/printer.py b/cellmlmanip/printer.py
index cfa22a9..76fc38d 100644
--- a/cellmlmanip/printer.py
+++ b/cellmlmanip/printer.py
@@ -403,4 +403,3 @@ class Printer(sympy.printing.printer.Printer):
     def _print_Symbol(self, expr):
         """ Handles Sympy Symbol objects. """
         return self._symbol_function(expr)
-
diff --git a/docs/source/conf.py b/docs/source/conf.py
index d5d034b..6607a9e 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -22,6 +22,7 @@
 # import cellmlmanip
 import sphinx
 
+
 # -- General configuration ------------------------------------------------
 
 # If your documentation needs a minimal Sphinx version, state it here.
@@ -185,4 +186,3 @@ texinfo_documents = [
      author, 'Cellmlmanip', 'One line description of project.',
      'Miscellaneous'),
 ]
-

Add model property to VariableDummy
- A variable dummy is tied to a model: it's not possible to re-use a variable dummy with a different model (e.g. a model stores all variable dummies it has when creating them, and they have a model-specific order_added property)
- I've got some use cases where we pass dummies around, but not necessarily the model object https://github.com/ModellingWebLab/weblab-fc/pull/158#discussion_r413871468

Thoughts @jonc125 @MauriceHendrix ?
diff --git a/tests/conftest.py b/tests/conftest.py
index 8abdeda..8eb2b6f 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -65,5 +65,3 @@ def bad_units_model():
     """
     model = shared.load_model('simple_model_invalid_units')
     return model
-
-
diff --git a/tests/shared.py b/tests/shared.py
index 8b94fcb..f100e2d 100644
--- a/tests/shared.py
+++ b/tests/shared.py
@@ -1,5 +1,6 @@
 # Module with cellmlmanip tests
 import os
+
 import cellmlmanip
 
 
diff --git a/tests/test_model_functions.py b/tests/test_model_functions.py
index bbbe05a..62e6f53 100644
--- a/tests/test_model_functions.py
+++ b/tests/test_model_functions.py
@@ -4,8 +4,7 @@ import pytest
 import sympy as sp
 
 from cellmlmanip import parser, units
-from cellmlmanip.model import Model, VariableDummy
-from cellmlmanip.model import FLOAT_PRECISION
+from cellmlmanip.model import FLOAT_PRECISION, Model, VariableDummy
 
 from . import shared
 
@@ -462,6 +461,7 @@ class TestModelFunctions():
             model.get_variable_by_name('newvar') is None
 
         newvar = model.add_variable('newvar', 'mV')
+        assert newvar.model is model
         assert newvar.is_real
         assert len(model.variables()) == 4
         assert model.get_variable_by_name('newvar')
@@ -490,6 +490,7 @@ class TestModelFunctions():
 
         model.remove_variable(i_stim)
 
+        assert i_stim.model is None
         with pytest.raises(KeyError):
             model.get_variable_by_name(i_stim.name)
         assert model.get_definition(i_stim) is None
@@ -506,6 +507,7 @@ class TestModelFunctions():
 
         model.remove_variable(v)
 
+        assert v.model is None
         with pytest.raises(KeyError):
             model.get_variable_by_name(v.name)
         assert model.get_definition(v) is None
@@ -519,6 +521,7 @@ class TestModelFunctions():
 
         model.remove_variable(t)
 
+        assert t.model is None
         with pytest.raises(KeyError):
             model.get_variable_by_name(t.name)
 
@@ -526,6 +529,7 @@ class TestModelFunctions():
         var = model.get_variable_by_name('membrane$E_R')
         model.remove_variable(var)
 
+        assert var.model is None
         with pytest.raises(KeyError):
             model.get_variable_by_name(var.name)
 
@@ -646,24 +650,28 @@ class TestModelFunctions():
         assert new_eqn.rhs.args[0].value == 0.001
         assert new_eqn.rhs.args[1] == source
 
-    def test_is_state(self, aslanidi_model):
-        """ Tests Model.is_state(). """
+    def test_variable_classification(self, aslanidi_model):
+        """ Tests Model.is_state() and Model.is_constant(). """
 
         # State variable
         v = aslanidi_model.get_variable_by_ontology_term((shared.OXMETA, 'membrane_voltage'))
         assert aslanidi_model.is_state(v)
+        assert not aslanidi_model.is_constant(v)
 
         # Intermediary variable
         i = aslanidi_model.get_variable_by_ontology_term((shared.OXMETA, 'membrane_fast_sodium_current'))
         assert not aslanidi_model.is_state(i)
+        assert not aslanidi_model.is_constant(i)
 
         # Free variable
         t = aslanidi_model.get_variable_by_ontology_term((shared.OXMETA, 'time'))
         assert not aslanidi_model.is_state(t)
+        assert not aslanidi_model.is_constant(t)
 
         # Constant
         c = aslanidi_model.get_variable_by_ontology_term((shared.OXMETA, 'membrane_capacitance'))
         assert not aslanidi_model.is_state(c)
+        assert aslanidi_model.is_constant(c)
 
     def test_transform_constants(self):
         """ Tests Model.transform_constants(). """
@@ -681,4 +689,3 @@ class TestModelFunctions():
         # And the equations should have matching units
         for eq in model.equations:
             model.check_left_right_units_equal(eq)
-
diff --git a/tests/test_parser.py b/tests/test_parser.py
index fc142fe..ee1d22d 100644
--- a/tests/test_parser.py
+++ b/tests/test_parser.py
@@ -317,4 +317,3 @@ class TestParser(object):
         p = parser.Parser(path)
         with pytest.raises(ValueError, match='Element model has extra content'):
             p.parse()
-
diff --git a/tests/test_printer.py b/tests/test_printer.py
index 79a954e..e8a32ee 100644
--- a/tests/test_printer.py
+++ b/tests/test_printer.py
@@ -3,11 +3,13 @@
 #
 import logging
 import math
+
 import pytest
 import sympy as sp
 
 import cellmlmanip.printer
 
+
 # Show more logging output
 logging.getLogger().setLevel(logging.INFO)
 
diff --git a/tests/test_rdf.py b/tests/test_rdf.py
index d716d83..94d15bd 100644
--- a/tests/test_rdf.py
+++ b/tests/test_rdf.py
@@ -4,8 +4,10 @@ import sympy
 
 import cellmlmanip
 import cellmlmanip.rdf
+
 from . import shared
 
+
 PYCMLMETA = 'https://chaste.comlab.ox.ac.uk/cellml/ns/pycml#'
 
 
@@ -284,4 +286,3 @@ def test_transfer_cmeta_id():
     assert v4.rdf_identity is not None
     with pytest.raises(ValueError):
         model.transfer_cmeta_id(v2, v4)
-
diff --git a/tests/test_unit_conversion.py b/tests/test_unit_conversion.py
index 23b9867..92ca867 100644
--- a/tests/test_unit_conversion.py
+++ b/tests/test_unit_conversion.py
@@ -3,6 +3,7 @@ from pint import DimensionalityError
 
 from cellmlmanip import units
 from cellmlmanip.model import DataDirectionFlow
+
 from . import shared
 
 

pip install -e .[dev]
parse_log_pytest
requirements.txt
pytest
3.6
requirements/base.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
attrs==22.2.0
-e git+https://github.com/ModellingWebLab/cellmlmanip.git@971d4264f95acb2f39a50f902b6171542f11947f#egg=cellmlmanip
certifi==2021.5.30
decorator==4.4.0
importlib-metadata==4.8.3
iniconfig==1.1.1
isodate==0.6.0
lxml==4.4.1
mpmath==1.1.0
networkx==2.3
packaging==21.3
Pint==0.9
pluggy==1.0.0
py==1.11.0
pyparsing==2.4.2
pytest==7.0.1
rdflib==4.2.2
six==1.12.0
sympy==1.4
tomli==1.2.3
typing_extensions==4.1.1
zipp==3.6.0

name: cellmlmanip
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
  - certifi=2021.5.30=py36h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.3=he6710b0_2
  - 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=1.1.1w=h7f8727e_0
  - pip=21.2.2=py36h06a4308_0
  - python=3.6.13=h12debd9_1
  - readline=8.2=h5eee18b_0
  - setuptools=58.0.4=py36h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - wheel=0.37.1=pyhd3eb1b0_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - attrs==22.2.0
      - decorator==4.4.0
      - importlib-metadata==4.8.3
      - iniconfig==1.1.1
      - isodate==0.6.0
      - lxml==4.4.1
      - mpmath==1.1.0
      - networkx==2.3
      - packaging==21.3
      - pint==0.9
      - pluggy==1.0.0
      - py==1.11.0
      - pyparsing==2.4.2
      - pytest==7.0.1
      - rdflib==4.2.2
      - six==1.12.0
      - sympy==1.4
      - tomli==1.2.3
      - typing-extensions==4.1.1
      - zipp==3.6.0
prefix: /opt/conda/envs/cellmlmanip

tests/test_model_functions.py::TestModelFunctions::test_add_variable tests/test_model_functions.py::TestModelFunctions::test_remove_variable tests/test_model_functions.py::TestModelFunctions::test_variable_classification
tests/test_model_functions.py::TestModelFunctions::test_graph_property tests/test_model_functions.py::TestModelFunctions::test_graph_for_dae tests/test_model_functions.py::TestModelFunctions::test_get_derived_quantities tests/test_model_functions.py::TestModelFunctions::test_get_state_variables tests/test_model_functions.py::TestModelFunctions::test_get_state_variables_2 tests/test_model_functions.py::TestModelFunctions::test_get_free_variable tests/test_model_functions.py::TestModelFunctions::test_get_derivatives tests/test_model_functions.py::TestModelFunctions::test_get_derivatives_2 tests/test_model_functions.py::TestModelFunctions::test_get_equations_for tests/test_model_functions.py::TestModelFunctions::test_get_definition tests/test_model_functions.py::TestModelFunctions::test_get_value tests/test_model_functions.py::TestModelFunctions::test_get_variable_by_cmeta_id tests/test_model_functions.py::TestModelFunctions::test_get_variable_by_name tests/test_model_functions.py::TestModelFunctions::test_get_variable_by_ontology_term tests/test_model_functions.py::TestModelFunctions::test_get_variables_by_rdf tests/test_model_functions.py::TestModelFunctions::test_add_equation tests/test_model_functions.py::TestModelFunctions::test_remove_equation tests/test_model_functions.py::TestModelFunctions::test_add_number tests/test_model_functions.py::TestModelFunctions::test_units tests/test_model_functions.py::TestModelFunctions::test_bad_units tests/test_model_functions.py::TestModelFunctions::test_find_variables_and_derivatives tests/test_model_functions.py::TestModelFunctions::test_find_variables_and_derivatives_2 tests/test_model_functions.py::TestModelFunctions::test_connect_variables tests/test_model_functions.py::TestModelFunctions::test_connect_variable2 tests/test_model_functions.py::TestModelFunctions::test_transform_constants tests/test_parser.py::TestParser::test_component_count tests/test_parser.py::TestParser::test_group_relationships tests/test_parser.py::TestParser::test_equations_count tests/test_parser.py::TestParser::test_rdf tests/test_parser.py::TestParser::test_connections_loaded tests/test_parser.py::TestParser::test_connections tests/test_parser.py::TestParser::test_add_units_to_equations tests/test_parser.py::TestParser::test_connect_to_hidden_component tests/test_parser.py::TestParser::test_bad_connection_units tests/test_parser.py::TestParser::test_new_base_units tests/test_parser.py::TestParser::test_units_with_multiplier tests/test_parser.py::TestParser::test_undefined_variable tests/test_parser.py::TestParser::test_multiple_math_elements tests/test_parser.py::TestParser::test_bad_unit_definitions tests/test_parser.py::TestParser::test_component_units_unsupported tests/test_parser.py::TestParser::test_reactions_unsupported tests/test_parser.py::TestParser::test_validation tests/test_printer.py::TestPrinter::test_numbers tests/test_printer.py::TestPrinter::test_symbols tests/test_printer.py::TestPrinter::test_addition tests/test_printer.py::TestPrinter::test_multiplication tests/test_printer.py::TestPrinter::test_powers tests/test_printer.py::TestPrinter::test_trig_functions tests/test_printer.py::TestPrinter::test_conditions tests/test_printer.py::TestPrinter::test_boolean_logic tests/test_printer.py::TestPrinter::test_piecewise_expressions tests/test_printer.py::TestPrinter::test_long_expression tests/test_printer.py::TestPrinter::test_unsupported_sympy_items tests/test_printer.py::TestPrinter::test_abs tests/test_rdf.py::test_cmeta_id tests/test_rdf.py::test_create_rdf_node tests/test_rdf.py::test_get_variable_by_ontology_term tests/test_rdf.py::test_get_ontology_terms_by_variable tests/test_rdf.py::test_get_ontology_terms_by_variable2 tests/test_rdf.py::test_has_cmeta_id tests/test_rdf.py::test_has_ontology_term_by_variable tests/test_rdf.py::test_has_ontology_annotation tests/test_rdf.py::test_get_rdf_annotation tests/test_rdf.py::test_add_rdf tests/test_rdf.py::test_add_cmeta_id tests/test_rdf.py::test_transfer_cmeta_id tests/test_unit_conversion.py::TestUnitConversion::test_bad_units tests/test_unit_conversion.py::TestUnitConversion::test_providing_unit_store tests/test_unit_conversion.py::TestUnitConversion::test_add_input_state_variable tests/test_unit_conversion.py::TestUnitConversion::test_add_input_state_variable_initial_zero tests/test_unit_conversion.py::TestUnitConversion::test_add_input_free_variable tests/test_unit_conversion.py::TestUnitConversion::test_add_input_literal_variable tests/test_unit_conversion.py::TestUnitConversion::test_add_input_free_variable_multiple tests/test_unit_conversion.py::TestUnitConversion::test_multiple_odes tests/test_unit_conversion.py::TestUnitConversion::test_multiple_odes_1 tests/test_unit_conversion.py::TestUnitConversion::test_add_output tests/test_unit_conversion.py::TestUnitConversion::test_add_output_state_variable tests/test_unit_conversion.py::TestUnitConversion::test_add_output_free_variable tests/test_unit_conversion.py::TestUnitConversion::test_convert_variable_invalid_arguments tests/test_unit_conversion.py::TestUnitConversion::test_convert_same_unit_different_name tests/test_unit_conversion.py::TestUnitConversion::test_convert_variable_no_cmeta_id tests/test_unit_conversion.py::TestUnitConversion::test_unique_names
cellmlmanip/model.py cellmlmanip/main.py cellmlmanip/printer.py cellmlmanip/parser.py docs/source/conf.py
cellmlmanip/model.py cellmlmanip/main.py cellmlmanip/printer.py cellmlmanip/parser.py docs/source/conf.py