Repository: FEniCS/ufl (language: python)

Issue:
Refactor FiniteElementBase
I've created this issue to take note of things we'd like to do to the FiniteElementBase class now that we're working on moving element definitions into Basix/finat

- [ ] Remove `__init__` and instead have `abstractmethod`s and `abstractproperty`s
- [ ] Change the `abstractmethod`s to `abstractproperty`s
- [ ] remove `family`, `degree` and `quad_scheme` from the base class
- [ ] Add type hints (particularly in abstract classes)
- [ ] Reconstruct method needs to be made to work with subclasses
- [ ] `symmetry` method needs thinking about
- [ ] `__add__` and `__mul__` need removing from base class
- [ ] component functions need looking at
Remove depcreacted way of defining Coefficients, Arguments etc.
The code consists of several deprecated ways of defining coefficients, arguments etc (used in old ufl files).
For instance:
- https://github.com/FEniCS/ufl/blob/main/ufl/domain.py#L279-L285
- https://github.com/FEniCS/ufl/blob/main/ufl/argument.py#L44-L49
- https://github.com/FEniCS/ufl/blob/a7426bd8533f2c819f7f164df9c197e277d058c3/ufl/coefficient.py#L41-L46


Discussion / hints:

Fix patch:
diff --git a/AUTHORS b/AUTHORS
index c3c6f37c6..5866e1c39 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -31,3 +31,4 @@ Contributors:
   | Nacime Bouziani       <nacime.bouziani@gmail.com>
   | Reuben W. Hill        <reuben.hill10@imperial.ac.uk>
   | Nacime Bouziani       <n.bouziani18@imperial.ac.uk>
+  | Matthew Scroggs       <matthew.scroggs.14@ucl.ac.uk>
diff --git a/demo/Constant.py b/demo/Constant.py
index 0a2205978..96acaf22c 100644
--- a/demo/Constant.py
+++ b/demo/Constant.py
@@ -18,20 +18,23 @@
 # Modified by Martin Sandve Alnes, 2009
 #
 # Test form for scalar and vector constants.
-from ufl import (Coefficient, Constant, FiniteElement, FunctionSpace, Mesh, TestFunction, TrialFunction, VectorConstant,
-                 VectorElement, dot, dx, grad, inner, triangle)
+from ufl import (Coefficient, Constant, FunctionSpace, Mesh, TestFunction, TrialFunction, VectorConstant, dot, dx, grad,
+                 inner, triangle)
+from ufl.finiteelement import FiniteElement
+from ufl.pullback import identity_pullback
+from ufl.sobolevspace import H1
 
 cell = triangle
-element = FiniteElement("Lagrange", cell, 1)
-domain = Mesh(VectorElement("Lagrange", cell, 1))
+element = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1)
+domain = Mesh(FiniteElement("Lagrange", cell, 1, (2, ), identity_pullback, H1))
 space = FunctionSpace(domain, element)
 
 v = TestFunction(space)
 u = TrialFunction(space)
 f = Coefficient(space)
 
-c = Constant(space)
-d = VectorConstant(space)
+c = Constant(domain)
+d = VectorConstant(domain)
 
 a = c * dot(grad(v), grad(u)) * dx
 L = inner(d, grad(v)) * dx
diff --git a/demo/ConvectionJacobi.py b/demo/ConvectionJacobi.py
index bf059ade6..5f3b2da10 100644
--- a/demo/ConvectionJacobi.py
+++ b/demo/ConvectionJacobi.py
@@ -2,10 +2,13 @@
 # Author: Martin Sandve Alnes
 # Date: 2008-10-03
 #
-from ufl import Coefficient, FunctionSpace, Mesh, TestFunction, TrialFunction, VectorElement, dot, dx, grad, triangle
+from ufl import Coefficient, FunctionSpace, Mesh, TestFunction, TrialFunction, dot, dx, grad, triangle
+from ufl.finiteelement import FiniteElement
+from ufl.pullback import identity_pullback
+from ufl.sobolevspace import H1
 
-element = VectorElement("Lagrange", triangle, 1)
-domain = Mesh(VectorElement("Lagrange", triangle, 1))
+element = FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1)
+domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
 space = FunctionSpace(domain, element)
 
 u = TrialFunction(space)
diff --git a/demo/ConvectionJacobi2.py b/demo/ConvectionJacobi2.py
index 7096cd32d..c88108a17 100644
--- a/demo/ConvectionJacobi2.py
+++ b/demo/ConvectionJacobi2.py
@@ -2,10 +2,13 @@
 # Author: Martin Sandve Alnes
 # Date: 2008-10-03
 #
-from ufl import Coefficient, FunctionSpace, Mesh, TestFunction, TrialFunction, VectorElement, dx, i, j, triangle
+from ufl import Coefficient, FunctionSpace, Mesh, TestFunction, TrialFunction, dx, i, j, triangle
+from ufl.finiteelement import FiniteElement
+from ufl.pullback import identity_pullback
+from ufl.sobolevspace import H1
 
-element = VectorElement("Lagrange", triangle, 1)
-domain = Mesh(VectorElement("Lagrange", triangle, 1))
+element = FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1)
+domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
 space = FunctionSpace(domain, element)
 
 u = TrialFunction(space)
diff --git a/demo/ConvectionVector.py b/demo/ConvectionVector.py
index d16a52ee5..e83e60698 100644
--- a/demo/ConvectionVector.py
+++ b/demo/ConvectionVector.py
@@ -2,10 +2,13 @@
 # Author: Martin Sandve Alnes
 # Date: 2008-10-03
 #
-from ufl import Coefficient, FunctionSpace, Mesh, TestFunction, VectorElement, dot, dx, grad, triangle
+from ufl import Coefficient, FunctionSpace, Mesh, TestFunction, dot, dx, grad, triangle
+from ufl.finiteelement import FiniteElement
+from ufl.pullback import identity_pullback
+from ufl.sobolevspace import H1
 
-element = VectorElement("Lagrange", triangle, 1)
-domain = Mesh(VectorElement("Lagrange", triangle, 1))
+element = FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1)
+domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
 space = FunctionSpace(domain, element)
 
 v = TestFunction(space)
diff --git a/demo/Elasticity.py b/demo/Elasticity.py
index 0061c7135..73513d5d6 100644
--- a/demo/Elasticity.py
+++ b/demo/Elasticity.py
@@ -3,10 +3,13 @@
 # Modified by: Martin Sandve Alnes
 # Date: 2009-01-12
 #
-from ufl import FunctionSpace, Mesh, TestFunction, TrialFunction, VectorElement, dx, grad, inner, tetrahedron
+from ufl import FunctionSpace, Mesh, TestFunction, TrialFunction, dx, grad, inner, tetrahedron
+from ufl.finiteelement import FiniteElement
+from ufl.pullback import identity_pullback
+from ufl.sobolevspace import H1
 
-element = VectorElement("Lagrange", tetrahedron, 1)
-domain = Mesh(VectorElement("Lagrange", tetrahedron, 1))
+element = FiniteElement("Lagrange", tetrahedron, 1, (3, ), identity_pullback, H1)
+domain = Mesh(FiniteElement("Lagrange", tetrahedron, 1, (3, ), identity_pullback, H1))
 space = FunctionSpace(domain, element)
 
 v = TestFunction(space)
diff --git a/demo/EnergyNorm.py b/demo/EnergyNorm.py
index 64bc8d88e..30f1ade40 100644
--- a/demo/EnergyNorm.py
+++ b/demo/EnergyNorm.py
@@ -17,10 +17,13 @@
 #
 # This example demonstrates how to define a functional, here
 # the energy norm (squared) for a reaction-diffusion problem.
-from ufl import Coefficient, FiniteElement, FunctionSpace, Mesh, VectorElement, dot, dx, grad, tetrahedron
+from ufl import Coefficient, FunctionSpace, Mesh, dot, dx, grad, tetrahedron
+from ufl.finiteelement import FiniteElement
+from ufl.pullback import identity_pullback
+from ufl.sobolevspace import H1
 
-element = FiniteElement("Lagrange", tetrahedron, 1)
-domain = Mesh(VectorElement("Lagrange", tetrahedron, 1))
+element = FiniteElement("Lagrange", tetrahedron, 1, (), identity_pullback, H1)
+domain = Mesh(FiniteElement("Lagrange", tetrahedron, 1, (3, ), identity_pullback, H1))
 space = FunctionSpace(domain, element)
 
 v = Coefficient(space)
diff --git a/demo/Equation.py b/demo/Equation.py
index 94c5ef445..33625545b 100644
--- a/demo/Equation.py
+++ b/demo/Equation.py
@@ -34,11 +34,13 @@
 # the unknown u to the right-hand side, all terms may
 # be listed on one line and left- and right-hand sides
 # extracted by lhs() and rhs().
-from ufl import (Coefficient, FiniteElement, FunctionSpace, Mesh, TestFunction, TrialFunction, VectorElement, dot, dx,
-                 grad, lhs, rhs, triangle)
+from ufl import Coefficient, FunctionSpace, Mesh, TestFunction, TrialFunction, dot, dx, grad, lhs, rhs, triangle
+from ufl.finiteelement import FiniteElement
+from ufl.pullback import identity_pullback
+from ufl.sobolevspace import H1
 
-element = FiniteElement("Lagrange", triangle, 1)
-domain = Mesh(VectorElement("Lagrange", triangle, 1))
+element = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1)
+domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
 space = FunctionSpace(domain, element)
 
 k = 0.1
diff --git a/demo/ExplicitConvection.py b/demo/ExplicitConvection.py
index f3b684d4d..c56d27376 100644
--- a/demo/ExplicitConvection.py
+++ b/demo/ExplicitConvection.py
@@ -2,10 +2,13 @@
 # Author: Martin Sandve Alnes
 # Date: 2008-10-03
 #
-from ufl import Coefficient, FunctionSpace, Mesh, TestFunction, TrialFunction, VectorElement, dot, dx, grad, triangle
+from ufl import Coefficient, FunctionSpace, Mesh, TestFunction, TrialFunction, dot, dx, grad, triangle
+from ufl.finiteelement import FiniteElement
+from ufl.pullback import identity_pullback
+from ufl.sobolevspace import H1
 
-element = VectorElement("Lagrange", triangle, 1)
-domain = Mesh(VectorElement("Lagrange", triangle, 1))
+element = FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1)
+domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
 space = FunctionSpace(domain, element)
 
 u = TrialFunction(space)
diff --git a/demo/FEEC.py b/demo/FEEC.py
deleted file mode 100644
index cf79198e7..000000000
--- a/demo/FEEC.py
+++ /dev/null
@@ -1,56 +0,0 @@
-# Copyright (C) 2010 Marie Rognes
-#
-# This file is part of UFL.
-#
-# UFL is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Lesser General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# UFL is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU Lesser General Public License for more details.
-#
-# You should have received a copy of the GNU Lesser General Public License
-# along with UFL. If not, see <http://www.gnu.org/licenses/>.
-
-"""
-This demo illustrates the FEEC notation
-
-  V = FiniteElement("P Lambda", cell, r, k)
-  V = FiniteElement("P- Lambda", cell, r, k)
-
-and their aliases.
-"""
-from ufl import (FiniteElement, FunctionSpace, Mesh, TestFunction, TestFunctions, TrialFunction, TrialFunctions,
-                 VectorElement, dx)
-from ufl import exterior_derivative as d
-from ufl import inner, interval, tetrahedron, triangle
-
-cells = [interval, triangle, tetrahedron]
-r = 1
-
-for cell in cells:
-    for family in ["P Lambda", "P- Lambda"]:
-        tdim = cell.topological_dimension()
-        for k in range(0, tdim + 1):
-
-            # Testing exterior derivative
-            V = FiniteElement(family, cell, r, form_degree=k)
-            domain = Mesh(VectorElement("Lagrange", cell, 1))
-            space = FunctionSpace(domain, V)
-            v = TestFunction(space)
-            u = TrialFunction(space)
-
-            a = inner(d(u), d(v)) * dx
-
-            # Testing mixed formulation of Hodge Laplace
-            if k > 0 and k < tdim + 1:
-                S = FiniteElement(family, cell, r, form_degree=k - 1)
-                W = S * V
-                mixed_space = FunctionSpace(domain, W)
-                (sigma, u) = TrialFunctions(mixed_space)
-                (tau, v) = TestFunctions(mixed_space)
-
-                a = (inner(sigma, tau) - inner(d(tau), u) + inner(d(sigma), v) + inner(d(u), d(v))) * dx
diff --git a/demo/FunctionOperators.py b/demo/FunctionOperators.py
index 79fa43244..075e7b8c1 100644
--- a/demo/FunctionOperators.py
+++ b/demo/FunctionOperators.py
@@ -16,11 +16,13 @@
 # along with UFL. If not, see <http://www.gnu.org/licenses/>.
 #
 # Test form for operators on Coefficients.
-from ufl import (Coefficient, FiniteElement, FunctionSpace, Mesh, TestFunction, TrialFunction, VectorElement, dot, dx,
-                 grad, max_value, sqrt, triangle)
+from ufl import Coefficient, FunctionSpace, Mesh, TestFunction, TrialFunction, dot, dx, grad, max_value, sqrt, triangle
+from ufl.finiteelement import FiniteElement
+from ufl.pullback import identity_pullback
+from ufl.sobolevspace import H1
 
-element = FiniteElement("Lagrange", triangle, 1)
-domain = Mesh(VectorElement("Lagrange", triangle, 1))
+element = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1)
+domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
 space = FunctionSpace(domain, element)
 
 v = TestFunction(space)
diff --git a/demo/H1norm.py b/demo/H1norm.py
index 2dd54a12a..9da6b28e4 100644
--- a/demo/H1norm.py
+++ b/demo/H1norm.py
@@ -2,10 +2,13 @@
 # Author: Martin Sandve Alnes
 # Date: 2008-10-03
 #
-from ufl import Coefficient, FiniteElement, FunctionSpace, Mesh, VectorElement, dot, dx, grad, triangle
+from ufl import Coefficient, FunctionSpace, Mesh, dot, dx, grad, triangle
+from ufl.finiteelement import FiniteElement
+from ufl.pullback import identity_pullback
+from ufl.sobolevspace import H1
 
-element = FiniteElement("Lagrange", triangle, 1)
-domain = Mesh(VectorElement("Lagrange", triangle, 1))
+element = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1)
+domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
 space = FunctionSpace(domain, element)
 
 f = Coefficient(space)
diff --git a/demo/HarmonicMap.py b/demo/HarmonicMap.py
index 854fae28f..8aa3ee5d2 100644
--- a/demo/HarmonicMap.py
+++ b/demo/HarmonicMap.py
@@ -3,13 +3,15 @@
 # Author: Martin Alnes
 # Date: 2009-04-09
 #
-from ufl import (Coefficient, FiniteElement, FunctionSpace, Mesh, VectorElement, derivative, dot, dx, grad, inner,
-                 triangle)
+from ufl import Coefficient, FunctionSpace, Mesh, derivative, dot, dx, grad, inner, triangle
+from ufl.finiteelement import FiniteElement
+from ufl.pullback import identity_pullback
+from ufl.sobolevspace import H1
 
 cell = triangle
-X = VectorElement("Lagrange", cell, 1)
-Y = FiniteElement("Lagrange", cell, 1)
-domain = Mesh(VectorElement("Lagrange", cell, 1))
+X = FiniteElement("Lagrange", cell, 1, (2, ), identity_pullback, H1)
+Y = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1)
+domain = Mesh(FiniteElement("Lagrange", cell, 1, (2, ), identity_pullback, H1))
 X_space = FunctionSpace(domain, X)
 Y_space = FunctionSpace(domain, Y)
 
diff --git a/demo/HarmonicMap2.py b/demo/HarmonicMap2.py
index 811d8b666..dfc47c3b1 100644
--- a/demo/HarmonicMap2.py
+++ b/demo/HarmonicMap2.py
@@ -3,14 +3,16 @@
 # Author: Martin Alnes
 # Date: 2009-04-09
 #
-from ufl import (Coefficient, FiniteElement, FunctionSpace, Mesh, VectorElement, derivative, dot, dx, grad, inner,
-                 split, triangle)
+from ufl import Coefficient, FunctionSpace, Mesh, derivative, dot, dx, grad, inner, split, triangle
+from ufl.finiteelement import FiniteElement, MixedElement
+from ufl.pullback import identity_pullback
+from ufl.sobolevspace import H1
 
 cell = triangle
-X = VectorElement("Lagrange", cell, 1)
-Y = FiniteElement("Lagrange", cell, 1)
-M = X * Y
-domain = Mesh(VectorElement("Lagrange", cell, 1))
+X = FiniteElement("Lagrange", cell, 1, (2, ), identity_pullback, H1)
+Y = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1)
+M = MixedElement([X, Y])
+domain = Mesh(FiniteElement("Lagrange", cell, 1, (2, ), identity_pullback, H1))
 space = FunctionSpace(domain, M)
 
 u = Coefficient(space)
diff --git a/demo/Heat.py b/demo/Heat.py
index 241cc708e..f695341b2 100644
--- a/demo/Heat.py
+++ b/demo/Heat.py
@@ -20,12 +20,14 @@
 # The bilinear form a(v, u1) and linear form L(v) for
 # one backward Euler step with the heat equation.
 #
-from ufl import (Coefficient, Constant, FiniteElement, FunctionSpace, Mesh, TestFunction, TrialFunction, VectorElement,
-                 dot, dx, grad, triangle)
+from ufl import Coefficient, Constant, FunctionSpace, Mesh, TestFunction, TrialFunction, dot, dx, grad, triangle
+from ufl.finiteelement import FiniteElement
+from ufl.pullback import identity_pullback
+from ufl.sobolevspace import H1
 
 cell = triangle
-element = FiniteElement("Lagrange", cell, 1)
-domain = Mesh(VectorElement("Lagrange", cell, 1))
+element = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1)
+domain = Mesh(FiniteElement("Lagrange", cell, 1, (2, ), identity_pullback, H1))
 space = FunctionSpace(domain, element)
 
 v = TestFunction(space)  # Test function
diff --git a/demo/HornSchunck.py b/demo/HornSchunck.py
index 139b8a0e3..d3ec43840 100644
--- a/demo/HornSchunck.py
+++ b/demo/HornSchunck.py
@@ -3,14 +3,16 @@
 #  http://code.google.com/p/debiosee/wiki/DemosOptiocFlowHornSchunck
 # but not tested so this could contain errors!
 #
-from ufl import (Coefficient, Constant, FiniteElement, FunctionSpace, Mesh, VectorElement, derivative, dot, dx, grad,
-                 inner, triangle)
+from ufl import Coefficient, Constant, FunctionSpace, Mesh, derivative, dot, dx, grad, inner, triangle
+from ufl.finiteelement import FiniteElement
+from ufl.pullback import identity_pullback
+from ufl.sobolevspace import H1
 
 # Finite element spaces for scalar and vector fields
 cell = triangle
-S = FiniteElement("CG", cell, 1)
-V = VectorElement("CG", cell, 1)
-domain = Mesh(VectorElement("Lagrange", cell, 1))
+S = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1)
+V = FiniteElement("Lagrange", cell, 1, (2, ), identity_pullback, H1)
+domain = Mesh(FiniteElement("Lagrange", cell, 1, (2, ), identity_pullback, H1))
 S_space = FunctionSpace(domain, S)
 V_space = FunctionSpace(domain, V)
 
diff --git a/demo/HyperElasticity.py b/demo/HyperElasticity.py
index 289afbb0a..abbc8314c 100644
--- a/demo/HyperElasticity.py
+++ b/demo/HyperElasticity.py
@@ -3,22 +3,24 @@
 # Date: 2008-12-22
 #
 
+from ufl import (Coefficient, Constant, FacetNormal, FunctionSpace, Identity, Mesh, SpatialCoordinate, TestFunction,
+                 TrialFunction, derivative, det, diff, dot, ds, dx, exp, grad, inner, inv, tetrahedron, tr, variable)
+from ufl.finiteelement import FiniteElement
 # Modified by Garth N. Wells, 2009
-from ufl import (Coefficient, Constant, FacetNormal, FiniteElement, FunctionSpace, Identity, Mesh, SpatialCoordinate,
-                 TensorElement, TestFunction, TrialFunction, VectorElement, derivative, det, diff, dot, ds, dx, exp,
-                 grad, inner, inv, tetrahedron, tr, variable)
+from ufl.pullback import identity_pullback
+from ufl.sobolevspace import H1
 
 # Cell and its properties
 cell = tetrahedron
-domain = Mesh(VectorElement("Lagrange", cell, 1))
+domain = Mesh(FiniteElement("Lagrange", cell, 1, (3, ), identity_pullback, H1))
 d = cell.geometric_dimension()
 N = FacetNormal(domain)
 x = SpatialCoordinate(domain)
 
 # Elements
-u_element = VectorElement("CG", cell, 2)
-p_element = FiniteElement("CG", cell, 1)
-A_element = TensorElement("CG", cell, 1)
+u_element = FiniteElement("Lagrange", cell, 2, (3, ), identity_pullback, H1)
+p_element = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1)
+A_element = FiniteElement("Lagrange", cell, 1, (3, 3), identity_pullback, H1)
 
 # Spaces
 u_space = FunctionSpace(domain, u_element)
diff --git a/demo/HyperElasticity1D.py b/demo/HyperElasticity1D.py
index aaacc2ce9..9dfcbed96 100644
--- a/demo/HyperElasticity1D.py
+++ b/demo/HyperElasticity1D.py
@@ -2,12 +2,14 @@
 # Author: Martin Sandve Alnes
 # Date: 2008-10-03
 #
-from ufl import (Coefficient, Constant, FiniteElement, FunctionSpace, Mesh, VectorElement, derivative, dx, exp,
-                 interval, variable)
+from ufl import Coefficient, Constant, FunctionSpace, Mesh, derivative, dx, exp, interval, variable
+from ufl.finiteelement import FiniteElement
+from ufl.pullback import identity_pullback
+from ufl.sobolevspace import H1
 
 cell = interval
-element = FiniteElement("CG", cell, 2)
-domain = Mesh(VectorElement("Lagrange", cell, 1))
+element = FiniteElement("Lagrange", cell, 2, (), identity_pullback, H1)
+domain = Mesh(FiniteElement("Lagrange", cell, 1, (1, ), identity_pullback, H1))
 space = FunctionSpace(domain, element)
 
 u = Coefficient(space)
diff --git a/demo/L2norm.py b/demo/L2norm.py
index ca8d78cda..31050a9df 100644
--- a/demo/L2norm.py
+++ b/demo/L2norm.py
@@ -2,10 +2,13 @@
 # Author: Martin Sandve Alnes
 # Date: 2008-10-03
 #
-from ufl import Coefficient, FiniteElement, FunctionSpace, Mesh, VectorElement, dx, triangle
+from ufl import Coefficient, FunctionSpace, Mesh, dx, triangle
+from ufl.finiteelement import FiniteElement
+from ufl.pullback import identity_pullback
+from ufl.sobolevspace import H1
 
-element = FiniteElement("Lagrange", triangle, 1)
-domain = Mesh(VectorElement("Lagrange", triangle, 1))
+element = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1)
+domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
 space = FunctionSpace(domain, element)
 
 f = Coefficient(space)
diff --git a/demo/Mass.py b/demo/Mass.py
index a1603c146..4f083140b 100644
--- a/demo/Mass.py
+++ b/demo/Mass.py
@@ -20,10 +20,13 @@
 # Last changed: 2009-03-02
 #
 # The bilinear form for a mass matrix.
-from ufl import FiniteElement, FunctionSpace, Mesh, TestFunction, TrialFunction, VectorElement, dx, triangle
+from ufl import FunctionSpace, Mesh, TestFunction, TrialFunction, dx, triangle
+from ufl.finiteelement import FiniteElement
+from ufl.pullback import identity_pullback
+from ufl.sobolevspace import H1
 
-element = FiniteElement("Lagrange", triangle, 1)
-domain = Mesh(VectorElement("Lagrange", triangle, 1))
+element = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1)
+domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
 space = FunctionSpace(domain, element)
 
 u = TrialFunction(space)
diff --git a/demo/MassAD.py b/demo/MassAD.py
index 57dabdc7a..ed36e8c2c 100644
--- a/demo/MassAD.py
+++ b/demo/MassAD.py
@@ -2,10 +2,13 @@
 # Author: Martin Sandve Alnes
 # Date: 2008-10-28
 #
-from ufl import Coefficient, FiniteElement, FunctionSpace, Mesh, VectorElement, derivative, dx, triangle
+from ufl import Coefficient, FunctionSpace, Mesh, derivative, dx, triangle
+from ufl.finiteelement import FiniteElement
+from ufl.pullback import identity_pullback
+from ufl.sobolevspace import H1
 
-element = FiniteElement("Lagrange", triangle, 1)
-domain = Mesh(VectorElement("Lagrange", triangle, 1))
+element = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1)
+domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
 space = FunctionSpace(domain, element)
 
 u = Coefficient(space)
diff --git a/demo/MixedElasticity.py b/demo/MixedElasticity.py
index 0fac9d36b..6fe1a96a3 100644
--- a/demo/MixedElasticity.py
+++ b/demo/MixedElasticity.py
@@ -17,8 +17,11 @@
 #
 # First added:  2008-10-03
 # Last changed: 2011-07-22
-from ufl import (FunctionSpace, Mesh, MixedElement, TestFunctions, TrialFunctions, VectorElement, as_vector, div, dot,
-                 dx, inner, skew, tetrahedron, tr)
+from ufl import (FunctionSpace, Mesh, TestFunctions, TrialFunctions, as_vector, div, dot, dx, inner, skew, tetrahedron,
+                 tr)
+from ufl.finiteelement import FiniteElement, MixedElement
+from ufl.pullback import contravariant_piola, identity_pullback
+from ufl.sobolevspace import H1, L2, HDiv
 
 
 def skw(tau):
@@ -32,18 +35,13 @@ def skw(tau):
 
 # Finite element exterior calculus syntax
 r = 1
-S = VectorElement("P Lambda", cell, r, form_degree=n - 1)
-V = VectorElement("P Lambda", cell, r - 1, form_degree=n)
-Q = VectorElement("P Lambda", cell, r - 1, form_degree=n)
+S = FiniteElement("vector BDM", cell, r, (3, 3), contravariant_piola, HDiv)
+V = FiniteElement("Discontinuous Lagrange", cell, r - 1, (3, ), identity_pullback, L2)
+Q = FiniteElement("Discontinuous Lagrange", cell, r - 1, (3, ), identity_pullback, L2)
 
-# Alternative syntax:
-# S = VectorElement("BDM", cell, r)
-# V = VectorElement("Discontinuous Lagrange", cell, r-1)
-# Q = VectorElement("Discontinuous Lagrange", cell, r-1)
+W = MixedElement([S, V, Q])
 
-W = MixedElement(S, V, Q)
-
-domain = Mesh(VectorElement("Lagrange", cell, 1))
+domain = Mesh(FiniteElement("Lagrange", cell, 1, (3, ), identity_pullback, H1))
 space = FunctionSpace(domain, W)
 
 (sigma, u, gamma) = TrialFunctions(space)
diff --git a/demo/MixedMixedElement.py b/demo/MixedMixedElement.py
index ddffe3c97..0d1b1df20 100644
--- a/demo/MixedMixedElement.py
+++ b/demo/MixedMixedElement.py
@@ -16,8 +16,11 @@
 # along with UFL. If not, see <http://www.gnu.org/licenses/>.
 #
 # A mixed element of mixed elements
-from ufl import FiniteElement, triangle
+from ufl import triangle
+from ufl.finiteelement import FiniteElement, MixedElement
+from ufl.pullback import identity_pullback
+from ufl.sobolevspace import H1
 
-P3 = FiniteElement("Lagrange", triangle, 3)
+P3 = FiniteElement("Lagrange", triangle, 3, (), identity_pullback, H1)
 
-element = (P3 * P3) * (P3 * P3)
+element = MixedElement([[P3, P3], [P3, P3]])
diff --git a/demo/MixedPoisson.py b/demo/MixedPoisson.py
index 57a112521..1580372a3 100644
--- a/demo/MixedPoisson.py
+++ b/demo/MixedPoisson.py
@@ -23,15 +23,17 @@
 # a mixed formulation of Poisson's equation with BDM
 # (Brezzi-Douglas-Marini) elements.
 #
-from ufl import (Coefficient, FiniteElement, FunctionSpace, Mesh, TestFunctions, TrialFunctions, VectorElement, div,
-                 dot, dx, triangle)
+from ufl import Coefficient, FunctionSpace, Mesh, TestFunctions, TrialFunctions, div, dot, dx, triangle
+from ufl.finiteelement import FiniteElement, MixedElement
+from ufl.pullback import contravariant_piola, identity_pullback
+from ufl.sobolevspace import H1, HDiv
 
 cell = triangle
-BDM1 = FiniteElement("Brezzi-Douglas-Marini", cell, 1)
-DG0 = FiniteElement("Discontinuous Lagrange", cell, 0)
+BDM1 = FiniteElement("Brezzi-Douglas-Marini", cell, 1, (2, ), contravariant_piola, HDiv)
+DG0 = FiniteElement("Discontinuous Lagrange", cell, 0, (), identity_pullback, H1)
 
-element = BDM1 * DG0
-domain = Mesh(VectorElement("Lagrange", cell, 1))
+element = MixedElement([BDM1, DG0])
+domain = Mesh(FiniteElement("Lagrange", cell, 1, (2, ), identity_pullback, H1))
 space = FunctionSpace(domain, element)
 dg0_space = FunctionSpace(domain, DG0)
 
diff --git a/demo/MixedPoisson2.py b/demo/MixedPoisson2.py
index 4711bb97e..29268a309 100644
--- a/demo/MixedPoisson2.py
+++ b/demo/MixedPoisson2.py
@@ -3,14 +3,16 @@
 # Modified by: Martin Sandve Alnes
 # Date: 2009-02-12
 #
-from ufl import (FacetNormal, FiniteElement, FunctionSpace, Mesh, TestFunctions, TrialFunctions, VectorElement, div,
-                 dot, ds, dx, tetrahedron)
+from ufl import FacetNormal, FunctionSpace, Mesh, TestFunctions, TrialFunctions, div, dot, ds, dx, tetrahedron
+from ufl.finiteelement import FiniteElement, MixedElement
+from ufl.pullback import contravariant_piola, identity_pullback
+from ufl.sobolevspace import H1, HDiv
 
 cell = tetrahedron
-RT = FiniteElement("Raviart-Thomas", cell, 1)
-DG = FiniteElement("DG", cell, 0)
-MX = RT * DG
-domain = Mesh(VectorElement("Lagrange", cell, 1))
+RT = FiniteElement("Raviart-Thomas", cell, 1, (3, ), contravariant_piola, HDiv)
+DG = FiniteElement("DG", cell, 0, (), identity_pullback, H1)
+MX = MixedElement([RT, DG])
+domain = Mesh(FiniteElement("Lagrange", cell, 1, (3, ), identity_pullback, H1))
 space = FunctionSpace(domain, MX)
 
 (u, p) = TrialFunctions(space)
diff --git a/demo/NavierStokes.py b/demo/NavierStokes.py
index 0dc87bdda..14dfa5f56 100644
--- a/demo/NavierStokes.py
+++ b/demo/NavierStokes.py
@@ -21,11 +21,14 @@
 #
 # The bilinear form for the nonlinear term in the
 # Navier-Stokes equations with fixed convective velocity.
-from ufl import Coefficient, FunctionSpace, Mesh, TestFunction, TrialFunction, VectorElement, dot, dx, grad, tetrahedron
+from ufl import Coefficient, FunctionSpace, Mesh, TestFunction, TrialFunction, dot, dx, grad, tetrahedron
+from ufl.finiteelement import FiniteElement
+from ufl.pullback import identity_pullback
+from ufl.sobolevspace import H1
 
 cell = tetrahedron
-element = VectorElement("Lagrange", cell, 1)
-domain = Mesh(VectorElement("Lagrange", cell, 1))
+element = FiniteElement("Lagrange", cell, 1, (3, ), identity_pullback, H1)
+domain = Mesh(FiniteElement("Lagrange", cell, 1, (3, ), identity_pullback, H1))
 space = FunctionSpace(domain, element)
 
 v = TestFunction(space)
diff --git a/demo/NeumannProblem.py b/demo/NeumannProblem.py
index 85563e411..d384c4315 100644
--- a/demo/NeumannProblem.py
+++ b/demo/NeumannProblem.py
@@ -17,11 +17,13 @@
 #
 # The bilinear form a(v, u) and linear form L(v) for
 # Poisson's equation with Neumann boundary conditions.
-from ufl import (Coefficient, FunctionSpace, Mesh, TestFunction, TrialFunction, VectorElement, ds, dx, grad, inner,
-                 triangle)
+from ufl import Coefficient, FunctionSpace, Mesh, TestFunction, TrialFunction, ds, dx, grad, inner, triangle
+from ufl.finiteelement import FiniteElement
+from ufl.pullback import identity_pullback
+from ufl.sobolevspace import H1
 
-element = VectorElement("Lagrange", triangle, 1)
-domain = Mesh(VectorElement("Lagrange", triangle, 1))
+element = FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1)
+domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
 space = FunctionSpace(domain, element)
 
 v = TestFunction(space)
diff --git a/demo/NonlinearPoisson.py b/demo/NonlinearPoisson.py
index f71b2c41c..7604459b5 100644
--- a/demo/NonlinearPoisson.py
+++ b/demo/NonlinearPoisson.py
@@ -1,8 +1,10 @@
-from ufl import (Coefficient, FiniteElement, FunctionSpace, Mesh, TestFunction, TrialFunction, VectorElement, dot, dx,
-                 grad, triangle)
+from ufl import Coefficient, FunctionSpace, Mesh, TestFunction, TrialFunction, dot, dx, grad, triangle
+from ufl.finiteelement import FiniteElement
+from ufl.pullback import identity_pullback
+from ufl.sobolevspace import H1
 
-element = FiniteElement("Lagrange", triangle, 1)
-domain = Mesh(VectorElement("Lagrange", triangle, 1))
+element = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1)
+domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
 space = FunctionSpace(domain, element)
 
 v = TestFunction(space)
diff --git a/demo/P5tet.py b/demo/P5tet.py
index 44dbb7fb5..5114b6724 100644
--- a/demo/P5tet.py
+++ b/demo/P5tet.py
@@ -16,6 +16,9 @@
 # along with UFL. If not, see <http://www.gnu.org/licenses/>.
 #
 # A fifth degree Lagrange finite element on a tetrahedron
-from ufl import FiniteElement, tetrahedron
+from ufl import tetrahedron
+from ufl.finiteelement import FiniteElement
+from ufl.pullback import identity_pullback
+from ufl.sobolevspace import H1
 
-element = FiniteElement("Lagrange", tetrahedron, 5)
+element = FiniteElement("Lagrange", tetrahedron, 5, (), identity_pullback, H1)
diff --git a/demo/P5tri.py b/demo/P5tri.py
index eb616e896..ebfd6ba6f 100644
--- a/demo/P5tri.py
+++ b/demo/P5tri.py
@@ -16,6 +16,9 @@
 # along with UFL. If not, see <http://www.gnu.org/licenses/>.
 #
 # A fifth degree Lagrange finite element on a triangle
-from ufl import FiniteElement, triangle
+from ufl import triangle
+from ufl.finiteelement import FiniteElement
+from ufl.pullback import identity_pullback
+from ufl.sobolevspace import H1
 
-element = FiniteElement("Lagrange", triangle, 5)
+element = FiniteElement("Lagrange", triangle, 5, (), identity_pullback, H1)
diff --git a/demo/Poisson.py b/demo/Poisson.py
index 470b844ec..779273391 100644
--- a/demo/Poisson.py
+++ b/demo/Poisson.py
@@ -21,11 +21,13 @@
 # Last changed: 2009-03-02
 #
 # The bilinear form a(v, u) and linear form L(v) for Poisson's equation.
-from ufl import (Coefficient, FiniteElement, FunctionSpace, Mesh, TestFunction, TrialFunction, VectorElement, dx, grad,
-                 inner, triangle)
+from ufl import Coefficient, FunctionSpace, Mesh, TestFunction, TrialFunction, dx, grad, inner, triangle
+from ufl.finiteelement import FiniteElement
+from ufl.pullback import identity_pullback
+from ufl.sobolevspace import H1
 
-element = FiniteElement("Lagrange", triangle, 1)
-domain = Mesh(VectorElement("Lagrange", triangle, 1))
+element = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1)
+domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
 space = FunctionSpace(domain, element)
 
 u = TrialFunction(space)
diff --git a/demo/PoissonDG.py b/demo/PoissonDG.py
index 90c0c3af1..fd289213a 100644
--- a/demo/PoissonDG.py
+++ b/demo/PoissonDG.py
@@ -21,12 +21,15 @@
 # The bilinear form a(v, u) and linear form L(v) for
 # Poisson's equation in a discontinuous Galerkin (DG)
 # formulation.
-from ufl import (Coefficient, Constant, FacetNormal, FiniteElement, FunctionSpace, Mesh, TestFunction, TrialFunction,
-                 VectorElement, avg, dot, dS, ds, dx, grad, inner, jump, triangle)
+from ufl import (Coefficient, Constant, FacetNormal, FunctionSpace, Mesh, TestFunction, TrialFunction, avg, dot, dS, ds,
+                 dx, grad, inner, jump, triangle)
+from ufl.finiteelement import FiniteElement
+from ufl.pullback import identity_pullback
+from ufl.sobolevspace import H1, L2
 
 cell = triangle
-element = FiniteElement("Discontinuous Lagrange", cell, 1)
-domain = Mesh(VectorElement("Lagrange", cell, 1))
+element = FiniteElement("Discontinuous Lagrange", cell, 1, (), identity_pullback, L2)
+domain = Mesh(FiniteElement("Lagrange", cell, 1, (2, ), identity_pullback, H1))
 space = FunctionSpace(domain, element)
 
 v = TestFunction(space)
diff --git a/demo/PoissonSystem.py b/demo/PoissonSystem.py
index d42a96eca..29967d025 100644
--- a/demo/PoissonSystem.py
+++ b/demo/PoissonSystem.py
@@ -21,12 +21,14 @@
 #
 # The bilinear form a(v, u) and linear form L(v) for
 # Poisson's equation in system form (vector-valued).
-from ufl import (Coefficient, FunctionSpace, Mesh, TestFunction, TrialFunction, VectorElement, dot, dx, grad, inner,
-                 triangle)
+from ufl import Coefficient, FunctionSpace, Mesh, TestFunction, TrialFunction, dot, dx, grad, inner, triangle
+from ufl.finiteelement import FiniteElement
+from ufl.pullback import identity_pullback
+from ufl.sobolevspace import H1
 
 cell = triangle
-element = VectorElement("Lagrange", cell, 1)
-domain = Mesh(VectorElement("Lagrange", cell, 1))
+element = FiniteElement("Lagrange", cell, 1, (2, ), identity_pullback, H1)
+domain = Mesh(FiniteElement("Lagrange", cell, 1, (2, ), identity_pullback, H1))
 space = FunctionSpace(domain, element)
 
 v = TestFunction(space)
diff --git a/demo/PowAD.py b/demo/PowAD.py
index 1c0ad4e92..106f1f799 100644
--- a/demo/PowAD.py
+++ b/demo/PowAD.py
@@ -2,11 +2,13 @@
 # Author: Martin Sandve Alnes
 # Date: 2008-10-03
 #
-from ufl import (Coefficient, FiniteElement, FunctionSpace, Mesh, TestFunction, TrialFunction, VectorElement,
-                 derivative, dx, triangle)
+from ufl import Coefficient, FunctionSpace, Mesh, TestFunction, TrialFunction, derivative, dx, triangle
+from ufl.finiteelement import FiniteElement
+from ufl.pullback import identity_pullback
+from ufl.sobolevspace import H1
 
-element = FiniteElement("Lagrange", triangle, 1)
-domain = Mesh(VectorElement("Lagrange", triangle, 1))
+element = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1)
+domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
 space = FunctionSpace(domain, element)
 
 v = TestFunction(space)
diff --git a/demo/ProjectionSystem.py b/demo/ProjectionSystem.py
index e082b99e1..7548a0b22 100644
--- a/demo/ProjectionSystem.py
+++ b/demo/ProjectionSystem.py
@@ -1,8 +1,10 @@
-from ufl import (Coefficient, FiniteElement, FunctionSpace, Mesh, TestFunction, TrialFunction, VectorElement, dx,
-                 triangle)
+from ufl import Coefficient, FunctionSpace, Mesh, TestFunction, TrialFunction, dx, triangle
+from ufl.finiteelement import FiniteElement
+from ufl.pullback import identity_pullback
+from ufl.sobolevspace import H1
 
-element = FiniteElement("Lagrange", triangle, 1)
-domain = Mesh(VectorElement("Lagrange", triangle, 1))
+element = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1)
+domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
 space = FunctionSpace(domain, element)
 v = TestFunction(space)
 u = TrialFunction(space)
diff --git a/demo/QuadratureElement.py b/demo/QuadratureElement.py
index 3ab666e29..4e01cd31b 100644
--- a/demo/QuadratureElement.py
+++ b/demo/QuadratureElement.py
@@ -20,15 +20,17 @@
 #
 # The linearised bilinear form a(u,v) and linear form L(v) for
 # the nonlinear equation - div (1+u) grad u = f (non-linear Poisson)
-from ufl import (Coefficient, FiniteElement, FunctionSpace, Mesh, TestFunction, TrialFunction, VectorElement, dot, dx,
-                 grad, i, triangle)
+from ufl import Coefficient, FunctionSpace, Mesh, TestFunction, TrialFunction, dot, dx, grad, i, triangle
+from ufl.finiteelement import FiniteElement
+from ufl.pullback import identity_pullback
+from ufl.sobolevspace import H1
 
-element = FiniteElement("Lagrange", triangle, 2)
-domain = Mesh(VectorElement("Lagrange", triangle, 1))
+element = FiniteElement("Lagrange", triangle, 2, (), identity_pullback, H1)
+domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
 space = FunctionSpace(domain, element)
 
-QE = FiniteElement("Quadrature", triangle, 2, quad_scheme="default")
-sig = VectorElement("Quadrature", triangle, 1, quad_scheme="default")
+QE = FiniteElement("Quadrature", triangle, 2, (), identity_pullback, H1)
+sig = FiniteElement("Quadrature", triangle, 1, (2, ), identity_pullback, H1)
 
 qe_space = FunctionSpace(domain, QE)
 sig_space = FunctionSpace(domain, sig)
diff --git a/demo/RestrictedElement.py b/demo/RestrictedElement.py
deleted file mode 100644
index 6a07fa274..000000000
--- a/demo/RestrictedElement.py
+++ /dev/null
@@ -1,29 +0,0 @@
-# Copyright (C) 2009 Kristian B. Oelgaard
-#
-# This file is part of UFL.
-#
-# UFL is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Lesser General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# UFL is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU Lesser General Public License for more details.
-#
-# You should have received a copy of the GNU Lesser General Public License
-# along with UFL. If not, see <http://www.gnu.org/licenses/>.
-#
-# Restriction of a finite element.
-# The below syntax show how one can restrict a higher order Lagrange element
-# to only take into account those DOFs that live on the facets.
-from ufl import FiniteElement, FunctionSpace, Mesh, TestFunction, TrialFunction, VectorElement, avg, dS, ds, triangle
-
-# Restricted element
-CG_R = FiniteElement("Lagrange", triangle, 4)["facet"]
-domain = Mesh(VectorElement("Lagrange", triangle, 1))
-space = FunctionSpace(domain, CG_R)
-u_r = TrialFunction(space)
-v_r = TestFunction(space)
-a = avg(v_r) * avg(u_r) * dS + v_r * u_r * ds
diff --git a/demo/Stiffness.py b/demo/Stiffness.py
index 43f10ea2e..32bf00a54 100644
--- a/demo/Stiffness.py
+++ b/demo/Stiffness.py
@@ -2,10 +2,13 @@
 # Author: Martin Sandve Alnes
 # Date: 2008-10-03
 #
-from ufl import FiniteElement, FunctionSpace, Mesh, TestFunction, TrialFunction, VectorElement, dot, dx, grad, triangle
+from ufl import FunctionSpace, Mesh, TestFunction, TrialFunction, dot, dx, grad, triangle
+from ufl.finiteelement import FiniteElement
+from ufl.pullback import identity_pullback
+from ufl.sobolevspace import H1
 
-element = FiniteElement("Lagrange", triangle, 1)
-domain = Mesh(VectorElement("Lagrange", triangle, 1))
+element = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1)
+domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
 space = FunctionSpace(domain, element)
 
 u = TrialFunction(space)
diff --git a/demo/StiffnessAD.py b/demo/StiffnessAD.py
index affadc86b..59a8bcba4 100644
--- a/demo/StiffnessAD.py
+++ b/demo/StiffnessAD.py
@@ -2,11 +2,13 @@
 # Author: Martin Sandve Alnes
 # Date: 2008-10-30
 #
-from ufl import (Coefficient, FiniteElement, FunctionSpace, Mesh, VectorElement, action, adjoint, derivative, dx, grad,
-                 inner, triangle)
+from ufl import Coefficient, FunctionSpace, Mesh, action, adjoint, derivative, dx, grad, inner, triangle
+from ufl.finiteelement import FiniteElement
+from ufl.pullback import identity_pullback
+from ufl.sobolevspace import H1
 
-element = FiniteElement("Lagrange", triangle, 1)
-domain = Mesh(VectorElement("Lagrange", triangle, 1))
+element = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1)
+domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
 space = FunctionSpace(domain, element)
 
 w = Coefficient(space)
diff --git a/demo/Stokes.py b/demo/Stokes.py
index f128f7779..b4d240ffd 100644
--- a/demo/Stokes.py
+++ b/demo/Stokes.py
@@ -20,14 +20,16 @@
 #
 # The bilinear form a(v, u) and Linear form L(v) for the Stokes
 # equations using a mixed formulation (Taylor-Hood elements).
-from ufl import (Coefficient, FiniteElement, FunctionSpace, Mesh, TestFunctions, TrialFunctions, VectorElement, div,
-                 dot, dx, grad, inner, triangle)
+from ufl import Coefficient, FunctionSpace, Mesh, TestFunctions, TrialFunctions, div, dot, dx, grad, inner, triangle
+from ufl.finiteelement import FiniteElement, MixedElement
+from ufl.pullback import identity_pullback
+from ufl.sobolevspace import H1
 
 cell = triangle
-P2 = VectorElement("Lagrange", cell, 2)
-P1 = FiniteElement("Lagrange", cell, 1)
-TH = P2 * P1
-domain = Mesh(VectorElement("Lagrange", cell, 1))
+P2 = FiniteElement("Lagrange", cell, 2, (2, ), identity_pullback, H1)
+P1 = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1)
+TH = MixedElement([P2, P1])
+domain = Mesh(FiniteElement("Lagrange", cell, 1, (2, ), identity_pullback, H1))
 space = FunctionSpace(domain, TH)
 p2_space = FunctionSpace(domain, P2)
 
diff --git a/demo/StokesEquation.py b/demo/StokesEquation.py
index ea0c62bd7..949551846 100644
--- a/demo/StokesEquation.py
+++ b/demo/StokesEquation.py
@@ -19,14 +19,17 @@
 # equations using a mixed formulation (Taylor-Hood elements) in
 # combination with the lhs() and rhs() operators to extract the
 # bilinear and linear forms from an expression F = 0.
-from ufl import (Coefficient, FiniteElement, FunctionSpace, Mesh, TestFunctions, TrialFunctions, VectorElement, div,
-                 dot, dx, grad, inner, lhs, rhs, triangle)
+from ufl import (Coefficient, FunctionSpace, Mesh, TestFunctions, TrialFunctions, div, dot, dx, grad, inner, lhs, rhs,
+                 triangle)
+from ufl.finiteelement import FiniteElement, MixedElement
+from ufl.pullback import identity_pullback
+from ufl.sobolevspace import H1
 
 cell = triangle
-P2 = VectorElement("Lagrange", cell, 2)
-P1 = FiniteElement("Lagrange", cell, 1)
-TH = P2 * P1
-domain = Mesh(VectorElement("Lagrange", cell, 1))
+P2 = FiniteElement("Lagrange", cell, 2, (2, ), identity_pullback, H1)
+P1 = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1)
+TH = MixedElement([P2, P1])
+domain = Mesh(FiniteElement("Lagrange", cell, 1, (2, ), identity_pullback, H1))
 space = FunctionSpace(domain, TH)
 p2_space = FunctionSpace(domain, P2)
 
diff --git a/demo/SubDomain.py b/demo/SubDomain.py
index c55e59d75..4205a7790 100644
--- a/demo/SubDomain.py
+++ b/demo/SubDomain.py
@@ -17,11 +17,13 @@
 #
 # This example illustrates how to define a form over a
 # given subdomain of a mesh, in this case a functional.
-from ufl import (Coefficient, FiniteElement, FunctionSpace, Mesh, TestFunction, TrialFunction, VectorElement, ds, dx,
-                 tetrahedron)
+from ufl import Coefficient, FunctionSpace, Mesh, TestFunction, TrialFunction, ds, dx, tetrahedron
+from ufl.finiteelement import FiniteElement
+from ufl.pullback import identity_pullback
+from ufl.sobolevspace import H1
 
-element = FiniteElement("CG", tetrahedron, 1)
-domain = Mesh(VectorElement("Lagrange", tetrahedron, 1))
+element = FiniteElement("Lagrange", tetrahedron, 1, (), identity_pullback, H1)
+domain = Mesh(FiniteElement("Lagrange", tetrahedron, 1, (3, ), identity_pullback, H1))
 space = FunctionSpace(domain, element)
 
 v = TestFunction(space)
diff --git a/demo/SubDomains.py b/demo/SubDomains.py
index f8a19caa6..55e9ddbe5 100644
--- a/demo/SubDomains.py
+++ b/demo/SubDomains.py
@@ -17,10 +17,13 @@
 #
 # This simple example illustrates how forms can be defined on different sub domains.
 # It is supported for all three integral types.
-from ufl import FiniteElement, FunctionSpace, Mesh, TestFunction, TrialFunction, VectorElement, ds, dS, dx, tetrahedron
+from ufl import FunctionSpace, Mesh, TestFunction, TrialFunction, ds, dS, dx, tetrahedron
+from ufl.finiteelement import FiniteElement
+from ufl.pullback import identity_pullback
+from ufl.sobolevspace import H1
 
-element = FiniteElement("CG", tetrahedron, 1)
-domain = Mesh(VectorElement("Lagrange", tetrahedron, 1))
+element = FiniteElement("Lagrange", tetrahedron, 1, (), identity_pullback, H1)
+domain = Mesh(FiniteElement("Lagrange", tetrahedron, 1, (3, ), identity_pullback, H1))
 space = FunctionSpace(domain, element)
 
 v = TestFunction(space)
diff --git a/demo/TensorWeightedPoisson.py b/demo/TensorWeightedPoisson.py
index 0c76f32bf..6ebc8e3c1 100644
--- a/demo/TensorWeightedPoisson.py
+++ b/demo/TensorWeightedPoisson.py
@@ -17,12 +17,14 @@
 #
 # The bilinear form a(v, u) and linear form L(v) for
 # tensor-weighted Poisson's equation.
-from ufl import (Coefficient, FiniteElement, FunctionSpace, Mesh, TensorElement, TestFunction, TrialFunction,
-                 VectorElement, dx, grad, inner, triangle)
+from ufl import Coefficient, FunctionSpace, Mesh, TestFunction, TrialFunction, dx, grad, inner, triangle
+from ufl.finiteelement import FiniteElement
+from ufl.pullback import identity_pullback
+from ufl.sobolevspace import H1, L2
 
-P1 = FiniteElement("Lagrange", triangle, 1)
-P0 = TensorElement("Discontinuous Lagrange", triangle, 0, shape=(2, 2))
-domain = Mesh(VectorElement("Lagrange", triangle, 1))
+P1 = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1)
+P0 = FiniteElement("Discontinuous Lagrange", triangle, 0, (2, 2), identity_pullback, L2)
+domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
 p1_space = FunctionSpace(domain, P1)
 p0_space = FunctionSpace(domain, P0)
 
diff --git a/demo/VectorLaplaceGradCurl.py b/demo/VectorLaplaceGradCurl.py
index e8e281baf..f5d9b863f 100644
--- a/demo/VectorLaplaceGradCurl.py
+++ b/demo/VectorLaplaceGradCurl.py
@@ -18,8 +18,10 @@
 # The bilinear form a(v, u) and linear form L(v) for the Hodge Laplace
 # problem using 0- and 1-forms. Intended to demonstrate use of Nedelec
 # elements.
-from ufl import (Coefficient, FiniteElement, FunctionSpace, Mesh, TestFunctions, TrialFunctions, VectorElement, curl,
-                 dx, grad, inner, tetrahedron)
+from ufl import Coefficient, FunctionSpace, Mesh, TestFunctions, TrialFunctions, curl, dx, grad, inner, tetrahedron
+from ufl.finiteelement import FiniteElement, MixedElement
+from ufl.pullback import covariant_piola, identity_pullback
+from ufl.sobolevspace import H1, HCurl
 
 
 def HodgeLaplaceGradCurl(space, fspace):
@@ -36,13 +38,13 @@ def HodgeLaplaceGradCurl(space, fspace):
 cell = tetrahedron
 order = 1
 
-GRAD = FiniteElement("Lagrange", cell, order)
-CURL = FiniteElement("N1curl", cell, order)
+GRAD = FiniteElement("Lagrange", cell, order, (), identity_pullback, H1)
+CURL = FiniteElement("N1curl", cell, order, (3, ), covariant_piola, HCurl)
 
-VectorLagrange = VectorElement("Lagrange", cell, order + 1)
+VectorLagrange = FiniteElement("Lagrange", cell, order + 1, (3, ), identity_pullback, H1)
 
-domain = Mesh(VectorElement("Lagrange", cell, 1))
-space = FunctionSpace(domain, GRAD * CURL)
+domain = Mesh(FiniteElement("Lagrange", cell, 1, (3, ), identity_pullback, H1))
+space = FunctionSpace(domain, MixedElement([GRAD, CURL]))
 fspace = FunctionSpace(domain, VectorLagrange)
 
 a, L = HodgeLaplaceGradCurl(space, fspace)
diff --git a/demo/_TensorProductElement.py b/demo/_TensorProductElement.py
index 373feee99..9e6fb6ef0 100644
--- a/demo/_TensorProductElement.py
+++ b/demo/_TensorProductElement.py
@@ -17,12 +17,15 @@
 #
 # First added:  2012-08-16
 # Last changed: 2012-08-16
-from ufl import (FiniteElement, FunctionSpace, Mesh, TensorProductElement, TestFunction, TrialFunction, dx, interval,
-                 tetrahedron, triangle)
+from ufl import (FunctionSpace, Mesh, TensorProductElement, TestFunction, TrialFunction, dx, interval, tetrahedron,
+                 triangle)
+from ufl.finiteelement import FiniteElement
+from ufl.pullback import identity_pullback
+from ufl.sobolevspace import H1, L2
 
-V0 = FiniteElement("CG", triangle, 1)
-V1 = FiniteElement("DG", interval, 0)
-V2 = FiniteElement("DG", tetrahedron, 0)
+V0 = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1)
+V1 = FiniteElement("DG", interval, 0, (), identity_pullback, L2)
+V2 = FiniteElement("DG", tetrahedron, 0, (), identity_pullback, L2)
 
 V = TensorProductElement(V0, V1, V2)
 
diff --git a/setup.cfg b/setup.cfg
index ccd6223e3..4bbece177 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -59,7 +59,10 @@ ci =
 
 [flake8]
 max-line-length = 120
+builtins = ufl
 exclude = doc/sphinx/source/conf.py
+per-file-ignores =
+    */__init__.py: F401
 
 [pydocstyle]
 convention = google
diff --git a/ufl/__init__.py b/ufl/__init__.py
index 40821e8b7..59c19e61f 100644
--- a/ufl/__init__.py
+++ b/ufl/__init__.py
@@ -63,7 +63,6 @@
     - AbstractDomain
     - Mesh
     - MeshView
-    - TensorProductMesh
 
 * Sobolev spaces::
 
@@ -76,19 +75,15 @@
     - HEin
     - HDivDiv
 
-* Elements::
 
-    - FiniteElement
-    - MixedElement
-    - VectorElement
-    - TensorElement
-    - EnrichedElement
-    - NodalEnrichedElement
-    - RestrictedElement
-    - TensorProductElement
-    - HDivElement
-    - HCurlElement
-    - BrokenElement
+* Pull backs::
+
+    - identity_pullback
+    - contravariant_piola
+    - covariant_piola
+    - l2_piola
+    - double_contravariant_piola
+    - double_covariant_piola
 
 * Function spaces::
 
@@ -248,135 +243,62 @@
 
 __version__ = importlib.metadata.version("fenics-ufl")
 
-# README
-# Imports here should be what the user sees
-# which means we should _not_ import f.ex. "Grad", but "grad".
-# This way we expose the language, the operation "grad", but less
-# of the implementation, the particular class "Grad".
-##########
-
-# Utility functions (product is the counterpart of the built-in
-# python function sum, can be useful for users as well?)
-from ufl.utils.sequences import product
-
-# Types for geometric quantities
-
-from ufl.cell import as_cell, AbstractCell, Cell, TensorProductCell
-from ufl.domain import as_domain, AbstractDomain, Mesh, MeshView, TensorProductMesh
-from ufl.geometry import (
-    SpatialCoordinate,
-    FacetNormal, CellNormal,
-    CellVolume, CellDiameter, Circumradius, MinCellEdgeLength, MaxCellEdgeLength,
-    FacetArea, MinFacetEdgeLength, MaxFacetEdgeLength,
-    Jacobian, JacobianDeterminant, JacobianInverse
-)
-
-# Sobolev spaces
-from ufl.sobolevspace import L2, H1, H2, HDiv, HCurl, HEin, HDivDiv, HInf
-
-# Finite elements classes
-from ufl.finiteelement import (
-    FiniteElementBase, FiniteElement, MixedElement, VectorElement, TensorElement, EnrichedElement,
-    NodalEnrichedElement, RestrictedElement, TensorProductElement, HDivElement, HCurlElement, BrokenElement,
-    WithMapping)
-
-# Hook to extend predefined element families
-from ufl.finiteelement.elementlist import register_element, show_elements
-
-# Function spaces
-from ufl.functionspace import FunctionSpace, MixedFunctionSpace
-
-# Arguments
-from ufl.argument import Argument, Coargument, TestFunction, TrialFunction, Arguments, TestFunctions, TrialFunctions
-
-# Coefficients
-from ufl.coefficient import Coefficient, Cofunction, Coefficients
-from ufl.constant import Constant, VectorConstant, TensorConstant
-
-# Matrices
-from ufl.matrix import Matrix
-
-# Adjoints
-from ufl.adjoint import Adjoint
+from math import e, pi
 
-# Actions
+import ufl.exproperators as __exproperators
 from ufl.action import Action
-
-# Interpolates
+from ufl.adjoint import Adjoint
+from ufl.argument import Argument, Arguments, Coargument, TestFunction, TestFunctions, TrialFunction, TrialFunctions
+from ufl.cell import AbstractCell, Cell, TensorProductCell, as_cell
+from ufl.coefficient import Coefficient, Coefficients, Cofunction
+from ufl.constant import Constant, TensorConstant, VectorConstant
+from ufl.constantvalue import Identity, PermutationSymbol, as_ufl, zero
+from ufl.core.external_operator import ExternalOperator
 from ufl.core.interpolate import Interpolate, interpolate
-
-# Split function
-from ufl.split_functions import split
-
-# Literal constants
-from ufl.constantvalue import PermutationSymbol, Identity, zero, as_ufl
-
-# Indexing of tensor expressions
 from ufl.core.multiindex import Index, indices
-
-# Special functions for expression base classes
-# (ensure this is imported, since it attaches operators to Expr)
-import ufl.exproperators as __exproperators  # noqa: F401
-
-# Containers for expressions with value rank > 0
-from ufl.tensors import as_tensor, as_vector, as_matrix
-from ufl.tensors import unit_vector, unit_vectors, unit_matrix, unit_matrices
-
-# Operators
-from ufl.operators import (
-    rank, shape, conj, real, imag, outer, inner, dot, cross, perp,
-    det, inv, cofac, transpose, tr, diag, diag_vector, dev, skew, sym,
-    sqrt, exp, ln, erf, cos, sin, tan, acos, asin, atan, atan2, cosh, sinh, tanh,
-    bessel_J, bessel_Y, bessel_I, bessel_K, eq, ne, le, ge, lt, gt, And, Or, Not,
-    conditional, sign, max_value, min_value, variable, diff,
-    Dx, grad, div, curl, rot, nabla_grad, nabla_div, Dn, exterior_derivative,
-    jump, avg, cell_avg, facet_avg, elem_mult, elem_div, elem_pow, elem_op)
-
-# External Operator
-from ufl.core.external_operator import ExternalOperator
-
-# Measure classes
-from ufl.measure import Measure, register_integral_type, integral_types, custom_integral_types
-
-# Form class
-from ufl.form import Form, BaseForm, FormSum, ZeroBaseForm
-
-# Integral classes
+from ufl.domain import AbstractDomain, Mesh, MeshView
+from ufl.finiteelement import AbstractFiniteElement
+from ufl.form import BaseForm, Form, FormSum, ZeroBaseForm
+from ufl.formoperators import (action, adjoint, derivative, energy_norm, extract_blocks, functional, lhs, replace, rhs,
+                               sensitivity_rhs, system)
+from ufl.functionspace import FunctionSpace, MixedFunctionSpace
+from ufl.geometry import (CellDiameter, CellNormal, CellVolume, Circumradius, FacetArea, FacetNormal, Jacobian,
+                          JacobianDeterminant, JacobianInverse, MaxCellEdgeLength, MaxFacetEdgeLength,
+                          MinCellEdgeLength, MinFacetEdgeLength, SpatialCoordinate)
 from ufl.integral import Integral
-
-# Representations of transformed forms
-from ufl.formoperators import (replace, derivative, action, energy_norm, rhs, lhs,
-                               system, functional, adjoint, sensitivity_rhs, extract_blocks)
-
-# Predefined convenience objects
-from ufl.objects import (
-    vertex, interval, triangle, tetrahedron, pentatope, tesseract,
-    quadrilateral, hexahedron, prism, pyramid, facet,
-    i, j, k, l, p, q, r, s,
-    dx, ds, dS, dP,
-    dc, dC, dO, dI, dX,
-    ds_b, ds_t, ds_tb, ds_v, dS_h, dS_v)
-
-# Useful constants
-from math import e, pi
+from ufl.matrix import Matrix
+from ufl.measure import Measure, custom_integral_types, integral_types, register_integral_type
+from ufl.objects import (dc, dC, dI, dO, dP, ds, dS, ds_b, dS_h, ds_t, ds_tb, ds_v, dS_v, dx, dX, facet, hexahedron, i,
+                         interval, j, k, l, p, pentatope, prism, pyramid, q, quadrilateral, r, s, tesseract,
+                         tetrahedron, triangle, vertex)
+from ufl.operators import (And, Dn, Dx, Not, Or, acos, asin, atan, atan2, avg, bessel_I, bessel_J, bessel_K, bessel_Y,
+                           cell_avg, cofac, conditional, conj, cos, cosh, cross, curl, det, dev, diag, diag_vector,
+                           diff, div, dot, elem_div, elem_mult, elem_op, elem_pow, eq, erf, exp, exterior_derivative,
+                           facet_avg, ge, grad, gt, imag, inner, inv, jump, le, ln, lt, max_value, min_value, nabla_div,
+                           nabla_grad, ne, outer, perp, rank, real, rot, shape, sign, sin, sinh, skew, sqrt, sym, tan,
+                           tanh, tr, transpose, variable)
+from ufl.pullback import (AbstractPullback, MixedPullback, SymmetricPullback, contravariant_piola, covariant_piola,
+                          double_contravariant_piola, double_covariant_piola, identity_pullback, l2_piola)
+from ufl.sobolevspace import H1, H2, L2, HCurl, HDiv, HDivDiv, HEin, HInf
+from ufl.split_functions import split
+from ufl.tensors import as_matrix, as_tensor, as_vector, unit_matrices, unit_matrix, unit_vector, unit_vectors
+from ufl.utils.sequences import product
 
 __all__ = [
     'product',
     'as_cell', 'AbstractCell', 'Cell', 'TensorProductCell',
-    'as_domain', 'AbstractDomain', 'Mesh', 'MeshView', 'TensorProductMesh',
+    'AbstractDomain', 'Mesh', 'MeshView',
     'L2', 'H1', 'H2', 'HCurl', 'HDiv', 'HInf', 'HEin', 'HDivDiv',
+    'identity_pullback', 'l2_piola', 'contravariant_piola', 'covariant_piola',
+    'double_contravariant_piola', 'double_covariant_piola',
+    'l2_piola', 'MixedPullback', 'SymmetricPullback', 'AbstractPullback',
     'SpatialCoordinate',
     'CellVolume', 'CellDiameter', 'Circumradius',
     'MinCellEdgeLength', 'MaxCellEdgeLength',
     'FacetArea', 'MinFacetEdgeLength', 'MaxFacetEdgeLength',
     'FacetNormal', 'CellNormal',
     'Jacobian', 'JacobianDeterminant', 'JacobianInverse',
-    'FiniteElementBase', 'FiniteElement',
-    'MixedElement', 'VectorElement', 'TensorElement', 'EnrichedElement',
-    'NodalEnrichedElement', 'RestrictedElement', 'TensorProductElement',
-    'HDivElement', 'HCurlElement',
-    'BrokenElement', "WithMapping",
-    'register_element', 'show_elements',
+    'AbstractFiniteElement',
     'FunctionSpace', 'MixedFunctionSpace',
     'Argument', 'Coargument', 'TestFunction', 'TrialFunction',
     'Arguments', 'TestFunctions', 'TrialFunctions',
diff --git a/ufl/action.py b/ufl/action.py
index 6051b4b7c..5d6004994 100644
--- a/ufl/action.py
+++ b/ufl/action.py
@@ -7,14 +7,14 @@
 #
 # Modified by Nacime Bouziani, 2021-2022.
 
-from ufl.form import BaseForm, FormSum, Form, ZeroBaseForm
-from ufl.core.ufl_type import ufl_type
 from ufl.algebra import Sum
-from ufl.constantvalue import Zero
 from ufl.argument import Argument, Coargument
 from ufl.coefficient import BaseCoefficient, Coefficient, Cofunction
-from ufl.differentiation import CoefficientDerivative
+from ufl.constantvalue import Zero
 from ufl.core.base_form_operator import BaseFormOperator
+from ufl.core.ufl_type import ufl_type
+from ufl.differentiation import CoefficientDerivative
+from ufl.form import BaseForm, Form, FormSum, ZeroBaseForm
 from ufl.matrix import Matrix
 
 # --- The Action class represents the action of a numerical object that needs
@@ -120,6 +120,7 @@ def _analyze_form_arguments(self):
     def _analyze_domains(self):
         """Analyze which domains can be found in Action."""
         from ufl.domain import join_domains
+
         # Collect unique domains
         self._domains = join_domains([e.ufl_domain() for e in self.ufl_operands])
 
diff --git a/ufl/adjoint.py b/ufl/adjoint.py
index e0f86ec22..38f003870 100644
--- a/ufl/adjoint.py
+++ b/ufl/adjoint.py
@@ -8,9 +8,10 @@
 #
 # Modified by Nacime Bouziani, 2021-2022.
 
-from ufl.form import BaseForm, FormSum, ZeroBaseForm
 from ufl.argument import Coargument
 from ufl.core.ufl_type import ufl_type
+from ufl.form import BaseForm, FormSum, ZeroBaseForm
+
 # --- The Adjoint class represents the adjoint of a numerical object that
 #     needs to be computed at assembly time ---
 
@@ -87,6 +88,7 @@ def _analyze_form_arguments(self):
     def _analyze_domains(self):
         """Analyze which domains can be found in Adjoint."""
         from ufl.domain import join_domains
+
         # Collect unique domains
         self._domains = join_domains([e.ufl_domain() for e in self.ufl_operands])
 
diff --git a/ufl/algebra.py b/ufl/algebra.py
index 1bd1493be..376c16d02 100644
--- a/ufl/algebra.py
+++ b/ufl/algebra.py
@@ -7,14 +7,14 @@
 #
 # Modified by Anders Logg, 2008
 
-from ufl.core.ufl_type import ufl_type
+from ufl.checks import is_true_ufl_scalar, is_ufl_scalar
+from ufl.constantvalue import ComplexValue, IntValue, ScalarValue, Zero, as_ufl, zero
 from ufl.core.expr import ufl_err_str
 from ufl.core.operator import Operator
-from ufl.constantvalue import Zero, zero, ScalarValue, IntValue, ComplexValue, as_ufl
-from ufl.checks import is_ufl_scalar, is_true_ufl_scalar
+from ufl.core.ufl_type import ufl_type
 from ufl.index_combination_utils import merge_unique_indices
-from ufl.sorting import sorted_expr
 from ufl.precedence import parstr
+from ufl.sorting import sorted_expr
 
 # --- Algebraic operators ---
 
diff --git a/ufl/algorithms/__init__.py b/ufl/algorithms/__init__.py
index 690b237f8..8e3ce5a1a 100644
--- a/ufl/algorithms/__init__.py
+++ b/ufl/algorithms/__init__.py
@@ -56,74 +56,25 @@
     "load_forms",
 ]
 
-# Utilities for traversing over expression trees in different ways
-# from ufl.algorithms.traversal import iter_expressions
-
-# Keeping these imports here for backwards compatibility, doesn't cost
-# anything.  Prefer importing from ufl.corealg.traversal in future
-# code.
-# from ufl.corealg.traversal import pre_traversal
-from ufl.corealg.traversal import post_traversal
-# from ufl.corealg.traversal import traverse_terminals, traverse_unique_terminals
-
-
-# Utilities for extracting information from forms and expressions
-from ufl.algorithms.analysis import (
-    extract_type,
-    extract_arguments,
-    extract_coefficients,
-    # extract_arguments_and_coefficients,
-    extract_base_form_operators,
-    extract_elements,
-    extract_unique_elements,
-    extract_sub_elements,
-    sort_elements,
-)
-
-
-# Preprocessing a form to extract various meta data
-# from ufl.algorithms.formdata import FormData
-from ufl.algorithms.compute_form_data import compute_form_data, preprocess_form
-
-# Utilities for checking properties of forms
-from ufl.algorithms.signature import compute_form_signature
-
-# Utilities for error checking of forms
-from ufl.algorithms.checks import validate_form
-
-# Utilites for modifying expressions and forms
-from ufl.corealg.multifunction import MultiFunction
-from ufl.algorithms.transformer import Transformer, ReuseTransformer
-# from ufl.algorithms.transformer import is_post_handler
-from ufl.algorithms.transformer import apply_transformer
-from ufl.algorithms.transformer import strip_variables
-from ufl.algorithms.strip_terminal_data import strip_terminal_data
-from ufl.algorithms.strip_terminal_data import replace_terminal_data
-# from ufl.algorithms.replace import Replacer
-from ufl.algorithms.replace import replace
+from ufl.algorithms.ad import expand_derivatives
+from ufl.algorithms.analysis import (extract_arguments, extract_base_form_operators, extract_coefficients,
+                                     extract_elements, extract_sub_elements, extract_type, extract_unique_elements,
+                                     sort_elements)
 from ufl.algorithms.change_to_reference import change_to_reference_grad
-from ufl.algorithms.expand_compounds import expand_compounds
-# from ufl.algorithms.estimate_degrees import SumDegreeEstimator
+from ufl.algorithms.checks import validate_form
+from ufl.algorithms.compute_form_data import compute_form_data, preprocess_form
 from ufl.algorithms.estimate_degrees import estimate_total_polynomial_degree
+from ufl.algorithms.expand_compounds import expand_compounds
 from ufl.algorithms.expand_indices import expand_indices
-
-# Utilities for transforming complete Forms into other Forms
-from ufl.algorithms.formtransformations import compute_form_adjoint
-from ufl.algorithms.formtransformations import compute_form_action
-from ufl.algorithms.formtransformations import compute_energy_norm
-from ufl.algorithms.formtransformations import compute_form_lhs
-from ufl.algorithms.formtransformations import compute_form_rhs
-from ufl.algorithms.formtransformations import compute_form_functional
-from ufl.algorithms.formtransformations import compute_form_arities
-
+from ufl.algorithms.formfiles import load_forms, load_ufl_file, read_ufl_file
 from ufl.algorithms.formsplitter import FormSplitter
-
-# Utilities for Automatic Functional Differentiation
-from ufl.algorithms.ad import expand_derivatives
-
-# Utilities for form file handling
-from ufl.algorithms.formfiles import read_ufl_file
-from ufl.algorithms.formfiles import load_ufl_file
-from ufl.algorithms.formfiles import load_forms
-
+from ufl.algorithms.formtransformations import (compute_energy_norm, compute_form_action, compute_form_adjoint,
+                                                compute_form_arities, compute_form_functional, compute_form_lhs,
+                                                compute_form_rhs)
+from ufl.algorithms.replace import replace
+from ufl.algorithms.signature import compute_form_signature
+from ufl.algorithms.strip_terminal_data import replace_terminal_data, strip_terminal_data
+from ufl.algorithms.transformer import ReuseTransformer, Transformer, apply_transformer, strip_variables
+from ufl.corealg.multifunction import MultiFunction
+from ufl.corealg.traversal import post_traversal
 from ufl.utils.formatting import tree_format
diff --git a/ufl/algorithms/analysis.py b/ufl/algorithms/analysis.py
index a758351ce..ba3afe6c6 100644
--- a/ufl/algorithms/analysis.py
+++ b/ufl/algorithms/analysis.py
@@ -11,21 +11,20 @@
 
 from itertools import chain
 
-from ufl.utils.sorting import sorted_by_count, topological_sorting
-
-from ufl.core.terminal import Terminal
-from ufl.core.base_form_operator import BaseFormOperator
+from ufl.algorithms.traversal import iter_expressions
 from ufl.argument import BaseArgument, Coargument
 from ufl.coefficient import BaseCoefficient
 from ufl.constant import Constant
+from ufl.core.base_form_operator import BaseFormOperator
+from ufl.core.terminal import Terminal
+from ufl.corealg.traversal import traverse_unique_terminals, unique_pre_traversal
 from ufl.form import BaseForm, Form
-from ufl.algorithms.traversal import iter_expressions
-from ufl.corealg.traversal import unique_pre_traversal, traverse_unique_terminals
-
+from ufl.utils.sorting import sorted_by_count, topological_sorting
 
 # TODO: Some of these can possibly be optimised by implementing
 # inlined stack based traversal algorithms
 
+
 def _sorted_by_number_and_part(seq):
     """Sort items by number and part."""
     return sorted(seq, key=lambda x: (x.number(), x.part()))
@@ -237,7 +236,7 @@ def extract_unique_elements(form):
 
 def extract_sub_elements(elements):
     """Build sorted tuple of all sub elements (including parent element)."""
-    sub_elements = tuple(chain(*[e.sub_elements() for e in elements]))
+    sub_elements = tuple(chain(*[e.sub_elements for e in elements]))
     if not sub_elements:
         return tuple(elements)
     return tuple(elements) + extract_sub_elements(sub_elements)
@@ -253,12 +252,12 @@ def sort_elements(elements):
     The ordering is based on sorting a directed acyclic graph.
     """
     # Set nodes
-    nodes = sorted(elements)
+    nodes = list(elements)
 
     # Set edges
     edges = dict((node, []) for node in nodes)
     for element in elements:
-        for sub_element in element.sub_elements():
+        for sub_element in element.sub_elements:
             edges[element].append(sub_element)
 
     # Sort graph
diff --git a/ufl/algorithms/apply_algebra_lowering.py b/ufl/algorithms/apply_algebra_lowering.py
index 998a139e6..e7dc4cd46 100644
--- a/ufl/algorithms/apply_algebra_lowering.py
+++ b/ufl/algorithms/apply_algebra_lowering.py
@@ -8,14 +8,12 @@
 #
 # Modified by Anders Logg, 2009-2010
 
-from ufl.classes import Product, Grad, Conj
-from ufl.core.multiindex import indices, Index
-from ufl.tensors import as_tensor, as_matrix, as_vector
-
-from ufl.compound_expressions import deviatoric_expr, determinant_expr, cofactor_expr, inverse_expr
-
-from ufl.corealg.multifunction import MultiFunction
 from ufl.algorithms.map_integrands import map_integrand_dags
+from ufl.classes import Conj, Grad, Product
+from ufl.compound_expressions import cofactor_expr, determinant_expr, deviatoric_expr, inverse_expr
+from ufl.core.multiindex import Index, indices
+from ufl.corealg.multifunction import MultiFunction
+from ufl.tensors import as_matrix, as_tensor, as_vector
 
 
 class LowerCompoundAlgebra(MultiFunction):
diff --git a/ufl/algorithms/apply_derivatives.py b/ufl/algorithms/apply_derivatives.py
index d34540904..7ef8a0a5f 100644
--- a/ufl/algorithms/apply_derivatives.py
+++ b/ufl/algorithms/apply_derivatives.py
@@ -10,34 +10,30 @@
 from collections import defaultdict
 from math import pi
 
+from ufl.action import Action
 from ufl.algorithms.analysis import extract_arguments
 from ufl.algorithms.map_integrands import map_integrand_dags
 from ufl.algorithms.replace_derivative_nodes import replace_derivative_nodes
+from ufl.argument import BaseArgument
 from ufl.checks import is_cellwise_constant
-from ufl.classes import (Coefficient, ComponentTensor, Conj, ConstantValue,
-                         ExprList, ExprMapping, FloatValue, FormArgument, Grad,
-                         Identity, Imag, Indexed, IndexSum, JacobianInverse,
-                         ListTensor, Product, Real, ReferenceGrad,
-                         ReferenceValue, SpatialCoordinate, Sum, Variable,
-                         Zero)
+from ufl.classes import (Coefficient, ComponentTensor, Conj, ConstantValue, ExprList, ExprMapping, FloatValue,
+                         FormArgument, Grad, Identity, Imag, Indexed, IndexSum, JacobianInverse, ListTensor, Product,
+                         Real, ReferenceGrad, ReferenceValue, SpatialCoordinate, Sum, Variable, Zero)
 from ufl.constantvalue import is_true_ufl_scalar, is_ufl_scalar
+from ufl.core.base_form_operator import BaseFormOperator
 from ufl.core.expr import ufl_err_str
 from ufl.core.multiindex import FixedIndex, MultiIndex, indices
 from ufl.core.terminal import Terminal
 from ufl.corealg.map_dag import map_expr_dag
 from ufl.corealg.multifunction import MultiFunction
-from ufl.differentiation import CoordinateDerivative, BaseFormCoordinateDerivative, BaseFormOperatorDerivative
+from ufl.differentiation import BaseFormCoordinateDerivative, BaseFormOperatorDerivative, CoordinateDerivative
 from ufl.domain import extract_unique_domain
-from ufl.operators import (bessel_I, bessel_J, bessel_K, bessel_Y, cell_avg,
-                           conditional, cos, cosh, exp, facet_avg, ln, sign,
-                           sin, sinh, sqrt)
-from ufl.tensors import (as_scalar, as_scalars, as_tensor, unit_indexed_tensor,
-                         unwrap_list_tensor)
-
-from ufl.argument import BaseArgument
-from ufl.action import Action
 from ufl.form import Form, ZeroBaseForm
-from ufl.core.base_form_operator import BaseFormOperator
+from ufl.operators import (bessel_I, bessel_J, bessel_K, bessel_Y, cell_avg, conditional, cos, cosh, exp, facet_avg, ln,
+                           sign, sin, sinh, sqrt)
+from ufl.pullback import CustomPullback, PhysicalPullback
+from ufl.tensors import as_scalar, as_scalars, as_tensor, unit_indexed_tensor, unwrap_list_tensor
+
 # TODO: Add more rulesets?
 # - DivRuleset
 # - CurlRuleset
@@ -597,7 +593,7 @@ def reference_value(self, o):
         """Differentiate a reference_value."""
         # grad(o) == grad(rv(f)) -> K_ji*rgrad(rv(f))_rj
         f = o.ufl_operands[0]
-        if f.ufl_element().mapping() == "physical":
+        if isinstance(f.ufl_element().pullback, PhysicalPullback):
             # TODO: Do we need to be more careful for immersed things?
             return ReferenceGrad(o)
 
@@ -836,7 +832,7 @@ def reference_value(self, o):
         # d/dv(o) == d/dv(rv(f)) = 0 if v is not f, or rv(dv/df)
         v = self._variable
         if isinstance(v, Coefficient) and o.ufl_operands[0] == v:
-            if v.ufl_element().mapping() != "identity":
+            if not v.ufl_element().pullback.is_identity:
                 # FIXME: This is a bit tricky, instead of Identity it is
                 #   actually inverse(transform), or we should rather not
                 #   convert to reference frame in the first place
@@ -1641,7 +1637,7 @@ def coordinate_derivative(self, o, f, w, v, cd):
         """Apply to a coordinate_derivative."""
         from ufl.algorithms import extract_unique_elements
         for space in extract_unique_elements(o):
-            if space.mapping() == "custom":
+            if isinstance(space.pullback, CustomPullback):
                 raise NotImplementedError(
                     "CoordinateDerivative is not supported for elements with custom pull back.")
 
diff --git a/ufl/algorithms/apply_function_pullbacks.py b/ufl/algorithms/apply_function_pullbacks.py
index ebd66866d..a8c1b8acf 100644
--- a/ufl/algorithms/apply_function_pullbacks.py
+++ b/ufl/algorithms/apply_function_pullbacks.py
@@ -6,152 +6,9 @@
 #
 # SPDX-License-Identifier:    LGPL-3.0-or-later
 
-from itertools import accumulate, chain, repeat
-
-import numpy
-
 from ufl.algorithms.map_integrands import map_integrand_dags
-from ufl.classes import (Jacobian, JacobianDeterminant, JacobianInverse,
-                         ReferenceValue)
-from ufl.core.multiindex import indices
+from ufl.classes import ReferenceValue
 from ufl.corealg.multifunction import MultiFunction, memoized_handler
-from ufl.domain import extract_unique_domain
-from ufl.tensors import as_tensor, as_vector
-from ufl.utils.sequences import product
-
-
-def sub_elements_with_mappings(element):
-    """Return an ordered list of the largest subelements that have a defined mapping."""
-    if element.mapping() != "undefined":
-        return [element]
-    elements = []
-    for subelm in element.sub_elements():
-        if subelm.mapping() != "undefined":
-            elements.append(subelm)
-        else:
-            elements.extend(sub_elements_with_mappings(subelm))
-    return elements
-
-
-def apply_known_single_pullback(r, element):
-    """Apply pullback with given mapping.
-
-    Args:
-        r: Expression wrapped in ReferenceValue
-        element: The element defining the mapping
-    """
-    # Need to pass in r rather than the physical space thing, because
-    # the latter may be a ListTensor or similar, rather than a
-    # Coefficient/Argument (in the case of mixed elements, see below
-    # in apply_single_function_pullbacks), to which we cannot apply ReferenceValue
-    mapping = element.mapping()
-    domain = extract_unique_domain(r)
-    if mapping == "physical":
-        return r
-    elif mapping == "identity" or mapping == "custom":
-        return r
-    elif mapping == "contravariant Piola":
-        J = Jacobian(domain)
-        detJ = JacobianDeterminant(J)
-        transform = (1.0 / detJ) * J
-        # Apply transform "row-wise" to TensorElement(PiolaMapped, ...)
-        *k, i, j = indices(len(r.ufl_shape) + 1)
-        kj = (*k, j)
-        f = as_tensor(transform[i, j] * r[kj], (*k, i))
-        return f
-    elif mapping == "covariant Piola":
-        K = JacobianInverse(domain)
-        # Apply transform "row-wise" to TensorElement(PiolaMapped, ...)
-        *k, i, j = indices(len(r.ufl_shape) + 1)
-        kj = (*k, j)
-        f = as_tensor(K[j, i] * r[kj], (*k, i))
-        return f
-    elif mapping == "L2 Piola":
-        detJ = JacobianDeterminant(domain)
-        return r / detJ
-    elif mapping == "double contravariant Piola":
-        J = Jacobian(domain)
-        detJ = JacobianDeterminant(J)
-        transform = (1.0 / detJ) * J
-        # Apply transform "row-wise" to TensorElement(PiolaMapped, ...)
-        *k, i, j, m, n = indices(len(r.ufl_shape) + 2)
-        kmn = (*k, m, n)
-        f = as_tensor((1.0 / detJ)**2 * J[i, m] * r[kmn] * J[j, n], (*k, i, j))
-        return f
-    elif mapping == "double covariant Piola":
-        K = JacobianInverse(domain)
-        # Apply transform "row-wise" to TensorElement(PiolaMapped, ...)
-        *k, i, j, m, n = indices(len(r.ufl_shape) + 2)
-        kmn = (*k, m, n)
-        f = as_tensor(K[m, i] * r[kmn] * K[n, j], (*k, i, j))
-        return f
-    else:
-        raise ValueError(f"Unsupported mapping: {mapping}.")
-
-
-def apply_single_function_pullbacks(r, element):
-    """Apply an appropriate pullback to something in physical space.
-
-    Args:
-        r: An expression wrapped in ReferenceValue.
-        element: The element this expression lives in.
-
-    Returns:
-        a pulled back expression.
-    """
-    mapping = element.mapping()
-    if r.ufl_shape != element.reference_value_shape():
-        raise ValueError(
-            f"Expecting reference space expression with shape '{element.reference_value_shape()}', "
-            f"got '{r.ufl_shape}'")
-    if mapping in {"physical", "identity",
-                   "contravariant Piola", "covariant Piola",
-                   "double contravariant Piola", "double covariant Piola",
-                   "L2 Piola", "custom"}:
-        # Base case in recursion through elements. If the element
-        # advertises a mapping we know how to handle, do that
-        # directly.
-        f = apply_known_single_pullback(r, element)
-        if f.ufl_shape != element.value_shape():
-            raise ValueError(f"Expecting pulled back expression with shape '{element.value_shape()}', "
-                             f"got '{f.ufl_shape}'")
-        return f
-    elif mapping in {"symmetries", "undefined"}:
-        # Need to pull back each unique piece of the reference space thing
-        gsh = element.value_shape()
-        rsh = r.ufl_shape
-        if mapping == "symmetries":
-            subelem = element.sub_elements()[0]
-            fcm = element.flattened_sub_element_mapping()
-            offsets = (product(subelem.reference_value_shape()) * i for i in fcm)
-            elements = repeat(subelem)
-        else:
-            elements = sub_elements_with_mappings(element)
-            # Python >= 3.8 has an initial keyword argument to
-            # accumulate, but 3.7 does not.
-            offsets = chain([0],
-                            accumulate(product(e.reference_value_shape())
-                                       for e in elements))
-        rflat = as_vector([r[idx] for idx in numpy.ndindex(rsh)])
-        g_components = []
-        # For each unique piece in reference space, apply the appropriate pullback
-        for offset, subelem in zip(offsets, elements):
-            sub_rsh = subelem.reference_value_shape()
-            rm = product(sub_rsh)
-            rsub = [rflat[offset + i] for i in range(rm)]
-            rsub = as_tensor(numpy.asarray(rsub).reshape(sub_rsh))
-            rmapped = apply_single_function_pullbacks(rsub, subelem)
-            # Flatten into the pulled back expression for the whole thing
-            g_components.extend([rmapped[idx]
-                                 for idx in numpy.ndindex(rmapped.ufl_shape)])
-        # And reshape appropriately
-        f = as_tensor(numpy.asarray(g_components).reshape(gsh))
-        if f.ufl_shape != element.value_shape():
-            raise ValueError(f"Expecting pulled back expression with shape '{element.value_shape()}', "
-                             f"got '{f.ufl_shape}'")
-        return f
-    else:
-        raise ValueError(f"Unsupported mapping type: {mapping}")
 
 
 class FunctionPullbackApplier(MultiFunction):
@@ -172,7 +29,18 @@ def form_argument(self, o):
         """Apply to a form_argument."""
         # Represent 0-derivatives of form arguments on reference
         # element
-        f = apply_single_function_pullbacks(ReferenceValue(o), o.ufl_element())
+        r = ReferenceValue(o)
+        element = o.ufl_element()
+
+        if r.ufl_shape != element.reference_value_shape:
+            raise ValueError(
+                f"Expecting reference space expression with shape '{element.reference_value_shape}', "
+                f"got '{r.ufl_shape}'")
+        f = element.pullback.apply(r)
+        if f.ufl_shape != element.value_shape:
+            raise ValueError(f"Expecting pulled back expression with shape '{element.value_shape}', "
+                             f"got '{f.ufl_shape}'")
+
         assert f.ufl_shape == o.ufl_shape
         return f
 
diff --git a/ufl/algorithms/apply_geometry_lowering.py b/ufl/algorithms/apply_geometry_lowering.py
index 21ea99f3c..c1a39d863 100644
--- a/ufl/algorithms/apply_geometry_lowering.py
+++ b/ufl/algorithms/apply_geometry_lowering.py
@@ -14,14 +14,10 @@
 from functools import reduce
 from itertools import combinations
 
-from ufl.classes import (CellCoordinate, CellEdgeVectors, CellFacetJacobian,
-                         CellOrientation, CellOrigin, CellVertices, CellVolume,
-                         Expr, FacetEdgeVectors, FacetJacobian,
-                         FacetJacobianDeterminant, FloatValue, Form, Integral,
-                         Jacobian, JacobianDeterminant, JacobianInverse,
-                         MaxCellEdgeLength, ReferenceCellVolume,
-                         ReferenceFacetVolume, ReferenceGrad, ReferenceNormal,
-                         SpatialCoordinate)
+from ufl.classes import (CellCoordinate, CellEdgeVectors, CellFacetJacobian, CellOrientation, CellOrigin, CellVertices,
+                         CellVolume, Expr, FacetEdgeVectors, FacetJacobian, FacetJacobianDeterminant, FloatValue, Form,
+                         Integral, Jacobian, JacobianDeterminant, JacobianInverse, MaxCellEdgeLength,
+                         ReferenceCellVolume, ReferenceFacetVolume, ReferenceGrad, ReferenceNormal, SpatialCoordinate)
 from ufl.compound_expressions import cross_expr, determinant_expr, inverse_expr
 from ufl.core.multiindex import Index, indices
 from ufl.corealg.map_dag import map_expr_dag
@@ -54,7 +50,7 @@ def jacobian(self, o):
         if self._preserve_types[o._ufl_typecode_]:
             return o
         domain = extract_unique_domain(o)
-        if domain.ufl_coordinate_element().mapping() != "identity":
+        if not domain.ufl_coordinate_element().pullback.is_identity:
             raise ValueError("Piola mapped coordinates are not implemented.")
         # Note: No longer supporting domain.coordinates(), always
         # preserving SpatialCoordinate object.  However if Jacobians
@@ -155,7 +151,7 @@ def spatial_coordinate(self, o):
         """
         if self._preserve_types[o._ufl_typecode_]:
             return o
-        if extract_unique_domain(o).ufl_coordinate_element().mapping() != "identity":
+        if not extract_unique_domain(o).ufl_coordinate_element().pullback.is_identity:
             raise ValueError("Piola mapped coordinates are not implemented.")
         # No longer supporting domain.coordinates(), always preserving
         # SpatialCoordinate object.
@@ -284,7 +280,7 @@ def _reduce_cell_edge_length(self, o, reduction_op):
 
         domain = extract_unique_domain(o)
 
-        if not domain.ufl_coordinate_element().degree() == 1:
+        if domain.ufl_coordinate_element().embedded_superdegree > 1:
             # Don't lower bendy cells, instead leave it to form compiler
             warnings.warn("Only know how to compute cell edge lengths of P1 or Q1 cell.")
             return o
@@ -309,7 +305,7 @@ def cell_diameter(self, o):
 
         domain = extract_unique_domain(o)
 
-        if not domain.ufl_coordinate_element().degree() in {1, (1, 1)}:
+        if domain.ufl_coordinate_element().embedded_superdegree > 1:
             # Don't lower bendy cells, instead leave it to form compiler
             warnings.warn("Only know how to compute cell diameter of P1 or Q1 cell.")
             return o
@@ -346,7 +342,7 @@ def _reduce_facet_edge_length(self, o, reduction_op):
         if domain.ufl_cell().topological_dimension() < 3:
             raise ValueError("Facet edge lengths only make sense for topological dimension >= 3.")
 
-        elif not domain.ufl_coordinate_element().degree() == 1:
+        elif domain.ufl_coordinate_element().embedded_superdegree > 1:
             # Don't lower bendy cells, instead leave it to form compiler
             warnings.warn("Only know how to compute facet edge lengths of P1 or Q1 cell.")
             return o
diff --git a/ufl/algorithms/apply_integral_scaling.py b/ufl/algorithms/apply_integral_scaling.py
index de5cc9fa5..7e5e35ff2 100644
--- a/ufl/algorithms/apply_integral_scaling.py
+++ b/ufl/algorithms/apply_integral_scaling.py
@@ -6,11 +6,11 @@
 #
 # SPDX-License-Identifier:    LGPL-3.0-or-later
 
-from ufl.classes import JacobianDeterminant, FacetJacobianDeterminant, QuadratureWeight, Form, Integral
-from ufl.measure import custom_integral_types, point_integral_types
-from ufl.differentiation import CoordinateDerivative
 from ufl.algorithms.apply_geometry_lowering import apply_geometry_lowering
 from ufl.algorithms.estimate_degrees import estimate_total_polynomial_degree
+from ufl.classes import FacetJacobianDeterminant, Form, Integral, JacobianDeterminant, QuadratureWeight
+from ufl.differentiation import CoordinateDerivative
+from ufl.measure import custom_integral_types, point_integral_types
 
 
 def compute_integrand_scaling_factor(integral):
diff --git a/ufl/algorithms/apply_restrictions.py b/ufl/algorithms/apply_restrictions.py
index cd98315ae..8f788a009 100644
--- a/ufl/algorithms/apply_restrictions.py
+++ b/ufl/algorithms/apply_restrictions.py
@@ -157,7 +157,7 @@ def facet_normal(self, o):
         gd = D.geometric_dimension()
         td = D.topological_dimension()
 
-        if e._is_linear() and gd == td:
+        if e.embedded_superdegree <= 1 and e in H1 and gd == td:
             # For meshes with a continuous linear non-manifold
             # coordinate field, the facet normal from side - points in
             # the opposite direction of the one from side +.  We must
diff --git a/ufl/algorithms/balancing.py b/ufl/algorithms/balancing.py
index fdb6682c5..477ec3f6f 100644
--- a/ufl/algorithms/balancing.py
+++ b/ufl/algorithms/balancing.py
@@ -6,8 +6,8 @@
 #
 # SPDX-License-Identifier:    LGPL-3.0-or-later
 
-from ufl.classes import (CellAvg, FacetAvg, Grad, Indexed, NegativeRestricted,
-                         PositiveRestricted, ReferenceGrad, ReferenceValue)
+from ufl.classes import (CellAvg, FacetAvg, Grad, Indexed, NegativeRestricted, PositiveRestricted, ReferenceGrad,
+                         ReferenceValue)
 from ufl.corealg.map_dag import map_expr_dag
 from ufl.corealg.multifunction import MultiFunction
 
diff --git a/ufl/algorithms/change_to_reference.py b/ufl/algorithms/change_to_reference.py
index 452f0d896..c232d3c7b 100644
--- a/ufl/algorithms/change_to_reference.py
+++ b/ufl/algorithms/change_to_reference.py
@@ -6,18 +6,14 @@
 #
 # SPDX-License-Identifier:    LGPL-3.0-or-later
 
-from ufl.core.multiindex import indices
-from ufl.corealg.multifunction import MultiFunction
-from ufl.corealg.map_dag import map_expr_dag
-
-from ufl.classes import ReferenceGrad, Grad, Restricted, ReferenceValue, JacobianInverse
-
-from ufl.tensors import as_tensor
-
 from ufl.algorithms.apply_function_pullbacks import apply_function_pullbacks
 from ufl.algorithms.apply_geometry_lowering import apply_geometry_lowering
 from ufl.checks import is_cellwise_constant
-
+from ufl.classes import Grad, JacobianInverse, ReferenceGrad, ReferenceValue, Restricted
+from ufl.core.multiindex import indices
+from ufl.corealg.map_dag import map_expr_dag
+from ufl.corealg.multifunction import MultiFunction
+from ufl.tensors import as_tensor
 
 """
 # Some notes:
diff --git a/ufl/algorithms/check_arities.py b/ufl/algorithms/check_arities.py
index 80a917e03..c93727ad8 100644
--- a/ufl/algorithms/check_arities.py
+++ b/ufl/algorithms/check_arities.py
@@ -1,10 +1,10 @@
 """Check arities."""
 from itertools import chain
 
-from ufl.corealg.traversal import traverse_unique_terminals
-from ufl.corealg.multifunction import MultiFunction
-from ufl.corealg.map_dag import map_expr_dag
 from ufl.classes import Argument, Zero
+from ufl.corealg.map_dag import map_expr_dag
+from ufl.corealg.multifunction import MultiFunction
+from ufl.corealg.traversal import traverse_unique_terminals
 
 
 class ArityMismatch(BaseException):
diff --git a/ufl/algorithms/check_restrictions.py b/ufl/algorithms/check_restrictions.py
index d2c4ecd7e..df75e279e 100644
--- a/ufl/algorithms/check_restrictions.py
+++ b/ufl/algorithms/check_restrictions.py
@@ -6,8 +6,8 @@
 #
 # SPDX-License-Identifier:    LGPL-3.0-or-later
 
-from ufl.corealg.multifunction import MultiFunction
 from ufl.corealg.map_dag import map_expr_dag
+from ufl.corealg.multifunction import MultiFunction
 
 
 class RestrictionChecker(MultiFunction):
diff --git a/ufl/algorithms/checks.py b/ufl/algorithms/checks.py
index b5f6aa109..9257b160d 100644
--- a/ufl/algorithms/checks.py
+++ b/ufl/algorithms/checks.py
@@ -9,18 +9,17 @@
 # Modified by Anders Logg, 2008-2009.
 # Modified by Mehdi Nikbakht, 2010.
 
-# UFL classes
-from ufl.core.expr import ufl_err_str
-from ufl.form import Form
+from ufl.algorithms.check_restrictions import check_restrictions
+# UFL algorithms
+from ufl.algorithms.traversal import iter_expressions
 from ufl.argument import Argument
 from ufl.coefficient import Coefficient
 from ufl.constantvalue import is_true_ufl_scalar
-
-# UFL algorithms
-from ufl.algorithms.traversal import iter_expressions
+# UFL classes
+from ufl.core.expr import ufl_err_str
 from ufl.corealg.traversal import traverse_unique_terminals
-from ufl.algorithms.check_restrictions import check_restrictions
 from ufl.domain import extract_unique_domain
+from ufl.form import Form
 
 
 def validate_form(form):  # TODO: Can we make this return a list of errors instead of raising exception?
diff --git a/ufl/algorithms/comparison_checker.py b/ufl/algorithms/comparison_checker.py
index 32a9098fc..e61aeecf3 100644
--- a/ufl/algorithms/comparison_checker.py
+++ b/ufl/algorithms/comparison_checker.py
@@ -1,10 +1,10 @@
 """Algorithm to check for 'comparison' nodes in a form when the user is in 'complex mode'."""
 
-from ufl.corealg.multifunction import MultiFunction
-from ufl.algorithms.map_integrands import map_integrand_dags
 from ufl.algebra import Real
-from ufl.constantvalue import RealValue, Zero
+from ufl.algorithms.map_integrands import map_integrand_dags
 from ufl.argument import Argument
+from ufl.constantvalue import RealValue, Zero
+from ufl.corealg.multifunction import MultiFunction
 from ufl.geometry import GeometricQuantity
 
 
diff --git a/ufl/algorithms/compute_form_data.py b/ufl/algorithms/compute_form_data.py
index 5a393987a..f8c9df4ac 100644
--- a/ufl/algorithms/compute_form_data.py
+++ b/ufl/algorithms/compute_form_data.py
@@ -11,30 +11,26 @@
 
 from itertools import chain
 
-from ufl.utils.sequences import max_degree
-
-from ufl.classes import GeometricFacetQuantity, Coefficient, Form, FunctionSpace
-from ufl.corealg.traversal import traverse_unique_terminals
 from ufl.algorithms.analysis import extract_coefficients, extract_sub_elements, unique_tuple
-from ufl.algorithms.formdata import FormData
-from ufl.algorithms.formtransformations import compute_form_arities
-from ufl.algorithms.check_arities import check_form_arity
-
+from ufl.algorithms.apply_algebra_lowering import apply_algebra_lowering
+from ufl.algorithms.apply_derivatives import apply_coordinate_derivatives, apply_derivatives
 # These are the main symbolic processing steps:
 from ufl.algorithms.apply_function_pullbacks import apply_function_pullbacks
-from ufl.algorithms.apply_algebra_lowering import apply_algebra_lowering
-from ufl.algorithms.apply_derivatives import apply_derivatives, apply_coordinate_derivatives
-from ufl.algorithms.apply_integral_scaling import apply_integral_scaling
 from ufl.algorithms.apply_geometry_lowering import apply_geometry_lowering
-from ufl.algorithms.apply_restrictions import apply_restrictions, apply_default_restrictions
-from ufl.algorithms.estimate_degrees import estimate_total_polynomial_degree
-from ufl.algorithms.remove_complex_nodes import remove_complex_nodes
+from ufl.algorithms.apply_integral_scaling import apply_integral_scaling
+from ufl.algorithms.apply_restrictions import apply_default_restrictions, apply_restrictions
+from ufl.algorithms.check_arities import check_form_arity
 from ufl.algorithms.comparison_checker import do_comparison_check
-
 # See TODOs at the call sites of these below:
-from ufl.algorithms.domain_analysis import build_integral_data
-from ufl.algorithms.domain_analysis import reconstruct_form_from_integral_data
-from ufl.algorithms.domain_analysis import group_form_integrals
+from ufl.algorithms.domain_analysis import (build_integral_data, group_form_integrals,
+                                            reconstruct_form_from_integral_data)
+from ufl.algorithms.estimate_degrees import estimate_total_polynomial_degree
+from ufl.algorithms.formdata import FormData
+from ufl.algorithms.formtransformations import compute_form_arities
+from ufl.algorithms.remove_complex_nodes import remove_complex_nodes
+from ufl.classes import Coefficient, Form, FunctionSpace, GeometricFacetQuantity
+from ufl.corealg.traversal import traverse_unique_terminals
+from ufl.utils.sequences import max_degree
 
 
 def _auto_select_degree(elements):
@@ -46,7 +42,7 @@ def _auto_select_degree(elements):
     """
     # Use max degree of all elements, at least 1 (to work with
     # Lagrange elements)
-    return max_degree({e.degree() for e in elements} - {None} | {1})
+    return max_degree({e.embedded_superdegree for e in elements} - {None} | {1})
 
 
 def _compute_element_mapping(form):
@@ -74,7 +70,7 @@ def _compute_element_mapping(form):
         reconstruct = False
 
         # Set cell
-        cell = element.cell()
+        cell = element.cell
         if cell is None:
             domains = form.ufl_domains()
             if not all(domains[0].ufl_cell() == d.ufl_cell()
@@ -84,7 +80,7 @@ def _compute_element_mapping(form):
             reconstruct = True
 
         # Set degree
-        degree = element.degree()
+        degree = element.embedded_superdegree
         if degree is None:
             degree = common_degree
             reconstruct = True
@@ -138,7 +134,7 @@ def _check_elements(form_data):
     """Check elements."""
     for element in chain(form_data.unique_elements,
                          form_data.unique_sub_elements):
-        if element.cell() is None:
+        if element.cell is None:
             raise ValueError(f"Found element with undefined cell: {element}")
 
 
diff --git a/ufl/algorithms/coordinate_derivative_helpers.py b/ufl/algorithms/coordinate_derivative_helpers.py
index d94a8eed1..77b1c19bf 100644
--- a/ufl/algorithms/coordinate_derivative_helpers.py
+++ b/ufl/algorithms/coordinate_derivative_helpers.py
@@ -9,10 +9,10 @@
 #
 # SPDX-License-Identifier:    LGPL-3.0-or-later
 
-from ufl.differentiation import CoordinateDerivative
-from ufl.corealg.multifunction import MultiFunction
-from ufl.corealg.map_dag import map_expr_dags
 from ufl.classes import Integral
+from ufl.corealg.map_dag import map_expr_dags
+from ufl.corealg.multifunction import MultiFunction
+from ufl.differentiation import CoordinateDerivative
 
 
 class CoordinateDerivativeIsOutermostChecker(MultiFunction):
diff --git a/ufl/algorithms/domain_analysis.py b/ufl/algorithms/domain_analysis.py
index 86fc603d8..3a11b123a 100644
--- a/ufl/algorithms/domain_analysis.py
+++ b/ufl/algorithms/domain_analysis.py
@@ -6,18 +6,17 @@
 #
 # SPDX-License-Identifier:    LGPL-3.0-or-later
 
+import numbers
+import typing
 from collections import defaultdict
 
 import ufl
-from ufl.integral import Integral
+from ufl.algorithms.coordinate_derivative_helpers import attach_coordinate_derivatives, strip_coordinate_derivatives
 from ufl.form import Form
+from ufl.integral import Integral
 from ufl.protocols import id_or_none
 from ufl.sorting import cmp_expr, sorted_expr
 from ufl.utils.sorting import canonicalize_metadata, sorted_by_key
-from ufl.algorithms.coordinate_derivative_helpers import (
-    attach_coordinate_derivatives, strip_coordinate_derivatives)
-import numbers
-import typing
 
 
 class IntegralData(object):
diff --git a/ufl/algorithms/estimate_degrees.py b/ufl/algorithms/estimate_degrees.py
index a5889fe2c..ccaf64c7e 100644
--- a/ufl/algorithms/estimate_degrees.py
+++ b/ufl/algorithms/estimate_degrees.py
@@ -11,10 +11,10 @@
 
 import warnings
 
-from ufl.corealg.multifunction import MultiFunction
 from ufl.checks import is_cellwise_constant
 from ufl.constantvalue import IntValue
 from ufl.corealg.map_dag import map_expr_dags
+from ufl.corealg.multifunction import MultiFunction
 from ufl.domain import extract_unique_domain
 from ufl.form import Form
 from ufl.integral import Integral
@@ -52,14 +52,14 @@ def geometric_quantity(self, v):
             return 0
         else:
             # As a heuristic, just returning domain degree to bump up degree somewhat
-            return extract_unique_domain(v).ufl_coordinate_element().degree()
+            return extract_unique_domain(v).ufl_coordinate_element().embedded_superdegree
 
     def spatial_coordinate(self, v):
         """Apply to spatial_coordinate.
 
         A coordinate provides additional degrees depending on coordinate field of domain.
         """
-        return extract_unique_domain(v).ufl_coordinate_element().degree()
+        return extract_unique_domain(v).ufl_coordinate_element().embedded_superdegree
 
     def cell_coordinate(self, v):
         """Apply to cell_coordinate.
@@ -74,7 +74,7 @@ def argument(self, v):
         A form argument provides a degree depending on the element,
         or the default degree if the element has no degree.
         """
-        return v.ufl_element().degree()  # FIXME: Use component to improve accuracy for mixed elements
+        return v.ufl_element().embedded_superdegree  # FIXME: Use component to improve accuracy for mixed elements
 
     def coefficient(self, v):
         """Apply to coefficient.
@@ -84,7 +84,7 @@ def coefficient(self, v):
         """
         e = v.ufl_element()
         e = self.element_replace_map.get(e, e)
-        d = e.degree()  # FIXME: Use component to improve accuracy for mixed elements
+        d = e.embedded_superdegree  # FIXME: Use component to improve accuracy for mixed elements
         if d is None:
             d = self.default_degree
         return d
diff --git a/ufl/algorithms/expand_compounds.py b/ufl/algorithms/expand_compounds.py
index 04290729b..97eecc499 100644
--- a/ufl/algorithms/expand_compounds.py
+++ b/ufl/algorithms/expand_compounds.py
@@ -9,6 +9,7 @@
 # Modified by Anders Logg, 2009-2010
 
 import warnings
+
 from ufl.algorithms.apply_algebra_lowering import apply_algebra_lowering
 
 
diff --git a/ufl/algorithms/expand_indices.py b/ufl/algorithms/expand_indices.py
index b36923ac5..1523bd4c4 100644
--- a/ufl/algorithms/expand_indices.py
+++ b/ufl/algorithms/expand_indices.py
@@ -11,12 +11,12 @@
 #
 # Modified by Anders Logg, 2009.
 
-from ufl.utils.stacks import Stack, StackDict
+from ufl.algorithms.transformer import ReuseTransformer, apply_transformer
 from ufl.classes import Terminal
 from ufl.constantvalue import Zero
-from ufl.core.multiindex import Index, FixedIndex, MultiIndex
+from ufl.core.multiindex import FixedIndex, Index, MultiIndex
 from ufl.differentiation import Grad
-from ufl.algorithms.transformer import ReuseTransformer, apply_transformer
+from ufl.utils.stacks import Stack, StackDict
 
 
 class IndexExpander(ReuseTransformer):
@@ -58,10 +58,10 @@ def form_argument(self, x):
                 raise ValueError("Component size mismatch.")
 
             # Map it through an eventual symmetry mapping
-            s = e.symmetry()
-            c = s.get(c, c)
-            if r != len(c):
-                raise ValueError("Component size mismatch after symmetry mapping.")
+            if len(e.components) > 1:
+                c = min(i for i, j in e.components.items() if j == e.components[c])
+                if r != len(c):
+                    raise ValueError("Component size mismatch after symmetry mapping.")
 
             return x[c]
 
diff --git a/ufl/algorithms/formdata.py b/ufl/algorithms/formdata.py
index c5162c140..6f1048aec 100644
--- a/ufl/algorithms/formdata.py
+++ b/ufl/algorithms/formdata.py
@@ -8,7 +8,7 @@
 #
 # Modified by Anders Logg, 2008.
 
-from ufl.utils.formatting import lstr, tstr, estr
+from ufl.utils.formatting import estr, lstr, tstr
 
 
 class FormData(object):
diff --git a/ufl/algorithms/formfiles.py b/ufl/algorithms/formfiles.py
index 0bb4f38a5..7927f3a11 100644
--- a/ufl/algorithms/formfiles.py
+++ b/ufl/algorithms/formfiles.py
@@ -12,13 +12,14 @@
 import io
 import os
 import re
-from ufl.utils.sorting import sorted_by_key
-from ufl.form import Form
-from ufl.finiteelement import FiniteElementBase
-from ufl.core.expr import Expr
-from ufl.constant import Constant
+
 from ufl.argument import Argument
 from ufl.coefficient import Coefficient
+from ufl.constant import Constant
+from ufl.core.expr import Expr
+from ufl.finiteelement import AbstractFiniteElement
+from ufl.form import Form
+from ufl.utils.sorting import sorted_by_key
 
 
 class FileData(object):
@@ -93,7 +94,7 @@ def interpret_ufl_namespace(namespace):
     # Object to hold all returned data
     ufd = FileData()
 
-    # Extract object names for Form, Coefficient and FiniteElementBase objects
+    # Extract object names for Form, Coefficient and AbstractFiniteElement objects
     # The use of id(obj) as key in object_names is necessary
     # because we need to distinguish between instances,
     # and not just between objects with different values.
@@ -106,7 +107,7 @@ def interpret_ufl_namespace(namespace):
             # FIXME: Remove after FFC is updated to use reserved_objects:
             ufd.object_names[name] = value
             ufd.object_by_name[name] = value
-        elif isinstance(value, (FiniteElementBase, Coefficient, Constant, Argument, Form, Expr)):
+        elif isinstance(value, (AbstractFiniteElement, Coefficient, Constant, Argument, Form, Expr)):
             # Store instance <-> name mappings for important objects
             # without a reserved name
             ufd.object_names[id(value)] = name
@@ -149,8 +150,8 @@ def get_form(name):
     # Validate types
     if not isinstance(ufd.elements, (list, tuple)):
         raise ValueError(f"Expecting 'elements' to be a list or tuple, not '{type(ufd.elements)}''.")
-    if not all(isinstance(e, FiniteElementBase) for e in ufd.elements):
-        raise ValueError("Expecting 'elements' to be a list of FiniteElementBase instances.")
+    if not all(isinstance(e, AbstractFiniteElement) for e in ufd.elements):
+        raise ValueError("Expecting 'elements' to be a list of AbstractFiniteElement instances.")
 
     # Get list of exported coefficients
     functions = []
diff --git a/ufl/algorithms/formsplitter.py b/ufl/algorithms/formsplitter.py
index 755b479df..ef9b2bbef 100644
--- a/ufl/algorithms/formsplitter.py
+++ b/ufl/algorithms/formsplitter.py
@@ -8,12 +8,12 @@
 #
 # Modified by Cecile Daversin-Catty, 2018
 
-from ufl.corealg.multifunction import MultiFunction
 from ufl.algorithms.map_integrands import map_integrand_dags
-from ufl.constantvalue import Zero
-from ufl.tensors import as_vector
 from ufl.argument import Argument
+from ufl.constantvalue import Zero
+from ufl.corealg.multifunction import MultiFunction
 from ufl.functionspace import FunctionSpace
+from ufl.tensors import as_vector
 
 
 class FormSplitter(MultiFunction):
diff --git a/ufl/algorithms/formtransformations.py b/ufl/algorithms/formtransformations.py
index 6a473c71a..58d168c14 100644
--- a/ufl/algorithms/formtransformations.py
+++ b/ufl/algorithms/formtransformations.py
@@ -13,17 +13,16 @@
 import warnings
 from logging import debug
 
-# All classes:
-from ufl.core.expr import ufl_err_str
-from ufl.argument import Argument
-from ufl.coefficient import Coefficient
-from ufl.constantvalue import Zero
 from ufl.algebra import Conj
-
 # Other algorithms:
 from ufl.algorithms.map_integrands import map_integrands
-from ufl.algorithms.transformer import Transformer
 from ufl.algorithms.replace import replace
+from ufl.algorithms.transformer import Transformer
+from ufl.argument import Argument
+from ufl.coefficient import Coefficient
+from ufl.constantvalue import Zero
+# All classes:
+from ufl.core.expr import ufl_err_str
 
 
 # FIXME: Don't use this below, it makes partextracter more expensive than necessary
diff --git a/ufl/algorithms/map_integrands.py b/ufl/algorithms/map_integrands.py
index 250360f83..55266dfb9 100644
--- a/ufl/algorithms/map_integrands.py
+++ b/ufl/algorithms/map_integrands.py
@@ -10,13 +10,13 @@
 # as part of a careful refactoring process, and this file depends on ufl.form
 # which drags in a lot of stuff.
 
-from ufl.core.expr import Expr
-from ufl.corealg.map_dag import map_expr_dag
-from ufl.integral import Integral
-from ufl.form import Form, BaseForm, FormSum, ZeroBaseForm
 from ufl.action import Action
 from ufl.adjoint import Adjoint
 from ufl.constantvalue import Zero
+from ufl.core.expr import Expr
+from ufl.corealg.map_dag import map_expr_dag
+from ufl.form import BaseForm, Form, FormSum, ZeroBaseForm
+from ufl.integral import Integral
 
 
 def map_integrands(function, form, only_integral_type=None):
diff --git a/ufl/algorithms/remove_complex_nodes.py b/ufl/algorithms/remove_complex_nodes.py
index 12a5b34e1..7d97a5731 100644
--- a/ufl/algorithms/remove_complex_nodes.py
+++ b/ufl/algorithms/remove_complex_nodes.py
@@ -1,8 +1,8 @@
 """Algorithm for removing conj, real, and imag nodes from a form for when the user is in 'real mode'."""
 
-from ufl.corealg.multifunction import MultiFunction
-from ufl.constantvalue import ComplexValue
 from ufl.algorithms.map_integrands import map_integrand_dags
+from ufl.constantvalue import ComplexValue
+from ufl.corealg.multifunction import MultiFunction
 
 
 class ComplexNodeRemoval(MultiFunction):
diff --git a/ufl/algorithms/renumbering.py b/ufl/algorithms/renumbering.py
index 2f3d94a21..303457dd6 100644
--- a/ufl/algorithms/renumbering.py
+++ b/ufl/algorithms/renumbering.py
@@ -5,11 +5,11 @@
 #
 # SPDX-License-Identifier:    LGPL-3.0-or-later
 
-from ufl.core.expr import Expr
-from ufl.core.multiindex import Index, FixedIndex, MultiIndex
-from ufl.variable import Label, Variable
 from ufl.algorithms.transformer import ReuseTransformer, apply_transformer
 from ufl.classes import Zero
+from ufl.core.expr import Expr
+from ufl.core.multiindex import FixedIndex, Index, MultiIndex
+from ufl.variable import Label, Variable
 
 
 class VariableRenumberingTransformer(ReuseTransformer):
diff --git a/ufl/algorithms/replace.py b/ufl/algorithms/replace.py
index fb62095cd..e96ecc577 100644
--- a/ufl/algorithms/replace.py
+++ b/ufl/algorithms/replace.py
@@ -8,11 +8,13 @@
 #
 # Modified by Anders Logg, 2009-2010
 
-from ufl.classes import CoefficientDerivative, Interpolate, ExternalOperator, Form
+from ufl.algorithms.analysis import has_exact_type
+from ufl.algorithms.map_integrands import map_integrand_dags
+from ufl.classes import CoefficientDerivative, Form
 from ufl.constantvalue import as_ufl
+from ufl.core.external_operator import ExternalOperator
+from ufl.core.interpolate import Interpolate
 from ufl.corealg.multifunction import MultiFunction
-from ufl.algorithms.map_integrands import map_integrand_dags
-from ufl.algorithms.analysis import has_exact_type
 
 
 class Replacer(MultiFunction):
diff --git a/ufl/algorithms/replace_derivative_nodes.py b/ufl/algorithms/replace_derivative_nodes.py
index d779a790f..9c822fafb 100644
--- a/ufl/algorithms/replace_derivative_nodes.py
+++ b/ufl/algorithms/replace_derivative_nodes.py
@@ -1,11 +1,11 @@
 """Algorithm for replacing derivative nodes in a BaseForm or Expr."""
 
 import ufl
-from ufl.corealg.multifunction import MultiFunction
-from ufl.algorithms.map_integrands import map_integrand_dags
 from ufl.algorithms.analysis import extract_arguments
-from ufl.tensors import ListTensor
+from ufl.algorithms.map_integrands import map_integrand_dags
 from ufl.constantvalue import as_ufl
+from ufl.corealg.multifunction import MultiFunction
+from ufl.tensors import ListTensor
 
 
 class DerivativeNodeReplacer(MultiFunction):
diff --git a/ufl/algorithms/signature.py b/ufl/algorithms/signature.py
index da3c516dc..7f9dca8b8 100644
--- a/ufl/algorithms/signature.py
+++ b/ufl/algorithms/signature.py
@@ -6,13 +6,11 @@
 # SPDX-License-Identifier:    LGPL-3.0-or-later
 
 import hashlib
-from ufl.classes import (Label,
-                         Index, MultiIndex,
-                         Coefficient, Argument,
-                         GeometricQuantity, ConstantValue, Constant,
-                         ExprList, ExprMapping)
-from ufl.corealg.traversal import traverse_unique_terminals, unique_post_traversal
+
 from ufl.algorithms.domain_analysis import canonicalize_metadata
+from ufl.classes import (Argument, Coefficient, Constant, ConstantValue, ExprList, ExprMapping, GeometricQuantity,
+                         Index, Label, MultiIndex)
+from ufl.corealg.traversal import traverse_unique_terminals, unique_post_traversal
 
 
 def compute_multiindex_hashdata(expr, index_numbering):
diff --git a/ufl/algorithms/strip_terminal_data.py b/ufl/algorithms/strip_terminal_data.py
index 04e48336d..4909febe4 100644
--- a/ufl/algorithms/strip_terminal_data.py
+++ b/ufl/algorithms/strip_terminal_data.py
@@ -3,11 +3,9 @@
 In the stripped version, any data-carrying objects have been extracted to a mapping.
 """
 
-from ufl.classes import Form, Integral
-from ufl.classes import Argument, Coefficient, Constant
-from ufl.classes import FunctionSpace, TensorProductFunctionSpace, MixedFunctionSpace
-from ufl.classes import Mesh, MeshView, TensorProductMesh
 from ufl.algorithms.replace import replace
+from ufl.classes import (Argument, Coefficient, Constant, Form, FunctionSpace, Integral, Mesh, MeshView,
+                         MixedFunctionSpace, TensorProductFunctionSpace)
 from ufl.corealg.map_dag import map_expr_dag
 from ufl.corealg.multifunction import MultiFunction
 
@@ -123,8 +121,5 @@ def strip_domain(domain):
     elif isinstance(domain, MeshView):
         return MeshView(strip_domain(domain.ufl_mesh()),
                         domain.topological_dimension(), domain.ufl_id())
-    elif isinstance(domain, TensorProductMesh):
-        meshes = [strip_domain(mesh) for mesh in domain.ufl_meshes()]
-        return TensorProductMesh(meshes, domain.ufl_id())
     else:
         raise NotImplementedError(f"{type(domain)} cannot be stripped")
diff --git a/ufl/algorithms/traversal.py b/ufl/algorithms/traversal.py
index 7a5c39918..78a0f1b2f 100644
--- a/ufl/algorithms/traversal.py
+++ b/ufl/algorithms/traversal.py
@@ -8,14 +8,12 @@
 #
 # Modified by Anders Logg, 2008
 
-from ufl.core.expr import Expr
-from ufl.integral import Integral
 from ufl.action import Action
 from ufl.adjoint import Adjoint
-from ufl.form import Form, FormSum, BaseForm
-
+from ufl.core.expr import Expr
+from ufl.form import BaseForm, Form, FormSum
+from ufl.integral import Integral
 
-# --- Traversal utilities ---
 
 def iter_expressions(a):
     """Utility function to handle Form, Integral and any Expr the same way when inspecting expressions.
diff --git a/ufl/argument.py b/ufl/argument.py
index 876354301..81b94b9ec 100644
--- a/ufl/argument.py
+++ b/ufl/argument.py
@@ -14,17 +14,14 @@
 # Modified by Cecile Daversin-Catty, 2018.
 # Modified by Ignacia Fierro-Piccardo 2023.
 
-import warnings
 import numbers
 
-from ufl.core.ufl_type import ufl_type
 from ufl.core.terminal import FormArgument
-from ufl.split_functions import split
-from ufl.finiteelement import FiniteElementBase
-from ufl.domain import default_domain
+from ufl.core.ufl_type import ufl_type
+from ufl.duals import is_dual, is_primal
 from ufl.form import BaseForm
-from ufl.functionspace import AbstractFunctionSpace, FunctionSpace, MixedFunctionSpace
-from ufl.duals import is_primal, is_dual
+from ufl.functionspace import AbstractFunctionSpace, MixedFunctionSpace
+from ufl.split_functions import split
 
 # Export list for ufl.classes (TODO: not actually classes: drop? these are in ufl.*)
 __all_classes__ = ["TestFunction", "TrialFunction", "TestFunctions", "TrialFunctions"]
@@ -44,19 +41,11 @@ def __getnewargs__(self):
 
     def __init__(self, function_space, number, part=None):
         """initialise."""
-        if isinstance(function_space, FiniteElementBase):
-            # For legacy support for UFL files using cells, we map the cell to
-            # the default Mesh
-            element = function_space
-            domain = default_domain(element.cell())
-            function_space = FunctionSpace(domain, element)
-            warnings.warn("The use of FiniteElement as an input to Argument will be deprecated by December 2023. "
-                          "Please, use FunctionSpace instead", FutureWarning)
-        elif not isinstance(function_space, AbstractFunctionSpace):
-            raise ValueError("Expecting a FunctionSpace or FiniteElement.")
+        if not isinstance(function_space, AbstractFunctionSpace):
+            raise ValueError("Expecting a FunctionSpace.")
 
         self._ufl_function_space = function_space
-        self._ufl_shape = function_space.ufl_element().value_shape()
+        self._ufl_shape = function_space.ufl_element().value_shape
 
         if not isinstance(number, numbers.Integral):
             raise ValueError(f"Expecting an int for number, not {number}")
diff --git a/ufl/averaging.py b/ufl/averaging.py
index dc6801cc6..899cc3ca8 100644
--- a/ufl/averaging.py
+++ b/ufl/averaging.py
@@ -6,9 +6,9 @@
 #
 # SPDX-License-Identifier:    LGPL-3.0-or-later
 
+from ufl.constantvalue import ConstantValue
 from ufl.core.operator import Operator
 from ufl.core.ufl_type import ufl_type
-from ufl.constantvalue import ConstantValue
 
 
 @ufl_type(inherit_shape_from_operand=0,
diff --git a/ufl/cell.py b/ufl/cell.py
index 85a556474..e7df446fe 100644
--- a/ufl/cell.py
+++ b/ufl/cell.py
@@ -7,14 +7,14 @@
 # SPDX-License-Identifier:    LGPL-3.0-or-later
 
 from __future__ import annotations
+
 import functools
 import numbers
 import typing
 import weakref
-
-from ufl.core.ufl_type import UFLObject
 from abc import abstractmethod
 
+from ufl.core.ufl_type import UFLObject
 
 __all_classes__ = ["AbstractCell", "Cell", "TensorProductCell"]
 
diff --git a/ufl/checks.py b/ufl/checks.py
index cd47a272c..5b4dc8ce1 100644
--- a/ufl/checks.py
+++ b/ufl/checks.py
@@ -9,7 +9,10 @@
 # Modified by Anders Logg, 2008-2009
 
 from ufl.core.expr import Expr
+from ufl.core.terminal import FormArgument
 from ufl.corealg.traversal import traverse_unique_terminals
+from ufl.geometry import GeometricQuantity
+from ufl.sobolevspace import H1
 
 
 def is_python_scalar(expression):
@@ -30,32 +33,23 @@ def is_true_ufl_scalar(expression):
 def is_cellwise_constant(expr):
     """Return whether expression is constant over a single cell."""
     # TODO: Implement more accurately considering e.g. derivatives?
-    return all(t.is_cellwise_constant() for t in traverse_unique_terminals(expr))
+    return all(e.is_cellwise_constant() for e in traverse_unique_terminals(expr))
 
 
-def is_globally_constant(expr):
-    """Check if an expression is globally constant.
+def is_scalar_constant_expression(expr):
+    """Check if an expression is a globally constant scalar expression."""
+    if is_python_scalar(expr):
+        return True
+    if expr.ufl_shape:
+        return False
 
-    This includes spatially independent constant coefficients that
-    are not known before assembly time.
-    """
     # TODO: This does not consider gradients of coefficients, so false
     # negatives are possible.
-    # from ufl.argument import Argument
-    # from ufl.coefficient import Coefficient
-    from ufl.core.terminal import FormArgument
-    from ufl.geometry import GeometricQuantity
     for e in traverse_unique_terminals(expr):
         # Return False if any single terminal is not constant
-        if e._ufl_is_literal_:
-            # Accept literals first, they are the most common
-            # terminals
-            continue
-        elif isinstance(e, FormArgument):
-            # Accept only Real valued Arguments and Coefficients
-            if e.ufl_element()._is_globally_constant():
-                continue
-            else:
+        if isinstance(e, FormArgument):
+            # Accept only globally constant Arguments and Coefficients
+            if e.ufl_element().embedded_superdegree > 0 or e.ufl_element() not in H1:
                 return False
         elif isinstance(e, GeometricQuantity):
             # Reject all geometric quantities, they all vary over
@@ -64,12 +58,3 @@ def is_globally_constant(expr):
 
     # All terminals passed constant check
     return True
-
-
-def is_scalar_constant_expression(expr):
-    """Check if an expression is a globally constant scalar expression."""
-    if is_python_scalar(expr):
-        return True
-    if expr.ufl_shape:
-        return False
-    return is_globally_constant(expr)
diff --git a/ufl/classes.py b/ufl/classes.py
index 64d12284e..b65c802d4 100644
--- a/ufl/classes.py
+++ b/ufl/classes.py
@@ -19,42 +19,42 @@
 # This will be populated part by part below
 __all__ = []
 
-
 # Import all submodules, triggering execution of the ufl_type class
 # decorator for each Expr class.
 
-# Base classes of Expr type hierarchy
-import ufl.core.expr
-import ufl.core.terminal
-import ufl.core.operator
-
-# Terminal types
-import ufl.constantvalue
+import ufl.algebra
 import ufl.argument
+import ufl.averaging
+import ufl.cell
 import ufl.coefficient
+import ufl.conditional
+import ufl.constantvalue
+import ufl.core.expr
+import ufl.core.multiindex
+import ufl.core.operator
+import ufl.core.terminal
+import ufl.differentiation
+import ufl.domain
+import ufl.equation
+import ufl.exprcontainers
+import ufl.finiteelement
+import ufl.form
+import ufl.functionspace
 import ufl.geometry
-
-# Operator types
-import ufl.averaging
 import ufl.indexed
 import ufl.indexsum
-import ufl.variable
-import ufl.tensors
-import ufl.algebra
-import ufl.tensoralgebra
+import ufl.integral
 import ufl.mathfunctions
-import ufl.differentiation
-import ufl.conditional
-import ufl.restriction
-import ufl.exprcontainers
+import ufl.measure
+import ufl.pullback
 import ufl.referencevalue
-
-# Make sure we import exproperators which attaches special functions
-# to Expr
+import ufl.restriction
+import ufl.sobolevspace
+import ufl.tensoralgebra
+import ufl.tensors
+import ufl.variable
 from ufl import exproperators as __exproperators
 
-# Make sure to import modules with new Expr subclasses here!
-
 # Collect all classes in sets automatically classified by some properties
 all_ufl_classes = set(ufl.core.expr.Expr._ufl_all_classes_)
 abstract_classes = set(c for c in all_ufl_classes if c._ufl_is_abstract_)
@@ -96,32 +96,15 @@ def populate_namespace_with_module_classes(mod, loc):
     return names
 
 
-import ufl.cell  # noqa E401
 __all__ += populate_namespace_with_module_classes(ufl.cell, locals())
-
-import ufl.finiteelement  # noqa E401
 __all__ += populate_namespace_with_module_classes(ufl.finiteelement, locals())
-
-import ufl.domain  # noqa E401
 __all__ += populate_namespace_with_module_classes(ufl.domain, locals())
-
-import ufl.functionspace  # noqa E401
 __all__ += populate_namespace_with_module_classes(ufl.functionspace, locals())
-
-import ufl.core.multiindex  # noqa E401
 __all__ += populate_namespace_with_module_classes(ufl.core.multiindex, locals())
-
-import ufl.argument  # noqa E401
 __all__ += populate_namespace_with_module_classes(ufl.argument, locals())
-
-import ufl.measure  # noqa E401
 __all__ += populate_namespace_with_module_classes(ufl.measure, locals())
-
-import ufl.integral  # noqa E401
 __all__ += populate_namespace_with_module_classes(ufl.integral, locals())
-
-import ufl.form  # noqa E401
 __all__ += populate_namespace_with_module_classes(ufl.form, locals())
-
-import ufl.equation  # noqa E401
 __all__ += populate_namespace_with_module_classes(ufl.equation, locals())
+__all__ += populate_namespace_with_module_classes(ufl.pullback, locals())
+__all__ += populate_namespace_with_module_classes(ufl.sobolevspace, locals())
diff --git a/ufl/coefficient.py b/ufl/coefficient.py
index c5ba8dac7..584d06c3f 100644
--- a/ufl/coefficient.py
+++ b/ufl/coefficient.py
@@ -10,19 +10,15 @@
 # Modified by Massimiliano Leoni, 2016.
 # Modified by Cecile Daversin-Catty, 2018.
 # Modified by Ignacia Fierro-Piccardo 2023.
-import warnings
 
-from ufl.core.ufl_type import ufl_type
-from ufl.core.terminal import FormArgument
 from ufl.argument import Argument
-from ufl.finiteelement import FiniteElementBase
-from ufl.domain import default_domain
-from ufl.functionspace import AbstractFunctionSpace, FunctionSpace, MixedFunctionSpace
+from ufl.core.terminal import FormArgument
+from ufl.core.ufl_type import ufl_type
+from ufl.duals import is_dual, is_primal
 from ufl.form import BaseForm
+from ufl.functionspace import AbstractFunctionSpace, MixedFunctionSpace
 from ufl.split_functions import split
 from ufl.utils.counted import Counted
-from ufl.duals import is_primal, is_dual
-
 
 # --- The Coefficient class represents a coefficient in a form ---
 
@@ -45,19 +41,11 @@ def __init__(self, function_space, count=None):
         """Initalise."""
         Counted.__init__(self, count, Coefficient)
 
-        if isinstance(function_space, FiniteElementBase):
-            # For legacy support for .ufl files using cells, we map
-            # the cell to The Default Mesh
-            element = function_space
-            domain = default_domain(element.cell())
-            function_space = FunctionSpace(domain, element)
-            warnings.warn("The use of FiniteElement as an input to Coefficient will be deprecated by December 2023. "
-                          "Please, use FunctionSpace instead", FutureWarning)
-        elif not isinstance(function_space, AbstractFunctionSpace):
-            raise ValueError("Expecting a FunctionSpace or FiniteElement.")
+        if not isinstance(function_space, AbstractFunctionSpace):
+            raise ValueError("Expecting a FunctionSpace.")
 
         self._ufl_function_space = function_space
-        self._ufl_shape = function_space.ufl_element().value_shape()
+        self._ufl_shape = function_space.ufl_element().value_shape
 
         self._repr = "BaseCoefficient(%s, %s)" % (
             repr(self._ufl_function_space), repr(self._count))
diff --git a/ufl/compound_expressions.py b/ufl/compound_expressions.py
index 246ce765f..6b9c63402 100644
--- a/ufl/compound_expressions.py
+++ b/ufl/compound_expressions.py
@@ -9,10 +9,10 @@
 
 import warnings
 
-from ufl.core.multiindex import indices, Index
-from ufl.tensors import as_tensor, as_matrix, as_vector
-from ufl.operators import sqrt
 from ufl.constantvalue import Zero, zero
+from ufl.core.multiindex import Index, indices
+from ufl.operators import sqrt
+from ufl.tensors import as_matrix, as_tensor, as_vector
 
 # Note: To avoid typing errors, the expressions for cofactor and
 # deviatoric parts below were created with the script
diff --git a/ufl/conditional.py b/ufl/conditional.py
index 422be6ff2..b9f9bda16 100644
--- a/ufl/conditional.py
+++ b/ufl/conditional.py
@@ -7,13 +7,13 @@
 
 import warnings
 
+from ufl.checks import is_true_ufl_scalar
+from ufl.constantvalue import as_ufl
 from ufl.core.expr import ufl_err_str
-from ufl.core.ufl_type import ufl_type
 from ufl.core.operator import Operator
-from ufl.constantvalue import as_ufl
-from ufl.precedence import parstr
+from ufl.core.ufl_type import ufl_type
 from ufl.exprequals import expr_equals
-from ufl.checks import is_true_ufl_scalar
+from ufl.precedence import parstr
 
 # --- Condition classes ---
 
diff --git a/ufl/constant.py b/ufl/constant.py
index 2edb21c8e..74eb81932 100644
--- a/ufl/constant.py
+++ b/ufl/constant.py
@@ -6,8 +6,8 @@
 #
 # SPDX-License-Identifier:    LGPL-3.0-or-later
 
-from ufl.core.ufl_type import ufl_type
 from ufl.core.terminal import Terminal
+from ufl.core.ufl_type import ufl_type
 from ufl.domain import as_domain
 from ufl.utils.counted import Counted
 
diff --git a/ufl/constantvalue.py b/ufl/constantvalue.py
index 64a44276a..82d9ed84e 100644
--- a/ufl/constantvalue.py
+++ b/ufl/constantvalue.py
@@ -12,15 +12,13 @@
 from math import atan2
 
 import ufl
+# --- Helper functions imported here for compatibility---
+from ufl.checks import is_python_scalar, is_true_ufl_scalar, is_ufl_scalar  # noqa: F401
 from ufl.core.expr import Expr
+from ufl.core.multiindex import FixedIndex, Index
 from ufl.core.terminal import Terminal
-from ufl.core.multiindex import Index, FixedIndex
 from ufl.core.ufl_type import ufl_type
 
-# --- Helper functions imported here for compatibility---
-from ufl.checks import is_python_scalar, is_ufl_scalar, is_true_ufl_scalar  # noqa: F401
-
-
 # Precision for float formatting
 precision = None
 
diff --git a/ufl/core/base_form_operator.py b/ufl/core/base_form_operator.py
index e2aa2475d..a136244cb 100644
--- a/ufl/core/base_form_operator.py
+++ b/ufl/core/base_form_operator.py
@@ -15,10 +15,10 @@
 from collections import OrderedDict
 
 from ufl.argument import Argument, Coargument
+from ufl.constantvalue import as_ufl
 from ufl.core.operator import Operator
-from ufl.form import BaseForm
 from ufl.core.ufl_type import ufl_type
-from ufl.constantvalue import as_ufl
+from ufl.form import BaseForm
 from ufl.functionspace import AbstractFunctionSpace
 from ufl.utils.counted import Counted
 
diff --git a/ufl/core/expr.py b/ufl/core/expr.py
index 9f1851643..e6b2ce3d8 100644
--- a/ufl/core/expr.py
+++ b/ufl/core/expr.py
@@ -21,8 +21,6 @@
 from ufl.core.ufl_type import UFLType, update_ufl_type_attributes
 
 
-# --- The base object for all UFL expression tree nodes ---
-
 class Expr(object, metaclass=UFLType):
     """Base class for all UFL expression types.
 
@@ -208,10 +206,6 @@ def __init__(self):
 
         # "__str__",
         # "__repr__",
-
-        # TODO: Add checks for methods/properties of terminals only?
-        # Required for terminals:
-        # "is_cellwise_constant", # TODO: Rename to ufl_is_cellwise_constant?
     )
 
     # --- Global variables for collecting all types ---
diff --git a/ufl/core/interpolate.py b/ufl/core/interpolate.py
index e751c66c9..86198c680 100644
--- a/ufl/core/interpolate.py
+++ b/ufl/core/interpolate.py
@@ -8,14 +8,14 @@
 #
 # Modified by Nacime Bouziani, 2021-2022
 
-from ufl.core.ufl_type import ufl_type
-from ufl.constantvalue import as_ufl
-from ufl.functionspace import AbstractFunctionSpace
-from ufl.argument import Coargument, Argument
+from ufl.argument import Argument, Coargument
 from ufl.coefficient import Cofunction
-from ufl.form import Form
+from ufl.constantvalue import as_ufl
 from ufl.core.base_form_operator import BaseFormOperator
+from ufl.core.ufl_type import ufl_type
 from ufl.duals import is_dual
+from ufl.form import Form
+from ufl.functionspace import AbstractFunctionSpace
 
 
 @ufl_type(num_ops="varying", is_differential=True)
diff --git a/ufl/core/multiindex.py b/ufl/core/multiindex.py
index ca66a619a..81409ce7d 100644
--- a/ufl/core/multiindex.py
+++ b/ufl/core/multiindex.py
@@ -9,9 +9,9 @@
 # Modified by Massimiliano Leoni, 2016.
 
 
-from ufl.utils.counted import Counted
-from ufl.core.ufl_type import ufl_type
 from ufl.core.terminal import Terminal
+from ufl.core.ufl_type import ufl_type
+from ufl.utils.counted import Counted
 
 # Export list for ufl.classes
 __all_classes__ = ["IndexBase", "FixedIndex", "Index"]
diff --git a/ufl/core/operator.py b/ufl/core/operator.py
index 7a8de1cc1..c2e31b87e 100644
--- a/ufl/core/operator.py
+++ b/ufl/core/operator.py
@@ -12,8 +12,6 @@
 from ufl.core.ufl_type import ufl_type
 
 
-# --- Base class for operator objects ---
-
 @ufl_type(is_abstract=True, is_terminal=False)
 class Operator(Expr):
     """Base class for all operators, i.e. non-terminal expression types."""
diff --git a/ufl/core/ufl_type.py b/ufl/core/ufl_type.py
index b83d30094..b3bfa0bd6 100644
--- a/ufl/core/ufl_type.py
+++ b/ufl/core/ufl_type.py
@@ -9,14 +9,15 @@
 # Modified by Matthew Scroggs, 2023
 
 from __future__ import annotations
+
 import typing
 import warnings
-
-from ufl.core.compute_expr_hash import compute_expr_hash
-from ufl.utils.formatting import camel2underscore
 from abc import ABC, abstractmethod
+
 # Avoid circular import
 import ufl.core as core
+from ufl.core.compute_expr_hash import compute_expr_hash
+from ufl.utils.formatting import camel2underscore
 
 
 class UFLObject(ABC):
diff --git a/ufl/corealg/map_dag.py b/ufl/corealg/map_dag.py
index 946a86ff8..f299130c1 100644
--- a/ufl/corealg/map_dag.py
+++ b/ufl/corealg/map_dag.py
@@ -8,8 +8,8 @@
 # Modified by Massimiliano Leoni, 2016
 
 from ufl.core.expr import Expr
-from ufl.corealg.traversal import unique_post_traversal, cutoff_unique_post_traversal
 from ufl.corealg.multifunction import MultiFunction
+from ufl.corealg.traversal import cutoff_unique_post_traversal, unique_post_traversal
 
 
 def map_expr_dag(function, expression,  compress=True, vcache=None, rcache=None):
diff --git a/ufl/differentiation.py b/ufl/differentiation.py
index e3820603a..34c003b85 100644
--- a/ufl/differentiation.py
+++ b/ufl/differentiation.py
@@ -6,13 +6,13 @@
 #
 # SPDX-License-Identifier:    LGPL-3.0-or-later
 
+from ufl.argument import Argument, Coargument
 from ufl.checks import is_cellwise_constant
 from ufl.coefficient import Coefficient
-from ufl.argument import Argument, Coargument
 from ufl.constantvalue import Zero
+from ufl.core.base_form_operator import BaseFormOperator
 from ufl.core.expr import Expr
 from ufl.core.operator import Operator
-from ufl.core.base_form_operator import BaseFormOperator
 from ufl.core.terminal import Terminal
 from ufl.core.ufl_type import ufl_type
 from ufl.domain import extract_unique_domain, find_geometric_dimension
@@ -101,7 +101,7 @@ def __init__(self, base_form, coefficients, arguments,
 
     def _analyze_form_arguments(self):
         """Collect the arguments of the corresponding BaseForm."""
-        from ufl.algorithms.analysis import extract_type, extract_coefficients
+        from ufl.algorithms.analysis import extract_coefficients, extract_type
         base_form, _, arguments, _ = self.ufl_operands
 
         def arg_type(x):
diff --git a/ufl/domain.py b/ufl/domain.py
index 59de4f121..ecdea376a 100644
--- a/ufl/domain.py
+++ b/ufl/domain.py
@@ -9,14 +9,14 @@
 import numbers
 import warnings
 
-from ufl.cell import AbstractCell, TensorProductCell, as_cell
+from ufl.cell import AbstractCell
 from ufl.core.ufl_id import attach_ufl_id
 from ufl.core.ufl_type import attach_operators_from_hash_data
 from ufl.corealg.traversal import traverse_unique_terminals
-from ufl.finiteelement.tensorproductelement import TensorProductElement
+from ufl.sobolevspace import H1
 
 # Export list for ufl.classes
-__all_classes__ = ["AbstractDomain", "Mesh", "MeshView", "TensorProductMesh"]
+__all_classes__ = ["AbstractDomain", "Mesh", "MeshView"]
 
 
 class AbstractDomain(object):
@@ -68,22 +68,15 @@ def __init__(self, coordinate_element, ufl_id=None, cargo=None):
 
         # No longer accepting coordinates provided as a Coefficient
         from ufl.coefficient import Coefficient
-        if isinstance(coordinate_element, Coefficient):
+        if isinstance(coordinate_element, (Coefficient, AbstractCell)):
             raise ValueError("Expecting a coordinate element in the ufl.Mesh construct.")
 
-        # Accept a cell in place of an element for brevity Mesh(triangle)
-        if isinstance(coordinate_element, AbstractCell):
-            from ufl.finiteelement import VectorElement
-            cell = coordinate_element
-            coordinate_element = VectorElement("Lagrange", cell, 1,
-                                               dim=cell.geometric_dimension())
-
         # Store coordinate element
         self._ufl_coordinate_element = coordinate_element
 
         # Derive dimensions from element
-        gdim, = coordinate_element.value_shape()
-        tdim = coordinate_element.cell().topological_dimension()
+        gdim, = coordinate_element.value_shape
+        tdim = coordinate_element.cell.topological_dimension()
         AbstractDomain.__init__(self, tdim, gdim)
 
     def ufl_cargo(self):
@@ -96,11 +89,12 @@ def ufl_coordinate_element(self):
 
     def ufl_cell(self):
         """Get the cell."""
-        return self._ufl_coordinate_element.cell()
+        return self._ufl_coordinate_element.cell
 
     def is_piecewise_linear_simplex_domain(self):
         """Check if the domain is a piecewise linear simplex."""
-        return (self._ufl_coordinate_element.degree() == 1) and self.ufl_cell().is_simplex()
+        ce = self._ufl_coordinate_element
+        return ce.embedded_superdegree <= 1 and ce in H1 and self.ufl_cell().is_simplex()
 
     def __repr__(self):
         """Representation."""
@@ -142,8 +136,8 @@ def __init__(self, mesh, topological_dimension, ufl_id=None):
 
         # Derive dimensions from element
         coordinate_element = mesh.ufl_coordinate_element()
-        gdim, = coordinate_element.value_shape()
-        tdim = coordinate_element.cell().topological_dimension()
+        gdim, = coordinate_element.value_shape
+        tdim = coordinate_element.cell.topological_dimension()
         AbstractDomain.__init__(self, tdim, gdim)
 
     def ufl_mesh(self):
@@ -187,107 +181,6 @@ def _ufl_sort_key_(self):
                 "MeshView", typespecific)
 
 
-@attach_operators_from_hash_data
-@attach_ufl_id
-class TensorProductMesh(AbstractDomain):
-    """Symbolic representation of a mesh."""
-
-    def __init__(self, meshes, ufl_id=None):
-        """Initialise."""
-        self._ufl_id = self._init_ufl_id(ufl_id)
-
-        # TODO: Error checking of meshes
-        self._ufl_meshes = meshes
-
-        # TODO: Is this what we want to do?
-        # Build cell from mesh cells
-        self._ufl_cell = TensorProductCell(*[mesh.ufl_cell() for mesh in meshes])
-
-        # TODO: Is this what we want to do?
-        # Build coordinate element from mesh coordinate elements
-        self._ufl_coordinate_element = TensorProductElement([mesh.ufl_coordinate_element() for mesh in meshes])
-
-        # Derive dimensions from meshes
-        gdim = sum(mesh.geometric_dimension() for mesh in meshes)
-        tdim = sum(mesh.topological_dimension() for mesh in meshes)
-
-        AbstractDomain.__init__(self, tdim, gdim)
-
-    def ufl_coordinate_element(self):
-        """Get the coordinate element."""
-        return self._ufl_coordinate_element
-
-    def ufl_cell(self):
-        """Get the cell."""
-        return self._ufl_cell
-
-    def ufl_meshes(self):
-        """Get the UFL meshes."""
-        return self._ufl_meshes
-
-    def is_piecewise_linear_simplex_domain(self):
-        """Check if the domain is a piecewise linear simplex."""
-        return False  # TODO: Any cases this is True
-
-    def __repr__(self):
-        """Representation."""
-        r = "TensorProductMesh(%s, %s)" % (repr(self._ufl_meshes), repr(self._ufl_id))
-        return r
-
-    def __str__(self):
-        """Format as a string."""
-        return "<TensorProductMesh #%s with meshes %s>" % (
-            self._ufl_id, self._ufl_meshes)
-
-    def _ufl_hash_data_(self):
-        """UFL hash data."""
-        return (self._ufl_id,) + tuple(mesh._ufl_hash_data_() for mesh in self._ufl_meshes)
-
-    def _ufl_signature_data_(self, renumbering):
-        """UFL signature data."""
-        return ("TensorProductMesh",) + tuple(mesh._ufl_signature_data_(renumbering) for mesh in self._ufl_meshes)
-
-    # NB! Dropped __lt__ here, don't want users to write 'mesh1 <
-    # mesh2'.
-    def _ufl_sort_key_(self):
-        """UFL sort key."""
-        typespecific = (self._ufl_id, tuple(mesh._ufl_sort_key_() for mesh in self._ufl_meshes))
-        return (self.geometric_dimension(), self.topological_dimension(),
-                "TensorProductMesh", typespecific)
-
-
-# --- Utility conversion functions
-
-def affine_mesh(cell, ufl_id=None):
-    """Create a Mesh over a given cell type with an affine geometric parameterization."""
-    from ufl.finiteelement import VectorElement
-    cell = as_cell(cell)
-    gdim = cell.geometric_dimension()
-    degree = 1
-    coordinate_element = VectorElement("Lagrange", cell, degree, dim=gdim)
-    return Mesh(coordinate_element, ufl_id=ufl_id)
-
-
-_default_domains = {}
-
-
-def default_domain(cell):
-    """Create a singular default Mesh from a cell, always returning the same Mesh object for the same cell."""
-    global _default_domains
-
-    warnings.warn("default_domain is deprecated.", FutureWarning)
-
-    assert isinstance(cell, AbstractCell)
-    domain = _default_domains.get(cell)
-    if domain is None:
-        # Create one and only one affine Mesh with a negative ufl_id
-        # to avoid id collision
-        ufl_id = -(len(_default_domains) + 1)
-        domain = affine_mesh(cell, ufl_id=ufl_id)
-        _default_domains[cell] = domain
-    return domain
-
-
 def as_domain(domain):
     """Convert any valid object to an AbstractDomain type."""
     if isinstance(domain, AbstractDomain):
@@ -297,15 +190,7 @@ def as_domain(domain):
     try:
         return extract_unique_domain(domain)
     except AttributeError:
-        try:
-            # Legacy UFL files
-            # TODO: Make this conversion in the relevant constructors
-            # closer to the user interface?
-            # TODO: Make this configurable to be an error from the dolfin side?
-            cell = as_cell(domain)
-            return default_domain(cell)
-        except ValueError:
-            return domain.ufl_domain()
+        return domain.ufl_domain()
 
 
 def sort_domains(domains):
@@ -388,7 +273,7 @@ def find_geometric_dimension(expr):
         if hasattr(t, "ufl_element"):
             element = t.ufl_element()
             if element is not None:
-                cell = element.cell()
+                cell = element.cell
                 if cell is not None:
                     gdims.add(cell.geometric_dimension())
 
diff --git a/ufl/exprcontainers.py b/ufl/exprcontainers.py
index 60ae13f6f..8da36299c 100644
--- a/ufl/exprcontainers.py
+++ b/ufl/exprcontainers.py
@@ -5,15 +5,15 @@
 #
 # SPDX-License-Identifier:    LGPL-3.0-or-later
 
+from ufl.argument import Coargument
+from ufl.coefficient import Cofunction
 from ufl.core.expr import Expr
 from ufl.core.operator import Operator
 from ufl.core.ufl_type import ufl_type
-from ufl.coefficient import Cofunction
-from ufl.argument import Coargument
-
 
 # --- Non-tensor types ---
 
+
 @ufl_type(num_ops="varying")
 class ExprList(Operator):
     """List of Expr objects. For internal use, never to be created by end users."""
diff --git a/ufl/exproperators.py b/ufl/exproperators.py
index f0b38ec79..60eb87566 100644
--- a/ufl/exproperators.py
+++ b/ufl/exproperators.py
@@ -14,25 +14,23 @@
 
 import numbers
 
-from ufl.utils.stacks import StackDict
-from ufl.core.expr import Expr
+from ufl.algebra import Abs, Division, Power, Product, Sum
+from ufl.conditional import GE, GT, LE, LT
 from ufl.constantvalue import Zero, as_ufl
-from ufl.algebra import Sum, Product, Division, Power, Abs
-from ufl.tensoralgebra import Transposed, Inner
-from ufl.core.multiindex import MultiIndex, Index, indices
-from ufl.indexed import Indexed
-from ufl.indexsum import IndexSum
-from ufl.tensors import as_tensor, ComponentTensor
-from ufl.restriction import PositiveRestricted, NegativeRestricted
+from ufl.core.expr import Expr
+from ufl.core.multiindex import Index, MultiIndex, indices
 from ufl.differentiation import Grad
-from ufl.index_combination_utils import create_slice_indices, merge_overlapping_indices
-
 from ufl.exprequals import expr_equals
+from ufl.index_combination_utils import create_slice_indices, merge_overlapping_indices
+from ufl.indexed import Indexed
+from ufl.indexsum import IndexSum
+from ufl.restriction import NegativeRestricted, PositiveRestricted
+from ufl.tensoralgebra import Inner, Transposed
+from ufl.tensors import ComponentTensor, as_tensor
+from ufl.utils.stacks import StackDict
 
 # --- Boolean operators ---
 
-from ufl.conditional import LE, GE, LT, GT
-
 
 def _le(left, right):
     """A boolean expresion (left <= right) for use with conditional."""
diff --git a/ufl/finiteelement.py b/ufl/finiteelement.py
new file mode 100644
index 000000000..9a4e43d6e
--- /dev/null
+++ b/ufl/finiteelement.py
@@ -0,0 +1,368 @@
+"""This module defines the UFL finite element classes."""
+# Copyright (C) 2008-2016 Martin Sandve Alnæs
+#
+# This file is part of UFL (https://www.fenicsproject.org)
+#
+# SPDX-License-Identifier:    LGPL-3.0-or-later
+#
+# Modified by Kristian B. Oelgaard
+# Modified by Marie E. Rognes 2010, 2012
+# Modified by Massimiliano Leoni, 2016
+# Modified by Matthew Scroggs, 2023
+
+from __future__ import annotations
+
+import abc as _abc
+import typing as _typing
+
+import numpy as np
+
+from ufl.cell import Cell as _Cell
+from ufl.pullback import AbstractPullback as _AbstractPullback
+from ufl.pullback import IdentityPullback as _IdentityPullback
+from ufl.pullback import MixedPullback as _MixedPullback
+from ufl.pullback import SymmetricPullback as _SymmetricPullback
+from ufl.sobolevspace import SobolevSpace as _SobolevSpace
+from ufl.utils.sequences import product
+
+__all_classes__ = ["AbstractFiniteElement", "FiniteElement", "MixedElement", "SymmetricElement"]
+
+
+class AbstractFiniteElement(_abc.ABC):
+    """Base class for all finite elements.
+
+    To make your element library compatible with UFL, you should make a subclass of AbstractFiniteElement
+    and provide implementions of all the abstract methods and properties. All methods and properties
+    that are not marked as abstract are implemented here and should not need to be overwritten in your
+    subclass.
+
+    An example of how the methods in your subclass could be implemented can be found in Basix; see
+    https://github.com/FEniCS/basix/blob/main/python/basix/ufl.py
+    """
+
+    @_abc.abstractmethod
+    def __repr__(self) -> str:
+        """Format as string for evaluation as Python object."""
+
+    @_abc.abstractmethod
+    def __str__(self) -> str:
+        """Format as string for nice printing."""
+
+    @_abc.abstractmethod
+    def __hash__(self) -> int:
+        """Return a hash."""
+
+    @_abc.abstractmethod
+    def __eq__(self, other: AbstractFiniteElement) -> bool:
+        """Check if this element is equal to another element."""
+
+    @_abc.abstractproperty
+    def sobolev_space(self) -> _SobolevSpace:
+        """Return the underlying Sobolev space."""
+
+    @_abc.abstractproperty
+    def pullback(self) -> _AbstractPullback:
+        """Return the pullback for this element."""
+
+    @_abc.abstractproperty
+    def embedded_superdegree(self) -> _typing.Union[int, None]:
+        """Return the degree of the minimum degree Lagrange space that spans this element.
+
+        This returns the degree of the lowest degree Lagrange space such that the polynomial
+        space of the Lagrange space is a superspace of this element's polynomial space. If this
+        element contains basis functions that are not in any Lagrange space, this function should
+        return None.
+
+        Note that on a simplex cells, the polynomial space of Lagrange space is a complete polynomial
+        space, but on other cells this is not true. For example, on quadrilateral cells, the degree 1
+        Lagrange space includes the degree 2 polynomial xy.
+        """
+
+    @_abc.abstractproperty
+    def embedded_subdegree(self) -> int:
+        """Return the degree of the maximum degree Lagrange space that is spanned by this element.
+
+        This returns the degree of the highest degree Lagrange space such that the polynomial
+        space of the Lagrange space is a subspace of this element's polynomial space. If this
+        element's polynomial space does not include the constant function, this function should
+        return -1.
+
+        Note that on a simplex cells, the polynomial space of Lagrange space is a complete polynomial
+        space, but on other cells this is not true. For example, on quadrilateral cells, the degree 1
+        Lagrange space includes the degree 2 polynomial xy.
+        """
+
+    @_abc.abstractproperty
+    def cell(self) -> _Cell:
+        """Return the cell of the finite element."""
+
+    @_abc.abstractproperty
+    def reference_value_shape(self) -> _typing.Tuple[int, ...]:
+        """Return the shape of the value space on the reference cell."""
+
+    @_abc.abstractproperty
+    def sub_elements(self) -> _typing.List:
+        """Return list of sub-elements.
+
+        This function does not recurse: ie it does not extract the sub-elements
+        of sub-elements.
+        """
+
+    def __ne__(self, other: AbstractFiniteElement) -> bool:
+        """Check if this element is different to another element."""
+        return not self.__eq__(other)
+
+    def is_cellwise_constant(self) -> bool:
+        """Check whether this element is spatially constant over each cell."""
+        return self.embedded_superdegree == 0
+
+    def _ufl_hash_data_(self) -> str:
+        """Return UFL hash data."""
+        return repr(self)
+
+    def _ufl_signature_data_(self) -> str:
+        """Return UFL signature data."""
+        return repr(self)
+
+    @property
+    def components(self) -> _typing.Dict[_typing.Tuple[int, ...], int]:
+        """Get the numbering of the components of the element.
+
+        Returns:
+            A map from the components of the values on a physical cell (eg (0, 1))
+            to flat component numbers on the reference cell (eg 1)
+        """
+        if isinstance(self.pullback, _SymmetricPullback):
+            return self.pullback._symmetry
+
+        if len(self.sub_elements) == 0:
+            return {(): 0}
+
+        components = {}
+        offset = 0
+        c_offset = 0
+        for e in self.sub_elements:
+            for i, j in enumerate(np.ndindex(e.value_shape)):
+                components[(offset + i, )] = c_offset + e.components[j]
+            c_offset += max(e.components.values()) + 1
+            offset += e.value_size
+        return components
+
+    @property
+    def value_shape(self) -> _typing.Tuple[int, ...]:
+        """Return the shape of the value space on the physical domain."""
+        return self.pullback.physical_value_shape(self)
+
+    @property
+    def value_size(self) -> int:
+        """Return the integer product of the value shape."""
+        return product(self.value_shape)
+
+    @property
+    def reference_value_size(self) -> int:
+        """Return the integer product of the reference value shape."""
+        return product(self.reference_value_shape)
+
+    @property
+    def num_sub_elements(self) -> int:
+        """Return number of sub-elements.
+
+        This function does not recurse: ie it does not count the sub-elements of
+        sub-elements.
+        """
+        return len(self.sub_elements)
+
+
+class FiniteElement(AbstractFiniteElement):
+    """A directly defined finite element."""
+    __slots__ = ("_repr", "_str", "_family", "_cell", "_degree",
+                 "_reference_value_shape", "_pullback", "_sobolev_space",
+                 "_sub_elements", "_subdegree")
+
+    def __init__(
+        self, family: str, cell: _Cell, degree: int,
+        reference_value_shape: _typing.Tuple[int, ...], pullback: _AbstractPullback,
+        sobolev_space: _SobolevSpace, sub_elements=[],
+        _repr: _typing.Optional[str] = None, _str: _typing.Optional[str] = None,
+        subdegree: _typing.Optional[int] = None,
+    ):
+        """Initialise a finite element.
+
+        This class should only be used for testing
+
+        Args:
+            family: The family name of the element
+            cell: The cell on which the element is defined
+            degree: The polynomial degree of the element
+            reference_value_shape: The reference value shape of the element
+            pullback: The pullback to use
+            sobolev_space: The Sobolev space containing this element
+            sub_elements: Sub elements of this element
+            _repr: A string representation of this elements
+            _str: A string for printing
+            subdegree: The embedded subdegree of this element
+        """
+        if subdegree is None:
+            self._subdegree = degree
+        else:
+            self._subdegree = subdegree
+        if _repr is None:
+            if len(sub_elements) > 0:
+                self._repr = (
+                    f"ufl.finiteelement.FiniteElement(\"{family}\", {cell}, {degree}, "
+                    f"{reference_value_shape}, {pullback}, {sobolev_space}, {sub_elements!r})")
+            else:
+                self._repr = (
+                    f"ufl.finiteelement.FiniteElement(\"{family}\", {cell}, {degree}, "
+                    f"{reference_value_shape}, {pullback}, {sobolev_space})")
+        else:
+            self._repr = _repr
+        if _str is None:
+            self._str = f"<{family}{degree} on a {cell}>"
+        else:
+            self._str = _str
+        self._family = family
+        self._cell = cell
+        self._degree = degree
+        self._reference_value_shape = reference_value_shape
+        self._pullback = pullback
+        self._sobolev_space = sobolev_space
+        self._sub_elements = sub_elements
+
+    def __repr__(self) -> str:
+        """Format as string for evaluation as Python object."""
+        return self._repr
+
+    def __str__(self) -> str:
+        """Format as string for nice printing."""
+        return self._str
+
+    def __hash__(self) -> int:
+        """Return a hash."""
+        return hash(f"{self!r}")
+
+    def __eq__(self, other) -> bool:
+        """Check if this element is equal to another element."""
+        return type(self) is type(other) and repr(self) == repr(other)
+
+    @property
+    def sobolev_space(self) -> _SobolevSpace:
+        """Return the underlying Sobolev space."""
+        return self._sobolev_space
+
+    @property
+    def pullback(self) -> _AbstractPullback:
+        """Return the pullback for this element."""
+        return self._pullback
+
+    @property
+    def embedded_superdegree(self) -> _typing.Union[int, None]:
+        """Return the degree of the minimum degree Lagrange space that spans this element.
+
+        This returns the degree of the lowest degree Lagrange space such that the polynomial
+        space of the Lagrange space is a superspace of this element's polynomial space. If this
+        element contains basis functions that are not in any Lagrange space, this function should
+        return None.
+
+        Note that on a simplex cells, the polynomial space of Lagrange space is a complete polynomial
+        space, but on other cells this is not true. For example, on quadrilateral cells, the degree 1
+        Lagrange space includes the degree 2 polynomial xy.
+        """
+        return self._degree
+
+    @property
+    def embedded_subdegree(self) -> int:
+        """Return the degree of the maximum degree Lagrange space that is spanned by this element.
+
+        This returns the degree of the highest degree Lagrange space such that the polynomial
+        space of the Lagrange space is a subspace of this element's polynomial space. If this
+        element's polynomial space does not include the constant function, this function should
+        return -1.
+
+        Note that on a simplex cells, the polynomial space of Lagrange space is a complete polynomial
+        space, but on other cells this is not true. For example, on quadrilateral cells, the degree 1
+        Lagrange space includes the degree 2 polynomial xy.
+        """
+        return self._subdegree
+
+    @property
+    def cell(self) -> _Cell:
+        """Return the cell of the finite element."""
+        return self._cell
+
+    @property
+    def reference_value_shape(self) -> _typing.Tuple[int, ...]:
+        """Return the shape of the value space on the reference cell."""
+        return self._reference_value_shape
+
+    @property
+    def sub_elements(self) -> _typing.List:
+        """Return list of sub-elements.
+
+        This function does not recurse: ie it does not extract the sub-elements
+        of sub-elements.
+        """
+        return self._sub_elements
+
+
+class SymmetricElement(FiniteElement):
+    """A symmetric finite element."""
+
+    def __init__(
+        self,
+        symmetry: _typing.Dict[_typing.Tuple[int, ...], int],
+        sub_elements: _typing.List[AbstractFiniteElement]
+    ):
+        """Initialise a symmetric element.
+
+        This class should only be used for testing
+
+        Args:
+            symmetry: Map from physical components to reference components
+            sub_elements: Sub-elements of this element
+        """
+        self._sub_elements = sub_elements
+        pullback = _SymmetricPullback(self, symmetry)
+        reference_value_shape = (sum(e.reference_value_size for e in sub_elements), )
+        degree = max(e.embedded_superdegree for e in sub_elements)
+        cell = sub_elements[0].cell
+        for e in sub_elements:
+            if e.cell != cell:
+                raise ValueError("All sub-elements must be defined on the same cell")
+        sobolev_space = max(e.sobolev_space for e in sub_elements)
+
+        super().__init__(
+            "Symmetric element", cell, degree, reference_value_shape, pullback,
+            sobolev_space, sub_elements=sub_elements,
+            _repr=(f"ufl.finiteelement.SymmetricElement({symmetry!r}, {sub_elements!r})"),
+            _str=f"<symmetric element on a {cell}>")
+
+
+class MixedElement(FiniteElement):
+    """A mixed element."""
+
+    def __init__(self, sub_elements):
+        """Initialise a mixed element.
+
+        This class should only be used for testing
+
+        Args:
+            sub_elements: Sub-elements of this element
+        """
+        sub_elements = [MixedElement(e) if isinstance(e, list) else e for e in sub_elements]
+        cell = sub_elements[0].cell
+        for e in sub_elements:
+            assert e.cell == cell
+        degree = max(e.embedded_superdegree for e in sub_elements)
+        reference_value_shape = (sum(e.reference_value_size for e in sub_elements), )
+        if all(isinstance(e.pullback, _IdentityPullback) for e in sub_elements):
+            pullback = _IdentityPullback()
+        else:
+            pullback = _MixedPullback(self)
+        sobolev_space = max(e.sobolev_space for e in sub_elements)
+
+        super().__init__(
+            "Mixed element", cell, degree, reference_value_shape, pullback, sobolev_space,
+            sub_elements=sub_elements,
+            _repr=f"ufl.finiteelement.MixedElement({sub_elements!r})",
+            _str=f"<MixedElement with {len(sub_elements)} sub-element(s)>")
diff --git a/ufl/finiteelement/__init__.py b/ufl/finiteelement/__init__.py
deleted file mode 100644
index f7a7c94a3..000000000
--- a/ufl/finiteelement/__init__.py
+++ /dev/null
@@ -1,42 +0,0 @@
-# flake8: noqa
-"""This module defines the UFL finite element classes."""
-
-# Copyright (C) 2008-2016 Martin Sandve Alnæs
-#
-# This file is part of UFL (https://www.fenicsproject.org)
-#
-# SPDX-License-Identifier:    LGPL-3.0-or-later
-#
-# Modified by Kristian B. Oelgaard
-# Modified by Marie E. Rognes 2010, 2012
-# Modified by Andrew T. T. McRae 2014
-# Modified by Lawrence Mitchell 2014
-
-from ufl.finiteelement.finiteelementbase import FiniteElementBase
-from ufl.finiteelement.finiteelement import FiniteElement
-from ufl.finiteelement.mixedelement import MixedElement
-from ufl.finiteelement.mixedelement import VectorElement
-from ufl.finiteelement.mixedelement import TensorElement
-from ufl.finiteelement.enrichedelement import EnrichedElement
-from ufl.finiteelement.enrichedelement import NodalEnrichedElement
-from ufl.finiteelement.restrictedelement import RestrictedElement
-from ufl.finiteelement.tensorproductelement import TensorProductElement
-from ufl.finiteelement.hdivcurl import HDivElement, HCurlElement, WithMapping
-from ufl.finiteelement.brokenelement import BrokenElement
-
-# Export list for ufl.classes
-__all_classes__ = [
-    "FiniteElementBase",
-    "FiniteElement",
-    "MixedElement",
-    "VectorElement",
-    "TensorElement",
-    "EnrichedElement",
-    "NodalEnrichedElement",
-    "RestrictedElement",
-    "TensorProductElement",
-    "HDivElement",
-    "HCurlElement",
-    "BrokenElement",
-    "WithMapping"
-    ]
diff --git a/ufl/form.py b/ufl/form.py
index ecbd4b17d..4fb474f1c 100644
--- a/ufl/form.py
+++ b/ufl/form.py
@@ -831,6 +831,7 @@ def _analyze_form_arguments(self):
     def _analyze_domains(self):
         """Analyze which domains can be found in FormSum."""
         from ufl.domain import join_domains
+
         # Collect unique domains
         self._domains = join_domains([component.ufl_domain() for component in self._components])
 
diff --git a/ufl/formatting/ufl2unicode.py b/ufl/formatting/ufl2unicode.py
index 842e78e3f..90a5d7bf7 100644
--- a/ufl/formatting/ufl2unicode.py
+++ b/ufl/formatting/ufl2unicode.py
@@ -3,11 +3,11 @@
 import numbers
 
 import ufl
-from ufl.corealg.multifunction import MultiFunction
+from ufl.algorithms import compute_form_data
+from ufl.core.multiindex import FixedIndex, Index
 from ufl.corealg.map_dag import map_expr_dag
-from ufl.core.multiindex import Index, FixedIndex
+from ufl.corealg.multifunction import MultiFunction
 from ufl.form import Form
-from ufl.algorithms import compute_form_data
 
 try:
     import colorama
diff --git a/ufl/formoperators.py b/ufl/formoperators.py
index b71acd9d9..d5cc31117 100644
--- a/ufl/formoperators.py
+++ b/ufl/formoperators.py
@@ -9,37 +9,29 @@
 # Modified by Massimiliano Leoni, 2016
 # Modified by Cecile Daversin-Catty, 2018
 
-from ufl.form import Form, FormSum, BaseForm, ZeroBaseForm, as_form
-from ufl.core.expr import Expr, ufl_err_str
-from ufl.core.base_form_operator import BaseFormOperator
-from ufl.split_functions import split
-from ufl.exprcontainers import ExprList, ExprMapping
-from ufl.variable import Variable
-from ufl.finiteelement import MixedElement
+from ufl.action import Action
+from ufl.adjoint import Adjoint
+from ufl.algorithms import replace  # noqa: F401
+from ufl.algorithms import (compute_energy_norm, compute_form_action, compute_form_adjoint, compute_form_functional,
+                            compute_form_lhs, compute_form_rhs, expand_derivatives, extract_arguments, formsplitter)
 from ufl.argument import Argument
 from ufl.coefficient import Coefficient, Cofunction
-from ufl.adjoint import Adjoint
-from ufl.action import Action
-from ufl.differentiation import (CoefficientDerivative, CoordinateDerivative,
-                                 BaseFormDerivative, BaseFormCoordinateDerivative,
-                                 BaseFormOperatorDerivative, BaseFormOperatorCoordinateDerivative)
-from ufl.constantvalue import is_true_ufl_scalar, as_ufl
-from ufl.indexed import Indexed
+from ufl.constantvalue import as_ufl, is_true_ufl_scalar
+from ufl.core.base_form_operator import BaseFormOperator
+from ufl.core.expr import Expr, ufl_err_str
 from ufl.core.multiindex import FixedIndex, MultiIndex
-from ufl.tensors import as_tensor, ListTensor
-from ufl.sorting import sorted_expr
+from ufl.differentiation import (BaseFormCoordinateDerivative, BaseFormDerivative, BaseFormOperatorCoordinateDerivative,
+                                 BaseFormOperatorDerivative, CoefficientDerivative, CoordinateDerivative)
+from ufl.exprcontainers import ExprList, ExprMapping
+from ufl.finiteelement import MixedElement
+from ufl.form import BaseForm, Form, FormSum, ZeroBaseForm, as_form
 from ufl.functionspace import FunctionSpace
 from ufl.geometry import SpatialCoordinate
-
-# An exception to the rule that ufl.* does not depend on ufl.algorithms.* ...
-from ufl.algorithms import compute_form_adjoint, compute_form_action
-from ufl.algorithms import compute_energy_norm
-from ufl.algorithms import compute_form_lhs, compute_form_rhs, compute_form_functional
-from ufl.algorithms import expand_derivatives, extract_arguments
-from ufl.algorithms import formsplitter
-
-# Part of the external interface
-from ufl.algorithms import replace  # noqa
+from ufl.indexed import Indexed
+from ufl.sorting import sorted_expr
+from ufl.split_functions import split
+from ufl.tensors import ListTensor, as_tensor
+from ufl.variable import Variable
 
 
 def extract_blocks(form, i=None, j=None):
@@ -223,14 +215,14 @@ def _handle_derivative_arguments(form, coefficient, argument):
 
         # Create argument and split it if in a mixed space
         function_spaces = [c.ufl_function_space() for c in coefficients]
-        domains = [fs.ufl_domain() for fs in function_spaces]
-        elements = [fs.ufl_element() for fs in function_spaces]
         if len(function_spaces) == 1:
             arguments = (Argument(function_spaces[0], number, part),)
         else:
             # Create in mixed space over assumed (for now) same domain
+            domains = [fs.ufl_domain() for fs in function_spaces]
+            elements = [fs.ufl_element() for fs in function_spaces]
             assert all(fs.ufl_domain() == domains[0] for fs in function_spaces)
-            elm = MixedElement(*elements)
+            elm = MixedElement(elements)
             fs = FunctionSpace(domains[0], elm)
             arguments = split(Argument(fs, number, part))
     else:
diff --git a/ufl/functionspace.py b/ufl/functionspace.py
index 7c3cfda43..e2cc86dbc 100644
--- a/ufl/functionspace.py
+++ b/ufl/functionspace.py
@@ -42,7 +42,7 @@ def __init__(self, domain, element):
         """Initialise."""
         if domain is None:
             # DOLFIN hack
-            # TODO: Is anything expected from element.cell() in this case?
+            # TODO: Is anything expected from element.cell in this case?
             pass
         else:
             try:
@@ -50,7 +50,7 @@ def __init__(self, domain, element):
             except AttributeError:
                 raise ValueError("Expected non-abstract domain for initalization of function space.")
             else:
-                if element.cell() != domain_cell:
+                if element.cell != domain_cell:
                     raise ValueError("Non-matching cell of finite element and domain.")
 
         AbstractFunctionSpace.__init__(self)
diff --git a/ufl/geometry.py b/ufl/geometry.py
index e06ec7793..a8f6c7720 100644
--- a/ufl/geometry.py
+++ b/ufl/geometry.py
@@ -9,6 +9,7 @@
 from ufl.core.terminal import Terminal
 from ufl.core.ufl_type import ufl_type
 from ufl.domain import as_domain, extract_unique_domain
+from ufl.sobolevspace import H1
 
 """
 Possible coordinate bootstrapping:
@@ -675,7 +676,8 @@ def is_cellwise_constant(self):
         # facets. Seems like too much work to fix right now.  Only
         # true for a piecewise linear coordinate field with simplex
         # _facets_.
-        is_piecewise_linear = self._domain.ufl_coordinate_element().degree() == 1
+        ce = self._domain.ufl_coordinate_element()
+        is_piecewise_linear = ce.embedded_superdegree <= 1 and ce in H1
         return is_piecewise_linear and self._domain.ufl_cell().has_simplex_facets()
 
 
diff --git a/ufl/index_combination_utils.py b/ufl/index_combination_utils.py
index 6d820244e..1f7682ae0 100644
--- a/ufl/index_combination_utils.py
+++ b/ufl/index_combination_utils.py
@@ -7,10 +7,10 @@
 
 from ufl.core.multiindex import FixedIndex, Index, indices
 
-
 # FIXME: Some of these might be merged into one function, some might
 # be optimized
 
+
 def unique_sorted_indices(indices):
     """Get unique sorted indices.
 
diff --git a/ufl/indexed.py b/ufl/indexed.py
index 43bae7f10..b22f3d7ef 100644
--- a/ufl/indexed.py
+++ b/ufl/indexed.py
@@ -8,18 +8,16 @@
 
 from ufl.constantvalue import Zero
 from ufl.core.expr import Expr, ufl_err_str
-from ufl.core.ufl_type import ufl_type
+from ufl.core.multiindex import FixedIndex, Index, MultiIndex
 from ufl.core.operator import Operator
-from ufl.core.multiindex import Index, FixedIndex, MultiIndex
+from ufl.core.ufl_type import ufl_type
 from ufl.index_combination_utils import unique_sorted_indices
 from ufl.precedence import parstr
 
 
-# --- Indexed expression ---
-
 @ufl_type(is_shaping=True, num_ops=2, is_terminal_modifier=True)
 class Indexed(Operator):
-    """Indexed."""
+    """Indexed expression."""
 
     __slots__ = (
         "ufl_free_indices",
diff --git a/ufl/indexsum.py b/ufl/indexsum.py
index 3df273255..b13829484 100644
--- a/ufl/indexsum.py
+++ b/ufl/indexsum.py
@@ -7,16 +7,16 @@
 # SPDX-License-Identifier:    LGPL-3.0-or-later
 
 
-from ufl.core.ufl_type import ufl_type
+from ufl.constantvalue import Zero
 from ufl.core.expr import Expr, ufl_err_str
-from ufl.core.operator import Operator
 from ufl.core.multiindex import MultiIndex
+from ufl.core.operator import Operator
+from ufl.core.ufl_type import ufl_type
 from ufl.precedence import parstr
-from ufl.constantvalue import Zero
-
 
 # --- Sum over an index ---
 
+
 @ufl_type(num_ops=2)
 class IndexSum(Operator):
     """Index sum."""
diff --git a/ufl/integral.py b/ufl/integral.py
index f92aefbc5..d680f5a77 100644
--- a/ufl/integral.py
+++ b/ufl/integral.py
@@ -10,8 +10,8 @@
 # Modified by Massimiliano Leoni, 2016.
 
 import ufl
-from ufl.core.expr import Expr
 from ufl.checks import is_python_scalar, is_scalar_constant_expression
+from ufl.core.expr import Expr
 from ufl.measure import Measure  # noqa
 from ufl.protocols import id_or_none
 
diff --git a/ufl/legacy/__init__.py b/ufl/legacy/__init__.py
new file mode 100644
index 000000000..06a0b12ca
--- /dev/null
+++ b/ufl/legacy/__init__.py
@@ -0,0 +1,25 @@
+"""Legacy UFL features."""
+# Copyright (C) 2008-2016 Martin Sandve Alnæs
+#
+# This file is part of UFL (https://www.fenicsproject.org)
+#
+# SPDX-License-Identifier:    LGPL-3.0-or-later
+#
+# Modified by Kristian B. Oelgaard
+# Modified by Marie E. Rognes 2010, 2012
+# Modified by Andrew T. T. McRae 2014
+# Modified by Lawrence Mitchell 2014
+
+import warnings as _warnings
+
+from ufl.legacy.brokenelement import BrokenElement
+from ufl.legacy.enrichedelement import EnrichedElement, NodalEnrichedElement
+from ufl.legacy.finiteelement import FiniteElement
+from ufl.legacy.finiteelementbase import FiniteElementBase
+from ufl.legacy.hdivcurl import HCurlElement, HDivElement, WithMapping
+from ufl.legacy.mixedelement import MixedElement, TensorElement, VectorElement
+from ufl.legacy.restrictedelement import RestrictedElement
+from ufl.legacy.tensorproductelement import TensorProductElement
+
+_warnings.warn("The features in ufl.legacy are deprecated and will be removed in a future version.",
+               FutureWarning)
diff --git a/ufl/finiteelement/brokenelement.py b/ufl/legacy/brokenelement.py
similarity index 86%
rename from ufl/finiteelement/brokenelement.py
rename to ufl/legacy/brokenelement.py
index 2467e9d0b..2563b868f 100644
--- a/ufl/finiteelement/brokenelement.py
+++ b/ufl/legacy/brokenelement.py
@@ -8,7 +8,7 @@
 #
 # Modified by Massimiliano Leoni, 2016
 
-from ufl.finiteelement.finiteelementbase import FiniteElementBase
+from ufl.legacy.finiteelementbase import FiniteElementBase
 from ufl.sobolevspace import L2
 
 
@@ -19,11 +19,11 @@ def __init__(self, element):
         self._element = element
 
         family = "BrokenElement"
-        cell = element.cell()
+        cell = element.cell
         degree = element.degree()
         quad_scheme = element.quadrature_scheme()
-        value_shape = element.value_shape()
-        reference_value_shape = element.reference_value_shape()
+        value_shape = element.value_shape
+        reference_value_shape = element.reference_value_shape
         FiniteElementBase.__init__(self, family, cell, degree,
                                    quad_scheme, value_shape, reference_value_shape)
 
@@ -35,6 +35,7 @@ def mapping(self):
         """Doc."""
         return self._element.mapping()
 
+    @property
     def sobolev_space(self):
         """Return the underlying Sobolev space."""
         return L2
diff --git a/ufl/finiteelement/elementlist.py b/ufl/legacy/elementlist.py
similarity index 99%
rename from ufl/finiteelement/elementlist.py
rename to ufl/legacy/elementlist.py
index 90c783d4f..712b94bb1 100644
--- a/ufl/finiteelement/elementlist.py
+++ b/ufl/legacy/elementlist.py
@@ -16,12 +16,12 @@
 # Modified by Robert Kloefkorn, 2022
 
 import warnings
+
 from numpy import asarray
 
-from ufl.sobolevspace import L2, H1, H2, HDiv, HCurl, HEin, HDivDiv, HInf
-from ufl.utils.formatting import istr
 from ufl.cell import Cell, TensorProductCell
-
+from ufl.sobolevspace import H1, H2, L2, HCurl, HDiv, HDivDiv, HEin, HInf
+from ufl.utils.formatting import istr
 
 # List of valid elements
 ufl_elements = {}
diff --git a/ufl/finiteelement/enrichedelement.py b/ufl/legacy/enrichedelement.py
similarity index 83%
rename from ufl/finiteelement/enrichedelement.py
rename to ufl/legacy/enrichedelement.py
index 81e2d88cc..76420f00b 100644
--- a/ufl/finiteelement/enrichedelement.py
+++ b/ufl/legacy/enrichedelement.py
@@ -10,7 +10,7 @@
 # Modified by Marie E. Rognes 2010, 2012
 # Modified by Massimiliano Leoni, 2016
 
-from ufl.finiteelement.finiteelementbase import FiniteElementBase
+from ufl.legacy.finiteelementbase import FiniteElementBase
 
 
 class EnrichedElementBase(FiniteElementBase):
@@ -20,8 +20,8 @@ def __init__(self, *elements):
         """Doc."""
         self._elements = elements
 
-        cell = elements[0].cell()
-        if not all(e.cell() == cell for e in elements[1:]):
+        cell = elements[0].cell
+        if not all(e.cell == cell for e in elements[1:]):
             raise ValueError("Cell mismatch for sub elements of enriched element.")
 
         if isinstance(elements[0].degree(), int):
@@ -38,12 +38,12 @@ def __init__(self, *elements):
         if not all(qs == quad_scheme for qs in quad_schemes):
             raise ValueError("Quadrature scheme mismatch.")
 
-        value_shape = elements[0].value_shape()
-        if not all(e.value_shape() == value_shape for e in elements[1:]):
+        value_shape = elements[0].value_shape
+        if not all(e.value_shape == value_shape for e in elements[1:]):
             raise ValueError("Element value shape mismatch.")
 
-        reference_value_shape = elements[0].reference_value_shape()
-        if not all(e.reference_value_shape() == reference_value_shape for e in elements[1:]):
+        reference_value_shape = elements[0].reference_value_shape
+        if not all(e.reference_value_shape == reference_value_shape for e in elements[1:]):
             raise ValueError("Element reference value shape mismatch.")
 
         # mapping = elements[0].mapping() # FIXME: This fails for a mixed subelement here.
@@ -62,15 +62,16 @@ def mapping(self):
         """Doc."""
         return self._elements[0].mapping()
 
+    @property
     def sobolev_space(self):
         """Return the underlying Sobolev space."""
         elements = [e for e in self._elements]
-        if all(e.sobolev_space() == elements[0].sobolev_space()
+        if all(e.sobolev_space == elements[0].sobolev_space
                for e in elements):
-            return elements[0].sobolev_space()
+            return elements[0].sobolev_space
         else:
             # Find smallest shared Sobolev space over all sub elements
-            spaces = [e.sobolev_space() for e in elements]
+            spaces = [e.sobolev_space for e in elements]
             superspaces = [{s} | set(s.parents) for s in spaces]
             intersect = set.intersection(*superspaces)
             for s in intersect.copy():
@@ -92,6 +93,22 @@ def reconstruct(self, **kwargs):
         """Doc."""
         return type(self)(*[e.reconstruct(**kwargs) for e in self._elements])
 
+    @property
+    def embedded_subdegree(self):
+        """Return embedded subdegree."""
+        if isinstance(self._degree, int):
+            return self._degree
+        else:
+            return min(e.embedded_subdegree for e in self._elements)
+
+    @property
+    def embedded_superdegree(self):
+        """Return embedded superdegree."""
+        if isinstance(self._degree, int):
+            return self._degree
+        else:
+            return max(e.embedded_superdegree for e in self._elements)
+
 
 class EnrichedElement(EnrichedElementBase):
     r"""The vector sum of several finite element spaces.
diff --git a/ufl/finiteelement/finiteelement.py b/ufl/legacy/finiteelement.py
similarity index 93%
rename from ufl/finiteelement/finiteelement.py
rename to ufl/legacy/finiteelement.py
index 0962e82a3..1166ed741 100644
--- a/ufl/finiteelement/finiteelement.py
+++ b/ufl/legacy/finiteelement.py
@@ -10,12 +10,10 @@
 # Modified by Anders Logg 2014
 # Modified by Massimiliano Leoni, 2016
 
+from ufl.cell import TensorProductCell, as_cell
+from ufl.legacy.elementlist import canonical_element_description, simplices
+from ufl.legacy.finiteelementbase import FiniteElementBase
 from ufl.utils.formatting import istr
-from ufl.cell import as_cell
-
-from ufl.cell import TensorProductCell
-from ufl.finiteelement.elementlist import canonical_element_description, simplices
-from ufl.finiteelement.finiteelementbase import FiniteElementBase
 
 
 class FiniteElement(FiniteElementBase):
@@ -37,9 +35,10 @@ def __new__(cls,
 
         if isinstance(cell, TensorProductCell):
             # Delay import to avoid circular dependency at module load time
-            from ufl.finiteelement.tensorproductelement import TensorProductElement
-            from ufl.finiteelement.enrichedelement import EnrichedElement
-            from ufl.finiteelement.hdivcurl import HDivElement as HDiv, HCurlElement as HCurl
+            from ufl.legacy.enrichedelement import EnrichedElement
+            from ufl.legacy.hdivcurl import HCurlElement as HCurl
+            from ufl.legacy.hdivcurl import HDivElement as HDiv
+            from ufl.legacy.tensorproductelement import TensorProductElement
 
             family, short_name, degree, value_shape, reference_value_shape, sobolev_space, mapping = \
                 canonical_element_description(family, cell, degree, form_degree)
@@ -171,7 +170,7 @@ def __init__(self,
         else:
             var_str = ", variant=%s" % repr(v)
         self._repr = "FiniteElement(%s, %s, %s%s%s)" % (
-            repr(self.family()), repr(self.cell()), repr(self.degree()), quad_str, var_str)
+            repr(self.family()), repr(self.cell), repr(self.degree()), quad_str, var_str)
         assert '"' not in self._repr
 
     def __repr__(self):
@@ -190,6 +189,7 @@ def mapping(self):
         """Return the mapping type for this element ."""
         return self._mapping
 
+    @property
     def sobolev_space(self):
         """Return the underlying Sobolev space."""
         return self._sobolev_space
@@ -203,7 +203,7 @@ def reconstruct(self, family=None, cell=None, degree=None, quad_scheme=None, var
         if family is None:
             family = self.family()
         if cell is None:
-            cell = self.cell()
+            cell = self.cell
         if degree is None:
             degree = self.degree()
         if quad_scheme is None:
@@ -219,7 +219,7 @@ def __str__(self):
         v = self.variant()
         v = "" if v is None else "(%s)" % v
         return "<%s%s%s%s on a %s>" % (self._short_name, istr(self.degree()),
-                                       qs, v, self.cell())
+                                       qs, v, self.cell)
 
     def shortstr(self):
         """Format as string for pretty printing."""
@@ -228,7 +228,7 @@ def shortstr(self):
     def __getnewargs__(self):
         """Return the arguments which pickle needs to recreate the object."""
         return (self.family(),
-                self.cell(),
+                self.cell,
                 self.degree(),
                 None,
                 self.quadrature_scheme(),
diff --git a/ufl/finiteelement/finiteelementbase.py b/ufl/legacy/finiteelementbase.py
similarity index 81%
rename from ufl/finiteelement/finiteelementbase.py
rename to ufl/legacy/finiteelementbase.py
index 7f34252fc..88eba6c8c 100644
--- a/ufl/finiteelement/finiteelementbase.py
+++ b/ufl/legacy/finiteelementbase.py
@@ -10,15 +10,18 @@
 # Modified by Marie E. Rognes 2010, 2012
 # Modified by Massimiliano Leoni, 2016
 
-from ufl.utils.sequences import product
+from abc import abstractmethod, abstractproperty
+
+from ufl import pullback
 from ufl.cell import AbstractCell, as_cell
-from abc import ABC, abstractmethod
+from ufl.finiteelement import AbstractFiniteElement
+from ufl.utils.sequences import product
 
 
-class FiniteElementBase(ABC):
+class FiniteElementBase(AbstractFiniteElement):
     """Base class for all finite elements."""
     __slots__ = ("_family", "_cell", "_degree", "_quad_scheme",
-                 "_value_shape", "_reference_value_shape", "__weakref__")
+                 "_value_shape", "_reference_value_shape")
 
     # TODO: Not all these should be in the base class! In particular
     # family, degree, and quad_scheme do not belong here.
@@ -49,7 +52,7 @@ def __repr__(self):
         """Format as string for evaluation as Python object."""
         pass
 
-    @abstractmethod
+    @abstractproperty
     def sobolev_space(self):
         """Return the underlying Sobolev space."""
         pass
@@ -104,14 +107,13 @@ def variant(self):
 
     def degree(self, component=None):
         """Return polynomial degree of finite element."""
-        # FIXME: Consider embedded_degree concept for more accurate
-        # degree, see blueprint
         return self._degree
 
     def quadrature_scheme(self):
         """Return quadrature scheme of finite element."""
         return self._quad_scheme
 
+    @property
     def cell(self):
         """Return cell of finite element."""
         return self._cell
@@ -120,21 +122,25 @@ def is_cellwise_constant(self, component=None):
         """Return whether the basis functions of this element is spatially constant over each cell."""
         return self._is_globally_constant() or self.degree() == 0
 
+    @property
     def value_shape(self):
         """Return the shape of the value space on the global domain."""
         return self._value_shape
 
+    @property
     def reference_value_shape(self):
         """Return the shape of the value space on the reference cell."""
         return self._reference_value_shape
 
+    @property
     def value_size(self):
         """Return the integer product of the value shape."""
-        return product(self.value_shape())
+        return product(self.value_shape)
 
+    @property
     def reference_value_size(self):
         """Return the integer product of the reference value shape."""
-        return product(self.reference_value_shape())
+        return product(self.reference_value_shape)
 
     def symmetry(self):  # FIXME: different approach
         r"""Return the symmetry dict.
@@ -148,7 +154,7 @@ def symmetry(self):  # FIXME: different approach
 
     def _check_component(self, i):
         """Check that component index i is valid."""
-        sh = self.value_shape()
+        sh = self.value_shape
         r = len(sh)
         if not (len(i) == r and all(j < k for (j, k) in zip(i, sh))):
             raise ValueError(
@@ -174,7 +180,7 @@ def extract_component(self, i):
 
     def _check_reference_component(self, i):
         """Check that reference component index i is valid."""
-        sh = self.value_shape()
+        sh = self.value_shape
         r = len(sh)
         if not (len(i) == r and all(j < k for (j, k) in zip(i, sh))):
             raise ValueError(
@@ -201,10 +207,12 @@ def extract_reference_component(self, i):
         self._check_reference_component(i)
         return (i, self)
 
+    @property
     def num_sub_elements(self):
         """Return number of sub-elements."""
         return 0
 
+    @property
     def sub_elements(self):
         """Return list of sub-elements."""
         return []
@@ -213,20 +221,20 @@ def __add__(self, other):
         """Add two elements, creating an enriched element."""
         if not isinstance(other, FiniteElementBase):
             raise ValueError(f"Can't add element and {other.__class__}.")
-        from ufl.finiteelement import EnrichedElement
+        from ufl.legacy import EnrichedElement
         return EnrichedElement(self, other)
 
     def __mul__(self, other):
         """Multiply two elements, creating a mixed element."""
         if not isinstance(other, FiniteElementBase):
             raise ValueError("Can't multiply element and {other.__class__}.")
-        from ufl.finiteelement import MixedElement
+        from ufl.legacy import MixedElement
         return MixedElement(self, other)
 
     def __getitem__(self, index):
         """Restrict finite element to a subdomain, subcomponent or topology (cell)."""
         if index in ("facet", "interior"):
-            from ufl.finiteelement import RestrictedElement
+            from ufl.legacy import RestrictedElement
             return RestrictedElement(self, index)
         else:
             raise KeyError(f"Invalid index for restriction: {repr(index)}")
@@ -234,3 +242,35 @@ def __getitem__(self, index):
     def __iter__(self):
         """Iter."""
         raise TypeError(f"'{type(self).__name__}' object is not iterable")
+
+    @property
+    def embedded_superdegree(self):
+        """Doc."""
+        return self.degree()
+
+    @property
+    def embedded_subdegree(self):
+        """Doc."""
+        return self.degree()
+
+    @property
+    def pullback(self):
+        """Get the pull back."""
+        if self.mapping() == "identity":
+            return pullback.identity_pullback
+        elif self.mapping() == "L2 Piola":
+            return pullback.l2_piola
+        elif self.mapping() == "covariant Piola":
+            return pullback.covariant_piola
+        elif self.mapping() == "contravariant Piola":
+            return pullback.contravariant_piola
+        elif self.mapping() == "double covariant Piola":
+            return pullback.double_covariant_piola
+        elif self.mapping() == "double contravariant Piola":
+            return pullback.double_contravariant_piola
+        elif self.mapping() == "custom":
+            return pullback.custom_pullback
+        elif self.mapping() == "physical":
+            return pullback.physical_pullback
+
+        raise ValueError(f"Unsupported mapping: {self.mapping()}")
diff --git a/ufl/finiteelement/hdivcurl.py b/ufl/legacy/hdivcurl.py
similarity index 81%
rename from ufl/finiteelement/hdivcurl.py
rename to ufl/legacy/hdivcurl.py
index fde6ff33b..3f693017e 100644
--- a/ufl/finiteelement/hdivcurl.py
+++ b/ufl/legacy/hdivcurl.py
@@ -7,8 +7,8 @@
 #
 # Modified by Massimiliano Leoni, 2016
 
-from ufl.finiteelement.finiteelementbase import FiniteElementBase
-from ufl.sobolevspace import HDiv, HCurl, L2
+from ufl.legacy.finiteelementbase import FiniteElementBase
+from ufl.sobolevspace import L2, HCurl, HDiv
 
 
 class HDivElement(FiniteElementBase):
@@ -20,11 +20,11 @@ def __init__(self, element):
         self._element = element
 
         family = "TensorProductElement"
-        cell = element.cell()
+        cell = element.cell
         degree = element.degree()
         quad_scheme = element.quadrature_scheme()
-        value_shape = (element.cell().geometric_dimension(),)
-        reference_value_shape = (element.cell().topological_dimension(),)
+        value_shape = (element.cell.geometric_dimension(),)
+        reference_value_shape = (element.cell.topological_dimension(),)
 
         # Skipping TensorProductElement constructor! Bad code smell, refactor to avoid this non-inheritance somehow.
         FiniteElementBase.__init__(self, family, cell, degree,
@@ -38,6 +38,7 @@ def mapping(self):
         """Doc."""
         return "contravariant Piola"
 
+    @property
     def sobolev_space(self):
         """Return the underlying Sobolev space."""
         return HDiv
@@ -58,6 +59,16 @@ def shortstr(self):
         """Format as string for pretty printing."""
         return f"HDivElement({self._element.shortstr()})"
 
+    @property
+    def embedded_subdegree(self):
+        """Return embedded subdegree."""
+        return self._element.embedded_subdegree
+
+    @property
+    def embedded_superdegree(self):
+        """Return embedded superdegree."""
+        return self._element.embedded_superdegree
+
 
 class HCurlElement(FiniteElementBase):
     """A curl-conforming version of an outer product element, assuming this makes mathematical sense."""
@@ -68,10 +79,10 @@ def __init__(self, element):
         self._element = element
 
         family = "TensorProductElement"
-        cell = element.cell()
+        cell = element.cell
         degree = element.degree()
         quad_scheme = element.quadrature_scheme()
-        cell = element.cell()
+        cell = element.cell
         value_shape = (cell.geometric_dimension(),)
         reference_value_shape = (cell.topological_dimension(),)  # TODO: Is this right?
         # Skipping TensorProductElement constructor! Bad code smell,
@@ -87,6 +98,7 @@ def mapping(self):
         """Doc."""
         return "covariant Piola"
 
+    @property
     def sobolev_space(self):
         """Return the underlying Sobolev space."""
         return HCurl
@@ -136,36 +148,39 @@ def __repr__(self):
         """Doc."""
         return f"WithMapping({repr(self.wrapee)}, '{self._mapping}')"
 
+    @property
     def value_shape(self):
         """Doc."""
-        gdim = self.cell().geometric_dimension()
+        gdim = self.cell.geometric_dimension()
         mapping = self.mapping()
         if mapping in {"covariant Piola", "contravariant Piola"}:
             return (gdim,)
         elif mapping in {"double covariant Piola", "double contravariant Piola"}:
             return (gdim, gdim)
         else:
-            return self.wrapee.value_shape()
+            return self.wrapee.value_shape
 
+    @property
     def reference_value_shape(self):
         """Doc."""
-        tdim = self.cell().topological_dimension()
+        tdim = self.cell.topological_dimension()
         mapping = self.mapping()
         if mapping in {"covariant Piola", "contravariant Piola"}:
             return (tdim,)
         elif mapping in {"double covariant Piola", "double contravariant Piola"}:
             return (tdim, tdim)
         else:
-            return self.wrapee.reference_value_shape()
+            return self.wrapee.reference_value_shape
 
     def mapping(self):
         """Doc."""
         return self._mapping
 
+    @property
     def sobolev_space(self):
         """Return the underlying Sobolev space."""
         if self.wrapee.mapping() == self.mapping():
-            return self.wrapee.sobolev_space()
+            return self.wrapee.sobolev_space
         else:
             return L2
 
@@ -186,3 +201,13 @@ def __str__(self):
     def shortstr(self):
         """Doc."""
         return f"WithMapping({self.wrapee.shortstr()}, {self._mapping})"
+
+    @property
+    def embedded_subdegree(self):
+        """Return embedded subdegree."""
+        return self._element.embedded_subdegree
+
+    @property
+    def embedded_superdegree(self):
+        """Return embedded superdegree."""
+        return self._element.embedded_superdegree
diff --git a/ufl/finiteelement/mixedelement.py b/ufl/legacy/mixedelement.py
similarity index 86%
rename from ufl/finiteelement/mixedelement.py
rename to ufl/legacy/mixedelement.py
index ca16a3d80..97f194e55 100644
--- a/ufl/finiteelement/mixedelement.py
+++ b/ufl/legacy/mixedelement.py
@@ -10,13 +10,15 @@
 # Modified by Anders Logg 2014
 # Modified by Massimiliano Leoni, 2016
 
-from ufl.permutation import compute_indices
-from ufl.utils.sequences import product, max_degree
-from ufl.utils.indexflattening import flatten_multiindex, unflatten_index, shape_to_strides
-from ufl.cell import as_cell
+import numpy as np
 
-from ufl.finiteelement.finiteelementbase import FiniteElementBase
-from ufl.finiteelement.finiteelement import FiniteElement
+from ufl.cell import as_cell
+from ufl.legacy.finiteelement import FiniteElement
+from ufl.legacy.finiteelementbase import FiniteElementBase
+from ufl.permutation import compute_indices
+from ufl.pullback import IdentityPullback, MixedPullback, SymmetricPullback
+from ufl.utils.indexflattening import flatten_multiindex, shape_to_strides, unflatten_index
+from ufl.utils.sequences import max_degree, product
 
 
 class MixedElement(FiniteElementBase):
@@ -38,7 +40,7 @@ def __init__(self, *elements, **kwargs):
         self._sub_elements = elements
 
         # Pick the first cell, for now all should be equal
-        cells = tuple(sorted(set(element.cell() for element in elements) - set([None])))
+        cells = tuple(sorted(set(element.cell for element in elements) - set([None])))
         self._cells = cells
         if cells:
             cell = cells[0]
@@ -58,8 +60,8 @@ def __init__(self, *elements, **kwargs):
                 raise ValueError("Quadrature scheme mismatch for sub elements of mixed element.")
 
         # Compute value sizes in global and reference configurations
-        value_size_sum = sum(product(s.value_shape()) for s in self._sub_elements)
-        reference_value_size_sum = sum(product(s.reference_value_shape()) for s in self._sub_elements)
+        value_size_sum = sum(product(s.value_shape) for s in self._sub_elements)
+        reference_value_size_sum = sum(product(s.reference_value_shape) for s in self._sub_elements)
 
         # Default value shape: Treated simply as all subelement values
         # unpacked in a vector.
@@ -110,7 +112,7 @@ def symmetry(self):
         # Base index of the current subelement into mixed value
         j = 0
         for e in self._sub_elements:
-            sh = e.value_shape()
+            sh = e.value_shape
             st = shape_to_strides(sh)
             # Map symmetries of subelement into index space of this
             # element
@@ -120,13 +122,14 @@ def symmetry(self):
                 sm[(j0,)] = (j1,)
             # Update base index for next element
             j += product(sh)
-        if j != product(self.value_shape()):
+        if j != product(self.value_shape):
             raise ValueError("Size mismatch in symmetry algorithm.")
         return sm or {}
 
+    @property
     def sobolev_space(self):
         """Doc."""
-        return max(e.sobolev_space() for e in self._sub_elements)
+        return max(e.sobolev_space for e in self._sub_elements)
 
     def mapping(self):
         """Doc."""
@@ -135,10 +138,12 @@ def mapping(self):
         else:
             return "undefined"
 
+    @property
     def num_sub_elements(self):
         """Return number of sub elements."""
         return len(self._sub_elements)
 
+    @property
     def sub_elements(self):
         """Return list of sub elements."""
         return self._sub_elements
@@ -153,14 +158,14 @@ def extract_subelement_component(self, i):
         self._check_component(i)
 
         # Select between indexing modes
-        if len(self.value_shape()) == 1:
+        if len(self.value_shape) == 1:
             # Indexing into a long vector of flattened subelement
             # shapes
             j, = i
 
             # Find subelement for this index
             for sub_element_index, e in enumerate(self._sub_elements):
-                sh = e.value_shape()
+                sh = e.value_shape
                 si = product(sh)
                 if j < si:
                     break
@@ -198,13 +203,13 @@ def extract_subelement_reference_component(self, i):
         self._check_reference_component(i)
 
         # Select between indexing modes
-        assert len(self.reference_value_shape()) == 1
+        assert len(self.reference_value_shape) == 1
         # Indexing into a long vector of flattened subelement shapes
         j, = i
 
         # Find subelement for this index
         for sub_element_index, e in enumerate(self._sub_elements):
-            sh = e.reference_value_shape()
+            sh = e.reference_value_shape
             si = product(sh)
             if j < si:
                 break
@@ -228,7 +233,7 @@ def extract_reference_component(self, i):
     def is_cellwise_constant(self, component=None):
         """Return whether the basis functions of this element is spatially constant over each cell."""
         if component is None:
-            return all(e.is_cellwise_constant() for e in self.sub_elements())
+            return all(e.is_cellwise_constant() for e in self.sub_elements)
         else:
             i, e = self.extract_component(component)
             return e.is_cellwise_constant()
@@ -241,14 +246,30 @@ def degree(self, component=None):
             i, e = self.extract_component(component)
             return e.degree()
 
+    @property
+    def embedded_subdegree(self):
+        """Return embedded subdegree."""
+        if isinstance(self._degree, int):
+            return self._degree
+        else:
+            return min(e.embedded_subdegree for e in self.sub_elements)
+
+    @property
+    def embedded_superdegree(self):
+        """Return embedded superdegree."""
+        if isinstance(self._degree, int):
+            return self._degree
+        else:
+            return max(e.embedded_superdegree for e in self.sub_elements)
+
     def reconstruct(self, **kwargs):
         """Doc."""
-        return MixedElement(*[e.reconstruct(**kwargs) for e in self.sub_elements()])
+        return MixedElement(*[e.reconstruct(**kwargs) for e in self.sub_elements])
 
     def variant(self):
         """Doc."""
         try:
-            variant, = {e.variant() for e in self.sub_elements()}
+            variant, = {e.variant() for e in self.sub_elements}
             return variant
         except ValueError:
             return None
@@ -263,6 +284,14 @@ def shortstr(self):
         tmp = ", ".join(element.shortstr() for element in self._sub_elements)
         return "Mixed<" + tmp + ">"
 
+    @property
+    def pullback(self):
+        """Get the pull back."""
+        for e in self.sub_elements:
+            if not isinstance(e.pullback, IdentityPullback):
+                return MixedPullback(self)
+        return IdentityPullback()
+
 
 class VectorElement(MixedElement):
     """A special case of a mixed finite element where all elements are equal."""
@@ -274,7 +303,7 @@ def __init__(self, family, cell=None, degree=None, dim=None,
         """Create vector element (repeated mixed element)."""
         if isinstance(family, FiniteElementBase):
             sub_element = family
-            cell = sub_element.cell()
+            cell = sub_element.cell
             variant = sub_element.variant()
         else:
             if cell is not None:
@@ -296,14 +325,14 @@ def __init__(self, family, cell=None, degree=None, dim=None,
         sub_elements = [sub_element] * dim
 
         # Compute value shapes
-        value_shape = (dim,) + sub_element.value_shape()
-        reference_value_shape = (dim,) + sub_element.reference_value_shape()
+        value_shape = (dim,) + sub_element.value_shape
+        reference_value_shape = (dim,) + sub_element.reference_value_shape
 
         # Initialize element data
         MixedElement.__init__(self, sub_elements, value_shape=value_shape,
                               reference_value_shape=reference_value_shape)
 
-        FiniteElementBase.__init__(self, sub_element.family(), sub_element.cell(), sub_element.degree(),
+        FiniteElementBase.__init__(self, sub_element.family(), sub_element.cell, sub_element.degree(),
                                    sub_element.quadrature_scheme(), value_shape, reference_value_shape)
 
         self._sub_element = sub_element
@@ -323,7 +352,7 @@ def __repr__(self):
     def reconstruct(self, **kwargs):
         """Doc."""
         sub_element = self._sub_element.reconstruct(**kwargs)
-        return VectorElement(sub_element, dim=len(self.sub_elements()))
+        return VectorElement(sub_element, dim=len(self.sub_elements))
 
     def variant(self):
         """Return the variant used to initialise the element."""
@@ -356,7 +385,7 @@ def __init__(self, family, cell=None, degree=None, shape=None,
         """Create tensor element (repeated mixed element with optional symmetries)."""
         if isinstance(family, FiniteElementBase):
             sub_element = family
-            cell = sub_element.cell()
+            cell = sub_element.cell
             variant = sub_element.variant()
         else:
             if cell is not None:
@@ -423,8 +452,8 @@ def __init__(self, family, cell=None, degree=None, shape=None,
             reference_value_shape = shape
             self._mapping = sub_element.mapping()
 
-        value_shape = value_shape + sub_element.value_shape()
-        reference_value_shape = reference_value_shape + sub_element.reference_value_shape()
+        value_shape = value_shape + sub_element.value_shape
+        reference_value_shape = reference_value_shape + sub_element.reference_value_shape
         # Initialize element data
         MixedElement.__init__(self, sub_elements, value_shape=value_shape,
                               reference_value_shape=reference_value_shape)
@@ -445,6 +474,25 @@ def __init__(self, family, cell=None, degree=None, shape=None,
         self._repr = (f"TensorElement({repr(sub_element)}, shape={shape}, "
                       f"symmetry={symmetry}{var_str})")
 
+    @property
+    def pullback(self):
+        """Get pull back."""
+        if len(self._symmetry) > 0:
+            sub_element_value_shape = self.sub_elements[0].value_shape
+            for e in self.sub_elements:
+                if e.value_shape != sub_element_value_shape:
+                    raise ValueError("Sub-elements must all have the same value size")
+            symmetry = {}
+            n = 0
+            for i in np.ndindex(self.value_shape[:len(self.value_shape)-len(sub_element_value_shape)]):
+                if i in self._symmetry and self._symmetry[i] in symmetry:
+                    symmetry[i] = symmetry[self._symmetry[i]]
+                else:
+                    symmetry[i] = n
+                    n += 1
+            return SymmetricPullback(self, symmetry)
+        return super().pullback
+
     def __repr__(self):
         """Doc."""
         return self._repr
@@ -501,7 +549,7 @@ def __str__(self):
         else:
             sym = ""
         return ("<tensor element with shape %s of %s%s>" %
-                (self.value_shape(), self._sub_element, sym))
+                (self.value_shape, self._sub_element, sym))
 
     def shortstr(self):
         """Format as string for pretty printing."""
@@ -510,5 +558,5 @@ def shortstr(self):
             sym = " with symmetries (%s)" % tmp
         else:
             sym = ""
-        return "Tensor<%s x %s%s>" % (self.value_shape(),
+        return "Tensor<%s x %s%s>" % (self.value_shape,
                                       self._sub_element.shortstr(), sym)
diff --git a/ufl/finiteelement/restrictedelement.py b/ufl/legacy/restrictedelement.py
similarity index 91%
rename from ufl/finiteelement/restrictedelement.py
rename to ufl/legacy/restrictedelement.py
index 7ad71b3a7..767444087 100644
--- a/ufl/finiteelement/restrictedelement.py
+++ b/ufl/legacy/restrictedelement.py
@@ -10,7 +10,7 @@
 # Modified by Marie E. Rognes 2010, 2012
 # Modified by Massimiliano Leoni, 2016
 
-from ufl.finiteelement.finiteelementbase import FiniteElementBase
+from ufl.legacy.finiteelementbase import FiniteElementBase
 from ufl.sobolevspace import L2
 
 valid_restriction_domains = ("interior", "facet", "face", "edge", "vertex")
@@ -26,11 +26,11 @@ def __init__(self, element, restriction_domain):
         if restriction_domain not in valid_restriction_domains:
             raise ValueError(f"Expecting one of the strings: {valid_restriction_domains}")
 
-        FiniteElementBase.__init__(self, "RestrictedElement", element.cell(),
+        FiniteElementBase.__init__(self, "RestrictedElement", element.cell,
                                    element.degree(),
                                    element.quadrature_scheme(),
-                                   element.value_shape(),
-                                   element.reference_value_shape())
+                                   element.value_shape,
+                                   element.reference_value_shape)
 
         self._element = element
 
@@ -40,12 +40,13 @@ def __repr__(self):
         """Doc."""
         return f"RestrictedElement({repr(self._element)}, {repr(self._restriction_domain)})"
 
+    @property
     def sobolev_space(self):
         """Doc."""
         if self._restriction_domain == "interior":
             return L2
         else:
-            return self._element.sobolev_space()
+            return self._element.sobolev_space
 
     def is_cellwise_constant(self):
         """Return whether the basis functions of this element is spatially constant over each cell."""
@@ -89,13 +90,15 @@ def symmetry(self):
         """
         return self._element.symmetry()
 
+    @property
     def num_sub_elements(self):
         """Return number of sub elements."""
-        return self._element.num_sub_elements()
+        return self._element.num_sub_elements
 
+    @property
     def sub_elements(self):
         """Return list of sub elements."""
-        return self._element.sub_elements()
+        return self._element.sub_elements
 
     def num_restricted_sub_elements(self):
         """Return number of restricted sub elements."""
diff --git a/ufl/finiteelement/tensorproductelement.py b/ufl/legacy/tensorproductelement.py
similarity index 83%
rename from ufl/finiteelement/tensorproductelement.py
rename to ufl/legacy/tensorproductelement.py
index 07aad6e8f..d9f1ff1bd 100644
--- a/ufl/finiteelement/tensorproductelement.py
+++ b/ufl/legacy/tensorproductelement.py
@@ -13,10 +13,9 @@
 from itertools import chain
 
 from ufl.cell import TensorProductCell, as_cell
+from ufl.legacy.finiteelementbase import FiniteElementBase
 from ufl.sobolevspace import DirectionalSobolevSpace
 
-from ufl.finiteelement.finiteelementbase import FiniteElementBase
-
 
 class TensorProductElement(FiniteElementBase):
     r"""The tensor product of :math:`d` element spaces.
@@ -43,7 +42,7 @@ def __init__(self, *elements, **kwargs):
 
         if cell is None:
             # Define cell as the product of each elements cell
-            cell = TensorProductCell(*[e.cell() for e in elements])
+            cell = TensorProductCell(*[e.cell for e in elements])
         else:
             cell = as_cell(cell)
 
@@ -54,8 +53,8 @@ def __init__(self, *elements, **kwargs):
         quad_scheme = None
 
         # match FIAT implementation
-        value_shape = tuple(chain(*[e.value_shape() for e in elements]))
-        reference_value_shape = tuple(chain(*[e.reference_value_shape() for e in elements]))
+        value_shape = tuple(chain(*[e.value_shape for e in elements]))
+        reference_value_shape = tuple(chain(*[e.reference_value_shape for e in elements]))
         if len(value_shape) > 1:
             raise ValueError("Product of vector-valued elements not supported")
         if len(reference_value_shape) > 1:
@@ -80,39 +79,42 @@ def mapping(self):
         else:
             return "undefined"
 
+    @property
     def sobolev_space(self):
         """Return the underlying Sobolev space of the TensorProductElement."""
         elements = self._sub_elements
-        if all(e.sobolev_space() == elements[0].sobolev_space()
+        if all(e.sobolev_space == elements[0].sobolev_space
                for e in elements):
-            return elements[0].sobolev_space()
+            return elements[0].sobolev_space
         else:
             # Generate a DirectionalSobolevSpace which contains
             # continuity information parametrized by spatial index
             orders = []
             for e in elements:
-                e_dim = e.cell().geometric_dimension()
-                e_order = (e.sobolev_space()._order,) * e_dim
+                e_dim = e.cell.geometric_dimension()
+                e_order = (e.sobolev_space._order,) * e_dim
                 orders.extend(e_order)
             return DirectionalSobolevSpace(orders)
 
+    @property
     def num_sub_elements(self):
         """Return number of subelements."""
         return len(self._sub_elements)
 
+    @property
     def sub_elements(self):
         """Return subelements (factors)."""
         return self._sub_elements
 
     def reconstruct(self, **kwargs):
         """Doc."""
-        cell = kwargs.pop("cell", self.cell())
-        return TensorProductElement(*[e.reconstruct(**kwargs) for e in self.sub_elements()], cell=cell)
+        cell = kwargs.pop("cell", self.cell)
+        return TensorProductElement(*[e.reconstruct(**kwargs) for e in self.sub_elements], cell=cell)
 
     def variant(self):
         """Doc."""
         try:
-            variant, = {e.variant() for e in self.sub_elements()}
+            variant, = {e.variant() for e in self.sub_elements}
             return variant
         except ValueError:
             return None
@@ -126,3 +128,13 @@ def shortstr(self):
         """Short pretty-print."""
         return "TensorProductElement(%s, cell=%s)" \
             % (', '.join([e.shortstr() for e in self._sub_elements]), str(self._cell))
+
+    @property
+    def embedded_superdegree(self):
+        """Doc."""
+        return sum(self.degree())
+
+    @property
+    def embedded_subdegree(self):
+        """Doc."""
+        return min(self.degree())
diff --git a/ufl/mathfunctions.py b/ufl/mathfunctions.py
index 24ab0354d..704304d6c 100644
--- a/ufl/mathfunctions.py
+++ b/ufl/mathfunctions.py
@@ -8,15 +8,15 @@
 # Modified by Anders Logg, 2008
 # Modified by Kristian B. Oelgaard, 2011
 
-import math
 import cmath
+import math
 import numbers
 import warnings
 
+from ufl.constantvalue import (ComplexValue, ConstantValue, FloatValue, IntValue, RealValue, Zero, as_ufl,
+                               is_true_ufl_scalar)
 from ufl.core.operator import Operator
 from ufl.core.ufl_type import ufl_type
-from ufl.constantvalue import (is_true_ufl_scalar, Zero, RealValue, FloatValue, IntValue, ComplexValue,
-                               ConstantValue, as_ufl)
 
 """
 TODO: Include additional functions available in <cmath> (need derivatives as well):
diff --git a/ufl/matrix.py b/ufl/matrix.py
index 7cf8e55bd..7fb1f5c07 100644
--- a/ufl/matrix.py
+++ b/ufl/matrix.py
@@ -7,15 +7,15 @@
 #
 # Modified by Nacime Bouziani, 2021-2022.
 
-from ufl.form import BaseForm
-from ufl.core.ufl_type import ufl_type
 from ufl.argument import Argument
+from ufl.core.ufl_type import ufl_type
+from ufl.form import BaseForm
 from ufl.functionspace import AbstractFunctionSpace
 from ufl.utils.counted import Counted
 
-
 # --- The Matrix class represents a matrix, an assembled two form ---
 
+
 @ufl_type()
 class Matrix(BaseForm, Counted):
     """An assemble linear operator between two function spaces."""
@@ -68,6 +68,7 @@ def _analyze_form_arguments(self):
     def _analyze_domains(self):
         """Analyze which domains can be found in a Matrix."""
         from ufl.domain import join_domains
+
         # Collect unique domains
         self._domains = join_domains([fs.ufl_domain() for fs in self._ufl_function_spaces])
 
diff --git a/ufl/measure.py b/ufl/measure.py
index e457842ab..97d719501 100644
--- a/ufl/measure.py
+++ b/ufl/measure.py
@@ -10,16 +10,14 @@
 # Modified by Massimiliano Leoni, 2016.
 
 import numbers
-
 from itertools import chain
 
-from ufl.core.expr import Expr
 from ufl.checks import is_true_ufl_scalar
 from ufl.constantvalue import as_ufl
-from ufl.domain import as_domain, AbstractDomain, extract_domains
+from ufl.core.expr import Expr
+from ufl.domain import AbstractDomain, as_domain, extract_domains
 from ufl.protocols import id_or_none
 
-
 # Export list for ufl.classes
 __all_classes__ = ["Measure", "MeasureSum", "MeasureProduct"]
 
@@ -123,9 +121,11 @@ def __init__(self,
         self._integral_type = as_integral_type(integral_type)
 
         # Check that we either have a proper AbstractDomain or none
-        self._domain = None if domain is None else as_domain(domain)
-        if not (self._domain is None or isinstance(self._domain, AbstractDomain)):
-            raise ValueError("Invalid domain.")
+        if domain is not None:
+            domain = as_domain(domain)
+            if not isinstance(domain, AbstractDomain):
+                raise ValueError("Invalid domain.")
+        self._domain = domain
 
         # Store subdomain data
         self._subdomain_data = subdomain_data
@@ -231,11 +231,11 @@ def __call__(self, subdomain_id=None, metadata=None, domain=None,
         # over entire domain.  To do this we need to hijack the first
         # argument:
         if subdomain_id is not None and (
-            isinstance(subdomain_id, AbstractDomain) or hasattr(subdomain_id, 'ufl_domain')
+            isinstance(subdomain_id, AbstractDomain) or hasattr(subdomain_id, "ufl_domain")
         ):
             if domain is not None:
                 raise ValueError("Ambiguous: setting domain both as keyword argument and first argument.")
-            subdomain_id, domain = "everywhere", as_domain(subdomain_id)
+            subdomain_id, domain = "everywhere", subdomain_id
 
         # If degree or scheme is set, inject into metadata. This is a
         # quick fix to enable the dx(..., degree=3) notation.
@@ -344,8 +344,8 @@ def __rmul__(self, integrand):
         Integration properties are taken from this Measure object.
         """
         # Avoid circular imports
-        from ufl.integral import Integral
         from ufl.form import Form
+        from ufl.integral import Integral
 
         # Allow python literals: 1*dx and 1.0*dx
         if isinstance(integrand, (int, float)):
diff --git a/ufl/objects.py b/ufl/objects.py
index 7e740cf72..fc57ea2ae 100644
--- a/ufl/objects.py
+++ b/ufl/objects.py
@@ -8,10 +8,9 @@
 # Modified by Anders Logg, 2008
 # Modified by Kristian Oelgaard, 2009
 
-from ufl.core.multiindex import indices
 from ufl.cell import Cell
-from ufl.measure import Measure
-from ufl.measure import integral_type_to_measure_name
+from ufl.core.multiindex import indices
+from ufl.measure import Measure, integral_type_to_measure_name
 
 # Default indices
 i, j, k, l = indices(4)  # noqa: E741
diff --git a/ufl/operators.py b/ufl/operators.py
index 8f99d501b..9dc5add03 100644
--- a/ufl/operators.py
+++ b/ufl/operators.py
@@ -14,30 +14,27 @@
 # Modified by Kristian B. Oelgaard, 2011
 # Modified by Massimiliano Leoni, 2016.
 
-import warnings
 import operator
+import warnings
 
-from ufl.form import Form
-from ufl.constantvalue import Zero, RealValue, ComplexValue, as_ufl
-from ufl.differentiation import VariableDerivative, Grad, Div, Curl, NablaGrad, NablaDiv
-from ufl.tensoralgebra import (
-    Transposed, Inner, Outer, Dot, Cross, Perp,
-    Determinant, Inverse, Cofactor, Trace, Deviatoric, Skew, Sym)
-from ufl.coefficient import Coefficient
-from ufl.variable import Variable
-from ufl.tensors import as_tensor, as_matrix, as_vector, ListTensor
-from ufl.conditional import (
-    EQ, NE, AndCondition, OrCondition, NotCondition, Conditional, MaxValue, MinValue)
-from ufl.algebra import Conj, Real, Imag
-from ufl.mathfunctions import (
-    Sqrt, Exp, Ln, Erf, Cos, Sin, Tan, Cosh, Sinh, Tanh, Acos, Asin, Atan, Atan2,
-    BesselJ, BesselY, BesselI, BesselK)
+from ufl import sobolevspace
+from ufl.algebra import Conj, Imag, Real
 from ufl.averaging import CellAvg, FacetAvg
-from ufl.indexed import Indexed
-from ufl.geometry import SpatialCoordinate, FacetNormal
 from ufl.checks import is_cellwise_constant
+from ufl.coefficient import Coefficient
+from ufl.conditional import EQ, NE, AndCondition, Conditional, MaxValue, MinValue, NotCondition, OrCondition
+from ufl.constantvalue import ComplexValue, RealValue, Zero, as_ufl
+from ufl.differentiation import Curl, Div, Grad, NablaDiv, NablaGrad, VariableDerivative
 from ufl.domain import extract_domains
-from ufl import sobolevspace
+from ufl.form import Form
+from ufl.geometry import FacetNormal, SpatialCoordinate
+from ufl.indexed import Indexed
+from ufl.mathfunctions import (Acos, Asin, Atan, Atan2, BesselI, BesselJ, BesselK, BesselY, Cos, Cosh, Erf, Exp, Ln,
+                               Sin, Sinh, Sqrt, Tan, Tanh)
+from ufl.tensoralgebra import (Cofactor, Cross, Determinant, Deviatoric, Dot, Inner, Inverse, Outer, Perp, Skew, Sym,
+                               Trace, Transposed)
+from ufl.tensors import ListTensor, as_matrix, as_tensor, as_vector
+from ufl.variable import Variable
 
 # --- Basic operators ---
 
@@ -671,7 +668,12 @@ def exterior_derivative(f):
             raise NotImplementedError
         index = int(indices[0])
         element = expression.ufl_element()
-        element = element.extract_component(index)[1]
+        while index != 0:
+            for e in element.sub_elements:
+                if e.value_size > index:
+                    element = e
+                    break
+                index -= e.value_size
     elif isinstance(f, ListTensor):
         f0 = f.ufl_operands[0]
         f0expr, f0indices = f0.ufl_operands  # FIXME: Assumption on type of f0!!!
@@ -679,7 +681,12 @@ def exterior_derivative(f):
             raise NotImplementedError
         index = int(f0indices[0])
         element = f0expr.ufl_element()
-        element = element.extract_component(index)[1]
+        while index != 0:
+            for e in element.sub_elements:
+                if e.value_size > index:
+                    element = e
+                    break
+                index -= e.value_size
     else:
         try:
             element = f.ufl_element()
@@ -687,7 +694,7 @@ def exterior_derivative(f):
             raise ValueError(f"Unable to determine element from {f}")
 
     gdim = element.cell().geometric_dimension()
-    space = element.sobolev_space()
+    space = element.sobolev_space
 
     if space == sobolevspace.L2:
         return f
diff --git a/ufl/precedence.py b/ufl/precedence.py
index 2c3b705a1..0aea48b20 100644
--- a/ufl/precedence.py
+++ b/ufl/precedence.py
@@ -8,9 +8,9 @@
 
 import warnings
 
-
 # FIXME: This code is crap...
 
+
 def parstr(child, parent, pre="(", post=")", format=str):
     """Parstr."""
     # Execute when needed instead of on import, which leads to all
@@ -41,8 +41,8 @@ def parstr(child, parent, pre="(", post=")", format=str):
 
 def build_precedence_list():
     """Build precedence list."""
-    from ufl.classes import (Operator, Terminal, Sum, IndexSum, Product, Division, Power,
-                             MathFunction, BesselFunction, Abs, Indexed)
+    from ufl.classes import (Abs, BesselFunction, Division, Indexed, IndexSum, MathFunction, Operator, Power, Product,
+                             Sum, Terminal)
 
     # TODO: Fill in other types...
     # Power <= Transposed
@@ -74,7 +74,7 @@ def build_precedence_mapping(precedence_list):
 
     Utility function used by some external code.
     """
-    from ufl.classes import Expr, all_ufl_classes, abstract_classes
+    from ufl.classes import Expr, abstract_classes, all_ufl_classes
     pm = {}
     missing = set()
     # Assign integer values for each precedence level
diff --git a/ufl/pullback.py b/ufl/pullback.py
new file mode 100644
index 000000000..dee12f0ce
--- /dev/null
+++ b/ufl/pullback.py
@@ -0,0 +1,552 @@
+"""Pull back and push forward maps."""
+# Copyright (C) 2023 Matthew Scroggs, David Ham, Garth Wells
+#
+# This file is part of UFL (https://www.fenicsproject.org)
+#
+# SPDX-License-Identifier:    LGPL-3.0-or-later
+
+from __future__ import annotations
+
+import typing
+from abc import ABC, abstractmethod, abstractproperty
+from typing import TYPE_CHECKING
+
+import numpy as np
+
+from ufl.core.expr import Expr
+from ufl.core.multiindex import indices
+from ufl.domain import extract_unique_domain
+from ufl.tensors import as_tensor
+
+if TYPE_CHECKING:
+    from ufl.finiteelement import AbstractFiniteElement as _AbstractFiniteElement
+
+__all_classes__ = ["NonStandardPullbackException", "AbstractPullback", "IdentityPullback",
+                   "ContravariantPiola", "CovariantPiola", "L2Piola", "DoubleContravariantPiola",
+                   "DoubleCovariantPiola", "MixedPullback", "SymmetricPullback",
+                   "PhysicalPullback", "CustomPullback", "UndefinedPullback"]
+
+
+class NonStandardPullbackException(BaseException):
+    """Exception to raise if a map is non-standard."""
+    pass
+
+
+class AbstractPullback(ABC):
+    """An abstract pull back."""
+
+    @abstractmethod
+    def __repr__(self) -> str:
+        """Return a representation of the object."""
+
+    @abstractmethod
+    def physical_value_shape(self, element) -> typing.Tuple[int, ...]:
+        """Get the physical value shape when this pull back is applied to an element.
+
+        Args:
+            element: The element that the pull back is applied to
+
+        Returns:
+            The value shape when the pull back is applied to the given element
+        """
+
+    @abstractproperty
+    def is_identity(self) -> bool:
+        """Is this pull back the identity (or the identity applied to mutliple components)."""
+
+    def apply(self, expr: Expr) -> Expr:
+        """Apply the pull back.
+
+        Args:
+            expr: A function on a physical cell
+
+        Returns: The function pulled back to the reference cell
+        """
+        raise NonStandardPullbackException()
+
+
+class IdentityPullback(AbstractPullback):
+    """The identity pull back."""
+
+    def __repr__(self) -> str:
+        """Return a representation of the object."""
+        return "IdentityPullback()"
+
+    @property
+    def is_identity(self) -> bool:
+        """Is this pull back the identity (or the identity applied to mutliple components)."""
+        return True
+
+    def apply(self, expr):
+        """Apply the pull back.
+
+        Args:
+            expr: A function on a physical cell
+
+        Returns: The function pulled back to the reference cell
+        """
+        return expr
+
+    def physical_value_shape(self, element) -> typing.Tuple[int, ...]:
+        """Get the physical value shape when this pull back is applied to an element.
+
+        Args:
+            element: The element that the pull back is applied to
+
+        Returns:
+            The value shape when the pull back is applied to the given element
+        """
+        return element.reference_value_shape
+
+
+class ContravariantPiola(AbstractPullback):
+    """The contravariant Piola pull back."""
+
+    def __repr__(self) -> str:
+        """Return a representation of the object."""
+        return "ContravariantPiola()"
+
+    @property
+    def is_identity(self) -> bool:
+        """Is this pull back the identity (or the identity applied to mutliple components)."""
+        return False
+
+    def apply(self, expr):
+        """Apply the pull back.
+
+        Args:
+            expr: A function on a physical cell
+
+        Returns: The function pulled back to the reference cell
+        """
+        from ufl.classes import Jacobian, JacobianDeterminant
+
+        domain = extract_unique_domain(expr)
+        J = Jacobian(domain)
+        detJ = JacobianDeterminant(J)
+        transform = (1.0 / detJ) * J
+        # Apply transform "row-wise" to TensorElement(PiolaMapped, ...)
+        *k, i, j = indices(len(expr.ufl_shape) + 1)
+        kj = (*k, j)
+        return as_tensor(transform[i, j] * expr[kj], (*k, i))
+
+    def physical_value_shape(self, element) -> typing.Tuple[int, ...]:
+        """Get the physical value shape when this pull back is applied to an element.
+
+        Args:
+            element: The element that the pull back is applied to
+
+        Returns:
+            The value shape when the pull back is applied to the given element
+        """
+        gdim = element.cell.geometric_dimension()
+        return (gdim, ) + element.reference_value_shape[1:]
+
+
+class CovariantPiola(AbstractPullback):
+    """The covariant Piola pull back."""
+
+    def __repr__(self) -> str:
+        """Return a representation of the object."""
+        return "CovariantPiola()"
+
+    @property
+    def is_identity(self) -> bool:
+        """Is this pull back the identity (or the identity applied to mutliple components)."""
+        return False
+
+    def apply(self, expr):
+        """Apply the pull back.
+
+        Args:
+            expr: A function on a physical cell
+
+        Returns: The function pulled back to the reference cell
+        """
+        from ufl.classes import JacobianInverse
+
+        domain = extract_unique_domain(expr)
+        K = JacobianInverse(domain)
+        # Apply transform "row-wise" to TensorElement(PiolaMapped, ...)
+        *k, i, j = indices(len(expr.ufl_shape) + 1)
+        kj = (*k, j)
+        return as_tensor(K[j, i] * expr[kj], (*k, i))
+
+    def physical_value_shape(self, element) -> typing.Tuple[int, ...]:
+        """Get the physical value shape when this pull back is applied to an element.
+
+        Args:
+            element: The element that the pull back is applied to
+
+        Returns:
+            The value shape when the pull back is applied to the given element
+        """
+        gdim = element.cell.geometric_dimension()
+        return (gdim, ) + element.reference_value_shape[1:]
+
+
+class L2Piola(AbstractPullback):
+    """The L2 Piola pull back."""
+
+    def __repr__(self) -> str:
+        """Return a representation of the object."""
+        return "L2Piola()"
+
+    @property
+    def is_identity(self) -> bool:
+        """Is this pull back the identity (or the identity applied to mutliple components)."""
+        return False
+
+    def apply(self, expr):
+        """Apply the pull back.
+
+        Args:
+            expr: A function on a physical cell
+
+        Returns: The function pulled back to the reference cell
+        """
+        from ufl.classes import JacobianDeterminant
+
+        domain = extract_unique_domain(expr)
+        detJ = JacobianDeterminant(domain)
+        return expr / detJ
+
+    def physical_value_shape(self, element) -> typing.Tuple[int, ...]:
+        """Get the physical value shape when this pull back is applied to an element.
+
+        Args:
+            element: The element that the pull back is applied to
+
+        Returns:
+            The value shape when the pull back is applied to the given element
+        """
+        return element.reference_value_shape
+
+
+class DoubleContravariantPiola(AbstractPullback):
+    """The double contravariant Piola pull back."""
+
+    def __repr__(self) -> str:
+        """Return a representation of the object."""
+        return "DoubleContravariantPiola()"
+
+    @property
+    def is_identity(self) -> bool:
+        """Is this pull back the identity (or the identity applied to mutliple components)."""
+        return False
+
+    def apply(self, expr):
+        """Apply the pull back.
+
+        Args:
+            expr: A function on a physical cell
+
+        Returns: The function pulled back to the reference cell
+        """
+        from ufl.classes import Jacobian, JacobianDeterminant
+
+        domain = extract_unique_domain(expr)
+        J = Jacobian(domain)
+        detJ = JacobianDeterminant(J)
+        # Apply transform "row-wise" to TensorElement(PiolaMapped, ...)
+        *k, i, j, m, n = indices(len(expr.ufl_shape) + 2)
+        kmn = (*k, m, n)
+        return as_tensor((1.0 / detJ)**2 * J[i, m] * expr[kmn] * J[j, n], (*k, i, j))
+
+    def physical_value_shape(self, element) -> typing.Tuple[int, ...]:
+        """Get the physical value shape when this pull back is applied to an element.
+
+        Args:
+            element: The element that the pull back is applied to
+
+        Returns:
+            The value shape when the pull back is applied to the given element
+        """
+        gdim = element.cell.geometric_dimension()
+        return (gdim, gdim)
+
+
+class DoubleCovariantPiola(AbstractPullback):
+    """The double covariant Piola pull back."""
+
+    def __repr__(self) -> str:
+        """Return a representation of the object."""
+        return "DoubleCovariantPiola()"
+
+    @property
+    def is_identity(self) -> bool:
+        """Is this pull back the identity (or the identity applied to mutliple components)."""
+        return False
+
+    def apply(self, expr):
+        """Apply the pull back.
+
+        Args:
+            expr: A function on a physical cell
+
+        Returns: The function pulled back to the reference cell
+        """
+        from ufl.classes import JacobianInverse
+
+        domain = extract_unique_domain(expr)
+        K = JacobianInverse(domain)
+        # Apply transform "row-wise" to TensorElement(PiolaMapped, ...)
+        *k, i, j, m, n = indices(len(expr.ufl_shape) + 2)
+        kmn = (*k, m, n)
+        return as_tensor(K[m, i] * expr[kmn] * K[n, j], (*k, i, j))
+
+    def physical_value_shape(self, element) -> typing.Tuple[int, ...]:
+        """Get the physical value shape when this pull back is applied to an element.
+
+        Args:
+            element: The element that the pull back is applied to
+
+        Returns:
+            The value shape when the pull back is applied to the given element
+        """
+        gdim = element.cell.geometric_dimension()
+        return (gdim, gdim)
+
+
+class MixedPullback(AbstractPullback):
+    """Pull back for a mixed element."""
+
+    def __init__(self, element: _AbstractFiniteElement):
+        """Initalise.
+
+        Args:
+            element: The mixed element
+        """
+        self._element = element
+
+    def __repr__(self) -> str:
+        """Return a representation of the object."""
+        return f"MixedPullback({self._element!r})"
+
+    @property
+    def is_identity(self) -> bool:
+        """Is this pull back the identity (or the identity applied to mutliple components)."""
+        return all(e.pullback.is_identity for e in self._element.sub_elements)
+
+    def apply(self, expr):
+        """Apply the pull back.
+
+        Args:
+            expr: A function on a physical cell
+
+        Returns: The function pulled back to the reference cell
+        """
+        rflat = [expr[idx] for idx in np.ndindex(expr.ufl_shape)]
+        g_components = []
+        offset = 0
+        # For each unique piece in reference space, apply the appropriate pullback
+        for subelem in self._element.sub_elements:
+            rsub = as_tensor(np.asarray(
+                rflat[offset: offset + subelem.reference_value_size]
+            ).reshape(subelem.reference_value_shape))
+            rmapped = subelem.pullback.apply(rsub)
+            # Flatten into the pulled back expression for the whole thing
+            g_components.extend([rmapped[idx] for idx in np.ndindex(rmapped.ufl_shape)])
+            offset += subelem.reference_value_size
+        # And reshape appropriately
+        f = as_tensor(np.asarray(g_components).reshape(self._element.value_shape))
+        if f.ufl_shape != self._element.value_shape:
+            raise ValueError("Expecting pulled back expression with shape "
+                             f"'{self._element.value_shape}', got '{f.ufl_shape}'")
+        return f
+
+    def physical_value_shape(self, element) -> typing.Tuple[int, ...]:
+        """Get the physical value shape when this pull back is applied to an element.
+
+        Args:
+            element: The element that the pull back is applied to
+
+        Returns:
+            The value shape when the pull back is applied to the given element
+        """
+        assert element == self._element
+        dim = sum(e.value_size for e in self._element.sub_elements)
+        return (dim, )
+
+
+class SymmetricPullback(AbstractPullback):
+    """Pull back for an element with symmetry."""
+
+    def __init__(self, element: _AbstractFiniteElement, symmetry: typing.Dict[typing.tuple[int, ...], int]):
+        """Initalise.
+
+        Args:
+            element: The element
+            symmetry: A dictionary mapping from the component in physical space to the local component
+        """
+        self._element = element
+        self._symmetry = symmetry
+
+        self._sub_element_value_shape = element.sub_elements[0].value_shape
+        for e in element.sub_elements:
+            if e.value_shape != self._sub_element_value_shape:
+                raise ValueError("Sub-elements must all have the same value shape.")
+        self._block_shape = tuple(i + 1 for i in max(symmetry.keys()))
+
+    def __repr__(self) -> str:
+        """Return a representation of the object."""
+        return f"SymmetricPullback({self._element!r}, {self._symmetry!r})"
+
+    @property
+    def is_identity(self) -> bool:
+        """Is this pull back the identity (or the identity applied to mutliple components)."""
+        return all(e.pullback.is_identity for e in self._element.sub_elements)
+
+    def apply(self, expr):
+        """Apply the pull back.
+
+        Args:
+            expr: A function on a physical cell
+
+        Returns: The function pulled back to the reference cell
+        """
+        rflat = [expr[idx] for idx in np.ndindex(expr.ufl_shape)]
+        g_components = []
+        offsets = [0]
+        for subelem in self._element.sub_elements:
+            offsets.append(offsets[-1] + subelem.reference_value_size)
+        # For each unique piece in reference space, apply the appropriate pullback
+        for component in np.ndindex(self._block_shape):
+            i = self._symmetry[component]
+            subelem = self._element.sub_elements[i]
+            rsub = as_tensor(np.asarray(
+                rflat[offsets[i]:offsets[i+1]]
+            ).reshape(subelem.reference_value_shape))
+            rmapped = subelem.pullback.apply(rsub)
+            # Flatten into the pulled back expression for the whole thing
+            g_components.extend([rmapped[idx] for idx in np.ndindex(rmapped.ufl_shape)])
+        # And reshape appropriately
+        f = as_tensor(np.asarray(g_components).reshape(self._element.value_shape))
+        if f.ufl_shape != self._element.value_shape:
+            raise ValueError(f"Expecting pulled back expression with shape "
+                             f"'{self._element.value_shape}', got '{f.ufl_shape}'")
+        return f
+
+    def physical_value_shape(self, element) -> typing.Tuple[int, ...]:
+        """Get the physical value shape when this pull back is applied to an element.
+
+        Args:
+            element: The element that the pull back is applied to
+
+        Returns:
+            The value shape when the pull back is applied to the given element
+        """
+        assert element == self._element
+        return tuple(i + 1 for i in max(self._symmetry.keys()))
+
+
+class PhysicalPullback(AbstractPullback):
+    """Physical pull back.
+
+    This should probably be removed.
+    """
+
+    def __repr__(self) -> str:
+        """Return a representation of the object."""
+        return "PhysicalPullback()"
+
+    @property
+    def is_identity(self) -> bool:
+        """Is this pull back the identity (or the identity applied to mutliple components)."""
+        return True
+
+    def apply(self, expr):
+        """Apply the pull back.
+
+        Args:
+            expr: A function on a physical cell
+
+        Returns: The function pulled back to the reference cell
+        """
+        return expr
+
+    def physical_value_shape(self, element) -> typing.Tuple[int, ...]:
+        """Get the physical value shape when this pull back is applied to an element.
+
+        Args:
+            element: The element that the pull back is applied to
+
+        Returns:
+            The value shape when the pull back is applied to the given element
+        """
+        raise NotImplementedError()
+
+
+class CustomPullback(AbstractPullback):
+    """Custom pull back.
+
+    This should probably be removed.
+    """
+
+    def __repr__(self) -> str:
+        """Return a representation of the object."""
+        return "CustomPullback()"
+
+    @property
+    def is_identity(self) -> bool:
+        """Is this pull back the identity (or the identity applied to mutliple components)."""
+        return True
+
+    def apply(self, expr):
+        """Apply the pull back.
+
+        Args:
+            expr: A function on a physical cell
+
+        Returns: The function pulled back to the reference cell
+        """
+        return expr
+
+    def physical_value_shape(self, element) -> typing.Tuple[int, ...]:
+        """Get the physical value shape when this pull back is applied to an element.
+
+        Args:
+            element: The element that the pull back is applied to
+
+        Returns:
+            The value shape when the pull back is applied to the given element
+        """
+        raise NotImplementedError()
+
+
+class UndefinedPullback(AbstractPullback):
+    """Undefined pull back.
+
+    This should probably be removed.
+    """
+
+    def __repr__(self) -> str:
+        """Return a representation of the object."""
+        return "UndefinedPullback()"
+
+    @property
+    def is_identity(self) -> bool:
+        """Is this pull back the identity (or the identity applied to mutliple components)."""
+        return True
+
+    def physical_value_shape(self, element) -> typing.Tuple[int, ...]:
+        """Get the physical value shape when this pull back is applied to an element.
+
+        Args:
+            element: The element that the pull back is applied to
+
+        Returns:
+            The value shape when the pull back is applied to the given element
+        """
+        raise NotImplementedError()
+
+
+identity_pullback = IdentityPullback()
+covariant_piola = CovariantPiola()
+contravariant_piola = ContravariantPiola()
+l2_piola = L2Piola()
+double_covariant_piola = DoubleCovariantPiola()
+double_contravariant_piola = DoubleContravariantPiola()
+physical_pullback = PhysicalPullback()
+custom_pullback = CustomPullback()
+undefined_pullback = UndefinedPullback()
diff --git a/ufl/referencevalue.py b/ufl/referencevalue.py
index c1d13bcf1..5a4c5bb11 100644
--- a/ufl/referencevalue.py
+++ b/ufl/referencevalue.py
@@ -5,9 +5,9 @@
 #
 # SPDX-License-Identifier:    LGPL-3.0-or-later
 
-from ufl.core.ufl_type import ufl_type
 from ufl.core.operator import Operator
 from ufl.core.terminal import FormArgument
+from ufl.core.ufl_type import ufl_type
 
 
 @ufl_type(num_ops=1,
@@ -28,7 +28,7 @@ def __init__(self, f):
     @property
     def ufl_shape(self):
         """Get the UFL shape."""
-        return self.ufl_operands[0].ufl_element().reference_value_shape()
+        return self.ufl_operands[0].ufl_element().reference_value_shape
 
     def evaluate(self, x, mapping, component, index_values, derivatives=()):
         """Get child from mapping and return the component asked for."""
diff --git a/ufl/restriction.py b/ufl/restriction.py
index 77b03c71a..2871cd53f 100644
--- a/ufl/restriction.py
+++ b/ufl/restriction.py
@@ -6,12 +6,12 @@
 # SPDX-License-Identifier:    LGPL-3.0-or-later
 
 from ufl.core.operator import Operator
-from ufl.precedence import parstr
 from ufl.core.ufl_type import ufl_type
-
+from ufl.precedence import parstr
 
 # --- Restriction operators ---
 
+
 @ufl_type(is_abstract=True,
           num_ops=1,
           inherit_shape_from_operand=0,
diff --git a/ufl/sobolevspace.py b/ufl/sobolevspace.py
index b697a0685..9500ae422 100644
--- a/ufl/sobolevspace.py
+++ b/ufl/sobolevspace.py
@@ -18,6 +18,8 @@
 from functools import total_ordering
 from math import inf, isinf
 
+__all_classes__ = ["SobolevSpace", "DirectionalSobolevSpace"]
+
 
 @total_ordering
 class SobolevSpace(object):
@@ -83,24 +85,13 @@ def __contains__(self, other):
             raise TypeError("Unable to test for inclusion of a "
                             "SobolevSpace in another SobolevSpace. "
                             "Did you mean to use <= instead?")
-        return other.sobolev_space() == self or self in other.sobolev_space().parents
+        return (other.sobolev_space == self or
+                self in other.sobolev_space.parents)
 
     def __lt__(self, other):
         """In common with intrinsic Python sets, < indicates "is a proper subset of"."""
         return other in self.parents
 
-    def __call__(self, element):
-        """Syntax shortcut to create a HDivElement or HCurlElement."""
-        if self.name == "HDiv":
-            from ufl.finiteelement import HDivElement
-            return HDivElement(element)
-        elif self.name == "HCurl":
-            from ufl.finiteelement import HCurlElement
-            return HCurlElement(element)
-        raise NotImplementedError(
-            "SobolevSpace has no call operator (only the specific HDiv and HCurl instances)."
-        )
-
 
 @total_ordering
 class DirectionalSobolevSpace(SobolevSpace):
@@ -140,9 +131,9 @@ def __contains__(self, other):
             raise TypeError("Unable to test for inclusion of a "
                             "SobolevSpace in another SobolevSpace. "
                             "Did you mean to use <= instead?")
-        return (other.sobolev_space() == self or all(
-            self[i] in other.sobolev_space().parents
-            for i in self._spatial_indices))
+        return (other.sobolev_space == self or
+                all(self[i] in other.sobolev_space.parents
+                    for i in self._spatial_indices))
 
     def __eq__(self, other):
         """Check equality."""
diff --git a/ufl/sorting.py b/ufl/sorting.py
index 445fa0739..5efe2a44a 100644
--- a/ufl/sorting.py
+++ b/ufl/sorting.py
@@ -14,7 +14,6 @@
 
 from functools import cmp_to_key
 
-from ufl.core.expr import Expr
 from ufl.argument import Argument
 from ufl.coefficient import Coefficient
 from ufl.core.multiindex import FixedIndex, MultiIndex
@@ -98,7 +97,7 @@ def _cmp_terminal_by_repr(a, b):
 
 
 # Hack up a MultiFunction-like type dispatch for terminal comparisons
-_terminal_cmps = [_cmp_terminal_by_repr] * Expr._ufl_num_typecodes_
+_terminal_cmps = {}
 _terminal_cmps[MultiIndex._ufl_typecode_] = _cmp_multi_index
 _terminal_cmps[Argument._ufl_typecode_] = _cmp_argument
 _terminal_cmps[Coefficient._ufl_typecode_] = _cmp_coefficient
@@ -120,7 +119,11 @@ def cmp_expr(a, b):
         # Now we know that the type is the same, check further based
         # on type specific properties.
         if a._ufl_is_terminal_:
-            c = _terminal_cmps[x](a, b)
+            if x in _terminal_cmps:
+                c = _terminal_cmps[x](a, b)
+            else:
+                c = _cmp_terminal_by_repr(a, b)
+
             if c:
                 return c
         else:
diff --git a/ufl/split_functions.py b/ufl/split_functions.py
index f285f8ea5..886c98915 100644
--- a/ufl/split_functions.py
+++ b/ufl/split_functions.py
@@ -7,12 +7,11 @@
 #
 # Modified by Anders Logg, 2008
 
-from ufl.utils.sequences import product
-from ufl.finiteelement import TensorElement
-from ufl.tensors import as_vector, as_matrix, ListTensor
 from ufl.indexed import Indexed
 from ufl.permutation import compute_indices
+from ufl.tensors import ListTensor, as_matrix, as_vector
 from ufl.utils.indexflattening import flatten_multiindex, shape_to_strides
+from ufl.utils.sequences import product
 
 
 def split(v):
@@ -50,19 +49,15 @@ def split(v):
 
     # Special case: simple element, just return function in a tuple
     element = v.ufl_element()
-    if element.num_sub_elements() == 0:
+    if element.num_sub_elements == 0:
         assert end is None
         return (v,)
 
-    if isinstance(element, TensorElement):
-        if element.symmetry():
-            raise ValueError("Split not implemented for symmetric tensor elements.")
-
     if len(v.ufl_shape) != 1:
         raise ValueError("Don't know how to split tensor valued mixed functions without flattened index space.")
 
     # Compute value size and set default range end
-    value_size = product(element.value_shape())
+    value_size = element.value_size
     if end is None:
         end = value_size
     else:
@@ -70,19 +65,22 @@ def split(v):
         # corresponding to beginning of range
         j = begin
         while True:
-            sub_i, j = element.extract_subelement_component(j)
-            element = element.sub_elements()[sub_i]
+            for e in element.sub_elements:
+                if j < e.value_size:
+                    element = e
+                    break
+                j -= e.value_size
             # Then break when we find the subelement that covers the whole range
-            if product(element.value_shape()) == (end - begin):
+            if element.value_size == (end - begin):
                 break
 
     # Build expressions representing the subfunction of v for each subelement
     offset = begin
     sub_functions = []
-    for i, e in enumerate(element.sub_elements()):
+    for i, e in enumerate(element.sub_elements):
         # Get shape, size, indices, and v components
         # corresponding to subelement value
-        shape = e.value_shape()
+        shape = e.value_shape
         strides = shape_to_strides(shape)
         rank = len(shape)
         sub_size = product(shape)
diff --git a/ufl/tensoralgebra.py b/ufl/tensoralgebra.py
index a0291d060..733b24388 100644
--- a/ufl/tensoralgebra.py
+++ b/ufl/tensoralgebra.py
@@ -5,13 +5,13 @@
 #
 # SPDX-License-Identifier:    LGPL-3.0-or-later
 
+from ufl.algebra import Conj, Operator
+from ufl.constantvalue import Zero
 from ufl.core.expr import ufl_err_str
 from ufl.core.ufl_type import ufl_type
-from ufl.constantvalue import Zero
-from ufl.algebra import Operator, Conj
+from ufl.index_combination_utils import merge_nonoverlapping_indices
 from ufl.precedence import parstr
 from ufl.sorting import sorted_expr
-from ufl.index_combination_utils import merge_nonoverlapping_indices
 
 # Algebraic operations on tensors:
 # FloatValues:
diff --git a/ufl/tensors.py b/ufl/tensors.py
index d89045ce9..2fe829990 100644
--- a/ufl/tensors.py
+++ b/ufl/tensors.py
@@ -7,17 +7,17 @@
 #
 # Modified by Massimiliano Leoni, 2016.
 
-from ufl.core.ufl_type import ufl_type
+from ufl.constantvalue import Zero, as_ufl
 from ufl.core.expr import Expr
+from ufl.core.multiindex import FixedIndex, Index, MultiIndex, indices
 from ufl.core.operator import Operator
-from ufl.constantvalue import as_ufl, Zero
-from ufl.core.multiindex import Index, FixedIndex, MultiIndex, indices
-from ufl.indexed import Indexed
+from ufl.core.ufl_type import ufl_type
 from ufl.index_combination_utils import remove_indices
-
+from ufl.indexed import Indexed
 
 # --- Classes representing tensors of UFL expressions ---
 
+
 @ufl_type(is_shaping=True, num_ops="varying", inherit_indices_from_operand=0)
 class ListTensor(Operator):
     """Wraps a list of expressions into a tensor valued expression of one higher rank."""
diff --git a/ufl/utils/sequences.py b/ufl/utils/sequences.py
index 20ebe1dc9..9904287c4 100644
--- a/ufl/utils/sequences.py
+++ b/ufl/utils/sequences.py
@@ -7,6 +7,7 @@
 # SPDX-License-Identifier:    LGPL-3.0-or-later
 
 from functools import reduce
+
 import numpy
 
 
diff --git a/ufl/variable.py b/ufl/variable.py
index ee8587119..84a795434 100644
--- a/ufl/variable.py
+++ b/ufl/variable.py
@@ -8,12 +8,12 @@
 #
 # SPDX-License-Identifier:    LGPL-3.0-or-later
 
-from ufl.utils.counted import Counted
+from ufl.constantvalue import as_ufl
 from ufl.core.expr import Expr
-from ufl.core.ufl_type import ufl_type
-from ufl.core.terminal import Terminal
 from ufl.core.operator import Operator
-from ufl.constantvalue import as_ufl
+from ufl.core.terminal import Terminal
+from ufl.core.ufl_type import ufl_type
+from ufl.utils.counted import Counted
 
 
 @ufl_type()


Test patch:
diff --git a/.github/workflows/tsfc-tests.yml b/.github/workflows/tsfc-tests.yml
index 06b10a13d..a861b4728 100644
--- a/.github/workflows/tsfc-tests.yml
+++ b/.github/workflows/tsfc-tests.yml
@@ -32,7 +32,7 @@ jobs:
         with:
           path: ./tsfc
           repository: firedrakeproject/tsfc
-          ref: master
+          ref: mscroggs/newfl-legacy
       - name: Install tsfc
         run: |
           cd tsfc
diff --git a/test/test_algorithms.py b/test/test_algorithms.py
index 9beba0d9c..4b87c2851 100755
--- a/test/test_algorithms.py
+++ b/test/test_algorithms.py
@@ -6,23 +6,26 @@
 
 import pytest
 
-from ufl import (Argument, Coefficient, FacetNormal, FiniteElement, FunctionSpace, Mesh, TestFunction, TrialFunction,
-                 VectorElement, adjoint, div, dot, ds, dx, grad, inner, triangle)
+from ufl import (Argument, Coefficient, FacetNormal, FunctionSpace, Mesh, TestFunction, TrialFunction, adjoint, div,
+                 dot, ds, dx, grad, inner, triangle)
 from ufl.algorithms import (expand_derivatives, expand_indices, extract_arguments, extract_coefficients,
                             extract_elements, extract_unique_elements)
 from ufl.corealg.traversal import post_traversal, pre_traversal, unique_post_traversal, unique_pre_traversal
+from ufl.finiteelement import FiniteElement
+from ufl.pullback import identity_pullback
+from ufl.sobolevspace import H1
 
 # TODO: add more tests, covering all utility algorithms
 
 
 @pytest.fixture(scope='module')
 def element():
-    return FiniteElement("CG", triangle, 1)
+    return FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1)
 
 
 @pytest.fixture(scope='module')
 def domain():
-    return Mesh(VectorElement("CG", triangle, 1))
+    return Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
 
 
 @pytest.fixture(scope='module')
@@ -45,10 +48,10 @@ def coefficients(space):
 
 
 @pytest.fixture
-def forms(arguments, coefficients, space):
+def forms(arguments, coefficients, domain):
     v, u = arguments
     c, f = coefficients
-    n = FacetNormal(space)
+    n = FacetNormal(domain)
     a = u * v * dx
     L = f * v * dx
     b = u * v * dx(0) + inner(c * grad(u), grad(v)) * dx(1) + dot(n, grad(u)) * v * ds + f * v * dx
@@ -64,13 +67,13 @@ def test_extract_coefficients_vs_fixture(coefficients, forms):
     assert coefficients == tuple(extract_coefficients(forms[2]))
 
 
-def test_extract_elements_and_extract_unique_elements(forms, domain):
+def test_extract_elements_and_extract_unique_elements(forms, element, domain):
     b = forms[2]
     integrals = b.integrals_by_type("cell")
     integrals[0].integrand()
 
-    element1 = FiniteElement("CG", triangle, 1)
-    element2 = FiniteElement("CG", triangle, 1)
+    element1 = element
+    element2 = element
 
     space1 = FunctionSpace(domain, element1)
     space2 = FunctionSpace(domain, element2)
@@ -83,9 +86,7 @@ def test_extract_elements_and_extract_unique_elements(forms, domain):
     assert extract_unique_elements(a) == (element1,)
 
 
-def test_pre_and_post_traversal(domain):
-    element = FiniteElement("CG", "triangle", 1)
-    space = FunctionSpace(domain, element)
+def test_pre_and_post_traversal(space):
     v = TestFunction(space)
     f = Coefficient(space)
     g = Coefficient(space)
@@ -103,7 +104,7 @@ def test_pre_and_post_traversal(domain):
 
 
 def test_expand_indices(domain):
-    element = FiniteElement("Lagrange", triangle, 2)
+    element = FiniteElement("Lagrange", triangle, 2, (), identity_pullback, H1)
     space = FunctionSpace(domain, element)
     v = TestFunction(space)
     u = TrialFunction(space)
@@ -124,8 +125,8 @@ def evaluate(form):
 def test_adjoint(domain):
     cell = triangle
 
-    V1 = FiniteElement("CG", cell, 1)
-    V2 = FiniteElement("CG", cell, 2)
+    V1 = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1)
+    V2 = FiniteElement("Lagrange", cell, 2, (), identity_pullback, H1)
 
     s1 = FunctionSpace(domain, V1)
     s2 = FunctionSpace(domain, V2)
@@ -139,17 +140,17 @@ def test_adjoint(domain):
     assert u2.number() < v2.number()
 
     a = u * v * dx
-    a_arg_degrees = [arg.ufl_element().degree() for arg in extract_arguments(a)]
+    a_arg_degrees = [arg.ufl_element().embedded_superdegree for arg in extract_arguments(a)]
     assert a_arg_degrees == [2, 1]
 
     b = adjoint(a)
-    b_arg_degrees = [arg.ufl_element().degree() for arg in extract_arguments(b)]
+    b_arg_degrees = [arg.ufl_element().embedded_superdegree for arg in extract_arguments(b)]
     assert b_arg_degrees == [1, 2]
 
     c = adjoint(a, (u2, v2))
-    c_arg_degrees = [arg.ufl_element().degree() for arg in extract_arguments(c)]
+    c_arg_degrees = [arg.ufl_element().embedded_superdegree for arg in extract_arguments(c)]
     assert c_arg_degrees == [1, 2]
 
     d = adjoint(b)
-    d_arg_degrees = [arg.ufl_element().degree() for arg in extract_arguments(d)]
+    d_arg_degrees = [arg.ufl_element().embedded_superdegree for arg in extract_arguments(d)]
     assert d_arg_degrees == [2, 1]
diff --git a/test/test_apply_algebra_lowering.py b/test/test_apply_algebra_lowering.py
index 7b5c6db23..e1f496eb9 100755
--- a/test/test_apply_algebra_lowering.py
+++ b/test/test_apply_algebra_lowering.py
@@ -1,51 +1,60 @@
 import pytest
 
-from ufl import (Coefficient, FiniteElement, FunctionSpace, Index, Mesh, TensorElement, VectorElement, as_tensor,
-                 interval, sqrt, tetrahedron, triangle)
+from ufl import Coefficient, FunctionSpace, Index, Mesh, as_tensor, interval, sqrt, triangle
 from ufl.algorithms.renumbering import renumber_indices
 from ufl.compound_expressions import cross_expr, determinant_expr, inverse_expr
+from ufl.finiteelement import FiniteElement
+from ufl.pullback import identity_pullback
+from ufl.sobolevspace import H1
 
 
 @pytest.fixture
 def A0(request):
     return Coefficient(FunctionSpace(
-        Mesh(VectorElement("CG", interval, 1)), FiniteElement("CG", interval, 1)))
+        Mesh(FiniteElement("Lagrange", interval, 1,  (1, ), identity_pullback, H1)),
+        FiniteElement("Lagrange", interval, 1, (), identity_pullback, H1)))
 
 
 @pytest.fixture
 def A1(request):
     return Coefficient(FunctionSpace(
-        Mesh(VectorElement("CG", interval, 1)), TensorElement("CG", interval, 1)))
+        Mesh(FiniteElement("Lagrange", interval, 1, (1, ), identity_pullback, H1)),
+        FiniteElement("Lagrange", interval, 1, (1, 1), identity_pullback, H1)))
 
 
 @pytest.fixture
 def A2(request):
     return Coefficient(FunctionSpace(
-        Mesh(VectorElement("CG", triangle, 1)), TensorElement("CG", triangle, 1)))
+        Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1)),
+        FiniteElement("Lagrange", triangle, 1, (2, 2), identity_pullback, H1)))
 
 
 @pytest.fixture
 def A3(request):
     return Coefficient(FunctionSpace(
-        Mesh(VectorElement("CG", tetrahedron, 1)), TensorElement("CG", tetrahedron, 1)))
+        Mesh(FiniteElement("Lagrange", triangle, 1, (3, ), identity_pullback, H1)),
+        FiniteElement("Lagrange", triangle, 1, (3, 3), identity_pullback, H1)))
 
 
 @pytest.fixture
 def A21(request):
     return Coefficient(FunctionSpace(
-        Mesh(VectorElement("CG", triangle, 1)), TensorElement("CG", triangle, 1, shape=(2, 1))))
+        Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1)),
+        FiniteElement("Lagrange", triangle, 1, (2, 1), identity_pullback, H1)))
 
 
 @pytest.fixture
 def A31(request):
     return Coefficient(FunctionSpace(
-        Mesh(VectorElement("CG", triangle, 1)), TensorElement("CG", triangle, 1, shape=(3, 1))))
+        Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1)),
+        FiniteElement("Lagrange", triangle, 1, (3, 1), identity_pullback, H1)))
 
 
 @pytest.fixture
 def A32(request):
     return Coefficient(FunctionSpace(
-        Mesh(VectorElement("CG", triangle, 1)), TensorElement("CG", triangle, 1, shape=(3, 2))))
+        Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1)),
+        FiniteElement("Lagrange", triangle, 1, (3, 2), identity_pullback, H1)))
 
 
 def test_determinant0(A0):
diff --git a/test/test_apply_function_pullbacks.py b/test/test_apply_function_pullbacks.py
index e352d09e9..3cb73e898 100755
--- a/test/test_apply_function_pullbacks.py
+++ b/test/test_apply_function_pullbacks.py
@@ -1,15 +1,17 @@
 import numpy
 
-from ufl import (Cell, Coefficient, FiniteElement, FunctionSpace, Mesh, TensorElement, VectorElement, as_tensor,
-                 as_vector, dx, indices, triangle)
-from ufl.algorithms.apply_function_pullbacks import apply_single_function_pullbacks
+from ufl import Cell, Coefficient, FunctionSpace, Mesh, as_tensor, as_vector, dx, indices, triangle
 from ufl.algorithms.renumbering import renumber_indices
 from ufl.classes import Jacobian, JacobianDeterminant, JacobianInverse, ReferenceValue
+from ufl.finiteelement import FiniteElement, MixedElement, SymmetricElement
+from ufl.pullback import (contravariant_piola, covariant_piola, double_contravariant_piola, double_covariant_piola,
+                          identity_pullback, l2_piola)
+from ufl.sobolevspace import H1, L2, HCurl, HDiv, HDivDiv, HEin
 
 
 def check_single_function_pullback(g, mappings):
     expected = mappings[g]
-    actual = apply_single_function_pullbacks(ReferenceValue(g), g.ufl_element())
+    actual = g.ufl_element().pullback.apply(ReferenceValue(g))
     assert expected.ufl_shape == actual.ufl_shape
     for idx in numpy.ndindex(actual.ufl_shape):
         rexp = renumber_indices(expected[idx])
@@ -33,30 +35,34 @@ def check_single_function_pullback(g, mappings):
 def test_apply_single_function_pullbacks_triangle3d():
     triangle3d = Cell("triangle", geometric_dimension=3)
     cell = triangle3d
-    domain = Mesh(VectorElement("Lagrange", cell, 1))
-
-    UL2 = FiniteElement("DG L2", cell, 1)
-    U0 = FiniteElement("DG", cell, 0)
-    U = FiniteElement("CG", cell, 1)
-    V = VectorElement("CG", cell, 1)
-    Vd = FiniteElement("RT", cell, 1)
-    Vc = FiniteElement("N1curl", cell, 1)
-    T = TensorElement("CG", cell, 1)
-    S = TensorElement("CG", cell, 1, symmetry=True)
-    COV2T = FiniteElement("Regge", cell, 0)   # (0, 2)-symmetric tensors
-    CONTRA2T = FiniteElement("HHJ", cell, 0)  # (2, 0)-symmetric tensors
-
-    Uml2 = UL2*UL2
-    Um = U*U
-    Vm = U*V
-    Vdm = V*Vd
-    Vcm = Vd*Vc
-    Tm = Vc*T
-    Sm = T*S
-
-    Vd0 = Vd*U0  # case from failing ffc demo
-
-    W = S*T*Vc*Vd*V*U
+    domain = Mesh(FiniteElement("Lagrange", cell, 1, (3, ), identity_pullback, H1))
+
+    UL2 = FiniteElement("Discontinuous Lagrange", cell, 1, (), l2_piola, L2)
+    U0 = FiniteElement("Discontinuous Lagrange", cell, 0, (), identity_pullback, L2)
+    U = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1)
+    V = FiniteElement("Lagrange", cell, 1, (3, ), identity_pullback, H1)
+    Vd = FiniteElement("Raviart-Thomas", cell, 1, (2, ), contravariant_piola, HDiv)
+    Vc = FiniteElement("N1curl", cell, 1, (2, ), covariant_piola, HCurl)
+    T = FiniteElement("Lagrange", cell, 1, (3, 3), identity_pullback, H1)
+    S = SymmetricElement(
+        {(0, 0): 0, (1, 0): 1, (2, 0): 2, (0, 1): 1, (1, 1): 3, (2, 1): 4, (0, 2): 2, (1, 2): 4, (2, 2): 5},
+        [FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1) for _ in range(6)])
+    # (0, 2)-symmetric tensors
+    COV2T = FiniteElement("Regge", cell, 0, (2, 2), double_covariant_piola, HEin)
+    # (2, 0)-symmetric tensors
+    CONTRA2T = FiniteElement("HHJ", cell, 0, (2, 2), double_contravariant_piola, HDivDiv)
+
+    Uml2 = MixedElement([UL2, UL2])
+    Um = MixedElement([U, U])
+    Vm = MixedElement([U, V])
+    Vdm = MixedElement([V, Vd])
+    Vcm = MixedElement([Vd, Vc])
+    Tm = MixedElement([Vc, T])
+    Sm = MixedElement([T, S])
+
+    Vd0 = MixedElement([Vd, U0])  # case from failing ffc demo
+
+    W = MixedElement([S, T, Vc, Vd, V, U])
 
     ul2 = Coefficient(FunctionSpace(domain, UL2))
     u = Coefficient(FunctionSpace(domain, U))
@@ -229,25 +235,26 @@ def test_apply_single_function_pullbacks_triangle3d():
 
 def test_apply_single_function_pullbacks_triangle():
     cell = triangle
-    domain = Mesh(VectorElement("Lagrange", cell, 1))
-
-    Ul2 = FiniteElement("DG L2", cell, 1)
-    U = FiniteElement("CG", cell, 1)
-    V = VectorElement("CG", cell, 1)
-    Vd = FiniteElement("RT", cell, 1)
-    Vc = FiniteElement("N1curl", cell, 1)
-    T = TensorElement("CG", cell, 1)
-    S = TensorElement("CG", cell, 1, symmetry=True)
-
-    Uml2 = Ul2*Ul2
-    Um = U*U
-    Vm = U*V
-    Vdm = V*Vd
-    Vcm = Vd*Vc
-    Tm = Vc*T
-    Sm = T*S
-
-    W = S*T*Vc*Vd*V*U
+    domain = Mesh(FiniteElement("Lagrange", cell, 1, (2, ), identity_pullback, H1))
+
+    Ul2 = FiniteElement("Discontinuous Lagrange", cell, 1, (), l2_piola, L2)
+    U = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1)
+    V = FiniteElement("Lagrange", cell, 1, (2, ), identity_pullback, H1)
+    Vd = FiniteElement("Raviart-Thomas", cell, 1, (2, ), contravariant_piola, HDiv)
+    Vc = FiniteElement("N1curl", cell, 1, (2, ), covariant_piola, HCurl)
+    T = FiniteElement("Lagrange", cell, 1, (2, 2), identity_pullback, H1)
+    S = SymmetricElement({(0, 0): 0, (0, 1): 1, (1, 0): 1, (1, 1): 2}, [
+        FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1) for i in range(3)])
+
+    Uml2 = MixedElement([Ul2, Ul2])
+    Um = MixedElement([U, U])
+    Vm = MixedElement([U, V])
+    Vdm = MixedElement([V, Vd])
+    Vcm = MixedElement([Vd, Vc])
+    Tm = MixedElement([Vc, T])
+    Sm = MixedElement([T, S])
+
+    W = MixedElement([S, T, Vc, Vd, V, U])
 
     ul2 = Coefficient(FunctionSpace(domain, Ul2))
     u = Coefficient(FunctionSpace(domain, U))
diff --git a/test/test_apply_restrictions.py b/test/test_apply_restrictions.py
index 860b76d48..efb9ad514 100755
--- a/test/test_apply_restrictions.py
+++ b/test/test_apply_restrictions.py
@@ -1,18 +1,20 @@
 from pytest import raises
 
-from ufl import (Coefficient, FacetNormal, FiniteElement, FunctionSpace, Mesh, SpatialCoordinate, VectorElement,
-                 as_tensor, grad, i, triangle)
+from ufl import Coefficient, FacetNormal, FunctionSpace, Mesh, SpatialCoordinate, as_tensor, grad, i, triangle
 from ufl.algorithms.apply_restrictions import apply_default_restrictions, apply_restrictions
 from ufl.algorithms.renumbering import renumber_indices
+from ufl.finiteelement import FiniteElement
+from ufl.pullback import identity_pullback
+from ufl.sobolevspace import H1, L2
 
 
 def test_apply_restrictions():
     cell = triangle
-    V0 = FiniteElement("DG", cell, 0)
-    V1 = FiniteElement("Lagrange", cell, 1)
-    V2 = FiniteElement("Lagrange", cell, 2)
+    V0 = FiniteElement("Discontinuous Lagrange", cell, 0, (), identity_pullback, L2)
+    V1 = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1)
+    V2 = FiniteElement("Lagrange", cell, 2, (), identity_pullback, H1)
 
-    domain = Mesh(VectorElement("Lagrange", cell, 1))
+    domain = Mesh(FiniteElement("Lagrange", cell, 1, (2, ), identity_pullback, H1))
     v0_space = FunctionSpace(domain, V0)
     v1_space = FunctionSpace(domain, V1)
     v2_space = FunctionSpace(domain, V2)
diff --git a/test/test_arithmetic.py b/test/test_arithmetic.py
index f32e7bcfa..858f9ddd6 100755
--- a/test/test_arithmetic.py
+++ b/test/test_arithmetic.py
@@ -1,6 +1,9 @@
-from ufl import (Identity, Mesh, SpatialCoordinate, VectorElement, as_matrix, as_ufl, as_vector, elem_div, elem_mult,
-                 elem_op, sin, tetrahedron, triangle)
+from ufl import (Identity, Mesh, SpatialCoordinate, as_matrix, as_ufl, as_vector, elem_div, elem_mult, elem_op, sin,
+                 tetrahedron, triangle)
 from ufl.classes import ComplexValue, Division, FloatValue, IntValue
+from ufl.finiteelement import FiniteElement
+from ufl.pullback import identity_pullback
+from ufl.sobolevspace import H1
 
 
 def test_scalar_casting(self):
@@ -16,13 +19,13 @@ def test_scalar_casting(self):
 
 
 def test_ufl_float_division(self):
-    domain = Mesh(VectorElement("Lagrange", triangle, 1))
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     d = SpatialCoordinate(domain)[0] / 10.0  # TODO: Use mock instead of x
     self.assertIsInstance(d, Division)
 
 
 def test_float_ufl_division(self):
-    domain = Mesh(VectorElement("Lagrange", triangle, 1))
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     d = 3.14 / SpatialCoordinate(domain)[0]  # TODO: Use mock instead of x
     self.assertIsInstance(d, Division)
 
@@ -65,7 +68,7 @@ def test_elem_mult(self):
 
 
 def test_elem_mult_on_matrices(self):
-    domain = Mesh(VectorElement("Lagrange", triangle, 1))
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
 
     A = as_matrix(((1, 2), (3, 4)))
     B = as_matrix(((4, 5), (6, 7)))
@@ -83,7 +86,7 @@ def test_elem_mult_on_matrices(self):
 
 
 def test_elem_div(self):
-    domain = Mesh(VectorElement("Lagrange", tetrahedron, 1))
+    domain = Mesh(FiniteElement("Lagrange", tetrahedron, 1, (3, ), identity_pullback, H1))
     x, y, z = SpatialCoordinate(domain)
     A = as_matrix(((x, y, z), (3, 4, 5)))
     B = as_matrix(((7, 8, 9), (z, x, y)))
@@ -91,7 +94,7 @@ def test_elem_div(self):
 
 
 def test_elem_op(self):
-    domain = Mesh(VectorElement("Lagrange", tetrahedron, 1))
+    domain = Mesh(FiniteElement("Lagrange", tetrahedron, 1, (3, ), identity_pullback, H1))
     x, y, z = SpatialCoordinate(domain)
     A = as_matrix(((x, y, z), (3, 4, 5)))
     self.assertEqual(elem_op(sin, A), as_matrix(((sin(x), sin(y), sin(z)),
diff --git a/test/test_automatic_differentiation.py b/test/test_automatic_differentiation.py
index f8b2e840b..37bfd02f1 100755
--- a/test/test_automatic_differentiation.py
+++ b/test/test_automatic_differentiation.py
@@ -8,24 +8,27 @@
 import pytest
 
 from ufl import (And, Argument, CellDiameter, CellVolume, Circumradius, Coefficient, Constant, FacetArea, FacetNormal,
-                 FiniteElement, FunctionSpace, Identity, Jacobian, JacobianDeterminant, JacobianInverse,
-                 MaxCellEdgeLength, MaxFacetEdgeLength, Mesh, MinCellEdgeLength, MinFacetEdgeLength, Not, Or,
-                 PermutationSymbol, SpatialCoordinate, TensorElement, VectorElement, acos, as_matrix, as_tensor, as_ufl,
-                 as_vector, asin, atan, bessel_I, bessel_J, bessel_K, bessel_Y, cofac, conditional, cos, cross,
-                 derivative, det, dev, diff, dot, eq, erf, exp, ge, grad, gt, indices, inner, interval, inv, le, ln, lt,
-                 ne, outer, replace, sin, skew, sqrt, sym, tan, tetrahedron, tr, triangle, variable)
+                 FunctionSpace, Identity, Jacobian, JacobianDeterminant, JacobianInverse, MaxCellEdgeLength,
+                 MaxFacetEdgeLength, Mesh, MinCellEdgeLength, MinFacetEdgeLength, Not, Or, PermutationSymbol,
+                 SpatialCoordinate, acos, as_matrix, as_tensor, as_ufl, as_vector, asin, atan, bessel_I, bessel_J,
+                 bessel_K, bessel_Y, cofac, conditional, cos, cross, derivative, det, dev, diff, dot, eq, erf, exp, ge,
+                 grad, gt, indices, inner, interval, inv, le, ln, lt, ne, outer, replace, sin, skew, sqrt, sym, tan,
+                 tetrahedron, tr, triangle, variable)
 from ufl.algorithms import expand_derivatives
 from ufl.conditional import Conditional
 from ufl.corealg.traversal import unique_post_traversal
+from ufl.finiteelement import FiniteElement
+from ufl.pullback import identity_pullback
+from ufl.sobolevspace import H1, L2
 
 
 class ExpressionCollection(object):
 
     def __init__(self, cell):
         self.cell = cell
-        domain = Mesh(VectorElement("Lagrange", cell, 1))
-
         d = cell.geometric_dimension()
+        domain = Mesh(FiniteElement("Lagrange", cell, 1, (d, ), identity_pullback, H1))
+
         x = SpatialCoordinate(domain)
         n = FacetNormal(domain)
         c = CellVolume(domain)
@@ -45,9 +48,9 @@ def __init__(self, cell):
         ident = Identity(d)
         eps = PermutationSymbol(d)
 
-        U = FiniteElement("U", cell, None)
-        V = VectorElement("U", cell, None)
-        W = TensorElement("U", cell, None)
+        U = FiniteElement("Undefined", cell, None, (), identity_pullback, L2)
+        V = FiniteElement("Undefined", cell, None, (d, ), identity_pullback, L2)
+        W = FiniteElement("Undefined", cell, None, (d, d), identity_pullback, L2)
 
         u_space = FunctionSpace(domain, U)
         v_space = FunctionSpace(domain, V)
diff --git a/test/test_book_snippets.py b/test/test_book_snippets.py
deleted file mode 100755
index ed1088405..000000000
--- a/test/test_book_snippets.py
+++ /dev/null
@@ -1,547 +0,0 @@
-"""Test book snippets.
-
-This file contains snippets from the FEniCS book,
-and allows us to test that these can still run
-with future versions of UFL. Please don't change
-these and please do keep UFL compatible with these
-snippets as long as possible.
-"""
-
-from ufl import (Argument, Cell, Coefficient, Coefficients, Constant, Dx, FiniteElement, Identity, Index, MixedElement,
-                 SpatialCoordinate, TensorConstant, TensorElement, TestFunction, TestFunctions, TrialFunction,
-                 TrialFunctions, VectorConstant, VectorElement, action, adjoint, as_matrix, as_tensor, as_ufl,
-                 conditional, cos, derivative, diff, dot, ds, dx, exp, grad, i, indices, inner, j, k, lt, outer, pi,
-                 replace, sensitivity_rhs, sin, split, system, tetrahedron, tr, triangle, variable)
-from ufl.algorithms import (Transformer, compute_form_data, expand_compounds, expand_derivatives, expand_indices,
-                            post_traversal, tree_format)
-from ufl.corealg.multifunction import MultiFunction
-
-
-def test_uflcode_269(self):
-    # Finite element spaces
-    cell = tetrahedron
-    element = VectorElement("Lagrange", cell, 1)
-
-    # Form arguments
-    phi0 = TestFunction(element)
-    phi1 = TrialFunction(element)
-    u = Coefficient(element)
-    c1 = Constant(cell)
-    c2 = Constant(cell)
-
-    # Deformation gradient Fij = dXi/dxj
-    ident = Identity(cell.geometric_dimension())
-    F = ident + grad(u)
-
-    # Right Cauchy-Green strain tensor C with invariants
-    C = variable(F.T*F)
-    I_C = tr(C)
-    II_C = (I_C**2 - tr(C*C))/2
-
-    # Mooney-Rivlin constitutive law
-    W = c1*(I_C-3) + c2*(II_C-3)
-
-    # Second Piola-Kirchoff stress tensor
-    S = 2*diff(W, C)
-
-    # Weak forms
-    L = inner(F*S, grad(phi0))*dx
-    derivative(L, u, phi1)
-
-
-def test_uflcode_316(self):
-    shapestring = 'triangle'
-    cell = Cell(shapestring)  # noqa: F841
-
-
-def test_uflcode_323(self):
-    cell = tetrahedron  # noqa: F841
-
-
-def test_uflcode_356(self):
-    cell = tetrahedron
-
-    P = FiniteElement("Lagrange", cell, 1)
-    V = VectorElement("Lagrange", cell, 2)
-    T = TensorElement("DG", cell, 0, symmetry=True)
-
-    TH = V*P  # noqa: F841
-    ME = MixedElement(T, V, P)  # noqa: F841
-
-
-def test_uflcode_400(self):
-    V = FiniteElement("CG", triangle, 1)
-    f = Coefficient(V)
-    g = Coefficient(V)
-    h = Coefficient(V)
-    w = Coefficient(V)
-    v = TestFunction(V)
-    u = TrialFunction(V)
-    # ...
-    a = w*dot(grad(u), grad(v))*dx  # noqa: F841
-    L = f*v*dx + g**2*v*ds(0) + h*v*ds(1)  # noqa: F841
-
-
-def test_uflcode_469(self):
-    V = FiniteElement("CG", triangle, 1)
-    f = Coefficient(V)
-    g = Coefficient(V)
-    h = Coefficient(V)
-    v = TestFunction(V)
-    # ...
-    dx02 = dx(0, {"integration_order": 2})
-    dx14 = dx(1, {"integration_order": 4})
-    dx12 = dx(1, {"integration_order": 2})
-    L = f*v*dx02 + g*v*dx14 + h*v*dx12  # noqa: F841
-
-
-def test_uflcode_552(self):
-    element = FiniteElement("CG", triangle, 1)
-    # ...
-    phi = Argument(element, 2)  # noqa: F841
-    v = TestFunction(element)  # noqa: F841
-    u = TrialFunction(element)  # noqa: F841
-
-
-def test_uflcode_563(self):
-    cell = triangle
-    element = FiniteElement("CG", cell, 1)
-    # ...
-    w = Coefficient(element)  # noqa: F841
-    c = Constant(cell)  # noqa: F841
-    v = VectorConstant(cell)  # noqa: F841
-    M = TensorConstant(cell)  # noqa: F841
-
-
-def test_uflcode_574(self):
-    V0 = FiniteElement("CG", triangle, 1)
-    V1 = V0
-    # ...
-    V = V0*V1
-    u = Coefficient(V)
-    u0, u1 = split(u)
-
-
-def test_uflcode_582(self):
-    V0 = FiniteElement("CG", triangle, 1)
-    V1 = V0
-    # ...
-    V = V0*V1
-    u = Coefficient(V)
-    u0, u1 = split(u)
-    # ...
-    v0, v1 = TestFunctions(V)
-    u0, u1 = TrialFunctions(V)
-    f0, f1 = Coefficients(V)
-
-
-def test_uflcode_644(self):
-    V = VectorElement("CG", triangle, 1)
-    u = Coefficient(V)
-    v = Coefficient(V)
-    # ...
-    A = outer(u, v)
-    Aij = A[i, j]  # noqa: F841
-
-
-def test_uflcode_651(self):
-    V = VectorElement("CG", triangle, 1)
-    u = Coefficient(V)
-    v = Coefficient(V)
-    # ...
-    Aij = v[j]*u[i]
-    A = as_tensor(Aij, (i, j))  # noqa: F841
-
-
-def test_uflcode_671(self):
-    i = Index()  # noqa: F841
-    j, k, l = indices(3)  # noqa: F841, E741
-
-
-def test_uflcode_684(self):
-    V = VectorElement("CG", triangle, 1)
-    v = Coefficient(V)
-    # ...
-    th = pi/2
-    A = as_matrix([[cos(th), -sin(th)],
-                   [sin(th), cos(th)]])
-    u = A*v  # noqa: F841
-
-
-def test_uflcode_824(self):
-    V = VectorElement("CG", triangle, 1)
-    f = Coefficient(V)
-    # ...
-    df = Dx(f, i)
-    df = f.dx(i)  # noqa: F841
-
-
-def test_uflcode_886(self):
-    cell = triangle
-    # ...
-    x = SpatialCoordinate(cell)  # Original: x = cell.x
-    g = sin(x[0])
-    v = variable(g)
-    f = exp(v**2)
-    h = diff(f, v)  # noqa: F841
-    # ...
-    # print v
-    # print h
-
-
-def test_uflcode_930(self):
-    condition = lt(1, 0)
-    true_value = 1
-    false_value = 0
-    # ...
-    f = conditional(condition, true_value, false_value)  # noqa: F841
-
-
-def test_uflcode_1026(self):
-    element = FiniteElement("CG", triangle, 1)
-    # ...
-    v = TestFunction(element)
-    u = TrialFunction(element)
-    w = Coefficient(element)
-    f = 0.5*w**2*dx
-    F = derivative(f, w, v)
-    J = derivative(F, w, u)  # noqa: F841
-
-
-def test_uflcode_1050(self):
-    Vx = VectorElement("Lagrange", triangle, 1)
-    Vy = FiniteElement("Lagrange", triangle, 1)
-    u = Coefficient(Vx*Vy)
-    x, y = split(u)
-    f = inner(grad(x), grad(x))*dx + y*dot(x, x)*dx
-    F = derivative(f, u)
-    J = derivative(F, u)  # noqa: F841
-
-
-def test_uflcode_1085(self):
-    cell = triangle
-    # ...
-    V = VectorElement("Lagrange", cell, 1)
-    T = TensorElement("Lagrange", cell, 1)
-    u = TrialFunction(V)
-    v = TestFunction(V)
-    M = Coefficient(T)
-    a = M[i, j]*u[k].dx(j)*v[k].dx(i)*dx
-    astar = adjoint(a)  # noqa: F841
-
-
-def test_uflcode_1120(self):
-    cell = triangle
-    # ...
-    V = FiniteElement("Lagrange", cell, 1)
-    v = TestFunction(V)
-    f = Coefficient(V)
-    g = Coefficient(V)
-    L = f**2 / (2*g)*v*dx
-    L2 = replace(L, {f: g, g: 3})  # noqa: F841
-    L3 = g**2 / 6*v*dx  # noqa: F841
-
-
-def test_uflcode_1157(self):
-    cell = triangle
-    # ...
-    V = FiniteElement("Lagrange", cell, 1)
-    u = TrialFunction(V)
-    v = TestFunction(V)
-    f = Coefficient(V)
-    pde = u*v*dx - f*v*dx
-    a, L = system(pde)
-
-
-def test_uflcode_1190(self):
-    cell = triangle
-    element = FiniteElement("Lagrange", cell, 1)
-    V = element
-    u = TrialFunction(V)
-    v = TestFunction(V)
-    f = Coefficient(V)
-    c = variable(Coefficient(V))
-    pde = c*u*v*dx - c*f*v*dx
-    a, L = system(pde)
-    # ...
-    u = Coefficient(element)
-    sL = diff(L, c) - action(diff(a, c), u)  # noqa: F841
-
-
-def test_uflcode_1195(self):
-    cell = triangle
-    element = FiniteElement("Lagrange", cell, 1)
-    V = element
-    u = TrialFunction(V)
-    v = TestFunction(V)
-    f = Coefficient(V)
-    c = variable(Coefficient(V))
-    pde = c*u*v*dx - c*f*v*dx
-    a, L = system(pde)
-    u = Coefficient(element)
-    # ...
-    sL = sensitivity_rhs(a, u, L, c)  # noqa: F841
-
-
-def test_uflcode_1365(self):
-    e = 0
-    v = variable(e)
-    f = sin(v)
-    g = diff(f, v)  # noqa: F841
-
-
-def test_python_1446(self):
-    cell = triangle
-    V = FiniteElement("Lagrange", cell, 1)
-    u = TrialFunction(V)
-    v = TestFunction(V)
-    c = Constant(cell)
-    f = Coefficient(V)
-    e = c*f**2*u*v  # noqa: F841
-
-    # The linearized Graph functionality has been removed from UFL:
-    # from ufl.algorithms import Graph, partition
-    # G = Graph(e)
-    # V, E, = G
-
-    # print(("str(e) = %s\n" % str(e)))
-    # print(("\n".join("V[%d] = %s" % (i, v) for (i, v) in enumerate(V)), "\n"))
-    # print(("\n".join("E[%d] = %s" % (i, e) for (i, e) in enumerate(E)), "\n"))
-
-
-def test_python_1512(self):
-    cell = triangle
-    V = FiniteElement("Lagrange", cell, 1)
-    u = TrialFunction(V)
-    v = TestFunction(V)
-    c = Constant(cell)
-    f = Coefficient(V)
-    e = c*f**2*u*v  # noqa: F841
-
-    # The linearized Graph functionality has been removed from UFL:
-    # from ufl.algorithms import Graph, partition
-    # G = Graph(e)
-    # V, E, = G
-    # ...
-    # Vin = G.Vin()
-    # Vout = G.Vout()
-
-
-def test_python_1557(self):
-    cell = triangle
-    V = FiniteElement("Lagrange", cell, 1)
-    u = TrialFunction(V)
-    v = TestFunction(V)
-    c = Constant(cell)
-    f = Coefficient(V)
-    e = c*f**2*u*v  # noqa: F841
-
-    # The linearized Graph functionality has been removed from UFL:
-    # from ufl.algorithms import Graph, partition
-    # G = Graph(e)
-    # V, E, = G
-    # ...
-    # partitions, keys = partition(G)
-    # for deps in sorted(partitions.keys()):
-    #    P = partitions[deps]
-    # print "The following depends on", tuple(deps)
-    #    for i in sorted(P):
-    # print "V[%d] = %s" % (i, V[i])
-    # ...
-    #        v = V[i]
-
-
-def test_uflcode_1901(self):
-    cell = triangle
-    element = FiniteElement("Lagrange", cell, 1)
-    # ...
-    v = Argument(element, 2)  # noqa: F841
-    w = Coefficient(element)  # noqa: F841
-
-
-def test_python_1942(self):
-    def walk(expression, pre_action, post_action):
-        pre_action(expression)
-        for o in expression.ufl_operands:
-            walk(o)
-        post_action(expression)
-
-
-def test_python_1955(self):
-    def post_traversal(root):
-        for o in root.ufl_operands:
-            yield post_traversal(o)
-        yield root
-
-
-def test_python_1963(self):
-    def post_action(e):
-        # print str(e)
-        pass
-    cell = triangle
-    V = FiniteElement("Lagrange", cell, 1)
-    u = TrialFunction(V)
-    v = TestFunction(V)
-    c = Constant(cell)
-    f = Coefficient(V)
-    expression = c*f**2*u*v
-    # ...
-    for e in post_traversal(expression):
-        post_action(e)
-
-
-def test_python_1990(self):
-    expression = as_ufl(3)
-
-    def int_operation(x):
-        return 7
-
-    result = int_operation(expression)
-    self.assertTrue(result == 7)
-
-
-def test_python_2024(self):
-    class ExampleFunction(MultiFunction):
-
-        def __init__(self):
-            MultiFunction.__init__(self)
-
-        def terminal(self, expression):
-            return "Got a Terminal subtype %s." % type(expression)
-
-        def operator(self, expression):
-            return "Got an Operator subtype %s." % type(expression)
-
-        def argument(self, expression):
-            return "Got an Argument."
-
-        def sum(self, expression):
-            return "Got a Sum."
-
-    m = ExampleFunction()
-
-    cell = triangle
-    element = FiniteElement("Lagrange", cell, 1)
-    x = SpatialCoordinate(cell)  # Original: x = cell.x
-    if 0:
-        print((m(Argument(element, 2))))
-        print((m(x)))
-        print((m(x[0] + x[1])))
-        print((m(x[0] * x[1])))
-
-
-def test_python_2066(self):
-    def apply(e, multifunction):
-        ops = [apply(o, multifunction) for o in e.ufl_operands]
-        return multifunction(e, *ops)
-
-
-def test_python_2087(self):
-    class Replacer(Transformer):
-
-        def __init__(self, mapping):
-            Transformer.__init__(self)
-            self.mapping = mapping
-
-        def operator(self, e, *ops):
-            return e._ufl_expr_reconstruct_(*ops)
-
-        def terminal(self, e):
-            return self.mapping.get(e, e)
-
-    f = Constant(triangle)
-    r = Replacer({f: f**2})
-    g = r.visit(2*f)  # noqa: F841
-
-
-def test_python_2189(self):
-    V = FiniteElement("Lagrange", triangle, 1)
-    u = TestFunction(V)
-    v = TrialFunction(V)
-    f = Coefficient(V)
-
-    # Note no *dx! This is an expression, not a form.
-    a = dot(grad(f*u), grad(v))
-
-    ac = expand_compounds(a)
-    ad = expand_derivatives(ac)
-    ai = expand_indices(ad)
-
-    af = tree_format(a)  # noqa: F841
-    acf = tree_format(ac)  # noqa: F841
-    adf = "\n", tree_format(ad)  # noqa: F841
-    aif = tree_format(ai)  # noqa: F841
-
-    if 0:
-        print(("\na: ", str(a), "\n", tree_format(a)))
-        print(("\nac:", str(ac), "\n", tree_format(ac)))
-        print(("\nad:", str(ad), "\n", tree_format(ad)))
-        print(("\nai:", str(ai), "\n", tree_format(ai)))
-
-
-def test_python_2328(self):
-    cell = triangle
-    x = SpatialCoordinate(cell)  # Original: x = cell.x
-    e = x[0] + x[1]
-    # print e((0.5, 0.7)) # prints 1.2
-    # ...
-    self.assertEqual(e((0.5, 0.7)), 1.2)
-
-
-def test_python_2338(self):
-    cell = triangle
-    x = SpatialCoordinate(cell)  # Original: x = cell.x
-    # ...
-    c = Constant(cell)
-    e = c*(x[0] + x[1])
-    # print e((0.5, 0.7), { c: 10 }) # prints 12.0
-    # ...
-    self.assertEqual(e((0.5, 0.7), {c: 10}), 12.0)
-
-
-def test_python_2349(self):
-    element = VectorElement("Lagrange", triangle, 1)
-    c = Constant(triangle)
-    f = Coefficient(element)
-    e = c*(f[0] + f[1])
-
-    def fh(x):
-        return (x[0], x[1])
-    # print e((0.5, 0.7), { c: 10, f: fh }) # prints 12.0
-    # ...
-    self.assertEqual(e((0.5, 0.7), {c: 10, f: fh}), 12.0)
-
-
-def test_python_2364(self):
-    element = FiniteElement("Lagrange", triangle, 1)
-    g = Coefficient(element)
-    e = g**2 + g.dx(0)**2 + g.dx(1)**2
-
-    def gh(x, der=()):
-        if der == ():
-            return x[0]*x[1]
-        if der == (0,):
-            return x[1]
-        if der == (1,):
-            return x[0]
-    # print e((2, 3), { g: gh }) # prints 49
-    # ...
-    self.assertEqual(e((2, 3), {g: gh}), 49)
-
-
-def test_python_2462(self):
-    cell = triangle
-    element = FiniteElement("Lagrange", cell, 1)
-    V = element
-    u = TrialFunction(V)
-    v = TestFunction(V)
-    f = Coefficient(V)
-    c = variable(Coefficient(V))
-    pde = c*u*v*dx - c*f*v*dx
-    a, L = system(pde)
-    u = Coefficient(element)
-    myform = a
-    # ...
-    # print repr(preprocess(myform).preprocessed_form)
-    # ...
-    r = repr(compute_form_data(myform).preprocessed_form)  # noqa: F841
diff --git a/test/test_change_to_local.py b/test/test_change_to_local.py
index 80bdb2101..f0789fbba 100755
--- a/test/test_change_to_local.py
+++ b/test/test_change_to_local.py
@@ -1,16 +1,19 @@
 """Tests of the change to local representaiton algorithms."""
 
-from ufl import Coefficient, FiniteElement, FunctionSpace, Mesh, VectorElement, as_tensor, grad, indices, triangle
+from ufl import Coefficient, FunctionSpace, Mesh, as_tensor, grad, indices, triangle
 from ufl.algorithms import change_to_reference_grad
 from ufl.algorithms.renumbering import renumber_indices
 from ufl.classes import JacobianInverse, ReferenceGrad
+from ufl.finiteelement import FiniteElement
+from ufl.pullback import identity_pullback
+from ufl.sobolevspace import H1
 
 
 def test_change_to_reference_grad():
     cell = triangle
-    domain = Mesh(cell)
-    U = FunctionSpace(domain, FiniteElement("CG", cell, 1))
-    V = FunctionSpace(domain, VectorElement("CG", cell, 1))
+    domain = Mesh(FiniteElement("Lagrange", cell, 1, (2, ), identity_pullback, H1))
+    U = FunctionSpace(domain, FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1))
+    V = FunctionSpace(domain, FiniteElement("Lagrange", cell, 1, (2, ), identity_pullback, H1))
     u = Coefficient(U)
     v = Coefficient(V)
     Jinv = JacobianInverse(domain)
diff --git a/test/test_change_to_reference_frame.py b/test/test_change_to_reference_frame.py
index a7346b195..9b46b510c 100755
--- a/test/test_change_to_reference_frame.py
+++ b/test/test_change_to_reference_frame.py
@@ -1,7 +1,10 @@
 """Tests of the change to reference frame algorithm."""
 
-from ufl import Coefficient, FiniteElement, FunctionSpace, Mesh, TensorElement, VectorElement, triangle
+from ufl import Coefficient, FunctionSpace, Mesh, triangle
 from ufl.classes import Expr, ReferenceValue
+from ufl.finiteelement import FiniteElement
+from ufl.pullback import contravariant_piola, identity_pullback
+from ufl.sobolevspace import H1, HDiv
 
 
 def change_to_reference_frame(expr):
@@ -10,11 +13,11 @@ def change_to_reference_frame(expr):
 
 
 def test_change_unmapped_form_arguments_to_reference_frame():
-    U = FiniteElement("CG", triangle, 1)
-    V = VectorElement("CG", triangle, 1)
-    T = TensorElement("CG", triangle, 1)
+    U = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1)
+    V = FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1)
+    T = FiniteElement("Lagrange", triangle, 1, (2, 2), identity_pullback, H1)
 
-    domain = Mesh(VectorElement("Lagrange", triangle, 1))
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     u_space = FunctionSpace(domain, U)
     v_space = FunctionSpace(domain, V)
     t_space = FunctionSpace(domain, T)
@@ -28,9 +31,9 @@ def test_change_unmapped_form_arguments_to_reference_frame():
 
 
 def test_change_hdiv_form_arguments_to_reference_frame():
-    V = FiniteElement("RT", triangle, 1)
+    V = FiniteElement("Raviart-Thomas", triangle, 1, (2, ), contravariant_piola, HDiv)
 
-    domain = Mesh(VectorElement("Lagrange", triangle, 1))
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     v_space = FunctionSpace(domain, V)
 
     expr = Coefficient(v_space)
@@ -38,9 +41,9 @@ def test_change_hdiv_form_arguments_to_reference_frame():
 
 
 def test_change_hcurl_form_arguments_to_reference_frame():
-    V = FiniteElement("RT", triangle, 1)
+    V = FiniteElement("Raviart-Thomas", triangle, 1, (2, ), contravariant_piola, HDiv)
 
-    domain = Mesh(VectorElement("Lagrange", triangle, 1))
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     v_space = FunctionSpace(domain, V)
 
     expr = Coefficient(v_space)
diff --git a/test/test_check_arities.py b/test/test_check_arities.py
index 0d5521ac2..37ce7a26d 100755
--- a/test/test_check_arities.py
+++ b/test/test_check_arities.py
@@ -1,16 +1,19 @@
 import pytest
 
-from ufl import (Coefficient, FacetNormal, FunctionSpace, Mesh, SpatialCoordinate, TestFunction, TrialFunction,
-                 VectorElement, adjoint, cofac, conj, derivative, ds, dx, grad, inner, tetrahedron)
+from ufl import (Coefficient, FacetNormal, FunctionSpace, Mesh, SpatialCoordinate, TestFunction, TrialFunction, adjoint,
+                 cofac, conj, derivative, ds, dx, grad, inner, tetrahedron)
 from ufl.algorithms.check_arities import ArityMismatch
 from ufl.algorithms.compute_form_data import compute_form_data
+from ufl.finiteelement import FiniteElement
+from ufl.pullback import identity_pullback
+from ufl.sobolevspace import H1
 
 
 def test_check_arities():
     # Code from bitbucket issue #49
     cell = tetrahedron
-    D = Mesh(cell)
-    V = FunctionSpace(D, VectorElement("P", cell, 2))
+    D = Mesh(FiniteElement("Lagrange", cell, 1, (3, ), identity_pullback, H1))
+    V = FunctionSpace(D, FiniteElement("Lagrange", cell, 2, (3, ), identity_pullback, H1))
     dv = TestFunction(V)
     du = TrialFunction(V)
 
@@ -33,8 +36,8 @@ def test_check_arities():
 
 def test_complex_arities():
     cell = tetrahedron
-    D = Mesh(cell)
-    V = FunctionSpace(D, VectorElement("P", cell, 2))
+    D = Mesh(FiniteElement("Lagrange", cell, 1, (3, ), identity_pullback, H1))
+    V = FunctionSpace(D, FiniteElement("Lagrange", cell, 2, (3, ), identity_pullback, H1))
     v = TestFunction(V)
     u = TrialFunction(V)
 
diff --git a/test/test_classcoverage.py b/test/test_classcoverage.py
index 7b7d6f583..9a61261e3 100755
--- a/test/test_classcoverage.py
+++ b/test/test_classcoverage.py
@@ -4,18 +4,20 @@
 import ufl
 from ufl import *  # noqa: F403, F401
 from ufl import (And, Argument, CellDiameter, CellVolume, Circumradius, Coefficient, Constant, FacetArea, FacetNormal,
-                 FiniteElement, FunctionSpace, Identity, Jacobian, JacobianDeterminant, JacobianInverse,
-                 MaxFacetEdgeLength, Mesh, MinFacetEdgeLength, MixedElement, Not, Or, PermutationSymbol,
-                 SpatialCoordinate, TensorConstant, TensorElement, VectorConstant, VectorElement, acos, action,
-                 as_matrix, as_tensor, as_ufl, as_vector, asin, atan, cell_avg, cofac, conditional, cos, cosh, cross,
-                 curl, derivative, det, dev, diff, div, dot, ds, dS, dx, eq, exp, facet_avg, ge, grad, gt, i, inner,
-                 inv, j, k, l, le, ln, lt, nabla_div, nabla_grad, ne, outer, rot, sin, sinh, skew, sqrt, sym, tan, tanh,
-                 tetrahedron, tr, transpose, triangle, variable)
+                 FunctionSpace, Identity, Jacobian, JacobianDeterminant, JacobianInverse, MaxFacetEdgeLength, Mesh,
+                 MinFacetEdgeLength, Not, Or, PermutationSymbol, SpatialCoordinate, TensorConstant, VectorConstant,
+                 acos, action, as_matrix, as_tensor, as_ufl, as_vector, asin, atan, cell_avg, cofac, conditional, cos,
+                 cosh, cross, curl, derivative, det, dev, diff, div, dot, ds, dS, dx, eq, exp, facet_avg, ge, grad, gt,
+                 i, inner, inv, j, k, l, le, ln, lt, nabla_div, nabla_grad, ne, outer, rot, sin, sinh, skew, sqrt, sym,
+                 tan, tanh, tetrahedron, tr, transpose, triangle, variable)
 from ufl.algorithms import *  # noqa: F403, F401
 from ufl.classes import *  # noqa: F403, F401
 from ufl.classes import (Acos, Asin, Atan, CellCoordinate, Cos, Cosh, Exp, Expr, FacetJacobian,
                          FacetJacobianDeterminant, FacetJacobianInverse, FloatValue, IntValue, Ln, Outer, Sin, Sinh,
                          Sqrt, Tan, Tanh, all_ufl_classes)
+from ufl.finiteelement import FiniteElement, MixedElement
+from ufl.pullback import identity_pullback
+from ufl.sobolevspace import H1
 
 has_repr = set()
 has_dict = set()
@@ -106,15 +108,15 @@ def testAll(self):
     cell = triangle
     dim = cell.geometric_dimension()
 
-    e0 = FiniteElement("CG", cell, 1)
-    e1 = VectorElement("CG", cell, 1)
-    e2 = TensorElement("CG", cell, 1)
-    e3 = MixedElement(e0, e1, e2)
+    e0 = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1)
+    e1 = FiniteElement("Lagrange", cell, 1, (2, ), identity_pullback, H1)
+    e2 = FiniteElement("Lagrange", cell, 1, (2, 2), identity_pullback, H1)
+    e3 = MixedElement([e0, e1, e2])
 
-    e13D = VectorElement("CG", tetrahedron, 1)
+    e13D = FiniteElement("Lagrange", tetrahedron, 1, (3, ), identity_pullback, H1)
 
-    domain = Mesh(VectorElement("Lagrange", cell, 1))
-    domain3D = Mesh(VectorElement("Lagrange", tetrahedron, 1))
+    domain = Mesh(FiniteElement("Lagrange", cell, 1, (dim, ), identity_pullback, H1))
+    domain3D = Mesh(FiniteElement("Lagrange", tetrahedron, 1, (3, ), identity_pullback, H1))
     e0_space = FunctionSpace(domain, e0)
     e1_space = FunctionSpace(domain, e1)
     e2_space = FunctionSpace(domain, e2)
@@ -134,7 +136,7 @@ def testAll(self):
     _test_object(v0, (), ())
     _test_object(v1, (dim,), ())
     _test_object(v2, (dim, dim), ())
-    _test_object(v3, (dim*dim+dim+1,), ())
+    _test_object(v3, (1 + dim + dim ** 2, ), ())
 
     f0 = Coefficient(e0_space)
     f1 = Coefficient(e1_space)
@@ -144,7 +146,7 @@ def testAll(self):
     _test_object(f0, (), ())
     _test_object(f1, (dim,), ())
     _test_object(f2, (dim, dim), ())
-    _test_object(f3, (dim*dim+dim+1,), ())
+    _test_object(f3, (1 + dim + dim ** 2, ), ())
 
     c = Constant(domain)
     _test_object(c, (), ())
@@ -227,7 +229,7 @@ def testAll(self):
     a = variable(v2)
     _test_object(a, (dim, dim), ())
     a = variable(v3)
-    _test_object(a, (dim*dim+dim+1,), ())
+    _test_object(a, (1 + dim + dim ** 2, ), ())
     a = variable(f0)
     _test_object(a, (), ())
     a = variable(f1)
@@ -235,7 +237,7 @@ def testAll(self):
     a = variable(f2)
     _test_object(a, (dim, dim), ())
     a = variable(f3)
-    _test_object(a, (dim*dim+dim+1,), ())
+    _test_object(a, (1 + dim + dim ** 2, ), ())
 
     # a = MultiIndex()
 
diff --git a/test/test_complex.py b/test/test_complex.py
index 999485ef1..6f3eda840 100755
--- a/test/test_complex.py
+++ b/test/test_complex.py
@@ -2,9 +2,9 @@
 
 import pytest
 
-from ufl import (Coefficient, FiniteElement, FunctionSpace, Mesh, TestFunction, TrialFunction, VectorElement, as_tensor,
-                 as_ufl, atan, conditional, conj, cos, cosh, dot, dx, exp, ge, grad, gt, imag, inner, le, ln, lt,
-                 max_value, min_value, outer, real, sin, sqrt, triangle)
+from ufl import (Coefficient, FunctionSpace, Mesh, TestFunction, TrialFunction, as_tensor, as_ufl, atan, conditional,
+                 conj, cos, cosh, dot, dx, exp, ge, grad, gt, imag, inner, le, ln, lt, max_value, min_value, outer,
+                 real, sin, sqrt, triangle)
 from ufl.algebra import Conj, Imag, Real
 from ufl.algorithms import estimate_total_polynomial_degree
 from ufl.algorithms.apply_algebra_lowering import apply_algebra_lowering
@@ -12,6 +12,9 @@
 from ufl.algorithms.formtransformations import compute_form_adjoint
 from ufl.algorithms.remove_complex_nodes import remove_complex_nodes
 from ufl.constantvalue import ComplexValue, Zero
+from ufl.finiteelement import FiniteElement
+from ufl.pullback import identity_pullback
+from ufl.sobolevspace import H1
 
 
 def test_conj(self):
@@ -45,8 +48,8 @@ def test_imag(self):
 
 def test_compute_form_adjoint(self):
     cell = triangle
-    element = FiniteElement('Lagrange', cell, 1)
-    domain = Mesh(VectorElement('Lagrange', cell, 1))
+    element = FiniteElement('Lagrange', cell, 1, (), identity_pullback, H1)
+    domain = Mesh(FiniteElement("Lagrange", cell, 1, (2, ), identity_pullback, H1))
     space = FunctionSpace(domain, element)
 
     u = TrialFunction(space)
@@ -74,8 +77,8 @@ def test_complex_algebra(self):
 
 def test_automatic_simplification(self):
     cell = triangle
-    element = FiniteElement("Lagrange", cell, 1)
-    domain = Mesh(VectorElement('Lagrange', cell, 1))
+    element = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1)
+    domain = Mesh(FiniteElement("Lagrange", cell, 1, (2, ), identity_pullback, H1))
     space = FunctionSpace(domain, element)
 
     v = TestFunction(space)
@@ -88,8 +91,8 @@ def test_automatic_simplification(self):
 
 def test_apply_algebra_lowering_complex(self):
     cell = triangle
-    element = FiniteElement("Lagrange", cell, 1)
-    domain = Mesh(VectorElement('Lagrange', cell, 1))
+    element = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1)
+    domain = Mesh(FiniteElement("Lagrange", cell, 1, (2, ), identity_pullback, H1))
     space = FunctionSpace(domain, element)
 
     v = TestFunction(space)
@@ -117,8 +120,8 @@ def test_apply_algebra_lowering_complex(self):
 
 def test_remove_complex_nodes(self):
     cell = triangle
-    element = FiniteElement("Lagrange", cell, 1)
-    domain = Mesh(VectorElement('Lagrange', cell, 1))
+    element = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1)
+    domain = Mesh(FiniteElement("Lagrange", cell, 1, (2, ), identity_pullback, H1))
     space = FunctionSpace(domain, element)
 
     u = TrialFunction(space)
@@ -140,8 +143,8 @@ def test_remove_complex_nodes(self):
 
 def test_comparison_checker(self):
     cell = triangle
-    element = FiniteElement("Lagrange", cell, 1)
-    domain = Mesh(VectorElement('Lagrange', cell, 1))
+    element = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1)
+    domain = Mesh(FiniteElement("Lagrange", cell, 1, (2, ), identity_pullback, H1))
     space = FunctionSpace(domain, element)
 
     u = TrialFunction(space)
@@ -168,8 +171,8 @@ def test_comparison_checker(self):
 
 def test_complex_degree_handling(self):
     cell = triangle
-    element = FiniteElement("Lagrange", cell, 3)
-    domain = Mesh(VectorElement('Lagrange', cell, 1))
+    element = FiniteElement("Lagrange", cell, 3, (), identity_pullback, H1)
+    domain = Mesh(FiniteElement("Lagrange", cell, 1, (2, ), identity_pullback, H1))
     space = FunctionSpace(domain, element)
 
     v = TestFunction(space)
diff --git a/test/test_conditionals.py b/test/test_conditionals.py
index e16fb1fc1..7b60054d5 100755
--- a/test/test_conditionals.py
+++ b/test/test_conditionals.py
@@ -3,23 +3,25 @@
 
 import pytest
 
-from ufl import (Coefficient, FiniteElement, FunctionSpace, Mesh, VectorElement, conditional, eq, ge, gt, le, lt, ne,
-                 triangle)
+from ufl import Coefficient, FunctionSpace, Mesh, conditional, eq, ge, gt, le, lt, ne, triangle
 from ufl.classes import EQ, GE, GT, LE, LT, NE
+from ufl.finiteelement import FiniteElement
+from ufl.pullback import identity_pullback
+from ufl.sobolevspace import H1
 
 
 @pytest.fixture
 def f():
-    element = FiniteElement("Lagrange", triangle, 1)
-    domain = Mesh(VectorElement('Lagrange', triangle, 1))
+    element = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1)
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     space = FunctionSpace(domain, element)
     return Coefficient(space)
 
 
 @pytest.fixture
 def g():
-    element = FiniteElement("Lagrange", triangle, 1)
-    domain = Mesh(VectorElement('Lagrange', triangle, 1))
+    element = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1)
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     space = FunctionSpace(domain, element)
     return Coefficient(space)
 
diff --git a/test/test_degree_estimation.py b/test/test_degree_estimation.py
index e0d6c05cd..dbadffef6 100755
--- a/test/test_degree_estimation.py
+++ b/test/test_degree_estimation.py
@@ -1,36 +1,30 @@
 __authors__ = "Martin Sandve Alnæs"
 __date__ = "2008-03-12 -- 2009-01-28"
 
-
-from ufl import (Argument, Coefficient, Coefficients, FacetNormal, FiniteElement, FunctionSpace, Mesh,
-                 SpatialCoordinate, TensorProductElement, VectorElement, cos, div, dot, grad, i, inner, nabla_div,
-                 nabla_grad, sin, tan, triangle)
+from ufl import (Argument, Coefficient, Coefficients, FacetNormal, FunctionSpace, Mesh, SpatialCoordinate, cos, div,
+                 dot, grad, i, inner, nabla_div, nabla_grad, sin, tan, triangle)
 from ufl.algorithms import estimate_total_polynomial_degree
+from ufl.finiteelement import FiniteElement, MixedElement
+from ufl.pullback import identity_pullback
+from ufl.sobolevspace import H1
 
 
 def test_total_degree_estimation():
-    V1 = FiniteElement("CG", triangle, 1)
-    V2 = FiniteElement("CG", triangle, 2)
-    VV = VectorElement("CG", triangle, 3)
-    VM = V1 * V2
-    O1 = TensorProductElement(V1, V1)
-    O2 = TensorProductElement(V2, V1)
+    V1 = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1)
+    V2 = FiniteElement("Lagrange", triangle, 2, (), identity_pullback, H1)
+    VV = FiniteElement("Lagrange", triangle, 3, (2, ), identity_pullback, H1)
+    VM = MixedElement([V1, V2])
 
-    domain = Mesh(VectorElement("Lagrange", triangle, 1))
-    tensor_domain = Mesh(VectorElement("Lagrange", O1.cell(), 1))
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
 
     v1_space = FunctionSpace(domain, V1)
     v2_space = FunctionSpace(domain, V2)
     vv_space = FunctionSpace(domain, VV)
     vm_space = FunctionSpace(domain, VM)
-    o1_space = FunctionSpace(tensor_domain, O1)
-    o2_space = FunctionSpace(tensor_domain, O2)
 
     v1 = Argument(v1_space, 2)
     v2 = Argument(v2_space, 3)
     f1, f2 = Coefficients(vm_space)
-    u1 = Coefficient(o1_space)
-    u2 = Coefficient(o2_space)
     vv = Argument(vv_space, 4)
     vu = Argument(vv_space, 5)
 
@@ -81,18 +75,6 @@ def test_total_degree_estimation():
     assert estimate_total_polynomial_degree(f2 ** 3 * v1) == 7
     assert estimate_total_polynomial_degree(f2 ** 3 * v1 + f1 * v1) == 7
 
-    # outer product tuple-degree tests
-    assert estimate_total_polynomial_degree(u1) == (1, 1)
-    assert estimate_total_polynomial_degree(u2) == (2, 1)
-    # derivatives should do nothing (don't know in which direction they act)
-    assert estimate_total_polynomial_degree(grad(u2)) == (2, 1)
-    assert estimate_total_polynomial_degree(u1 * u1) == (2, 2)
-    assert estimate_total_polynomial_degree(u2 * u1) == (3, 2)
-    assert estimate_total_polynomial_degree(u2 * u2) == (4, 2)
-    assert estimate_total_polynomial_degree(u1 ** 3) == (3, 3)
-    assert estimate_total_polynomial_degree(u1 ** 3 + u2 * u2) == (4, 3)
-    assert estimate_total_polynomial_degree(u2 ** 2 * u1) == (5, 3)
-
     # Math functions of constant values are constant values
     nx, ny = FacetNormal(domain)
     e = nx ** 2
@@ -115,9 +97,9 @@ def test_some_compound_types():
 
     etpd = estimate_total_polynomial_degree
 
-    P2 = FiniteElement("CG", triangle, 2)
-    V2 = VectorElement("CG", triangle, 2)
-    domain = Mesh(VectorElement("Lagrange", triangle, 1))
+    P2 = FiniteElement("Lagrange", triangle, 2, (), identity_pullback, H1)
+    V2 = FiniteElement("Lagrange", triangle, 2, (2, ), identity_pullback, H1)
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
 
     u = Coefficient(FunctionSpace(domain, P2))
     v = Coefficient(FunctionSpace(domain, V2))
diff --git a/test/test_derivative.py b/test/test_derivative.py
index 4b1972f27..63528d97f 100755
--- a/test/test_derivative.py
+++ b/test/test_derivative.py
@@ -3,12 +3,11 @@
 
 from itertools import chain
 
-from ufl import (CellDiameter, CellVolume, Circumradius, Coefficient, Constant, FacetArea, FacetNormal, FiniteElement,
-                 FunctionSpace, Identity, Index, Jacobian, JacobianInverse, Mesh, SpatialCoordinate, TensorElement,
-                 TestFunction, TrialFunction, VectorElement, acos, as_matrix, as_tensor, as_vector, asin, atan,
-                 conditional, cos, derivative, diff, dot, dx, exp, i, indices, inner, interval, j, k, ln, lt,
-                 nabla_grad, outer, quadrilateral, replace, sign, sin, split, sqrt, tan, tetrahedron, triangle,
-                 variable, zero)
+from ufl import (CellDiameter, CellVolume, Circumradius, Coefficient, Constant, FacetArea, FacetNormal, FunctionSpace,
+                 Identity, Index, Jacobian, JacobianInverse, Mesh, SpatialCoordinate, TestFunction, TrialFunction, acos,
+                 as_matrix, as_tensor, as_vector, asin, atan, conditional, cos, derivative, diff, dot, dx, exp, i,
+                 indices, inner, interval, j, k, ln, lt, nabla_grad, outer, quadrilateral, replace, sign, sin, split,
+                 sqrt, tan, tetrahedron, triangle, variable, zero)
 from ufl.algorithms import compute_form_data, expand_indices, strip_variables
 from ufl.algorithms.apply_algebra_lowering import apply_algebra_lowering
 from ufl.algorithms.apply_derivatives import apply_derivatives
@@ -16,6 +15,9 @@
 from ufl.classes import Indexed, MultiIndex, ReferenceGrad
 from ufl.constantvalue import as_ufl
 from ufl.domain import extract_unique_domain
+from ufl.finiteelement import FiniteElement, MixedElement
+from ufl.pullback import identity_pullback
+from ufl.sobolevspace import H1, L2
 
 
 def assertEqualBySampling(actual, expected):
@@ -72,8 +74,8 @@ def make_value(c):
 
 def _test(self, f, df):
     cell = triangle
-    element = FiniteElement("CG", cell, 1)
-    domain = Mesh(VectorElement("Lagrange", cell, 1))
+    element = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1)
+    domain = Mesh(FiniteElement("Lagrange", cell, 1, (2, ), identity_pullback, H1))
     space = FunctionSpace(domain, element)
 
     v = TestFunction(space)
@@ -103,175 +105,242 @@ def _test(self, f, df):
 
 
 def testScalarLiteral(self):
-    def f(w): return as_ufl(1)
+    def f(w):
+        return as_ufl(1)
+
+    def df(w, v):
+        return zero()
 
-    def df(w, v): return zero()
     _test(self, f, df)
 
 
 def testIdentityLiteral(self):
-    def f(w): return Identity(2)[i, i]
+    def f(w):
+        return Identity(2)[i, i]
+
+    def df(w, v):
+        return zero()
 
-    def df(w, v): return zero()
     _test(self, f, df)
 
 # --- Form arguments
 
 
 def testCoefficient(self):
-    def f(w): return w
+    def f(w):
+        return w
+
+    def df(w, v):
+        return v
 
-    def df(w, v): return v
     _test(self, f, df)
 
 
 def testArgument(self):
-    def f(w): return TestFunction(FunctionSpace(Mesh(VectorElement("Lagrange", triangle, 1)),
-                                                FiniteElement("CG", triangle, 1)))
+    def f(w):
+        return TestFunction(FunctionSpace(
+            Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1)),
+            FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1)))
 
-    def df(w, v): return zero()
+    def df(w, v):
+        return zero()
     _test(self, f, df)
 
 # --- Geometry
 
 
 def testSpatialCoordinate(self):
-    def f(w): return SpatialCoordinate(Mesh(VectorElement("Lagrange", triangle, 1)))[0]
+    def f(w):
+        return SpatialCoordinate(
+            Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1)))[0]
+
+    def df(w, v):
+        return zero()
 
-    def df(w, v): return zero()
     _test(self, f, df)
 
 
 def testFacetNormal(self):
-    def f(w): return FacetNormal(Mesh(VectorElement("Lagrange", triangle, 1)))[0]
+    def f(w):
+        return FacetNormal(
+            Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1)))[0]
 
-    def df(w, v): return zero()
-    _test(self, f, df)
+    def df(w, v):
+        return zero()
 
-# def testCellSurfaceArea(self):
-#    def f(w):     return CellSurfaceArea(Mesh(VectorElement("Lagrange", triangle, 1)))
-#    def df(w, v): return zero()
-#    _test(self, f, df)
+    _test(self, f, df)
 
 
 def testFacetArea(self):
-    def f(w): return FacetArea(Mesh(VectorElement("Lagrange", triangle, 1)))
+    def f(w):
+        return FacetArea(
+            Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1)))
+
+    def df(w, v):
+        return zero()
 
-    def df(w, v): return zero()
     _test(self, f, df)
 
 
 def testCellDiameter(self):
-    def f(w): return CellDiameter(Mesh(VectorElement("Lagrange", triangle, 1)))
+    def f(w):
+        return CellDiameter(
+            Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1)))
+
+    def df(w, v):
+        return zero()
 
-    def df(w, v): return zero()
     _test(self, f, df)
 
 
 def testCircumradius(self):
-    def f(w): return Circumradius(Mesh(VectorElement("Lagrange", triangle, 1)))
+    def f(w):
+        return Circumradius(Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1)))
 
-    def df(w, v): return zero()
+    def df(w, v):
+        return zero()
     _test(self, f, df)
 
 
 def testCellVolume(self):
-    def f(w): return CellVolume(Mesh(VectorElement("Lagrange", triangle, 1)))
+    def f(w):
+        return CellVolume(Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1)))
+
+    def df(w, v):
+        return zero()
 
-    def df(w, v): return zero()
     _test(self, f, df)
 
 # --- Basic operators
 
 
 def testSum(self):
-    def f(w): return w + 1
+    def f(w):
+        return w + 1
+
+    def df(w, v):
+        return v
 
-    def df(w, v): return v
     _test(self, f, df)
 
 
 def testProduct(self):
-    def f(w): return 3*w
+    def f(w):
+        return 3*w
+
+    def df(w, v):
+        return 3*v
 
-    def df(w, v): return 3*v
     _test(self, f, df)
 
 
 def testPower(self):
-    def f(w): return w**3
+    def f(w):
+        return w**3
+
+    def df(w, v):
+        return 3*w**2*v
 
-    def df(w, v): return 3*w**2*v
     _test(self, f, df)
 
 
 def testDivision(self):
-    def f(w): return w / 3.0
+    def f(w):
+        return w / 3.0
+
+    def df(w, v):
+        return v / 3.0
 
-    def df(w, v): return v / 3.0
     _test(self, f, df)
 
 
 def testDivision2(self):
-    def f(w): return 3.0 / w
+    def f(w):
+        return 3.0 / w
+
+    def df(w, v):
+        return -3.0 * v / w**2
 
-    def df(w, v): return -3.0 * v / w**2
     _test(self, f, df)
 
 
 def testExp(self):
-    def f(w): return exp(w)
+    def f(w):
+        return exp(w)
+
+    def df(w, v):
+        return v*exp(w)
 
-    def df(w, v): return v*exp(w)
     _test(self, f, df)
 
 
 def testLn(self):
-    def f(w): return ln(w)
+    def f(w):
+        return ln(w)
+
+    def df(w, v):
+        return v / w
 
-    def df(w, v): return v / w
     _test(self, f, df)
 
 
 def testCos(self):
-    def f(w): return cos(w)
+    def f(w):
+        return cos(w)
+
+    def df(w, v):
+        return -v*sin(w)
 
-    def df(w, v): return -v*sin(w)
     _test(self, f, df)
 
 
 def testSin(self):
-    def f(w): return sin(w)
+    def f(w):
+        return sin(w)
+
+    def df(w, v):
+        return v*cos(w)
 
-    def df(w, v): return v*cos(w)
     _test(self, f, df)
 
 
 def testTan(self):
-    def f(w): return tan(w)
+    def f(w):
+        return tan(w)
+
+    def df(w, v):
+        return v*2.0/(cos(2.0*w) + 1.0)
 
-    def df(w, v): return v*2.0/(cos(2.0*w) + 1.0)
     _test(self, f, df)
 
 
 def testAcos(self):
-    def f(w): return acos(w/1000)
+    def f(w):
+        return acos(w/1000)
+
+    def df(w, v):
+        return -(v/1000)/sqrt(1.0 - (w/1000)**2)
 
-    def df(w, v): return -(v/1000)/sqrt(1.0 - (w/1000)**2)
     _test(self, f, df)
 
 
 def testAsin(self):
-    def f(w): return asin(w/1000)
+    def f(w):
+        return asin(w/1000)
+
+    def df(w, v):
+        return (v/1000)/sqrt(1.0 - (w/1000)**2)
 
-    def df(w, v): return (v/1000)/sqrt(1.0 - (w/1000)**2)
     _test(self, f, df)
 
 
 def testAtan(self):
-    def f(w): return atan(w)
+    def f(w):
+        return atan(w)
+
+    def df(w, v):
+        return v/(1.0 + w**2)
 
-    def df(w, v): return v/(1.0 + w**2)
     _test(self, f, df)
 
 # FIXME: Add the new erf and bessel_*
@@ -280,19 +349,26 @@ def df(w, v): return v/(1.0 + w**2)
 
 
 def testAbs(self):
-    def f(w): return abs(w)
+    def f(w):
+        return abs(w)
+
+    def df(w, v):
+        return sign(w)*v
 
-    def df(w, v): return sign(w)*v
     _test(self, f, df)
 
 
 def testConditional(self):  # This will fail without bugfix in derivative
-    def cond(w): return lt(w, 1.0)
+    def cond(w):
+        return lt(w, 1.0)
 
-    def f(w): return conditional(cond(w), 2*w, 3*w)
+    def f(w):
+        return conditional(cond(w), 2*w, 3*w)
+
+    def df(w, v):
+        return (conditional(cond(w), 1, 0) * 2*v +
+                conditional(cond(w), 0, 1) * 3*v)
 
-    def df(w, v): return (conditional(cond(w), 1, 0) * 2*v +
-                          conditional(cond(w), 0, 1) * 3*v)
     _test(self, f, df)
 
 # --- Tensor algebra basics
@@ -306,7 +382,9 @@ def f(w):
         i, = indices(1)
         return a[i]*b[i]
 
-    def df(w, v): return 3*v + 4*2*w*v + 5*3*w**2*v
+    def df(w, v):
+        return 3*v + 4*2*w*v + 5*3*w**2*v
+
     _test(self, f, df)
 
 
@@ -336,8 +414,8 @@ def testListTensor(self):
 
 def test_single_scalar_coefficient_derivative(self):
     cell = triangle
-    V = FiniteElement("CG", cell, 1)
-    domain = Mesh(VectorElement("Lagrange", cell, 1))
+    V = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1)
+    domain = Mesh(FiniteElement("Lagrange", cell, 1, (2, ), identity_pullback, H1))
     space = FunctionSpace(domain, V)
     u = Coefficient(space)
     v = TestFunction(space)
@@ -348,8 +426,8 @@ def test_single_scalar_coefficient_derivative(self):
 
 def test_single_vector_coefficient_derivative(self):
     cell = triangle
-    V = VectorElement("CG", cell, 1)
-    domain = Mesh(VectorElement("Lagrange", cell, 1))
+    V = FiniteElement("Lagrange", cell, 1, (2, ), identity_pullback, H1)
+    domain = Mesh(FiniteElement("Lagrange", cell, 1, (2, ), identity_pullback, H1))
     space = FunctionSpace(domain, V)
     u = Coefficient(space)
     v = TestFunction(space)
@@ -361,10 +439,10 @@ def test_single_vector_coefficient_derivative(self):
 
 def test_multiple_coefficient_derivative(self):
     cell = triangle
-    V = FiniteElement("CG", cell, 1)
-    W = VectorElement("CG", cell, 1)
-    M = V*W
-    domain = Mesh(VectorElement("Lagrange", cell, 1))
+    V = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1)
+    W = FiniteElement("Lagrange", cell, 1, (2, ), identity_pullback, H1)
+    M = MixedElement([V, W])
+    domain = Mesh(FiniteElement("Lagrange", cell, 1, (2, ), identity_pullback, H1))
     v_space = FunctionSpace(domain, V)
     w_space = FunctionSpace(domain, W)
     m_space = FunctionSpace(domain, M)
@@ -387,9 +465,9 @@ def test_multiple_coefficient_derivative(self):
 def test_indexed_coefficient_derivative(self):
     cell = triangle
     ident = Identity(cell.geometric_dimension())
-    V = FiniteElement("CG", cell, 1)
-    W = VectorElement("CG", cell, 1)
-    domain = Mesh(VectorElement("Lagrange", cell, 1))
+    V = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1)
+    W = FiniteElement("Lagrange", cell, 1, (2, ), identity_pullback, H1)
+    domain = Mesh(FiniteElement("Lagrange", cell, 1, (2, ), identity_pullback, H1))
     v_space = FunctionSpace(domain, V)
     w_space = FunctionSpace(domain, W)
     u = Coefficient(w_space)
@@ -409,10 +487,10 @@ def test_indexed_coefficient_derivative(self):
 
 def test_multiple_indexed_coefficient_derivative(self):
     cell = tetrahedron
-    V = FiniteElement("CG", cell, 1)
-    V2 = V*V
-    W = VectorElement("CG", cell, 1)
-    domain = Mesh(VectorElement("Lagrange", cell, 1))
+    V = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1)
+    V2 = MixedElement([V, V])
+    W = FiniteElement("Lagrange", cell, 1, (3, ), identity_pullback, H1)
+    domain = Mesh(FiniteElement("Lagrange", cell, 1, (3, ), identity_pullback, H1))
     v2_space = FunctionSpace(domain, V2)
     w_space = FunctionSpace(domain, W)
     u = Coefficient(w_space)
@@ -428,10 +506,10 @@ def test_multiple_indexed_coefficient_derivative(self):
 
 def test_segregated_derivative_of_convection(self):
     cell = tetrahedron
-    V = FiniteElement("CG", cell, 1)
-    W = VectorElement("CG", cell, 1)
+    V = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1)
+    W = FiniteElement("Lagrange", cell, 1, (3, ), identity_pullback, H1)
 
-    domain = Mesh(VectorElement("Lagrange", cell, 1))
+    domain = Mesh(FiniteElement("Lagrange", cell, 1, (3, ), identity_pullback, H1))
     v_space = FunctionSpace(domain, V)
     w_space = FunctionSpace(domain, W)
 
@@ -467,9 +545,9 @@ def test_segregated_derivative_of_convection(self):
 
 
 def test_coefficient_derivatives(self):
-    V = FiniteElement("Lagrange", triangle, 1)
+    V = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1)
 
-    domain = Mesh(VectorElement("Lagrange", triangle, 1))
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     space = FunctionSpace(domain, V)
 
     dv = TestFunction(space)
@@ -493,10 +571,10 @@ def test_coefficient_derivatives(self):
 
 
 def test_vector_coefficient_scalar_derivatives(self):
-    V = FiniteElement("Lagrange", triangle, 1)
-    VV = VectorElement("Lagrange", triangle, 1)
+    V = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1)
+    VV = FiniteElement("vector Lagrange", triangle, 1, (2, ), identity_pullback, H1)
 
-    domain = Mesh(VectorElement("Lagrange", triangle, 1))
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     v_space = FunctionSpace(domain, V)
     vv_space = FunctionSpace(domain, VV)
 
@@ -521,10 +599,10 @@ def test_vector_coefficient_scalar_derivatives(self):
 
 
 def test_vector_coefficient_derivatives(self):
-    V = VectorElement("Lagrange", triangle, 1)
-    VV = TensorElement("Lagrange", triangle, 1)
+    V = FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1)
+    VV = FiniteElement("Lagrange", triangle, 1, (2, 2), identity_pullback, H1)
 
-    domain = Mesh(VectorElement("Lagrange", triangle, 1))
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     v_space = FunctionSpace(domain, V)
     vv_space = FunctionSpace(domain, VV)
 
@@ -550,10 +628,10 @@ def test_vector_coefficient_derivatives(self):
 
 
 def test_vector_coefficient_derivatives_of_product(self):
-    V = VectorElement("Lagrange", triangle, 1)
-    VV = TensorElement("Lagrange", triangle, 1)
+    V = FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1)
+    VV = FiniteElement("Lagrange", triangle, 1, (2, 2), identity_pullback, H1)
 
-    domain = Mesh(VectorElement("Lagrange", triangle, 1))
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     v_space = FunctionSpace(domain, V)
     vv_space = FunctionSpace(domain, VV)
 
@@ -591,8 +669,8 @@ def test_vector_coefficient_derivatives_of_product(self):
 
 def testHyperElasticity(self):
     cell = interval
-    element = FiniteElement("CG", cell, 2)
-    domain = Mesh(VectorElement("Lagrange", cell, 1))
+    element = FiniteElement("Lagrange", cell, 2, (), identity_pullback, H1)
+    domain = Mesh(FiniteElement("Lagrange", cell, 1, (1, ), identity_pullback, H1))
     space = FunctionSpace(domain, element)
     w = Coefficient(space)
     v = TestFunction(space)
@@ -669,9 +747,9 @@ def Nw(x, derivatives):
 
 def test_mass_derived_from_functional(self):
     cell = triangle
-    V = FiniteElement("CG", cell, 1)
+    V = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1)
 
-    domain = Mesh(VectorElement("Lagrange", cell, 1))
+    domain = Mesh(FiniteElement("Lagrange", cell, 1, (2, ), identity_pullback, H1))
     space = FunctionSpace(domain, V)
 
     v = TestFunction(space)
@@ -691,9 +769,9 @@ def test_mass_derived_from_functional(self):
 
 def test_derivative_replace_works_together(self):
     cell = triangle
-    V = FiniteElement("CG", cell, 1)
+    V = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1)
 
-    domain = Mesh(VectorElement("Lagrange", cell, 1))
+    domain = Mesh(FiniteElement("Lagrange", cell, 1, (2, ), identity_pullback, H1))
     space = FunctionSpace(domain, V)
 
     v = TestFunction(space)
@@ -716,8 +794,8 @@ def test_derivative_replace_works_together(self):
 
 
 def test_index_simplification_handles_repeated_indices(self):
-    mesh = Mesh(VectorElement("P", quadrilateral, 1))
-    V = FunctionSpace(mesh, TensorElement("DQ", quadrilateral, 0))
+    mesh = Mesh(FiniteElement("Lagrange", quadrilateral, 1, (2, ), identity_pullback, H1))
+    V = FunctionSpace(mesh, FiniteElement("DQ", quadrilateral, 0, (2, 2), identity_pullback, L2))
     K = JacobianInverse(mesh)
     G = outer(Identity(2), Identity(2))
     i, j, k, l, m, n = indices(6)
@@ -735,7 +813,7 @@ def test_index_simplification_handles_repeated_indices(self):
 
 
 def test_index_simplification_reference_grad(self):
-    mesh = Mesh(VectorElement("P", quadrilateral, 1))
+    mesh = Mesh(FiniteElement("Lagrange", quadrilateral, 1, (2, ), identity_pullback, H1))
     i, = indices(1)
     A = as_tensor(Indexed(Jacobian(mesh), MultiIndex((i, i))), (i,))
     expr = apply_derivatives(apply_geometry_lowering(
@@ -748,8 +826,8 @@ def test_index_simplification_reference_grad(self):
 # --- Scratch space
 
 def test_foobar(self):
-    element = VectorElement("Lagrange", triangle, 1)
-    domain = Mesh(VectorElement("Lagrange", triangle, 1))
+    element = FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1)
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     space = FunctionSpace(domain, element)
     v = TestFunction(space)
 
diff --git a/test/test_diff.py b/test/test_diff.py
index fd1ef4f34..5a53e5c8d 100755
--- a/test/test_diff.py
+++ b/test/test_diff.py
@@ -3,10 +3,13 @@
 
 import pytest
 
-from ufl import (Coefficient, FiniteElement, FunctionSpace, Mesh, SpatialCoordinate, VectorElement, as_vector, atan,
-                 cos, diff, exp, indices, ln, sin, tan, triangle, variable)
+from ufl import (Coefficient, FunctionSpace, Mesh, SpatialCoordinate, as_vector, atan, cos, diff, exp, indices, ln, sin,
+                 tan, triangle, variable)
 from ufl.algorithms import expand_derivatives
 from ufl.constantvalue import as_ufl
+from ufl.finiteelement import FiniteElement
+from ufl.pullback import identity_pullback
+from ufl.sobolevspace import H1
 
 
 def get_variables():
@@ -170,16 +173,16 @@ def df(v):
 
 
 def testCoefficient():
-    coord_elem = VectorElement("P", triangle, 1, dim=3)
+    coord_elem = FiniteElement("Lagrange", triangle, 1, (3, ), identity_pullback, H1)
     mesh = Mesh(coord_elem)
-    V = FunctionSpace(mesh, FiniteElement("P", triangle, 1))
+    V = FunctionSpace(mesh, FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1))
     v = Coefficient(V)
     assert round(expand_derivatives(diff(v, v))-1.0, 7) == 0
 
 
 def testDiffX():
     cell = triangle
-    domain = Mesh(VectorElement("Lagrange", cell, 1))
+    domain = Mesh(FiniteElement("Lagrange", cell, 1, (2, ), identity_pullback, H1))
     x = SpatialCoordinate(domain)
     f = x[0] ** 2 * x[1] ** 2
     i, = indices(1)
diff --git a/test/test_domains.py b/test/test_domains.py
index 88ee286ab..cb9df4ca0 100755
--- a/test/test_domains.py
+++ b/test/test_domains.py
@@ -1,24 +1,31 @@
+
 """Tests of domain language and attaching domains to forms."""
 
 import pytest
 from mockobjects import MockMesh
 
-from ufl import (Cell, Coefficient, Constant, FiniteElement, FunctionSpace, Mesh, VectorElement, ds, dS, dx, hexahedron,
-                 interval, quadrilateral, tetrahedron, triangle)
+import ufl  # noqa: F401
+from ufl import (Cell, Coefficient, Constant, FunctionSpace, Mesh, ds, dS, dx, hexahedron, interval, quadrilateral,
+                 tetrahedron, triangle)
 from ufl.algorithms import compute_form_data
+from ufl.finiteelement import FiniteElement
+from ufl.pullback import IdentityPullback  # noqa: F401
+from ufl.pullback import identity_pullback
+from ufl.sobolevspace import H1
 
-all_cells = (interval, triangle, tetrahedron,
-             quadrilateral, hexahedron)
+all_cells = (interval, triangle, tetrahedron, quadrilateral, hexahedron)
 
 
 def test_construct_domains_from_cells():
     for cell in all_cells:
-        Mesh(VectorElement("Lagrange", cell, 1))
+        d = cell.geometric_dimension()
+        Mesh(FiniteElement("Lagrange", cell, 1, (d, ), identity_pullback, H1))
 
 
 def test_construct_domains_with_names():
     for cell in all_cells:
-        e = VectorElement("Lagrange", cell, 1)
+        d = cell.geometric_dimension()
+        e = FiniteElement("Lagrange", cell, 1, (d, ), identity_pullback, H1)
         D2 = Mesh(e, ufl_id=2)
         D3 = Mesh(e, ufl_id=3)
         D3b = Mesh(e, ufl_id=3)
@@ -29,9 +36,13 @@ def test_construct_domains_with_names():
 def test_domains_sort_by_name():
     # This ordering is rather arbitrary, but at least this shows sorting is
     # working
-    domains1 = [Mesh(VectorElement("Lagrange", cell, 1), ufl_id=hash(cell.cellname()))
+    domains1 = [Mesh(FiniteElement("Lagrange", cell, 1, (cell.geometric_dimension(), ),
+                                   identity_pullback, H1),
+                     ufl_id=hash(cell.cellname()))
                 for cell in all_cells]
-    domains2 = [Mesh(VectorElement("Lagrange", cell, 1), ufl_id=hash(cell.cellname()))
+    domains2 = [Mesh(FiniteElement("Lagrange", cell, 1, (cell.geometric_dimension(), ),
+                                   identity_pullback, H1),
+                     ufl_id=hash(cell.cellname()))
                 for cell in sorted(all_cells)]
     sdomains = sorted(domains1, key=lambda D: (D.geometric_dimension(),
                                                D.topological_dimension(),
@@ -42,19 +53,19 @@ def test_domains_sort_by_name():
 
 
 def test_topdomain_creation():
-    D = Mesh(interval)
+    D = Mesh(FiniteElement("Lagrange", interval, 1, (1, ), identity_pullback, H1))
     assert D.geometric_dimension() == 1
-    D = Mesh(triangle)
+    D = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     assert D.geometric_dimension() == 2
-    D = Mesh(tetrahedron)
+    D = Mesh(FiniteElement("Lagrange", tetrahedron, 1, (3, ), identity_pullback, H1))
     assert D.geometric_dimension() == 3
 
 
 def test_cell_legacy_case():
     # Passing cell like old code does
-    D = Mesh(VectorElement("Lagrange", triangle, 1))
+    D = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
 
-    V = FiniteElement("CG", triangle, 1)
+    V = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1)
     f = Coefficient(FunctionSpace(D, V))
     assert f.ufl_domains() == (D, )
 
@@ -64,9 +75,9 @@ def test_cell_legacy_case():
 
 def test_simple_domain_case():
     # Creating domain from just cell with label like new dolfin will do
-    D = Mesh(triangle, ufl_id=3)
+    D = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1), ufl_id=3)
 
-    V = FunctionSpace(D, FiniteElement("CG", D.ufl_cell(), 1))
+    V = FunctionSpace(D, FiniteElement("Lagrange", D.ufl_cell(), 1, (), identity_pullback, "H1"))
     f = Coefficient(V)
     assert f.ufl_domains() == (D, )
 
@@ -79,11 +90,11 @@ def test_creating_domains_with_coordinate_fields():  # FIXME: Rewrite for new ap
 
     # Mesh with P2 representation of coordinates
     cell = triangle
-    P2 = VectorElement("CG", cell, 2)
+    P2 = FiniteElement("Lagrange", cell, 2, (2, ), identity_pullback, H1)
     domain = Mesh(P2)
 
     # Piecewise linear function space over quadratic mesh
-    element = FiniteElement("CG", cell, 1)
+    element = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1)
     V = FunctionSpace(domain, element)
 
     f = Coefficient(V)
@@ -105,66 +116,79 @@ def test_join_domains():
     mesh7 = MockMesh(7)
     mesh8 = MockMesh(8)
     triangle3 = Cell("triangle", geometric_dimension=3)
-    xa = VectorElement("CG", triangle, 1)
-    xb = VectorElement("CG", triangle, 1)
+    xa = FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1)
+    xb = FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1)
 
     # Equal domains are joined
-    assert 1 == len(join_domains([Mesh(triangle, ufl_id=7),
-                                  Mesh(triangle, ufl_id=7)]))
-    assert 1 == len(join_domains([Mesh(triangle, ufl_id=7, cargo=mesh7),
-                                  Mesh(triangle, ufl_id=7, cargo=mesh7)]))
+    assert 1 == len(join_domains([
+        Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1), ufl_id=7),
+        Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1), ufl_id=7)]))
+    assert 1 == len(join_domains([
+        Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1), ufl_id=7, cargo=mesh7),
+        Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1), ufl_id=7, cargo=mesh7)]))
     assert 1 == len(join_domains([Mesh(xa, ufl_id=3), Mesh(xa, ufl_id=3)]))
 
     # Different domains are not joined
-    assert 2 == len(join_domains([Mesh(triangle), Mesh(triangle)]))
-    assert 2 == len(join_domains([Mesh(triangle, ufl_id=7),
-                                  Mesh(triangle, ufl_id=8)]))
-    assert 2 == len(join_domains([Mesh(triangle, ufl_id=7),
-                                  Mesh(quadrilateral, ufl_id=8)]))
+    assert 2 == len(join_domains([
+        Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1)),
+        Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))]))
+    assert 2 == len(join_domains([
+        Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1), ufl_id=7),
+        Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1), ufl_id=8)]))
+    assert 2 == len(join_domains([
+        Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1), ufl_id=7),
+        Mesh(FiniteElement("Lagrange", quadrilateral, 1, (2, ), identity_pullback, H1), ufl_id=8)]))
     assert 2 == len(join_domains([Mesh(xa, ufl_id=7), Mesh(xa, ufl_id=8)]))
     assert 2 == len(join_domains([Mesh(xa), Mesh(xb)]))
 
-    # Incompatible cells require labeling
-    # self.assertRaises(BaseException, lambda: join_domains([Mesh(triangle), Mesh(triangle3)]))     # FIXME: Figure out
-    # self.assertRaises(BaseException, lambda: join_domains([Mesh(triangle),
-    # Mesh(quadrilateral)])) # FIXME: Figure out
-
     # Incompatible coordinates require labeling
-    xc = Coefficient(FunctionSpace(Mesh(triangle), VectorElement("CG", triangle, 1)))
-    xd = Coefficient(FunctionSpace(Mesh(triangle), VectorElement("CG", triangle, 1)))
+    xc = Coefficient(FunctionSpace(
+        Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1)),
+        FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1)))
+    xd = Coefficient(FunctionSpace(
+        Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1)),
+        FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1)))
     with pytest.raises(BaseException):
         join_domains([Mesh(xc), Mesh(xd)])
 
     # Incompatible data is checked if and only if the domains are the same
-    assert 2 == len(join_domains([Mesh(triangle, ufl_id=7, cargo=mesh7),
-                                  Mesh(triangle, ufl_id=8, cargo=mesh8)]))
-    assert 2 == len(join_domains([Mesh(triangle, ufl_id=7, cargo=mesh7),
-                                  Mesh(quadrilateral, ufl_id=8, cargo=mesh8)]))
+    assert 2 == len(join_domains([
+        Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1), ufl_id=7, cargo=mesh7),
+        Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1), ufl_id=8, cargo=mesh8)]))
+    assert 2 == len(join_domains([
+        Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1), ufl_id=7, cargo=mesh7),
+        Mesh(FiniteElement("Lagrange", quadrilateral, 1, (2, ), identity_pullback, H1),
+             ufl_id=8, cargo=mesh8)]))
     # Geometric dimensions must match
     with pytest.raises(BaseException):
-        join_domains([Mesh(triangle),
-                      Mesh(triangle3)])
+        join_domains([
+            Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1)),
+            Mesh(FiniteElement("Lagrange", triangle3, 1, (3, ), identity_pullback, H1))])
     with pytest.raises(BaseException):
-        join_domains([Mesh(triangle, ufl_id=7, cargo=mesh7),
-                      Mesh(triangle3, ufl_id=8, cargo=mesh8)])
+        join_domains([
+            Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1), ufl_id=7, cargo=mesh7),
+            Mesh(FiniteElement("Lagrange", triangle3, 1, (3, ), identity_pullback, H1), ufl_id=8, cargo=mesh8)])
     # Cargo and mesh ids must match
     with pytest.raises(BaseException):
-        Mesh(triangle, ufl_id=7, cargo=mesh8)
+        Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1), ufl_id=7, cargo=mesh8)
 
     # Nones are removed
-    assert 2 == len(join_domains([None, Mesh(triangle, ufl_id=3),
-                                  None, Mesh(triangle, ufl_id=3),
-                                  None, Mesh(triangle, ufl_id=4)]))
-    assert 2 == len(join_domains([Mesh(triangle, ufl_id=7), None,
-                                  Mesh(quadrilateral, ufl_id=8)]))
-    assert None not in join_domains([Mesh(triangle3, ufl_id=7), None,
-                                     Mesh(tetrahedron, ufl_id=8)])
+    assert 2 == len(join_domains([
+        None, Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1), ufl_id=3),
+        None, Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1), ufl_id=3),
+        None, Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1), ufl_id=4)]))
+    assert 2 == len(join_domains([
+        Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1), ufl_id=7), None,
+        Mesh(FiniteElement("Lagrange", quadrilateral, 1, (2, ), identity_pullback, H1), ufl_id=8)]))
+    assert None not in join_domains([
+        Mesh(FiniteElement("Lagrange", triangle3, 1, (3, ), identity_pullback, H1), ufl_id=7), None,
+        Mesh(FiniteElement("Lagrange", tetrahedron, 1, (3, ), identity_pullback, H1), ufl_id=8)])
 
 
 def test_everywhere_integrals_with_backwards_compatibility():
-    D = Mesh(triangle)
+    D = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
 
-    V = FunctionSpace(D, FiniteElement("CG", triangle, 1))
+    V = FunctionSpace(D, FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1))
     f = Coefficient(V)
 
     a = f * dx
@@ -184,9 +208,9 @@ def test_everywhere_integrals_with_backwards_compatibility():
 
 
 def test_merge_sort_integral_data():
-    D = Mesh(triangle)
+    D = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
 
-    V = FunctionSpace(D, FiniteElement("CG", triangle, 1))
+    V = FunctionSpace(D, FiniteElement("CG", triangle, 1, (), identity_pullback, H1))
 
     u = Coefficient(V)
     c = Constant(D)
diff --git a/test/test_duals.py b/test/test_duals.py
index 7b68d3cca..b82736e0f 100644
--- a/test/test_duals.py
+++ b/test/test_duals.py
@@ -3,24 +3,27 @@
 
 import pytest
 
-from ufl import (Action, Adjoint, Argument, Coargument, Coefficient, Cofunction, FiniteElement, FormSum, FunctionSpace,
-                 Matrix, Mesh, MixedFunctionSpace, TestFunction, TrialFunction, VectorElement, action, adjoint,
-                 derivative, dx, inner, interval, tetrahedron, triangle)
+from ufl import (Action, Adjoint, Argument, Coargument, Coefficient, Cofunction, FormSum, FunctionSpace, Matrix, Mesh,
+                 MixedFunctionSpace, TestFunction, TrialFunction, action, adjoint, derivative, dx, inner, interval,
+                 tetrahedron, triangle)
 from ufl.algorithms.ad import expand_derivatives
 from ufl.constantvalue import Zero
 from ufl.duals import is_dual, is_primal
+from ufl.finiteelement import FiniteElement
 from ufl.form import ZeroBaseForm
+from ufl.pullback import identity_pullback
+from ufl.sobolevspace import H1
 
 
 def test_mixed_functionspace(self):
     # Domains
-    domain_3d = Mesh(VectorElement("Lagrange", tetrahedron, 1))
-    domain_2d = Mesh(VectorElement("Lagrange", triangle, 1))
-    domain_1d = Mesh(VectorElement("Lagrange", interval, 1))
+    domain_3d = Mesh(FiniteElement("Lagrange", tetrahedron, 1, (3, ), identity_pullback, H1))
+    domain_2d = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
+    domain_1d = Mesh(FiniteElement("Lagrange", interval, 1, (1, ), identity_pullback, H1))
     # Finite elements
-    f_1d = FiniteElement("CG", interval, 1)
-    f_2d = FiniteElement("CG", triangle, 1)
-    f_3d = FiniteElement("CG", tetrahedron, 1)
+    f_1d = FiniteElement("Lagrange", interval, 1, (), identity_pullback, H1)
+    f_2d = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1)
+    f_3d = FiniteElement("Lagrange", tetrahedron, 1, (), identity_pullback, H1)
     # Function spaces
     V_3d = FunctionSpace(domain_3d, f_3d)
     V_2d = FunctionSpace(domain_2d, f_2d)
@@ -47,8 +50,8 @@ def test_mixed_functionspace(self):
 
 
 def test_dual_coefficients():
-    domain_2d = Mesh(VectorElement("Lagrange", triangle, 1))
-    f_2d = FiniteElement("CG", triangle, 1)
+    domain_2d = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
+    f_2d = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1)
     V = FunctionSpace(domain_2d, f_2d)
     V_dual = V.dual()
 
@@ -70,8 +73,8 @@ def test_dual_coefficients():
 
 
 def test_dual_arguments():
-    domain_2d = Mesh(VectorElement("Lagrange", triangle, 1))
-    f_2d = FiniteElement("CG", triangle, 1)
+    domain_2d = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
+    f_2d = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1)
     V = FunctionSpace(domain_2d, f_2d)
     V_dual = V.dual()
 
@@ -93,8 +96,8 @@ def test_dual_arguments():
 
 
 def test_addition():
-    domain_2d = Mesh(VectorElement("Lagrange", triangle, 1))
-    f_2d = FiniteElement("CG", triangle, 1)
+    domain_2d = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
+    f_2d = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1)
     V = FunctionSpace(domain_2d, f_2d)
     V_dual = V.dual()
 
@@ -131,8 +134,8 @@ def test_addition():
 
 
 def test_scalar_mult():
-    domain_2d = Mesh(VectorElement("Lagrange", triangle, 1))
-    f_2d = FiniteElement("CG", triangle, 1)
+    domain_2d = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
+    f_2d = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1)
     V = FunctionSpace(domain_2d, f_2d)
     V_dual = V.dual()
 
@@ -149,8 +152,8 @@ def test_scalar_mult():
 
 
 def test_adjoint():
-    domain_2d = Mesh(VectorElement("Lagrange", triangle, 1))
-    f_2d = FiniteElement("CG", triangle, 1)
+    domain_2d = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
+    f_2d = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1)
     V = FunctionSpace(domain_2d, f_2d)
     a = Matrix(V, V)
 
@@ -168,11 +171,11 @@ def test_adjoint():
 
 
 def test_action():
-    domain_2d = Mesh(VectorElement("Lagrange", triangle, 1))
-    f_2d = FiniteElement("CG", triangle, 1)
+    domain_2d = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
+    f_2d = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1)
     V = FunctionSpace(domain_2d, f_2d)
-    domain_1d = Mesh(VectorElement("Lagrange", interval, 1))
-    f_1d = FiniteElement("CG", interval, 1)
+    domain_1d = Mesh(FiniteElement("Lagrange", interval, 1, (1, ), identity_pullback, H1))
+    f_1d = FiniteElement("Lagrange", interval, 1, (), identity_pullback, H1)
     U = FunctionSpace(domain_1d, f_1d)
 
     a = Matrix(V, U)
@@ -228,11 +231,11 @@ def test_action():
 
 
 def test_differentiation():
-    domain_2d = Mesh(VectorElement("Lagrange", triangle, 1))
-    f_2d = FiniteElement("CG", triangle, 1)
+    domain_2d = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
+    f_2d = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1)
     V = FunctionSpace(domain_2d, f_2d)
-    domain_1d = Mesh(VectorElement("Lagrange", interval, 1))
-    f_1d = FiniteElement("CG", interval, 1)
+    domain_1d = Mesh(FiniteElement("Lagrange", interval, 1, (1, ), identity_pullback, H1))
+    f_1d = FiniteElement("Lagrange", interval, 1, (), identity_pullback, H1)
     U = FunctionSpace(domain_1d, f_1d)
 
     u = Coefficient(U)
@@ -291,8 +294,8 @@ def test_differentiation():
 
 
 def test_zero_base_form_mult():
-    domain_2d = Mesh(VectorElement("Lagrange", triangle, 1))
-    f_2d = FiniteElement("CG", triangle, 1)
+    domain_2d = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
+    f_2d = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1)
     V = FunctionSpace(domain_2d, f_2d)
     v = Argument(V, 0)
     Z = ZeroBaseForm((v, v))
diff --git a/test/test_equals.py b/test/test_equals.py
index 184d71c16..ddfa5058c 100755
--- a/test/test_equals.py
+++ b/test/test_equals.py
@@ -1,14 +1,17 @@
 """Test of expression comparison."""
 
-from ufl import Coefficient, Cofunction, FiniteElement, FunctionSpace, Mesh, VectorElement, triangle
+from ufl import Coefficient, Cofunction, FunctionSpace, Mesh, triangle
+from ufl.finiteelement import FiniteElement
+from ufl.pullback import identity_pullback
+from ufl.sobolevspace import H1
 
 
 def test_comparison_of_coefficients():
-    V = FiniteElement("CG", triangle, 1)
-    U = FiniteElement("CG", triangle, 2)
-    Ub = FiniteElement("CG", triangle, 2)
+    V = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1)
+    U = FiniteElement("Lagrange", triangle, 2, (), identity_pullback, H1)
+    Ub = FiniteElement("Lagrange", triangle, 2, (), identity_pullback, H1)
 
-    domain = Mesh(VectorElement("Lagrange", triangle, 1))
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     v_space = FunctionSpace(domain, V)
     u_space = FunctionSpace(domain, U)
     ub_space = FunctionSpace(domain, Ub)
@@ -36,11 +39,11 @@ def test_comparison_of_coefficients():
 
 
 def test_comparison_of_cofunctions():
-    V = FiniteElement("CG", triangle, 1)
-    U = FiniteElement("CG", triangle, 2)
-    Ub = FiniteElement("CG", triangle, 2)
+    V = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1)
+    U = FiniteElement("Lagrange", triangle, 2, (), identity_pullback, H1)
+    Ub = FiniteElement("Lagrange", triangle, 2, (), identity_pullback, H1)
 
-    domain = Mesh(VectorElement("Lagrange", triangle, 1))
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     v_space = FunctionSpace(domain, V)
     u_space = FunctionSpace(domain, U)
     ub_space = FunctionSpace(domain, Ub)
@@ -68,8 +71,8 @@ def test_comparison_of_cofunctions():
 
 
 def test_comparison_of_products():
-    V = FiniteElement("CG", triangle, 1)
-    domain = Mesh(VectorElement("Lagrange", triangle, 1))
+    V = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1)
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     v_space = FunctionSpace(domain, V)
     v = Coefficient(v_space)
     u = Coefficient(v_space)
@@ -82,8 +85,8 @@ def test_comparison_of_products():
 
 
 def test_comparison_of_sums():
-    V = FiniteElement("CG", triangle, 1)
-    domain = Mesh(VectorElement("Lagrange", triangle, 1))
+    V = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1)
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     v_space = FunctionSpace(domain, V)
     v = Coefficient(v_space)
     u = Coefficient(v_space)
@@ -96,8 +99,8 @@ def test_comparison_of_sums():
 
 
 def test_comparison_of_deeply_nested_expression():
-    V = FiniteElement("CG", triangle, 1)
-    domain = Mesh(VectorElement("Lagrange", triangle, 1))
+    V = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1)
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     v_space = FunctionSpace(domain, V)
     v = Coefficient(v_space, count=1)
     u = Coefficient(v_space, count=1)
diff --git a/test/test_evaluate.py b/test/test_evaluate.py
index 61e8af9e9..964740aac 100755
--- a/test/test_evaluate.py
+++ b/test/test_evaluate.py
@@ -3,10 +3,13 @@
 
 import math
 
-from ufl import (Argument, Coefficient, FiniteElement, FunctionSpace, Identity, Mesh, SpatialCoordinate, VectorElement,
-                 as_matrix, as_vector, cos, cross, det, dev, dot, exp, i, indices, inner, j, ln, outer, sin, skew, sqrt,
-                 sym, tan, tetrahedron, tr, triangle)
+from ufl import (Argument, Coefficient, FunctionSpace, Identity, Mesh, SpatialCoordinate, as_matrix, as_vector, cos,
+                 cross, det, dev, dot, exp, i, indices, inner, j, ln, outer, sin, skew, sqrt, sym, tan, tetrahedron, tr,
+                 triangle)
 from ufl.constantvalue import as_ufl
+from ufl.finiteelement import FiniteElement
+from ufl.pullback import identity_pullback
+from ufl.sobolevspace import H1
 
 
 def testScalars():
@@ -40,7 +43,7 @@ def testIdentity():
 
 def testCoords():
     cell = triangle
-    domain = Mesh(VectorElement("Lagrange", cell, 1))
+    domain = Mesh(FiniteElement("Lagrange", cell, 1, (2, ), identity_pullback, H1))
     x = SpatialCoordinate(domain)
     s = x[0] + x[1]
     e = s((5, 7))
@@ -50,8 +53,8 @@ def testCoords():
 
 def testFunction1():
     cell = triangle
-    element = FiniteElement("CG", cell, 1)
-    domain = Mesh(VectorElement("Lagrange", cell, 1))
+    element = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1)
+    domain = Mesh(FiniteElement("Lagrange", cell, 1, (2, ), identity_pullback, H1))
     space = FunctionSpace(domain, element)
     f = Coefficient(space)
     s = 3 * f
@@ -62,8 +65,8 @@ def testFunction1():
 
 def testFunction2():
     cell = triangle
-    element = FiniteElement("CG", cell, 1)
-    domain = Mesh(VectorElement("Lagrange", cell, 1))
+    element = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1)
+    domain = Mesh(FiniteElement("Lagrange", cell, 1, (2, ), identity_pullback, H1))
     space = FunctionSpace(domain, element)
     f = Coefficient(space)
 
@@ -77,8 +80,8 @@ def g(x):
 
 def testArgument2():
     cell = triangle
-    element = FiniteElement("CG", cell, 1)
-    domain = Mesh(VectorElement("Lagrange", cell, 1))
+    element = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1)
+    domain = Mesh(FiniteElement("Lagrange", cell, 1, (2, ), identity_pullback, H1))
     space = FunctionSpace(domain, element)
     f = Argument(space, 2)
 
@@ -92,7 +95,7 @@ def g(x):
 
 def testAlgebra():
     cell = triangle
-    domain = Mesh(VectorElement("Lagrange", cell, 1))
+    domain = Mesh(FiniteElement("Lagrange", cell, 1, (2, ), identity_pullback, H1))
     x = SpatialCoordinate(domain)
     s = 3 * (x[0] + x[1]) - 7 + x[0] ** (x[1] / 2)
     e = s((5, 7))
@@ -102,7 +105,7 @@ def testAlgebra():
 
 def testIndexSum():
     cell = triangle
-    domain = Mesh(VectorElement("Lagrange", cell, 1))
+    domain = Mesh(FiniteElement("Lagrange", cell, 1, (2, ), identity_pullback, H1))
     x = SpatialCoordinate(domain)
     i, = indices(1)
     s = x[i] * x[i]
@@ -113,7 +116,7 @@ def testIndexSum():
 
 def testIndexSum2():
     cell = triangle
-    domain = Mesh(VectorElement("Lagrange", cell, 1))
+    domain = Mesh(FiniteElement("Lagrange", cell, 1, (2, ), identity_pullback, H1))
     x = SpatialCoordinate(domain)
     ident = Identity(cell.geometric_dimension())
     i, j = indices(2)
@@ -125,7 +128,7 @@ def testIndexSum2():
 
 
 def testMathFunctions():
-    domain = Mesh(VectorElement("Lagrange", triangle, 1))
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     x = SpatialCoordinate(domain)[0]
 
     s = sin(x)
@@ -160,7 +163,7 @@ def testMathFunctions():
 
 
 def testListTensor():
-    domain = Mesh(VectorElement("Lagrange", triangle, 1))
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     x, y = SpatialCoordinate(domain)
 
     m = as_matrix([[x, y], [-y, -x]])
@@ -177,7 +180,7 @@ def testListTensor():
 
 
 def testComponentTensor1():
-    domain = Mesh(VectorElement("Lagrange", triangle, 1))
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     x = SpatialCoordinate(domain)
     m = as_vector(x[i], i)
 
@@ -188,7 +191,7 @@ def testComponentTensor1():
 
 
 def testComponentTensor2():
-    domain = Mesh(VectorElement("Lagrange", triangle, 1))
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     x = SpatialCoordinate(domain)
     xx = outer(x, x)
 
@@ -201,7 +204,7 @@ def testComponentTensor2():
 
 
 def testComponentTensor3():
-    domain = Mesh(VectorElement("Lagrange", triangle, 1))
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     x = SpatialCoordinate(domain)
     xx = outer(x, x)
 
@@ -214,8 +217,8 @@ def testComponentTensor3():
 
 
 def testCoefficient():
-    V = FiniteElement("CG", triangle, 1)
-    domain = Mesh(VectorElement("Lagrange", triangle, 1))
+    V = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1)
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     space = FunctionSpace(domain, V)
     f = Coefficient(space)
     e = f ** 2
@@ -226,8 +229,8 @@ def eval_f(x):
 
 
 def testCoefficientDerivative():
-    V = FiniteElement("CG", triangle, 1)
-    domain = Mesh(VectorElement("Lagrange", triangle, 1))
+    V = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1)
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     space = FunctionSpace(domain, V)
     f = Coefficient(space)
     e = f.dx(0) ** 2 + f.dx(1) ** 2
@@ -248,7 +251,7 @@ def eval_f(x, derivatives):
 
 
 def test_dot():
-    domain = Mesh(VectorElement("Lagrange", triangle, 1))
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     x = SpatialCoordinate(domain)
     s = dot(x, 2 * x)
     e = s((5, 7))
@@ -257,7 +260,7 @@ def test_dot():
 
 
 def test_inner():
-    domain = Mesh(VectorElement("Lagrange", triangle, 1))
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     x = SpatialCoordinate(domain)
     xx = as_matrix(((2 * x[0], 3 * x[0]), (2 * x[1], 3 * x[1])))
     s = inner(xx, 2 * xx)
@@ -267,7 +270,7 @@ def test_inner():
 
 
 def test_outer():
-    domain = Mesh(VectorElement("Lagrange", triangle, 1))
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     x = SpatialCoordinate(domain)
     xx = outer(outer(x, x), as_vector((2, 3)))
     s = inner(xx, 2 * xx)
@@ -277,7 +280,7 @@ def test_outer():
 
 
 def test_cross():
-    domain = Mesh(VectorElement("Lagrange", tetrahedron, 1))
+    domain = Mesh(FiniteElement("Lagrange", tetrahedron, 1, (3, ), identity_pullback, H1))
     x = SpatialCoordinate(domain)
     xv = (3, 5, 7)
 
@@ -310,7 +313,7 @@ def test_cross():
 
 
 def xtest_dev():
-    domain = Mesh(VectorElement("Lagrange", triangle, 1))
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     x = SpatialCoordinate(domain)
     xv = (5, 7)
     xx = outer(x, x)
@@ -322,7 +325,7 @@ def xtest_dev():
 
 
 def test_skew():
-    domain = Mesh(VectorElement("Lagrange", triangle, 1))
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     x = SpatialCoordinate(domain)
     xv = (5, 7)
     xx = outer(x, x)
@@ -334,7 +337,7 @@ def test_skew():
 
 
 def test_sym():
-    domain = Mesh(VectorElement("Lagrange", triangle, 1))
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     x = SpatialCoordinate(domain)
     xv = (5, 7)
     xx = outer(x, x)
@@ -346,7 +349,7 @@ def test_sym():
 
 
 def test_tr():
-    domain = Mesh(VectorElement("Lagrange", triangle, 1))
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     x = SpatialCoordinate(domain)
     xv = (5, 7)
     xx = outer(x, x)
@@ -357,7 +360,7 @@ def test_tr():
 
 
 def test_det2D():
-    domain = Mesh(VectorElement("Lagrange", triangle, 1))
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     x = SpatialCoordinate(domain)
     xv = (5, 7)
     a, b = 6.5, -4
diff --git a/test/test_expand_indices.py b/test/test_expand_indices.py
index 023b4ccb1..f8314df2c 100755
--- a/test/test_expand_indices.py
+++ b/test/test_expand_indices.py
@@ -8,10 +8,13 @@
 
 import pytest
 
-from ufl import (Coefficient, FiniteElement, FunctionSpace, Identity, Mesh, TensorElement, VectorElement, as_tensor,
-                 cos, det, div, dot, dx, exp, grad, i, inner, j, k, l, ln, nabla_div, nabla_grad, outer, sin, triangle)
+from ufl import (Coefficient, FunctionSpace, Identity, Mesh, as_tensor, cos, det, div, dot, dx, exp, grad, i, inner, j,
+                 k, l, ln, nabla_div, nabla_grad, outer, sin, triangle)
 from ufl.algorithms import compute_form_data, expand_derivatives, expand_indices
 from ufl.algorithms.renumbering import renumber_indices
+from ufl.finiteelement import FiniteElement
+from ufl.pullback import identity_pullback
+from ufl.sobolevspace import H1
 
 # TODO: Test expand_indices2 throuroughly for correctness, then efficiency:
 # expand_indices, expand_indices2 = expand_indices2, expand_indices
@@ -21,10 +24,10 @@ class Fixture:
 
     def __init__(self):
         cell = triangle
-        element = FiniteElement("Lagrange", cell, 1)
-        velement = VectorElement("Lagrange", cell, 1)
-        telement = TensorElement("Lagrange", cell, 1)
-        domain = Mesh(VectorElement("Lagrange", cell, 1))
+        element = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1)
+        velement = FiniteElement("Lagrange", cell, 1, (2, ), identity_pullback, H1)
+        telement = FiniteElement("Lagrange", cell, 1, (2, 2), identity_pullback, H1)
+        domain = Mesh(FiniteElement("Lagrange", cell, 1, (2, ), identity_pullback, H1))
         space = FunctionSpace(domain, element)
         vspace = FunctionSpace(domain, velement)
         tspace = FunctionSpace(domain, telement)
diff --git a/test/test_external_operator.py b/test/test_external_operator.py
index 13f4eaa65..0cdebb422 100644
--- a/test/test_external_operator.py
+++ b/test/test_external_operator.py
@@ -5,35 +5,37 @@
 
 import pytest
 
-# This imports everything external code will see from ufl
-from ufl import (Action, Argument, Coefficient, Constant, FiniteElement, Form, FunctionSpace, Mesh, TestFunction,
-                 TrialFunction, VectorElement, action, adjoint, cos, derivative, dx, inner, sin, triangle)
+from ufl import (Action, Argument, Coefficient, Constant, Form, FunctionSpace, Mesh, TestFunction, TrialFunction,
+                 action, adjoint, cos, derivative, dx, inner, sin, triangle)
 from ufl.algorithms import expand_derivatives
 from ufl.algorithms.apply_derivatives import apply_derivatives
 from ufl.core.external_operator import ExternalOperator
+from ufl.finiteelement import FiniteElement
 from ufl.form import BaseForm
+from ufl.pullback import identity_pullback
+from ufl.sobolevspace import H1
 
 
 @pytest.fixture
 def domain_2d():
-    return Mesh(VectorElement("Lagrange", triangle, 1))
+    return Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
 
 
 @pytest.fixture
 def V1(domain_2d):
-    f1 = FiniteElement("CG", triangle, 1)
+    f1 = FiniteElement("CG", triangle, 1, (), identity_pullback, H1)
     return FunctionSpace(domain_2d, f1)
 
 
 @pytest.fixture
 def V2(domain_2d):
-    f1 = FiniteElement("CG", triangle, 2)
+    f1 = FiniteElement("CG", triangle, 2, (), identity_pullback, H1)
     return FunctionSpace(domain_2d, f1)
 
 
 @pytest.fixture
 def V3(domain_2d):
-    f1 = FiniteElement("CG", triangle, 3)
+    f1 = FiniteElement("CG", triangle, 3, (), identity_pullback, H1)
     return FunctionSpace(domain_2d, f1)
 
 
diff --git a/test/test_ffcforms.py b/test/test_ffcforms.py
index a0dbf7e82..5ab8778ab 100755
--- a/test/test_ffcforms.py
+++ b/test/test_ffcforms.py
@@ -13,14 +13,17 @@
 # Examples copied from the FFC demo directory, examples contributed
 # by Johan Jansson, Kristian Oelgaard, Marie Rognes, and Garth Wells.
 
-from ufl import (Coefficient, Constant, Dx, FacetNormal, FiniteElement, FunctionSpace, Mesh, TensorElement,
-                 TestFunction, TestFunctions, TrialFunction, TrialFunctions, VectorConstant, VectorElement, avg, curl,
-                 div, dot, ds, dS, dx, grad, i, inner, j, jump, lhs, rhs, sqrt, tetrahedron, triangle)
+from ufl import (Coefficient, Constant, Dx, FacetNormal, FunctionSpace, Mesh, TestFunction, TestFunctions,
+                 TrialFunction, TrialFunctions, VectorConstant, avg, curl, div, dot, ds, dS, dx, grad, i, inner, j,
+                 jump, lhs, rhs, sqrt, tetrahedron, triangle)
+from ufl.finiteelement import FiniteElement, MixedElement
+from ufl.pullback import contravariant_piola, covariant_piola, identity_pullback
+from ufl.sobolevspace import H1, L2, HCurl, HDiv
 
 
 def testConstant():
-    element = FiniteElement("Lagrange", "triangle", 1)
-    domain = Mesh(VectorElement("Lagrange", "triangle", 1))
+    element = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1)
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     space = FunctionSpace(domain, element)
 
     v = TestFunction(space)
@@ -36,8 +39,8 @@ def testConstant():
 
 
 def testElasticity():
-    element = VectorElement("Lagrange", "tetrahedron", 1)
-    domain = Mesh(VectorElement("Lagrange", "tetrahedron", 1))
+    element = FiniteElement("Lagrange", tetrahedron, 1, (3, ), identity_pullback, H1)
+    domain = Mesh(FiniteElement("Lagrange", tetrahedron, 1, (3, ), identity_pullback, H1))
     space = FunctionSpace(domain, element)
 
     v = TestFunction(space)
@@ -52,8 +55,8 @@ def eps(v):
 
 
 def testEnergyNorm():
-    element = FiniteElement("Lagrange", "tetrahedron", 1)
-    domain = Mesh(VectorElement("Lagrange", "tetrahedron", 1))
+    element = FiniteElement("Lagrange", tetrahedron, 1, (), identity_pullback, H1)
+    domain = Mesh(FiniteElement("Lagrange", tetrahedron, 1, (3, ), identity_pullback, H1))
     space = FunctionSpace(domain, element)
 
     v = Coefficient(space)
@@ -61,8 +64,8 @@ def testEnergyNorm():
 
 
 def testEquation():
-    element = FiniteElement("Lagrange", "triangle", 1)
-    domain = Mesh(VectorElement("Lagrange", "triangle", 1))
+    element = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1)
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     space = FunctionSpace(domain, element)
 
     k = 0.1
@@ -78,8 +81,8 @@ def testEquation():
 
 
 def testFunctionOperators():
-    element = FiniteElement("Lagrange", "triangle", 1)
-    domain = Mesh(VectorElement("Lagrange", "triangle", 1))
+    element = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1)
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     space = FunctionSpace(domain, element)
 
     v = TestFunction(space)
@@ -93,8 +96,8 @@ def testFunctionOperators():
 
 
 def testHeat():
-    element = FiniteElement("Lagrange", "triangle", 1)
-    domain = Mesh(VectorElement("Lagrange", "triangle", 1))
+    element = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1)
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     space = FunctionSpace(domain, element)
 
     v = TestFunction(space)
@@ -109,8 +112,8 @@ def testHeat():
 
 
 def testMass():
-    element = FiniteElement("Lagrange", "tetrahedron", 3)
-    domain = Mesh(VectorElement("Lagrange", "tetrahedron", 1))
+    element = FiniteElement("Lagrange", tetrahedron, 3, (), identity_pullback, H1)
+    domain = Mesh(FiniteElement("Lagrange", tetrahedron, 1, (3, ), identity_pullback, H1))
     space = FunctionSpace(domain, element)
 
     v = TestFunction(space)
@@ -120,19 +123,18 @@ def testMass():
 
 
 def testMixedMixedElement():
-    P3 = FiniteElement("Lagrange", "triangle", 3)
-
-    element = (P3 * P3) * (P3 * P3)  # noqa: F841
+    P3 = FiniteElement("Lagrange", triangle, 3, (), identity_pullback, H1)
+    MixedElement([[P3, P3], [P3, P3]])
 
 
 def testMixedPoisson():
     q = 1
 
-    BDM = FiniteElement("Brezzi-Douglas-Marini", "triangle", q)
-    DG = FiniteElement("Discontinuous Lagrange", "triangle", q - 1)
+    BDM = FiniteElement("Brezzi-Douglas-Marini", triangle, q, (2, ), contravariant_piola, HDiv)
+    DG = FiniteElement("Discontinuous Lagrange", triangle, q - 1, (), identity_pullback, L2)
 
-    mixed_element = BDM * DG
-    domain = Mesh(VectorElement("Lagrange", "triangle", 1))
+    mixed_element = MixedElement([BDM, DG])
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     space = FunctionSpace(domain, mixed_element)
 
     (tau, w) = TestFunctions(space)
@@ -145,8 +147,8 @@ def testMixedPoisson():
 
 
 def testNavierStokes():
-    element = VectorElement("Lagrange", "tetrahedron", 1)
-    domain = Mesh(VectorElement("Lagrange", "tetrahedron", 1))
+    element = FiniteElement("Lagrange", tetrahedron, 1, (3, ), identity_pullback, H1)
+    domain = Mesh(FiniteElement("Lagrange", tetrahedron, 1, (3, ), identity_pullback, H1))
     space = FunctionSpace(domain, element)
 
     v = TestFunction(space)
@@ -159,8 +161,8 @@ def testNavierStokes():
 
 
 def testNeumannProblem():
-    element = VectorElement("Lagrange", "triangle", 1)
-    domain = Mesh(VectorElement("Lagrange", "triangle", 1))
+    element = FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1)
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     space = FunctionSpace(domain, element)
 
     v = TestFunction(space)
@@ -176,8 +178,8 @@ def testNeumannProblem():
 
 
 def testOptimization():
-    element = FiniteElement("Lagrange", "triangle", 3)
-    domain = Mesh(VectorElement("Lagrange", "triangle", 1))
+    element = FiniteElement("Lagrange", triangle, 3, (), identity_pullback, H1)
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     space = FunctionSpace(domain, element)
 
     v = TestFunction(space)
@@ -189,16 +191,16 @@ def testOptimization():
 
 
 def testP5tet():
-    element = FiniteElement("Lagrange", tetrahedron, 5)  # noqa: F841
+    FiniteElement("Lagrange", tetrahedron, 5, (), identity_pullback, H1)
 
 
 def testP5tri():
-    element = FiniteElement("Lagrange", triangle, 5)  # noqa: F841
+    FiniteElement("Lagrange", triangle, 5, (), identity_pullback, H1)
 
 
 def testPoissonDG():
-    element = FiniteElement("Discontinuous Lagrange", triangle, 1)
-    domain = Mesh(VectorElement("Lagrange", "triangle", 1))
+    element = FiniteElement("Discontinuous Lagrange", triangle, 1, (), identity_pullback, L2)
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     space = FunctionSpace(domain, element)
 
     v = TestFunction(space)
@@ -236,8 +238,8 @@ def testPoissonDG():
 
 
 def testPoisson():
-    element = FiniteElement("Lagrange", "triangle", 1)
-    domain = Mesh(VectorElement("Lagrange", "triangle", 1))
+    element = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1)
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     space = FunctionSpace(domain, element)
 
     v = TestFunction(space)
@@ -250,8 +252,8 @@ def testPoisson():
 
 
 def testPoissonSystem():
-    element = VectorElement("Lagrange", "triangle", 1)
-    domain = Mesh(VectorElement("Lagrange", "triangle", 1))
+    element = FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1)
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     space = FunctionSpace(domain, element)
 
     v = TestFunction(space)
@@ -270,8 +272,8 @@ def testProjection():
     # in FFC for a while. For DOLFIN, the current (global) L^2
     # projection can be extended to handle also local projections.
 
-    P1 = FiniteElement("Lagrange", "triangle", 1)
-    domain = Mesh(VectorElement("Lagrange", "triangle", 1))
+    P1 = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1)
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     space = FunctionSpace(domain, P1)
 
     v = TestFunction(space)  # noqa: F841
@@ -285,16 +287,16 @@ def testProjection():
 
 
 def testQuadratureElement():
-    element = FiniteElement("Lagrange", "triangle", 2)
+    element = FiniteElement("Lagrange", triangle, 2, (), identity_pullback, H1)
 
     # FFC notation:
-    # QE = QuadratureElement("triangle", 3)
-    # sig = VectorQuadratureElement("triangle", 3)
+    # QE = QuadratureElement(triangle, 3)
+    # sig = VectorQuadratureElement(triangle, 3)
 
-    QE = FiniteElement("Quadrature", "triangle", 3)
-    sig = VectorElement("Quadrature", "triangle", 3)
+    QE = FiniteElement("Quadrature", triangle, 3, (), identity_pullback, L2)
+    sig = FiniteElement("Quadrature", triangle, 3, (2, ), identity_pullback, L2)
 
-    domain = Mesh(VectorElement("Lagrange", "triangle", 1))
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     space = FunctionSpace(domain, element)
 
     v = TestFunction(space)
@@ -311,11 +313,11 @@ def testQuadratureElement():
 def testStokes():
     # UFLException: Shape mismatch in sum.
 
-    P2 = VectorElement("Lagrange", "triangle", 2)
-    P1 = FiniteElement("Lagrange", "triangle", 1)
-    TH = P2 * P1
+    P2 = FiniteElement("Lagrange", triangle, 2, (2, ), identity_pullback, H1)
+    P1 = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1)
+    TH = MixedElement([P2, P1])
 
-    domain = Mesh(VectorElement("Lagrange", "triangle", 1))
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     th_space = FunctionSpace(domain, TH)
     p2_space = FunctionSpace(domain, P2)
 
@@ -332,8 +334,8 @@ def testStokes():
 
 
 def testSubDomain():
-    element = FiniteElement("CG", "tetrahedron", 1)
-    domain = Mesh(VectorElement("Lagrange", "tetrahedron", 1))
+    element = FiniteElement("Lagrange", tetrahedron, 1, (), identity_pullback, H1)
+    domain = Mesh(FiniteElement("Lagrange", tetrahedron, 1, (3, ), identity_pullback, H1))
     space = FunctionSpace(domain, element)
 
     f = Coefficient(space)
@@ -342,8 +344,8 @@ def testSubDomain():
 
 
 def testSubDomains():
-    element = FiniteElement("CG", "tetrahedron", 1)
-    domain = Mesh(VectorElement("Lagrange", "tetrahedron", 1))
+    element = FiniteElement("Lagrange", tetrahedron, 1, (), identity_pullback, H1)
+    domain = Mesh(FiniteElement("Lagrange", tetrahedron, 1, (3, ), identity_pullback, H1))
     space = FunctionSpace(domain, element)
 
     v = TestFunction(space)
@@ -355,8 +357,8 @@ def testSubDomains():
 
 def testTensorWeightedPoisson():
     # FFC notation:
-    # P1 = FiniteElement("Lagrange", "triangle", 1)
-    # P0 = FiniteElement("Discontinuous Lagrange", "triangle", 0)
+    # P1 = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1)
+    # P0 = FiniteElement("Discontinuous Lagrange", triangle, 0, (), identity_pullback, L2)
     #
     # v = TestFunction(P1)
     # u = TrialFunction(P1)
@@ -371,10 +373,10 @@ def testTensorWeightedPoisson():
     #
     # a = dot(grad(v), mult(C, grad(u)))*dx
 
-    P1 = FiniteElement("Lagrange", "triangle", 1)
-    P0 = TensorElement("Discontinuous Lagrange", "triangle", 0, shape=(2, 2))
+    P1 = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1)
+    P0 = FiniteElement("Discontinuous Lagrange", triangle, 0, (2, 2), identity_pullback, L2)
 
-    domain = Mesh(VectorElement("Lagrange", "triangle", 1))
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     p1_space = FunctionSpace(domain, P1)
     p0_space = FunctionSpace(domain, P0)
 
@@ -401,16 +403,16 @@ def HodgeLaplaceGradCurl(space, fspace):
 
         return [a, L]
 
-    shape = "tetrahedron"
+    shape = tetrahedron
     order = 1
 
-    GRAD = FiniteElement("Lagrange", shape, order)
+    GRAD = FiniteElement("Lagrange", shape, order, (), identity_pullback, H1)
 
     # FFC notation: CURL = FiniteElement("Nedelec", shape, order-1)
-    CURL = FiniteElement("N1curl", shape, order)
+    CURL = FiniteElement("N1curl", shape, order, (3, ), covariant_piola, HCurl)
 
-    VectorLagrange = VectorElement("Lagrange", shape, order + 1)
-    domain = Mesh(VectorElement("Lagrange", shape, 1))
+    VectorLagrange = FiniteElement("Lagrange", shape, order + 1, (3, ), identity_pullback, H1)
+    domain = Mesh(FiniteElement("Lagrange", shape, 1, (3, ), identity_pullback, H1))
 
-    [a, L] = HodgeLaplaceGradCurl(FunctionSpace(domain, GRAD * CURL),
+    [a, L] = HodgeLaplaceGradCurl(FunctionSpace(domain, MixedElement([GRAD, CURL])),
                                   FunctionSpace(domain, VectorLagrange))
diff --git a/test/test_form.py b/test/test_form.py
index 554ed85c5..30ca5117a 100755
--- a/test/test_form.py
+++ b/test/test_form.py
@@ -1,27 +1,30 @@
 import pytest
 
-from ufl import (Coefficient, Cofunction, FiniteElement, Form, FormSum, FunctionSpace, Mesh, SpatialCoordinate,
-                 TestFunction, TrialFunction, VectorElement, dot, ds, dx, grad, inner, nabla_grad, triangle)
+from ufl import (Coefficient, Cofunction, Form, FormSum, FunctionSpace, Mesh, SpatialCoordinate, TestFunction,
+                 TrialFunction, dot, ds, dx, grad, inner, nabla_grad, triangle)
+from ufl.finiteelement import FiniteElement
 from ufl.form import BaseForm
+from ufl.pullback import identity_pullback
+from ufl.sobolevspace import H1
 
 
 @pytest.fixture
 def element():
     cell = triangle
-    element = FiniteElement("Lagrange", cell, 1)
+    element = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1)
     return element
 
 
 @pytest.fixture
 def domain():
     cell = triangle
-    return Mesh(VectorElement("Lagrange", cell, 1))
+    return Mesh(FiniteElement("Lagrange", cell, 1, (2, ), identity_pullback, H1))
 
 
 @pytest.fixture
 def mass(domain):
     cell = triangle
-    element = FiniteElement("Lagrange", cell, 1)
+    element = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1)
     space = FunctionSpace(domain, element)
     v = TestFunction(space)
     u = TrialFunction(space)
@@ -31,7 +34,7 @@ def mass(domain):
 @pytest.fixture
 def stiffness(domain):
     cell = triangle
-    element = FiniteElement("Lagrange", cell, 1)
+    element = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1)
     space = FunctionSpace(domain, element)
     v = TestFunction(space)
     u = TrialFunction(space)
@@ -41,7 +44,7 @@ def stiffness(domain):
 @pytest.fixture
 def convection(domain):
     cell = triangle
-    element = VectorElement("Lagrange", cell, 1)
+    element = FiniteElement("Lagrange", cell, 1, (2, ), identity_pullback, H1)
     space = FunctionSpace(domain, element)
     v = TestFunction(space)
     u = TrialFunction(space)
@@ -52,7 +55,7 @@ def convection(domain):
 @pytest.fixture
 def load(domain):
     cell = triangle
-    element = FiniteElement("Lagrange", cell, 1)
+    element = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1)
     space = FunctionSpace(domain, element)
     f = Coefficient(space)
     v = TestFunction(space)
@@ -62,7 +65,7 @@ def load(domain):
 @pytest.fixture
 def boundary_load(domain):
     cell = triangle
-    element = FiniteElement("Lagrange", cell, 1)
+    element = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1)
     space = FunctionSpace(domain, element)
     f = Coefficient(space)
     v = TestFunction(space)
@@ -100,8 +103,8 @@ def test_form_coefficients(element, domain):
 
 def test_form_domains():
     cell = triangle
-    domain = Mesh(VectorElement("Lagrange", cell, 1))
-    element = FiniteElement("Lagrange", cell, 1)
+    element = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1)
+    domain = Mesh(FiniteElement("Lagrange", cell, 1, (2, ), identity_pullback, H1))
     V = FunctionSpace(domain, element)
 
     v = TestFunction(V)
@@ -132,8 +135,8 @@ def test_form_integrals(mass, boundary_load):
 
 
 def test_form_call():
-    domain = Mesh(VectorElement("Lagrange", triangle, 1))
-    element = FiniteElement("Lagrange", triangle, 1)
+    element = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1)
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     V = FunctionSpace(domain, element)
     v = TestFunction(V)
     u = TrialFunction(V)
@@ -151,8 +154,8 @@ def test_form_call():
 
 
 def test_formsum(mass):
-    domain = Mesh(VectorElement("Lagrange", triangle, 1))
-    element = FiniteElement("Lagrange", triangle, 1)
+    element = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1)
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     V = FunctionSpace(domain, element)
     v = Cofunction(V.dual())
 
diff --git a/test/test_grad.py b/test/test_grad.py
index 4b5d9a996..21b269e2f 100755
--- a/test/test_grad.py
+++ b/test/test_grad.py
@@ -1,8 +1,11 @@
 """Test use of grad in various situations."""
 
-from ufl import (Coefficient, Constant, FiniteElement, TensorConstant, TensorElement, VectorConstant, VectorElement,
-                 div, dx, grad, indices, inner, interval, tetrahedron, triangle)
+from ufl import (Coefficient, Constant, TensorConstant, VectorConstant, div, dx, grad, indices, inner, interval,
+                 tetrahedron, triangle)
 from ufl.algorithms import compute_form_data
+from ufl.finiteelement import FiniteElement
+from ufl.pullback import identity_pullback
+from ufl.sobolevspace import H1
 
 
 def xtest_grad_div_curl_properties_in_1D(self):
@@ -20,9 +23,9 @@ def xtest_grad_div_curl_properties_in_3D(self):
 def _test_grad_div_curl_properties(self, cell):
     d = cell.geometric_dimension()
 
-    S = FiniteElement("CG", cell, 1)
-    V = VectorElement("CG", cell, 1)
-    T = TensorElement("CG", cell, 1)
+    S = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1)
+    V = FiniteElement("Lagrange", cell, 1, (d, ), identity_pullback, H1)
+    T = FiniteElement("Lagrange", cell, 1, (d, d), identity_pullback, H1)
 
     cs = Constant(cell)
     cv = VectorConstant(cell)
diff --git a/test/test_illegal.py b/test/test_illegal.py
index 40409ef6d..9931946a4 100755
--- a/test/test_illegal.py
+++ b/test/test_illegal.py
@@ -1,23 +1,26 @@
 import pytest
 
-from ufl import Argument, Coefficient, FiniteElement, FunctionSpace, Mesh, VectorElement
+from ufl import Argument, Coefficient, FunctionSpace, Mesh, triangle
+from ufl.finiteelement import FiniteElement
+from ufl.pullback import identity_pullback
+from ufl.sobolevspace import H1
 
 # TODO: Add more illegal expressions to check!
 
 
 @pytest.fixture
 def selement():
-    return FiniteElement("Lagrange", "triangle", 1)
+    return FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1)
 
 
 @pytest.fixture
 def velement():
-    return VectorElement("Lagrange", "triangle", 1)
+    return FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1)
 
 
 @pytest.fixture
 def domain():
-    return Mesh(VectorElement("Lagrange", "triangle", 1))
+    return Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
 
 
 @pytest.fixture
diff --git a/test/test_indexing.py b/test/test_indexing.py
index 5b7450e9b..4ffdc7eb5 100755
--- a/test/test_indexing.py
+++ b/test/test_indexing.py
@@ -1,12 +1,15 @@
 import pytest
 
-from ufl import Index, Mesh, SpatialCoordinate, VectorElement, outer, triangle
+from ufl import Index, Mesh, SpatialCoordinate, outer, triangle
 from ufl.classes import FixedIndex, Indexed, MultiIndex, Outer, Zero
+from ufl.finiteelement import FiniteElement
+from ufl.pullback import identity_pullback
+from ufl.sobolevspace import H1
 
 
 @pytest.fixture
 def domain():
-    return Mesh(VectorElement("Lagrange", triangle, 1))
+    return Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
 
 
 @pytest.fixture
diff --git a/test/test_indices.py b/test/test_indices.py
index e1ab15587..49cf16a1a 100755
--- a/test/test_indices.py
+++ b/test/test_indices.py
@@ -1,15 +1,18 @@
 import pytest
 
-from ufl import (Argument, Coefficient, FunctionSpace, Mesh, TensorElement, TestFunction, TrialFunction, VectorElement,
-                 as_matrix, as_tensor, as_vector, cos, dx, exp, i, indices, j, k, l, outer, sin, triangle)
+from ufl import (Argument, Coefficient, FunctionSpace, Mesh, TestFunction, TrialFunction, as_matrix, as_tensor,
+                 as_vector, cos, dx, exp, i, indices, j, k, l, outer, sin, triangle)
 from ufl.classes import IndexSum
+from ufl.finiteelement import FiniteElement
+from ufl.pullback import identity_pullback
+from ufl.sobolevspace import H1
 
 # TODO: add more expressions to test as many possible combinations of index notation as feasible...
 
 
 def test_vector_indices(self):
-    element = VectorElement("CG", "triangle", 1)
-    domain = Mesh(VectorElement("Lagrange", "triangle", 1))
+    element = FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1)
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     space = FunctionSpace(domain, element)
     u = Argument(space, 2)
     f = Coefficient(space)
@@ -18,8 +21,8 @@ def test_vector_indices(self):
 
 
 def test_tensor_indices(self):
-    element = TensorElement("CG", "triangle", 1)
-    domain = Mesh(VectorElement("Lagrange", "triangle", 1))
+    element = FiniteElement("Lagrange", triangle, 1, (2, 2), identity_pullback, H1)
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     space = FunctionSpace(domain, element)
     u = Argument(space, 2)
     f = Coefficient(space)
@@ -31,8 +34,8 @@ def test_tensor_indices(self):
 
 
 def test_indexed_sum1(self):
-    element = VectorElement("CG", "triangle", 1)
-    domain = Mesh(VectorElement("Lagrange", "triangle", 1))
+    element = FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1)
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     space = FunctionSpace(domain, element)
     u = Argument(space, 2)
     f = Coefficient(space)
@@ -42,8 +45,8 @@ def test_indexed_sum1(self):
 
 
 def test_indexed_sum2(self):
-    element = VectorElement("CG", "triangle", 1)
-    domain = Mesh(VectorElement("Lagrange", "triangle", 1))
+    element = FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1)
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     space = FunctionSpace(domain, element)
     v = Argument(space, 2)
     u = Argument(space, 3)
@@ -54,8 +57,8 @@ def test_indexed_sum2(self):
 
 
 def test_indexed_sum3(self):
-    element = VectorElement("CG", "triangle", 1)
-    domain = Mesh(VectorElement("Lagrange", "triangle", 1))
+    element = FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1)
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     space = FunctionSpace(domain, element)
     u = Argument(space, 2)
     f = Coefficient(space)
@@ -64,8 +67,8 @@ def test_indexed_sum3(self):
 
 
 def test_indexed_function1(self):
-    element = VectorElement("CG", "triangle", 1)
-    domain = Mesh(VectorElement("Lagrange", "triangle", 1))
+    element = FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1)
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     space = FunctionSpace(domain, element)
     v = Argument(space, 2)
     u = Argument(space, 3)
@@ -75,8 +78,8 @@ def test_indexed_function1(self):
 
 
 def test_indexed_function2(self):
-    element = VectorElement("CG", "triangle", 1)
-    domain = Mesh(VectorElement("Lagrange", "triangle", 1))
+    element = FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1)
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     space = FunctionSpace(domain, element)
     v = Argument(space, 2)
     u = Argument(space, 3)
@@ -92,8 +95,8 @@ def test_indexed_function2(self):
 
 
 def test_indexed_function3(self):
-    element = VectorElement("CG", "triangle", 1)
-    domain = Mesh(VectorElement("Lagrange", "triangle", 1))
+    element = FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1)
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     space = FunctionSpace(domain, element)
     Argument(space, 2)
     u = Argument(space, 3)
@@ -103,8 +106,8 @@ def test_indexed_function3(self):
 
 
 def test_vector_from_indices(self):
-    element = VectorElement("CG", "triangle", 1)
-    domain = Mesh(VectorElement("Lagrange", "triangle", 1))
+    element = FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1)
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     space = FunctionSpace(domain, element)
     v = TestFunction(space)
     u = TrialFunction(space)
@@ -121,8 +124,8 @@ def test_vector_from_indices(self):
 
 
 def test_matrix_from_indices(self):
-    element = VectorElement("CG", "triangle", 1)
-    domain = Mesh(VectorElement("Lagrange", "triangle", 1))
+    element = FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1)
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     space = FunctionSpace(domain, element)
     v = TestFunction(space)
     u = TrialFunction(space)
@@ -139,8 +142,8 @@ def test_matrix_from_indices(self):
 
 
 def test_vector_from_list(self):
-    element = VectorElement("CG", "triangle", 1)
-    domain = Mesh(VectorElement("Lagrange", "triangle", 1))
+    element = FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1)
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     space = FunctionSpace(domain, element)
     v = TestFunction(space)
     u = TrialFunction(space)
@@ -153,8 +156,8 @@ def test_vector_from_list(self):
 
 
 def test_matrix_from_list(self):
-    element = VectorElement("CG", "triangle", 1)
-    domain = Mesh(VectorElement("Lagrange", "triangle", 1))
+    element = FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1)
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     space = FunctionSpace(domain, element)
     v = TestFunction(space)
     u = TrialFunction(space)
@@ -174,8 +177,8 @@ def test_matrix_from_list(self):
 
 
 def test_tensor(self):
-    element = VectorElement("CG", "triangle", 1)
-    domain = Mesh(VectorElement("Lagrange", "triangle", 1))
+    element = FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1)
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     space = FunctionSpace(domain, element)
     v = TestFunction(space)
     u = TrialFunction(space)
@@ -214,8 +217,8 @@ def test_tensor(self):
 
 
 def test_indexed(self):
-    element = VectorElement("CG", "triangle", 1)
-    domain = Mesh(VectorElement("Lagrange", "triangle", 1))
+    element = FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1)
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     space = FunctionSpace(domain, element)
     v = TestFunction(space)
     u = TrialFunction(space)
@@ -235,8 +238,8 @@ def test_indexed(self):
 
 def test_spatial_derivative(self):
     cell = triangle
-    element = VectorElement("CG", cell, 1)
-    domain = Mesh(VectorElement("Lagrange", cell, 1))
+    element = FiniteElement("Lagrange", cell, 1, (2, ), identity_pullback, H1)
+    domain = Mesh(FiniteElement("Lagrange", cell, 1, (2, ), identity_pullback, H1))
     space = FunctionSpace(domain, element)
     v = TestFunction(space)
     u = TrialFunction(space)
diff --git a/test/test_interpolate.py b/test/test_interpolate.py
index 6deadc07d..71f3cd145 100644
--- a/test/test_interpolate.py
+++ b/test/test_interpolate.py
@@ -5,29 +5,32 @@
 
 import pytest
 
-from ufl import (Action, Adjoint, Argument, Coefficient, FiniteElement, FunctionSpace, Mesh, TestFunction,
-                 TrialFunction, VectorElement, action, adjoint, derivative, dx, grad, inner, replace, triangle)
+from ufl import (Action, Adjoint, Argument, Coefficient, FunctionSpace, Mesh, TestFunction, TrialFunction, action,
+                 adjoint, derivative, dx, grad, inner, replace, triangle)
 from ufl.algorithms.ad import expand_derivatives
 from ufl.algorithms.analysis import (extract_arguments, extract_arguments_and_coefficients, extract_base_form_operators,
                                      extract_coefficients)
 from ufl.algorithms.expand_indices import expand_indices
 from ufl.core.interpolate import Interpolate
+from ufl.finiteelement import FiniteElement
+from ufl.pullback import identity_pullback
+from ufl.sobolevspace import H1
 
 
 @pytest.fixture
 def domain_2d():
-    return Mesh(VectorElement("Lagrange", triangle, 1))
+    return Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
 
 
 @pytest.fixture
 def V1(domain_2d):
-    f1 = FiniteElement("CG", triangle, 1)
+    f1 = FiniteElement("CG", triangle, 1, (), identity_pullback, H1)
     return FunctionSpace(domain_2d, f1)
 
 
 @pytest.fixture
 def V2(domain_2d):
-    f1 = FiniteElement("CG", triangle, 2)
+    f1 = FiniteElement("CG", triangle, 2, (), identity_pullback, H1)
     return FunctionSpace(domain_2d, f1)
 
 
diff --git a/test/test_elements.py b/test/test_legacy.py
old mode 100755
new mode 100644
similarity index 81%
rename from test/test_elements.py
rename to test/test_legacy.py
index 370679662..fd3de052f
--- a/test/test_elements.py
+++ b/test/test_legacy.py
@@ -1,9 +1,26 @@
-from ufl import (Coefficient, FiniteElement, FunctionSpace, Mesh, MixedElement, TensorElement, VectorElement,
-                 WithMapping, dx, hexahedron, inner, interval, quadrilateral, tetrahedron, triangle)
+from ufl import (H1, Coefficient, FunctionSpace, Mesh, dx, hexahedron, identity_pullback, inner, interval,
+                 quadrilateral, tetrahedron, triangle)
+from ufl.legacy import FiniteElement, MixedElement, TensorElement, VectorElement, WithMapping
 
 all_cells = (interval, triangle, tetrahedron, quadrilateral, hexahedron)
 
-# TODO: cover all valid element definitions
+
+def test_legacy_vs_new():
+    from ufl.finiteelement import FiniteElement as NewFiniteElement
+    e = FiniteElement("Lagrange", triangle, 1)
+    new_e = NewFiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1)
+    assert e.sobolev_space == new_e.sobolev_space
+    assert e.pullback == new_e.pullback
+    assert e.embedded_superdegree == new_e.embedded_superdegree
+    assert e.embedded_subdegree == new_e.embedded_subdegree
+    assert e.cell == new_e.cell
+    assert e.reference_value_shape == new_e.reference_value_shape
+    assert e.value_shape == new_e.value_shape
+    assert e.reference_value_size == new_e.reference_value_size
+    assert e.value_size == new_e.value_size
+    assert e.num_sub_elements == new_e.num_sub_elements
+    assert e.sub_elements == new_e.sub_elements
+    assert e.is_cellwise_constant() == new_e.is_cellwise_constant()
 
 
 def test_scalar_galerkin():
@@ -11,12 +28,12 @@ def test_scalar_galerkin():
         for p in range(1, 10):
             for family in ("Lagrange", "CG", "Discontinuous Lagrange", "DG", "Discontinuous Lagrange L2", "DG L2"):
                 element = FiniteElement(family, cell, p)
-                assert element.value_shape() == ()
+                assert element.value_shape == ()
                 assert element == eval(repr(element))
     for p in range(1, 10):
         for family in ("TDG", "Discontinuous Taylor"):
             element = FiniteElement(family, interval, p)
-            assert element.value_shape() == ()
+            assert element.value_shape == ()
 
 
 def test_vector_galerkin():
@@ -27,7 +44,7 @@ def test_vector_galerkin():
         for p in range(1, 10):
             for family in ("Lagrange", "CG", "Discontinuous Lagrange", "DG", "Discontinuous Lagrange L2", "DG L2"):
                 element = VectorElement(family, cell, p)
-                assert element.value_shape() == shape
+                assert element.value_shape == shape
                 assert element == eval(repr(element))
                 for i in range(dim):
                     c = element.extract_component(i)
@@ -42,7 +59,7 @@ def test_tensor_galerkin():
         for p in range(1, 10):
             for family in ("Lagrange", "CG", "Discontinuous Lagrange", "DG", "Discontinuous Lagrange L2", "DG L2"):
                 element = TensorElement(family, cell, p)
-                assert element.value_shape() == shape
+                assert element.value_shape == shape
                 assert element == eval(repr(element))
                 for i in range(dim):
                     for j in range(dim):
@@ -65,7 +82,7 @@ def test_tensor_symmetry():
                             family, cell, p, shape=(dim, dim), symmetry=s)
                     else:
                         element = TensorElement(family, cell, p, symmetry=s)
-                    assert element.value_shape(), (dim == dim)
+                    assert element.value_shape, (dim == dim)
                     assert element == eval(repr(element))
                     for i in range(dim):
                         for j in range(dim):
@@ -80,6 +97,8 @@ def test_mixed_tensor_symmetries():
     V = VectorElement('CG', triangle, 1)
     T = TensorElement('CG', triangle, 1, symmetry=True)
 
+    print(T.pullback)
+
     # M has dimension 4+1, symmetries are 2->1
     M = T * S
     domain = Mesh(VectorElement("Lagrange", triangle, 1))
@@ -106,7 +125,7 @@ def test_bdm():
     for cell in (triangle, tetrahedron):
         dim = cell.geometric_dimension()
         element = FiniteElement("BDM", cell, 1)
-        assert element.value_shape() == (dim,)
+        assert element.value_shape == (dim,)
         assert element == eval(repr(element))
 
 
@@ -114,14 +133,14 @@ def test_vector_bdm():
     for cell in (triangle, tetrahedron):
         dim = cell.geometric_dimension()
         element = VectorElement("BDM", cell, 1)
-        assert element.value_shape(), (dim == dim)
+        assert element.value_shape, (dim == dim)
         assert element == eval(repr(element))
 
 
 def test_mtw():
     cell = triangle
     element = FiniteElement("MTW", cell, 3)
-    assert element.value_shape() == (cell.geometric_dimension(), )
+    assert element.value_shape == (cell.geometric_dimension(), )
     assert element == eval(repr(element))
     assert element.mapping() == "contravariant Piola"
 
@@ -133,8 +152,8 @@ def test_mixed():
         pelement = FiniteElement("CG", cell, 1)
         TH1 = MixedElement(velement, pelement)
         TH2 = velement * pelement
-        assert TH1.value_shape() == (dim + 1,)
-        assert TH2.value_shape() == (dim + 1,)
+        assert TH1.value_shape == (dim + 1,)
+        assert TH2.value_shape == (dim + 1,)
         assert repr(TH1) == repr(TH2)
         assert TH1 == eval(repr(TH2))
         assert TH2 == eval(repr(TH1))
@@ -147,8 +166,8 @@ def test_nested_mixed():
         pelement = FiniteElement("CG", cell, 1)
         TH1 = MixedElement((velement, pelement), pelement)
         TH2 = velement * pelement * pelement
-        assert TH1.value_shape() == (dim + 2,)
-        assert TH2.value_shape() == (dim + 2,)
+        assert TH1.value_shape == (dim + 2,)
+        assert TH2.value_shape == (dim + 2,)
         assert repr(TH1) == repr(TH2)
         assert TH1 == eval(repr(TH2))
         assert TH2 == eval(repr(TH1))
diff --git a/test/test_lhs_rhs.py b/test/test_lhs_rhs.py
index 4593a8aa1..2be0e584c 100755
--- a/test/test_lhs_rhs.py
+++ b/test/test_lhs_rhs.py
@@ -3,13 +3,16 @@
 # First added: 2011-11-09
 # Last changed: 2011-11-09
 
-from ufl import (Argument, Coefficient, Constant, FiniteElement, FunctionSpace, Mesh, TestFunction, TrialFunction,
-                 VectorElement, action, derivative, ds, dS, dx, exp, interval, system)
+from ufl import (Argument, Coefficient, Constant, FunctionSpace, Mesh, TestFunction, TrialFunction, action, derivative,
+                 ds, dS, dx, exp, interval, system)
+from ufl.finiteelement import FiniteElement
+from ufl.pullback import identity_pullback
+from ufl.sobolevspace import H1
 
 
 def test_lhs_rhs_simple():
-    V = FiniteElement("CG", interval, 1)
-    domain = Mesh(VectorElement("Lagrange", interval, 1))
+    V = FiniteElement("Lagrange", interval, 1, (), identity_pullback, H1)
+    domain = Mesh(FiniteElement("Lagrange", interval, 1, (1, ), identity_pullback, H1))
     space = FunctionSpace(domain, V)
     v = TestFunction(space)
     u = TrialFunction(space)
@@ -37,8 +40,8 @@ def test_lhs_rhs_simple():
 
 
 def test_lhs_rhs_derivatives():
-    V = FiniteElement("CG", interval, 1)
-    domain = Mesh(VectorElement("Lagrange", interval, 1))
+    V = FiniteElement("Lagrange", interval, 1, (), identity_pullback, H1)
+    domain = Mesh(FiniteElement("Lagrange", interval, 1, (1, ), identity_pullback, H1))
     space = FunctionSpace(domain, V)
     v = TestFunction(space)
     u = TrialFunction(space)
@@ -54,8 +57,8 @@ def test_lhs_rhs_derivatives():
 
 
 def test_lhs_rhs_slightly_obscure():
-    V = FiniteElement("CG", interval, 1)
-    domain = Mesh(VectorElement("Lagrange", interval, 1))
+    V = FiniteElement("Lagrange", interval, 1, (), identity_pullback, H1)
+    domain = Mesh(FiniteElement("Lagrange", interval, 1, (1, ), identity_pullback, H1))
     space = FunctionSpace(domain, V)
     u = TrialFunction(space)
     w = Argument(space, 2)
diff --git a/test/test_measures.py b/test/test_measures.py
index 86a8ea68c..2a31d51e1 100755
--- a/test/test_measures.py
+++ b/test/test_measures.py
@@ -2,7 +2,10 @@
 
 from mockobjects import MockMesh, MockMeshFunction
 
-from ufl import Cell, Coefficient, FiniteElement, FunctionSpace, Measure, Mesh, as_ufl, dC, dI, dO, triangle
+from ufl import Cell, Coefficient, FunctionSpace, Measure, Mesh, as_ufl, dC, dI, dO, triangle
+from ufl.finiteelement import FiniteElement
+from ufl.pullback import identity_pullback
+from ufl.sobolevspace import H1
 
 
 def test_construct_forms_from_default_measures():
@@ -57,7 +60,7 @@ def test_construct_forms_from_default_measures():
 
     # Check that we can create a basic form with default measure
     one = as_ufl(1)
-    one * dx(Mesh(triangle))
+    one * dx(Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1)))
 
 
 def test_foo():
@@ -67,7 +70,8 @@ def test_foo():
     tdim = 2
     cell = Cell("triangle", gdim)
     mymesh = MockMesh(9)
-    mydomain = Mesh(cell, ufl_id=9, cargo=mymesh)
+    mydomain = Mesh(FiniteElement("Lagrange", cell, 1, (gdim, ), identity_pullback, H1),
+                    ufl_id=9, cargo=mymesh)
 
     assert cell.topological_dimension() == tdim
     assert cell.geometric_dimension() == gdim
@@ -79,7 +83,7 @@ def test_foo():
     assert mydomain.ufl_cargo() == mymesh
 
     # Define a coefficient for use in tests below
-    V = FunctionSpace(mydomain, FiniteElement("CG", cell, 1))
+    V = FunctionSpace(mydomain, FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1))
     f = Coefficient(V)
 
     # Test definition of a custom measure with explicit parameters
diff --git a/test/test_mixed_function_space.py b/test/test_mixed_function_space.py
index 84af918e6..6e3e68356 100644
--- a/test/test_mixed_function_space.py
+++ b/test/test_mixed_function_space.py
@@ -1,20 +1,23 @@
 __authors__ = "Cecile Daversin Catty"
 __date__ = "2019-03-26 -- 2019-03-26"
 
-from ufl import (FiniteElement, FunctionSpace, Measure, Mesh, MixedFunctionSpace, TestFunctions, TrialFunctions,
-                 VectorElement, interval, tetrahedron, triangle)
+from ufl import (FunctionSpace, Measure, Mesh, MixedFunctionSpace, TestFunctions, TrialFunctions, interval, tetrahedron,
+                 triangle)
 from ufl.algorithms.formsplitter import extract_blocks
+from ufl.finiteelement import FiniteElement
+from ufl.pullback import identity_pullback
+from ufl.sobolevspace import H1
 
 
 def test_mixed_functionspace(self):
     # Domains
-    domain_3d = Mesh(VectorElement("Lagrange", tetrahedron, 1))
-    domain_2d = Mesh(VectorElement("Lagrange", triangle, 1))
-    domain_1d = Mesh(VectorElement("Lagrange", interval, 1))
+    domain_3d = Mesh(FiniteElement("Lagrange", tetrahedron, 1, (3, ), identity_pullback, H1))
+    domain_2d = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
+    domain_1d = Mesh(FiniteElement("Lagrange", interval, 1, (1, ), identity_pullback, H1))
     # Finite elements
-    f_1d = FiniteElement("CG", interval, 1)
-    f_2d = FiniteElement("CG", triangle, 1)
-    f_3d = FiniteElement("CG", tetrahedron, 1)
+    f_1d = FiniteElement("Lagrange", interval, 1, (), identity_pullback, H1)
+    f_2d = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1)
+    f_3d = FiniteElement("Lagrange", tetrahedron, 1, (), identity_pullback, H1)
     # Function spaces
     V_3d = FunctionSpace(domain_3d, f_3d)
     V_2d = FunctionSpace(domain_2d, f_2d)
@@ -33,9 +36,9 @@ def test_mixed_functionspace(self):
     (v_3d, v_2d, v_1d) = TestFunctions(V)
 
     # Measures
-    dx3 = Measure("dx", domain=V_3d)
-    dx2 = Measure("dx", domain=V_2d)
-    dx1 = Measure("dx", domain=V_1d)
+    dx3 = Measure("dx", domain=domain_3d)
+    dx2 = Measure("dx", domain=domain_2d)
+    dx1 = Measure("dx", domain=domain_1d)
 
     # Mixed variational form
     # LHS
diff --git a/test/test_new_ad.py b/test/test_new_ad.py
index 8643dd886..be6b6d403 100755
--- a/test/test_new_ad.py
+++ b/test/test_new_ad.py
@@ -1,8 +1,11 @@
-from ufl import (CellVolume, Coefficient, Constant, FacetNormal, FiniteElement, FunctionSpace, Identity, Mesh,
-                 SpatialCoordinate, TestFunction, VectorConstant, VectorElement, as_ufl, cos, derivative, diff, exp,
-                 grad, ln, sin, tan, triangle, variable, zero)
+from ufl import (CellVolume, Coefficient, Constant, FacetNormal, FunctionSpace, Identity, Mesh, SpatialCoordinate,
+                 TestFunction, VectorConstant, as_ufl, cos, derivative, diff, exp, grad, ln, sin, tan, triangle,
+                 variable, zero)
 from ufl.algorithms.apply_derivatives import GenericDerivativeRuleset, GradRuleset, apply_derivatives
 from ufl.algorithms.renumbering import renumber_indices
+from ufl.finiteelement import FiniteElement
+from ufl.pullback import identity_pullback
+from ufl.sobolevspace import H1, L2
 
 # Note: the old tests in test_automatic_differentiation.py are a bit messy
 #       but still cover many things that are not in here yet.
@@ -15,10 +18,10 @@
 def test_apply_derivatives_doesnt_change_expression_without_derivatives():
     cell = triangle
     d = cell.geometric_dimension()
-    V0 = FiniteElement("DG", cell, 0)
-    V1 = FiniteElement("Lagrange", cell, 1)
+    V0 = FiniteElement("Discontinuous Lagrange", cell, 0, (), identity_pullback, L2)
+    V1 = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1)
 
-    domain = Mesh(VectorElement("Lagrange", cell, 1))
+    domain = Mesh(FiniteElement("Lagrange", cell, 1, (d, ), identity_pullback, H1))
     v0_space = FunctionSpace(domain, V0)
     v1_space = FunctionSpace(domain, V1)
 
@@ -84,9 +87,9 @@ def test_literal_derivatives_are_zero():
         for v in variables:
             assert apply_derivatives(diff(lit, v)) == zero(lit.ufl_shape + v.ufl_shape)
 
-    V0 = FiniteElement("DG", cell, 0)
-    V1 = FiniteElement("Lagrange", cell, 1)
-    domain = Mesh(VectorElement("Lagrange", cell, 1))
+    V0 = FiniteElement("Discontinuous Lagrange", cell, 0, (), identity_pullback, L2)
+    V1 = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1)
+    domain = Mesh(FiniteElement("Lagrange", cell, 1, (d, ), identity_pullback, H1))
     v0_space = FunctionSpace(domain, V0)
     v1_space = FunctionSpace(domain, V1)
     u0 = Coefficient(v0_space)
@@ -109,14 +112,14 @@ def test_grad_ruleset():
     cell = triangle
     d = cell.geometric_dimension()
 
-    V0 = FiniteElement("DG", cell, 0)
-    V1 = FiniteElement("Lagrange", cell, 1)
-    V2 = FiniteElement("Lagrange", cell, 2)
-    W0 = VectorElement("DG", cell, 0)
-    W1 = VectorElement("Lagrange", cell, 1)
-    W2 = VectorElement("Lagrange", cell, 2)
+    V0 = FiniteElement("Discontinuous Lagrange", cell, 0, (), identity_pullback, L2)
+    V1 = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1)
+    V2 = FiniteElement("Lagrange", cell, 2, (), identity_pullback, H1)
+    W0 = FiniteElement("Discontinuous Lagrange", cell, 0, (2, ), identity_pullback, L2)
+    W1 = FiniteElement("Lagrange", cell, 1, (d, ), identity_pullback, H1)
+    W2 = FiniteElement("Lagrange", cell, 2, (d, ), identity_pullback, H1)
 
-    domain = Mesh(VectorElement("Lagrange", cell, 1))
+    domain = Mesh(FiniteElement("Lagrange", cell, 1, (d, ), identity_pullback, H1))
     v0_space = FunctionSpace(domain, V0)
     v1_space = FunctionSpace(domain, V1)
     v2_space = FunctionSpace(domain, V2)
diff --git a/test/test_pickle.py b/test/test_pickle.py
index 3189bde2b..5ab4026f7 100755
--- a/test/test_pickle.py
+++ b/test/test_pickle.py
@@ -10,17 +10,20 @@
 
 import pickle
 
-from ufl import (Coefficient, Constant, Dx, FacetNormal, FiniteElement, FunctionSpace, Identity, Mesh, TensorElement,
-                 TestFunction, TestFunctions, TrialFunction, TrialFunctions, VectorConstant, VectorElement, avg, curl,
-                 div, dot, dS, ds, dx, grad, i, inner, j, jump, lhs, rhs, sqrt, tetrahedron, triangle)
+from ufl import (Coefficient, Constant, Dx, FacetNormal, FunctionSpace, Identity, Mesh, TestFunction, TestFunctions,
+                 TrialFunction, TrialFunctions, VectorConstant, avg, curl, div, dot, dS, ds, dx, grad, i, inner, j,
+                 jump, lhs, rhs, sqrt, tetrahedron, triangle)
 from ufl.algorithms import compute_form_data
+from ufl.finiteelement import FiniteElement, MixedElement
+from ufl.pullback import contravariant_piola, covariant_piola, identity_pullback
+from ufl.sobolevspace import H1, L2, HCurl, HDiv
 
 p = pickle.HIGHEST_PROTOCOL
 
 
 def testConstant():
-    element = FiniteElement("Lagrange", "triangle", 1)
-    domain = Mesh(VectorElement("Lagrange", "triangle", 1))
+    element = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1)
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     space = FunctionSpace(domain, element)
 
     v = TestFunction(space)
@@ -44,8 +47,8 @@ def testConstant():
 
 
 def testElasticity():
-    element = VectorElement("Lagrange", "tetrahedron", 1)
-    domain = Mesh(VectorElement("Lagrange", "tetrahedron", 1))
+    element = FiniteElement("Lagrange", tetrahedron, 1, (3, ), identity_pullback, H1)
+    domain = Mesh(FiniteElement("Lagrange", tetrahedron, 1, (3, ), identity_pullback, H1))
     space = FunctionSpace(domain, element)
 
     v = TestFunction(space)
@@ -65,8 +68,8 @@ def eps(v):
 
 
 def testEnergyNorm():
-    element = FiniteElement("Lagrange", "tetrahedron", 1)
-    domain = Mesh(VectorElement("Lagrange", "tetrahedron", 1))
+    element = FiniteElement("Lagrange", tetrahedron, 1, (), identity_pullback, H1)
+    domain = Mesh(FiniteElement("Lagrange", tetrahedron, 1, (3, ), identity_pullback, H1))
     space = FunctionSpace(domain, element)
 
     v = Coefficient(space)
@@ -79,8 +82,8 @@ def testEnergyNorm():
 
 
 def testEquation():
-    element = FiniteElement("Lagrange", "triangle", 1)
-    domain = Mesh(VectorElement("Lagrange", "triangle", 1))
+    element = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1)
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     space = FunctionSpace(domain, element)
 
     k = 0.1
@@ -104,8 +107,8 @@ def testEquation():
 
 
 def testFunctionOperators():
-    element = FiniteElement("Lagrange", "triangle", 1)
-    domain = Mesh(VectorElement("Lagrange", "triangle", 1))
+    element = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1)
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     space = FunctionSpace(domain, element)
 
     v = TestFunction(space)
@@ -125,8 +128,8 @@ def testFunctionOperators():
 
 
 def testHeat():
-    element = FiniteElement("Lagrange", "triangle", 1)
-    domain = Mesh(VectorElement("Lagrange", "triangle", 1))
+    element = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1)
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     space = FunctionSpace(domain, element)
 
     v = TestFunction(space)
@@ -149,8 +152,8 @@ def testHeat():
 
 
 def testMass():
-    element = FiniteElement("Lagrange", "tetrahedron", 3)
-    domain = Mesh(VectorElement("Lagrange", "tetrahedron", 1))
+    element = FiniteElement("Lagrange", tetrahedron, 3, (), identity_pullback, H1)
+    domain = Mesh(FiniteElement("Lagrange", tetrahedron, 1, (3, ), identity_pullback, H1))
     space = FunctionSpace(domain, element)
 
     v = TestFunction(space)
@@ -165,9 +168,9 @@ def testMass():
 
 
 def testMixedMixedElement():
-    P3 = FiniteElement("Lagrange", "triangle", 3)
+    P3 = FiniteElement("Lagrange", triangle, 3, (), identity_pullback, H1)
 
-    element = (P3 * P3) * (P3 * P3)
+    element = MixedElement([[P3, P3], [P3, P3]])
 
     element_pickle = pickle.dumps(element, p)
     element_restore = pickle.loads(element_pickle)
@@ -178,11 +181,11 @@ def testMixedMixedElement():
 def testMixedPoisson():
     q = 1
 
-    BDM = FiniteElement("Brezzi-Douglas-Marini", "triangle", q)
-    DG = FiniteElement("Discontinuous Lagrange", "triangle", q - 1)
+    BDM = FiniteElement("Brezzi-Douglas-Marini", triangle, q, (2, ), contravariant_piola, HDiv)
+    DG = FiniteElement("Discontinuous Lagrange", triangle, q - 1, (), identity_pullback, L2)
 
-    mixed_element = BDM * DG
-    domain = Mesh(VectorElement("Lagrange", "triangle", 1))
+    mixed_element = MixedElement([BDM, DG])
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     mixed_space = FunctionSpace(domain, mixed_element)
     dg_space = FunctionSpace(domain, DG)
 
@@ -204,8 +207,8 @@ def testMixedPoisson():
 
 
 def testNavierStokes():
-    element = VectorElement("Lagrange", "tetrahedron", 1)
-    domain = Mesh(VectorElement("Lagrange", "tetrahedron", 1))
+    element = FiniteElement("Lagrange", tetrahedron, 1, (3, ), identity_pullback, H1)
+    domain = Mesh(FiniteElement("Lagrange", tetrahedron, 1, (3, ), identity_pullback, H1))
     space = FunctionSpace(domain, element)
 
     v = TestFunction(space)
@@ -223,8 +226,8 @@ def testNavierStokes():
 
 
 def testNeumannProblem():
-    element = VectorElement("Lagrange", "triangle", 1)
-    domain = Mesh(VectorElement("Lagrange", "triangle", 1))
+    element = FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1)
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     space = FunctionSpace(domain, element)
 
     v = TestFunction(space)
@@ -248,8 +251,8 @@ def testNeumannProblem():
 
 
 def testOptimization():
-    element = FiniteElement("Lagrange", "triangle", 3)
-    domain = Mesh(VectorElement("Lagrange", "triangle", 1))
+    element = FiniteElement("Lagrange", triangle, 3, (), identity_pullback, H1)
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     space = FunctionSpace(domain, element)
 
     v = TestFunction(space)
@@ -269,7 +272,7 @@ def testOptimization():
 
 
 def testP5tet():
-    element = FiniteElement("Lagrange", tetrahedron, 5)
+    element = FiniteElement("Lagrange", tetrahedron, 5, (), identity_pullback, H1)
 
     element_pickle = pickle.dumps(element, p)
     element_restore = pickle.loads(element_pickle)
@@ -278,15 +281,15 @@ def testP5tet():
 
 
 def testP5tri():
-    element = FiniteElement("Lagrange", triangle, 5)
+    element = FiniteElement("Lagrange", triangle, 5, (), identity_pullback, H1)
 
     element_pickle = pickle.dumps(element, p)
     pickle.loads(element_pickle)
 
 
 def testPoissonDG():
-    element = FiniteElement("Discontinuous Lagrange", triangle, 1)
-    domain = Mesh(VectorElement("Lagrange", triangle, 1))
+    element = FiniteElement("Discontinuous Lagrange", triangle, 1, (), identity_pullback, L2)
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     space = FunctionSpace(domain, element)
 
     v = TestFunction(space)
@@ -332,8 +335,8 @@ def testPoissonDG():
 
 
 def testPoisson():
-    element = FiniteElement("Lagrange", "triangle", 1)
-    domain = Mesh(VectorElement("Lagrange", "triangle", 1))
+    element = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1)
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     space = FunctionSpace(domain, element)
 
     v = TestFunction(space)
@@ -354,8 +357,8 @@ def testPoisson():
 
 
 def testPoissonSystem():
-    element = VectorElement("Lagrange", "triangle", 1)
-    domain = Mesh(VectorElement("Lagrange", "triangle", 1))
+    element = FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1)
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     space = FunctionSpace(domain, element)
 
     v = TestFunction(space)
@@ -378,16 +381,16 @@ def testPoissonSystem():
 
 
 def testQuadratureElement():
-    element = FiniteElement("Lagrange", "triangle", 2)
+    element = FiniteElement("Lagrange", triangle, 2, (), identity_pullback, H1)
 
     # FFC notation:
-    # QE = QuadratureElement("triangle", 3)
-    # sig = VectorQuadratureElement("triangle", 3)
+    # QE = QuadratureElement(triangle, 3)
+    # sig = VectorQuadratureElement(triangle, 3)
 
-    QE = FiniteElement("Quadrature", "triangle", 3)
-    sig = VectorElement("Quadrature", "triangle", 3)
+    QE = FiniteElement("Quadrature", triangle, 3, (), identity_pullback, L2)
+    sig = FiniteElement("Quadrature", triangle, 3, (2, ), identity_pullback, L2)
 
-    domain = Mesh(VectorElement("Lagrange", "triangle", 1))
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     space = FunctionSpace(domain, element)
     qe_space = FunctionSpace(domain, QE)
     sig_space = FunctionSpace(domain, sig)
@@ -414,11 +417,11 @@ def testQuadratureElement():
 def testStokes():
     # UFLException: Shape mismatch in sum.
 
-    P2 = VectorElement("Lagrange", "triangle", 2)
-    P1 = FiniteElement("Lagrange", "triangle", 1)
-    TH = P2 * P1
+    P2 = FiniteElement("Lagrange", triangle, 2, (2, ), identity_pullback, H1)
+    P1 = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1)
+    TH = MixedElement([P2, P1])
 
-    domain = Mesh(VectorElement("Lagrange", "triangle", 1))
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     th_space = FunctionSpace(domain, TH)
     p2_space = FunctionSpace(domain, P2)
 
@@ -443,8 +446,8 @@ def testStokes():
 
 
 def testSubDomain():
-    element = FiniteElement("CG", "tetrahedron", 1)
-    domain = Mesh(VectorElement("Lagrange", "tetrahedron", 1))
+    element = FiniteElement("Lagrange", tetrahedron, 1, (), identity_pullback, H1)
+    domain = Mesh(FiniteElement("Lagrange", tetrahedron, 1, (3, ), identity_pullback, H1))
     space = FunctionSpace(domain, element)
 
     f = Coefficient(space)
@@ -458,8 +461,8 @@ def testSubDomain():
 
 
 def testSubDomains():
-    element = FiniteElement("CG", "tetrahedron", 1)
-    domain = Mesh(VectorElement("Lagrange", "tetrahedron", 1))
+    element = FiniteElement("Lagrange", tetrahedron, 1, (), identity_pullback, H1)
+    domain = Mesh(FiniteElement("Lagrange", tetrahedron, 1, (3, ), identity_pullback, H1))
     space = FunctionSpace(domain, element)
 
     v = TestFunction(space)
@@ -476,8 +479,8 @@ def testSubDomains():
 
 def testTensorWeightedPoisson():
     # FFC notation:
-    # P1 = FiniteElement("Lagrange", "triangle", 1)
-    # P0 = FiniteElement("Discontinuous Lagrange", "triangle", 0)
+    # P1 = FiniteElement("Lagrange", triangle, 1)
+    # P0 = FiniteElement("Discontinuous Lagrange", triangle, 0)
     #
     # v = TestFunction(P1)
     # u = TrialFunction(P1)
@@ -492,10 +495,10 @@ def testTensorWeightedPoisson():
     #
     # a = dot(grad(v), mult(C, grad(u)))*dx
 
-    P1 = FiniteElement("Lagrange", "triangle", 1)
-    P0 = TensorElement("Discontinuous Lagrange", "triangle", 0, shape=(2, 2))
+    P1 = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1)
+    P0 = FiniteElement("Discontinuous Lagrange", triangle, 0, (2, 2), identity_pullback, L2)
 
-    domain = Mesh(VectorElement("Lagrange", "triangle", 1))
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     p1_space = FunctionSpace(domain, P1)
     p0_space = FunctionSpace(domain, P0)
 
@@ -527,18 +530,19 @@ def HodgeLaplaceGradCurl(space, fspace):
 
         return [a, L]
 
-    shape = "tetrahedron"
+    shape = tetrahedron
     order = 1
 
-    GRAD = FiniteElement("Lagrange", shape, order)
+    GRAD = FiniteElement("Lagrange", shape, order, (), identity_pullback, H1)
 
     # FFC notation: CURL = FiniteElement("Nedelec", shape, order-1)
-    CURL = FiniteElement("N1curl", shape, order)
+    CURL = FiniteElement("N1curl", shape, order, (3, ), covariant_piola, HCurl)
 
-    VectorLagrange = VectorElement("Lagrange", shape, order + 1)
-    domain = Mesh(VectorElement("Lagrange", shape, 1))
+    VectorLagrange = FiniteElement("Lagrange", shape, order + 1, (3, ), identity_pullback, H1)
+    domain = Mesh(FiniteElement("Lagrange", shape, 1, (3, ), identity_pullback, H1))
 
-    [a, L] = HodgeLaplaceGradCurl(FunctionSpace(domain, GRAD * CURL), FunctionSpace(domain, VectorLagrange))
+    [a, L] = HodgeLaplaceGradCurl(FunctionSpace(domain, MixedElement([GRAD, CURL])),
+                                  FunctionSpace(domain, VectorLagrange))
 
     a_pickle = pickle.dumps(a, p)
     a_restore = pickle.loads(a_pickle)
@@ -557,8 +561,8 @@ def testIdentity():
 
 
 def testFormData():
-    element = FiniteElement("Lagrange", "tetrahedron", 3)
-    domain = Mesh(VectorElement("Lagrange", "tetrahedron", 1))
+    element = FiniteElement("Lagrange", tetrahedron, 3, (), identity_pullback, H1)
+    domain = Mesh(FiniteElement("Lagrange", tetrahedron, 1, (3, ), identity_pullback, H1))
     space = FunctionSpace(domain, element)
 
     v = TestFunction(space)
diff --git a/test/test_piecewise_checks.py b/test/test_piecewise_checks.py
index 0536b62b1..d5d419d37 100755
--- a/test/test_piecewise_checks.py
+++ b/test/test_piecewise_checks.py
@@ -3,11 +3,14 @@
 import pytest
 
 from ufl import (Cell, CellDiameter, CellVolume, Circumradius, Coefficient, Constant, FacetArea, FacetNormal,
-                 FiniteElement, FunctionSpace, Jacobian, JacobianDeterminant, JacobianInverse, MaxFacetEdgeLength, Mesh,
-                 MinFacetEdgeLength, SpatialCoordinate, TestFunction, VectorElement, hexahedron, interval,
-                 quadrilateral, tetrahedron, triangle)
+                 FunctionSpace, Jacobian, JacobianDeterminant, JacobianInverse, MaxFacetEdgeLength, Mesh,
+                 MinFacetEdgeLength, SpatialCoordinate, TestFunction, hexahedron, interval, quadrilateral, tetrahedron,
+                 triangle)
 from ufl.checks import is_cellwise_constant
 from ufl.classes import CellCoordinate, FacetJacobian, FacetJacobianDeterminant, FacetJacobianInverse
+from ufl.finiteelement import FiniteElement
+from ufl.pullback import identity_pullback
+from ufl.sobolevspace import H1, L2, HInf
 
 
 def get_domains():
@@ -19,13 +22,15 @@ def get_domains():
         tetrahedron,
         hexahedron,
     ]
-    return [Mesh(cell) for cell in all_cells]
+    return [Mesh(FiniteElement("Lagrange", cell, 1, (cell.geometric_dimension(), ),
+                               identity_pullback, H1)) for cell in all_cells]
 
 
 def get_nonlinear():
     domains_with_quadratic_coordinates = []
     for D in get_domains():
-        V = VectorElement("CG", D.ufl_cell(), 2)
+        V = FiniteElement("Lagrange", D.ufl_cell(), 2, (D.ufl_cell().geometric_dimension(), ),
+                          identity_pullback, H1)
         E = Mesh(V)
         domains_with_quadratic_coordinates.append(E)
 
@@ -48,7 +53,8 @@ def domains(request):
     domains = get_domains()
     domains_with_linear_coordinates = []
     for D in domains:
-        V = VectorElement("CG", D.ufl_cell(), 1)
+        V = FiniteElement("Lagrange", D.ufl_cell(), 1, (D.ufl_cell().geometric_dimension(), ),
+                          identity_pullback, H1)
         E = Mesh(V)
         domains_with_linear_coordinates.append(E)
 
@@ -63,11 +69,14 @@ def affine_domains(request):
         triangle,
         tetrahedron,
     ]
-    affine_domains = [Mesh(cell) for cell in affine_cells]
+    affine_domains = [Mesh(FiniteElement("Lagrange", cell, 1, (cell.geometric_dimension(), ),
+                                         identity_pullback, H1))
+                      for cell in affine_cells]
 
     affine_domains_with_linear_coordinates = []
     for D in affine_domains:
-        V = VectorElement("CG", D.ufl_cell(), 1)
+        V = FiniteElement("Lagrange", D.ufl_cell(), 1, (D.ufl_cell().geometric_dimension(), ),
+                          identity_pullback, H1)
         E = Mesh(V)
         affine_domains_with_linear_coordinates.append(E)
 
@@ -84,10 +93,13 @@ def affine_facet_domains(request):
         quadrilateral,
         tetrahedron,
     ]
-    affine_facet_domains = [Mesh(cell) for cell in affine_facet_cells]
+    affine_facet_domains = [Mesh(FiniteElement(
+        "Lagrange", cell, 1, (cell.geometric_dimension(), ),
+        identity_pullback, H1)) for cell in affine_facet_cells]
     affine_facet_domains_with_linear_coordinates = []
     for D in affine_facet_domains:
-        V = VectorElement("CG", D.ufl_cell(), 1)
+        V = FiniteElement("Lagrange", D.ufl_cell(), 1, (D.ufl_cell().geometric_dimension(), ),
+                          identity_pullback, H1)
         E = Mesh(V)
         affine_facet_domains_with_linear_coordinates.append(E)
 
@@ -103,10 +115,13 @@ def nonaffine_domains(request):
         quadrilateral,
         hexahedron,
     ]
-    nonaffine_domains = [Mesh(cell) for cell in nonaffine_cells]
+    nonaffine_domains = [Mesh(FiniteElement(
+        "Lagrange", cell, 1, (cell.geometric_dimension(), ),
+        identity_pullback, H1)) for cell in nonaffine_cells]
     nonaffine_domains_with_linear_coordinates = []
     for D in nonaffine_domains:
-        V = VectorElement("CG", D.ufl_cell(), 1)
+        V = FiniteElement("Lagrange", D.ufl_cell(), 1, (D.ufl_cell().geometric_dimension(), ),
+                          identity_pullback, H1)
         E = Mesh(V)
         nonaffine_domains_with_linear_coordinates.append(E)
 
@@ -121,10 +136,13 @@ def nonaffine_facet_domains(request):
     nonaffine_facet_cells = [
         hexahedron,
     ]
-    nonaffine_facet_domains = [Mesh(cell) for cell in nonaffine_facet_cells]
+    nonaffine_facet_domains = [Mesh(FiniteElement(
+        "Lagrange", cell, 1, (cell.geometric_dimension(), ),
+        identity_pullback, H1)) for cell in nonaffine_facet_cells]
     nonaffine_facet_domains_with_linear_coordinates = []
     for D in nonaffine_facet_domains:
-        V = VectorElement("CG", D.ufl_cell(), 1)
+        V = FiniteElement("Lagrange", D.ufl_cell(), 1, (D.ufl_cell().geometric_dimension(), ),
+                          identity_pullback, H1)
         E = Mesh(V)
         nonaffine_facet_domains_with_linear_coordinates.append(E)
 
@@ -159,7 +177,7 @@ def test_coordinates_never_cellwise_constant(domains):
 
 def test_coordinates_never_cellwise_constant_vertex():
     # The only exception here:
-    domains = Mesh(Cell("vertex", 3))
+    domains = Mesh(FiniteElement("Lagrange", Cell("vertex", 3), 1, (3, ), identity_pullback, H1))
     assert domains.ufl_cell().cellname() == "vertex"
     e = SpatialCoordinate(domains)
     assert is_cellwise_constant(e)
@@ -216,12 +234,14 @@ def test_coefficient_sometimes_cellwise_constant(domains_not_linear):
     e = Constant(domains_not_linear)
     assert is_cellwise_constant(e)
 
-    V = FiniteElement("DG", domains_not_linear.ufl_cell(), 0)
-    domain = Mesh(VectorElement("Lagrange", domains_not_linear.ufl_cell(), 1))
+    V = FiniteElement("Discontinuous Lagrange", domains_not_linear.ufl_cell(), 0, (), identity_pullback, L2)
+    d = domains_not_linear.ufl_cell().geometric_dimension()
+    domain = Mesh(FiniteElement("Lagrange", domains_not_linear.ufl_cell(), 1, (d, ), identity_pullback, H1))
     space = FunctionSpace(domain, V)
     e = Coefficient(space)
     assert is_cellwise_constant(e)
-    V = FiniteElement("R", domains_not_linear.ufl_cell(), 0)
+
+    V = FiniteElement("Real", domains_not_linear.ufl_cell(), 0, (), identity_pullback, HInf)
     space = FunctionSpace(domain, V)
     e = Coefficient(space)
     assert is_cellwise_constant(e)
@@ -235,8 +255,9 @@ def test_coefficient_sometimes_cellwise_constant(domains_not_linear):
 
 
 def test_coefficient_mostly_not_cellwise_constant(domains_not_linear):
-    V = FiniteElement("DG", domains_not_linear.ufl_cell(), 1)
-    domain = Mesh(VectorElement("Lagrange", domains_not_linear.ufl_cell(), 1))
+    V = FiniteElement("Discontinuous Lagrange", domains_not_linear.ufl_cell(), 1, (), identity_pullback, L2)
+    d = domains_not_linear.ufl_cell().geometric_dimension()
+    domain = Mesh(FiniteElement("Lagrange", domains_not_linear.ufl_cell(), 1, (d, ), identity_pullback, H1))
     space = FunctionSpace(domain, V)
     e = Coefficient(space)
     assert not is_cellwise_constant(e)
diff --git a/test/test_reference_shapes.py b/test/test_reference_shapes.py
index db4ac3313..12c5028be 100755
--- a/test/test_reference_shapes.py
+++ b/test/test_reference_shapes.py
@@ -1,4 +1,7 @@
-from ufl import Cell, FiniteElement, MixedElement, TensorElement, VectorElement
+from ufl import Cell
+from ufl.finiteelement import FiniteElement, MixedElement, SymmetricElement
+from ufl.pullback import contravariant_piola, covariant_piola, identity_pullback
+from ufl.sobolevspace import H1, HCurl, HDiv
 
 
 def test_reference_shapes():
@@ -6,30 +9,32 @@ def test_reference_shapes():
 
     cell = Cell("triangle", 3)
 
-    V = FiniteElement("N1curl", cell, 1)
-    assert V.value_shape() == (3,)
-    assert V.reference_value_shape() == (2,)
+    V = FiniteElement("N1curl", cell, 1, (2, ), covariant_piola, HCurl)
+    assert V.value_shape == (3,)
+    assert V.reference_value_shape == (2,)
 
-    U = FiniteElement("RT", cell, 1)
-    assert U.value_shape() == (3,)
-    assert U.reference_value_shape() == (2,)
+    U = FiniteElement("Raviart-Thomas", cell, 1, (2, ), contravariant_piola, HDiv)
+    assert U.value_shape == (3,)
+    assert U.reference_value_shape == (2,)
 
-    W = FiniteElement("CG", cell, 1)
-    assert W.value_shape() == ()
-    assert W.reference_value_shape() == ()
+    W = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1)
+    assert W.value_shape == ()
+    assert W.reference_value_shape == ()
 
-    Q = VectorElement("CG", cell, 1)
-    assert Q.value_shape() == (3,)
-    assert Q.reference_value_shape() == (3,)
+    Q = FiniteElement("Lagrange", cell, 1, (3, ), identity_pullback, H1)
+    assert Q.value_shape == (3,)
+    assert Q.reference_value_shape == (3,)
 
-    T = TensorElement("CG", cell, 1)
-    assert T.value_shape() == (3, 3)
-    assert T.reference_value_shape() == (3, 3)
+    T = FiniteElement("Lagrange", cell, 1, (3, 3), identity_pullback, H1)
+    assert T.value_shape == (3, 3)
+    assert T.reference_value_shape == (3, 3)
 
-    S = TensorElement("CG", cell, 1, symmetry=True)
-    assert S.value_shape() == (3, 3)
-    assert S.reference_value_shape() == (6,)
+    S = SymmetricElement(
+        {(0, 0): 0, (1, 0): 1, (2, 0): 2, (0, 1): 1, (1, 1): 3, (2, 1): 4, (0, 2): 2, (1, 2): 4, (2, 2): 5},
+        [FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1) for _ in range(6)])
+    assert S.value_shape == (3, 3)
+    assert S.reference_value_shape == (6,)
 
-    M = MixedElement(V, U, W)
-    assert M.value_shape() == (7,)
-    assert M.reference_value_shape() == (5,)
+    M = MixedElement([V, U, W])
+    assert M.value_shape == (7,)
+    assert M.reference_value_shape == (5,)
diff --git a/test/test_scratch.py b/test/test_scratch.py
index 1ccceecc0..32ced3346 100755
--- a/test/test_scratch.py
+++ b/test/test_scratch.py
@@ -8,9 +8,12 @@
 
 import warnings
 
-from ufl import (Coefficient, FiniteElement, FunctionSpace, Identity, Mesh, TensorElement, TestFunction, VectorElement,
-                 as_matrix, as_tensor, as_vector, dx, grad, indices, inner, outer, triangle)
+from ufl import (Coefficient, FunctionSpace, Identity, Mesh, TestFunction, as_matrix, as_tensor, as_vector, dx, grad,
+                 indices, inner, outer, triangle)
 from ufl.classes import FixedIndex, FormArgument, Grad, Indexed, ListTensor, Zero
+from ufl.finiteelement import FiniteElement
+from ufl.pullback import identity_pullback
+from ufl.sobolevspace import H1
 from ufl.tensors import as_scalar, unit_indexed_tensor, unwrap_list_tensor
 
 
@@ -210,8 +213,8 @@ def test_unwrap_list_tensor(self):
 
 
 def test__forward_coefficient_ad__grad_of_scalar_coefficient(self):
-    U = FiniteElement("CG", triangle, 1)
-    domain = Mesh(VectorElement("Lagrange", triangle, 1))
+    U = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1)
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     space = FunctionSpace(domain, U)
     u = Coefficient(space)
     du = TestFunction(space)
@@ -236,8 +239,8 @@ def test__forward_coefficient_ad__grad_of_scalar_coefficient(self):
 
 
 def test__forward_coefficient_ad__grad_of_vector_coefficient(self):
-    V = VectorElement("CG", triangle, 1)
-    domain = Mesh(VectorElement("Lagrange", triangle, 1))
+    V = FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1)
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     space = FunctionSpace(domain, V)
     v = Coefficient(space)
     dv = TestFunction(space)
@@ -262,8 +265,8 @@ def test__forward_coefficient_ad__grad_of_vector_coefficient(self):
 
 
 def test__forward_coefficient_ad__grad_of_vector_coefficient__with_component_variation(self):
-    V = VectorElement("CG", triangle, 1)
-    domain = Mesh(VectorElement("Lagrange", triangle, 1))
+    V = FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1)
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     space = FunctionSpace(domain, V)
     v = Coefficient(space)
     dv = TestFunction(space)
@@ -319,8 +322,8 @@ def test__forward_coefficient_ad__grad_of_vector_coefficient__with_component_var
 
 
 def test__forward_coefficient_ad__grad_of_vector_coefficient__with_component_variation_in_list(self):
-    V = VectorElement("CG", triangle, 1)
-    domain = Mesh(VectorElement("Lagrange", triangle, 1))
+    V = FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1)
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     space = FunctionSpace(domain, V)
     v = Coefficient(space)
     dv = TestFunction(space)
@@ -376,8 +379,8 @@ def test__forward_coefficient_ad__grad_of_vector_coefficient__with_component_var
 
 
 def test__forward_coefficient_ad__grad_of_tensor_coefficient(self):
-    W = TensorElement("CG", triangle, 1)
-    domain = Mesh(VectorElement("Lagrange", triangle, 1))
+    W = FiniteElement("Lagrange", triangle, 1, (2, 2), identity_pullback, H1)
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     space = FunctionSpace(domain, W)
     w = Coefficient(space)
     dw = TestFunction(space)
@@ -402,8 +405,8 @@ def test__forward_coefficient_ad__grad_of_tensor_coefficient(self):
 
 
 def test__forward_coefficient_ad__grad_of_tensor_coefficient__with_component_variation(self):
-    W = TensorElement("CG", triangle, 1)
-    domain = Mesh(VectorElement("Lagrange", triangle, 1))
+    W = FiniteElement("Lagrange", triangle, 1, (2, 2), identity_pullback, H1)
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     space = FunctionSpace(domain, W)
     w = Coefficient(space)
     dw = TestFunction(space)
diff --git a/test/test_signature.py b/test/test_signature.py
index b0c6cd3d9..4e270d500 100755
--- a/test/test_signature.py
+++ b/test/test_signature.py
@@ -1,11 +1,13 @@
 """Test the computation of form signatures."""
 
-from ufl import (Argument, CellDiameter, CellVolume, Circumradius, Coefficient, FacetArea, FacetNormal, FiniteElement,
-                 FunctionSpace, Identity, Mesh, SpatialCoordinate, TensorElement, TestFunction, VectorElement,
-                 as_vector, diff, dot, ds, dx, hexahedron, indices, inner, interval, quadrilateral, tetrahedron,
-                 triangle, variable)
+from ufl import (Argument, CellDiameter, CellVolume, Circumradius, Coefficient, FacetArea, FacetNormal, FunctionSpace,
+                 Identity, Mesh, SpatialCoordinate, TestFunction, as_vector, diff, dot, ds, dx, hexahedron, indices,
+                 inner, interval, quadrilateral, tetrahedron, triangle, variable)
 from ufl.algorithms.signature import compute_multiindex_hashdata, compute_terminal_hashdata
 from ufl.classes import FixedIndex, MultiIndex
+from ufl.finiteelement import FiniteElement, SymmetricElement
+from ufl.pullback import identity_pullback
+from ufl.sobolevspace import H1, L2
 
 # TODO: Test compute_terminal_hashdata
 #   TODO: Check that form argument counts only affect the sig by their relative ordering
@@ -19,7 +21,8 @@
 def domain_numbering(*cells):
     renumbering = {}
     for i, cell in enumerate(cells):
-        domain = Mesh(VectorElement("Lagrange", cell, 1), ufl_id=i)
+        d = cell.geometric_dimension()
+        domain = Mesh(FiniteElement("Lagrange", cell, 1, (d, ), identity_pullback, H1), ufl_id=i)
         renumbering[domain] = i
     return renumbering
 
@@ -57,7 +60,7 @@ def test_terminal_hashdata_depends_on_literals(self):
     def forms():
         i, j = indices(2)
         for d, cell in [(2, triangle), (3, tetrahedron)]:
-            domain = Mesh(VectorElement("Lagrange", cell, 1), ufl_id=d-2)
+            domain = Mesh(FiniteElement("Lagrange", cell, 1, (d, ), identity_pullback, H1), ufl_id=d-2)
             x = SpatialCoordinate(domain)
             ident = Identity(d)
             for fv in (1.1, 2.2):
@@ -85,9 +88,9 @@ def forms():
         i, j = indices(2)
         cells = (triangle, tetrahedron)
         for i, cell in enumerate(cells):
-            domain = Mesh(VectorElement("Lagrange", cell, 1), ufl_id=i)
-
             d = cell.geometric_dimension()
+            domain = Mesh(FiniteElement("Lagrange", cell, 1, (d, ), identity_pullback, H1), ufl_id=i)
+
             x = SpatialCoordinate(domain)
             n = FacetNormal(domain)
             h = CellDiameter(domain)
@@ -119,28 +122,38 @@ def forms():
 def test_terminal_hashdata_depends_on_form_argument_properties(self):
     reprs = set()
     hashes = set()
-    nelm = 6
+    nelm = 5
     nreps = 2
 
     # Data
     cells = (triangle, tetrahedron)
     degrees = (1, 2)
-    families = ("CG", "Lagrange", "DG")
+    families = (("Lagrange", H1), ("Lagrange", H1), ("Discontinuous Lagrange", L2))
 
     def forms():
         for rep in range(nreps):
             for i, cell in enumerate(cells):
                 d = cell.geometric_dimension()
-                domain = Mesh(VectorElement("Lagrange", cell, 1), ufl_id=i)
+                domain = Mesh(FiniteElement("Lagrange", cell, 1, (d, ), identity_pullback, H1), ufl_id=i)
                 for degree in degrees:
-                    for family in families:
-                        V = FiniteElement(family, cell, degree)
-                        W = VectorElement(family, cell, degree)
-                        W2 = VectorElement(family, cell, degree, dim=d+1)
-                        T = TensorElement(family, cell, degree)
-                        S = TensorElement(family, cell, degree, symmetry=True)
-                        S2 = TensorElement(family, cell, degree, shape=(d, d), symmetry={(0, 0): (1, 1)})
-                        elements = [V, W, W2, T, S, S2]
+                    for family, sobolev in families:
+                        V = FiniteElement(family, cell, degree, (), identity_pullback, sobolev)
+                        W = FiniteElement(family, cell, degree, (d, ), identity_pullback, sobolev)
+                        W2 = FiniteElement(family, cell, degree, (d+1, ), identity_pullback, sobolev)
+                        T = FiniteElement(family, cell, degree, (d, d), identity_pullback, sobolev)
+                        if d == 2:
+                            S = SymmetricElement(
+                                {(0, 0): 0, (0, 1): 1, (1, 0): 1, (1, 1): 2},
+                                [FiniteElement(family, cell, degree, (), identity_pullback, sobolev)
+                                 for _ in range(3)])
+                        else:
+                            assert d == 3
+                            S = SymmetricElement(
+                                {(0, 0): 0, (0, 1): 1, (0, 2): 2, (1, 0): 1, (1, 1): 3,
+                                 (1, 2): 4, (2, 0): 2, (2, 1): 4, (2, 2): 5},
+                                [FiniteElement(family, cell, degree, (), identity_pullback, sobolev)
+                                 for _ in range(6)])
+                        elements = [V, W, W2, T, S]
                         assert len(elements) == nelm
 
                         for H in elements[:nelm]:
@@ -158,11 +171,10 @@ def forms():
                                 yield compute_terminal_hashdata(expr, renumbering)
 
     c, d, r, h = compute_unique_terminal_hashdatas(forms())
-    c1 = nreps * len(cells) * len(degrees) * len(families) * nelm * 2  # Number of cases with repetitions
+    c1 = nreps * len(cells) * len(degrees) * len(families) * nelm * 2
     assert c == c1
 
-    c0 = len(cells) * len(degrees) * (len(families)-1) * nelm * 2  # Number of unique cases, "CG" == "Lagrange"
-    # c0 = len(cells) * len(degrees) * (len(families)) * nelm * 2 # Number of unique cases, "CG" != "Lagrange"
+    c0 = len(cells) * len(degrees) * (len(families)-1) * nelm * 2
     assert d == c0
     assert r == c0
     assert h == c0
@@ -181,9 +193,10 @@ def test_terminal_hashdata_does_not_depend_on_coefficient_count_values_only_orde
     def forms():
         for rep in range(nreps):
             for i, cell in enumerate(cells):
-                domain = Mesh(VectorElement("Lagrange", cell, 1), ufl_id=i)
+                d = cell.geometric_dimension()
+                domain = Mesh(FiniteElement("Lagrange", cell, 1, (d, ), identity_pullback, H1), ufl_id=i)
                 for k in counts:
-                    V = FiniteElement("CG", cell, 2)
+                    V = FiniteElement("Lagrange", cell, 2, (), identity_pullback, H1)
                     space = FunctionSpace(domain, V)
                     f = Coefficient(space, count=k)
                     g = Coefficient(space, count=k+2)
@@ -219,9 +232,10 @@ def test_terminal_hashdata_does_depend_on_argument_number_values(self):
     def forms():
         for rep in range(nreps):
             for i, cell in enumerate(cells):
-                domain = Mesh(VectorElement("Lagrange", cell, 1), ufl_id=i)
+                d = cell.geometric_dimension()
+                domain = Mesh(FiniteElement("Lagrange", cell, 1, (d, ), identity_pullback, H1), ufl_id=i)
                 for k in counts:
-                    V = FiniteElement("CG", cell, 2)
+                    V = FiniteElement("Lagrange", cell, 2, (), identity_pullback, H1)
                     space = FunctionSpace(domain, V)
                     f = Argument(space, k)
                     g = Argument(space, k+2)
@@ -248,7 +262,8 @@ def test_domain_signature_data_does_not_depend_on_domain_label_value(self):
     s1s = set()
     s2s = set()
     for i, cell in enumerate(cells):
-        domain = VectorElement("Lagrange", cell, 1)
+        d = cell.geometric_dimension()
+        domain = FiniteElement("Lagrange", cell, 1, (d, ), identity_pullback, H1)
         d0 = Mesh(domain)
         d1 = Mesh(domain, ufl_id=1)
         d2 = Mesh(domain, ufl_id=2)
@@ -270,14 +285,17 @@ def test_terminal_hashdata_does_not_depend_on_domain_label_value(self):
     hashes = set()
     ufl_ids = [1, 2]
     cells = [triangle, quadrilateral]
-    domains = [Mesh(cell, ufl_id=ufl_id) for cell in cells for ufl_id in ufl_ids]
+    domains = [Mesh(FiniteElement("Lagrange", cell, 1, (cell.geometric_dimension(), ),
+                                  identity_pullback, H1),
+                    ufl_id=ufl_id) for cell in cells for ufl_id in ufl_ids]
     nreps = 2
     num_exprs = 2
 
     def forms():
         for rep in range(nreps):
             for domain in domains:
-                V = FunctionSpace(domain, FiniteElement("CG", domain.ufl_cell(), 2))
+                V = FunctionSpace(domain, FiniteElement("Lagrange", domain.ufl_cell(), 2, (),
+                                  identity_pullback, H1))
                 f = Coefficient(V, count=0)
                 v = TestFunction(V)
                 x = SpatialCoordinate(domain)
@@ -416,11 +434,12 @@ def check_unique_signatures(forms):
 
 def test_signature_is_affected_by_element_properties(self):
     def forms():
-        for family in ("CG", "DG"):
+        for family, sobolev in (("Lagrange", H1), ("Discontinuous Lagrange", L2)):
             for cell in (triangle, tetrahedron, quadrilateral):
-                domain = Mesh(VectorElement("Lagrange", cell, 1))
+                d = cell.geometric_dimension()
+                domain = Mesh(FiniteElement("Lagrange", cell, 1, (d, ), identity_pullback, H1))
                 for degree in (1, 2):
-                    V = FiniteElement(family, cell, degree)
+                    V = FiniteElement(family, cell, degree, (), identity_pullback, sobolev)
                     space = FunctionSpace(domain, V)
                     u = Coefficient(space)
                     v = TestFunction(space)
@@ -435,11 +454,12 @@ def forms():
 def test_signature_is_affected_by_domains(self):
     def forms():
         for cell in (triangle, tetrahedron):
-            domain = Mesh(VectorElement("Lagrange", cell, 1))
+            d = cell.geometric_dimension()
+            domain = Mesh(FiniteElement("Lagrange", cell, 1, (d, ), identity_pullback, H1))
             for di in (1, 2):
                 for dj in (1, 2):
                     for dk in (1, 2):
-                        V = FiniteElement("CG", cell, 1)
+                        V = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1)
                         space = FunctionSpace(domain, V)
                         u = Coefficient(space)
                         a = u*dx(di) + 2*u*dx(dj) + 3*u*ds(dk)
@@ -450,10 +470,12 @@ def forms():
 def test_signature_of_forms_with_diff(self):
     def forms():
         for i, cell in enumerate([triangle, tetrahedron]):
-            domain = Mesh(VectorElement("Lagrange", cell, 1), ufl_id=i)
+            d = cell.geometric_dimension()
+            domain = Mesh(FiniteElement("Lagrange", cell, 1, (d, ), identity_pullback, H1), ufl_id=i)
             for k in (1, 2, 3):
-                V = FiniteElement("CG", cell, 1)
-                W = VectorElement("CG", cell, 1)
+                d = cell.geometric_dimension()
+                V = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1)
+                W = FiniteElement("Lagrange", cell, 1, (d, ), identity_pullback, H1)
                 v_space = FunctionSpace(domain, V)
                 w_space = FunctionSpace(domain, W)
                 u = Coefficient(v_space)
@@ -470,8 +492,8 @@ def forms():
 
 def test_signature_of_form_depend_on_coefficient_numbering_across_integrals(self):
     cell = triangle
-    V = FiniteElement("CG", cell, 1)
-    domain = Mesh(VectorElement("Lagrange", cell, 1))
+    V = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1)
+    domain = Mesh(FiniteElement("Lagrange", cell, 1, (2, ), identity_pullback, H1))
     space = FunctionSpace(domain, V)
     f = Coefficient(space)
     g = Coefficient(space)
@@ -486,8 +508,9 @@ def test_signature_of_form_depend_on_coefficient_numbering_across_integrals(self
 def test_signature_of_forms_change_with_operators(self):
     def forms():
         for cell in (triangle, tetrahedron):
-            V = FiniteElement("CG", cell, 1)
-            domain = Mesh(VectorElement("Lagrange", cell, 1))
+            d = cell.geometric_dimension()
+            V = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1)
+            domain = Mesh(FiniteElement("Lagrange", cell, 1, (d, ), identity_pullback, H1))
             space = FunctionSpace(domain, V)
             u = Coefficient(space)
             v = Coefficient(space)
@@ -495,8 +518,8 @@ def forms():
                   (u+v)+(u/v),
                   (u+v)*(u/v),
                   (u*v)*(u*v),
-                  (u+v)*(u*v),  # (!) same
-                  # (u*v)*(u+v), # (!) same
+                  (u+v)*(u*v),  # H1 same
+                  # (u*v)*(u+v), # H1 same
                   (u*v)+(u+v),
                   ]
             for f in fs:
diff --git a/test/test_simplify.py b/test/test_simplify.py
index faa9d4e6e..66a176d9d 100755
--- a/test/test_simplify.py
+++ b/test/test_simplify.py
@@ -1,15 +1,17 @@
 import math
 
-from ufl import (Coefficient, FiniteElement, FunctionSpace, Mesh, TestFunction, TrialFunction, VectorConstant,
-                 VectorElement, acos, as_tensor, as_ufl, asin, atan, cos, cosh, dx, exp, i, j, ln, max_value, min_value,
-                 outer, sin, sinh, tan, tanh, triangle)
+from ufl import (Coefficient, FunctionSpace, Mesh, TestFunction, TrialFunction, VectorConstant, acos, as_tensor, as_ufl,
+                 asin, atan, cos, cosh, dx, exp, i, j, ln, max_value, min_value, outer, sin, sinh, tan, tanh, triangle)
 from ufl.algorithms import compute_form_data
+from ufl.finiteelement import FiniteElement
+from ufl.pullback import identity_pullback
+from ufl.sobolevspace import H1
 
 
 def xtest_zero_times_argument(self):
     # FIXME: Allow zero forms
-    element = FiniteElement("CG", triangle, 1)
-    domain = Mesh(VectorElement("Lagrange", triangle, 1))
+    element = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1)
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     space = FunctionSpace(domain, element)
     v = TestFunction(space)
     u = TrialFunction(space)
@@ -22,8 +24,8 @@ def xtest_zero_times_argument(self):
 
 
 def test_divisions(self):
-    element = FiniteElement("CG", triangle, 1)
-    domain = Mesh(VectorElement("Lagrange", triangle, 1))
+    element = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1)
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     space = FunctionSpace(domain, element)
     f = Coefficient(space)
 
@@ -49,8 +51,8 @@ def test_divisions(self):
 
 
 def test_products(self):
-    element = FiniteElement("CG", triangle, 1)
-    domain = Mesh(VectorElement("Lagrange", triangle, 1))
+    element = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1)
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     space = FunctionSpace(domain, element)
     f = Coefficient(space)
     g = Coefficient(space)
@@ -71,8 +73,8 @@ def test_products(self):
 
 
 def test_sums(self):
-    element = FiniteElement("CG", triangle, 1)
-    domain = Mesh(VectorElement("Lagrange", triangle, 1))
+    element = FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1)
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     space = FunctionSpace(domain, element)
     f = Coefficient(space)
     g = Coefficient(space)
@@ -122,7 +124,7 @@ def test_mathfunctions(self):
 
 
 def test_indexing(self):
-    domain = Mesh(VectorElement("Lagrange", triangle, 1))
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     u = VectorConstant(domain)
     v = VectorConstant(domain)
 
diff --git a/test/test_sobolevspace.py b/test/test_sobolevspace.py
index ba4eee118..28e42a24d 100755
--- a/test/test_sobolevspace.py
+++ b/test/test_sobolevspace.py
@@ -3,8 +3,9 @@
 
 from math import inf
 
-from ufl import (H1, H2, L2, EnrichedElement, FiniteElement, HCurl, HDiv, HInf, TensorProductElement, interval,
-                 quadrilateral, triangle)
+from ufl import H1, H2, L2, HCurl, HDiv, HInf, triangle
+from ufl.finiteelement import FiniteElement
+from ufl.pullback import contravariant_piola, covariant_piola, identity_pullback
 from ufl.sobolevspace import SobolevSpace  # noqa: F401
 from ufl.sobolevspace import DirectionalSobolevSpace
 
@@ -62,18 +63,9 @@ def xtest_contains_mixed():
 
 def test_contains_l2():
     l2_elements = [
-        FiniteElement("DG", triangle, 0),
-        FiniteElement("DG", triangle, 1),
-        FiniteElement("DG", triangle, 2),
-        FiniteElement("CR", triangle, 1),
-        # Tensor product elements:
-        TensorProductElement(FiniteElement("DG", interval, 1),
-                             FiniteElement("DG", interval, 1)),
-        TensorProductElement(FiniteElement("DG", interval, 1),
-                             FiniteElement("CG", interval, 2)),
-        # Enriched element:
-        EnrichedElement(FiniteElement("DG", triangle, 1),
-                        FiniteElement("B", triangle, 3))
+        FiniteElement("Discontinuous Lagrange", triangle, 0, (), identity_pullback, L2),
+        FiniteElement("Discontinuous Lagrange", triangle, 1, (), identity_pullback, L2),
+        FiniteElement("Discontinuous Lagrange", triangle, 2, (), identity_pullback, L2),
     ]
     for l2_element in l2_elements:
         assert l2_element in L2
@@ -89,19 +81,11 @@ def test_contains_l2():
 def test_contains_h1():
     h1_elements = [
         # Standard Lagrange elements:
-        FiniteElement("CG", triangle, 1),
-        FiniteElement("CG", triangle, 2),
+        FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1),
+        FiniteElement("Lagrange", triangle, 2, (), identity_pullback, H1),
         # Some special elements:
-        FiniteElement("HER", triangle),
-        FiniteElement("MTW", triangle),
-        # Tensor product elements:
-        TensorProductElement(FiniteElement("CG", interval, 1),
-                             FiniteElement("CG", interval, 1)),
-        TensorProductElement(FiniteElement("CG", interval, 2),
-                             FiniteElement("CG", interval, 2)),
-        # Enriched elements:
-        EnrichedElement(FiniteElement("CG", triangle, 2),
-                        FiniteElement("B", triangle, 3))
+        FiniteElement("MTW", triangle, 3, (2, ), contravariant_piola, H1),
+        FiniteElement("Hermite", triangle, 3, (), "custom", H1),
     ]
     for h1_element in h1_elements:
         assert h1_element in H1
@@ -116,8 +100,8 @@ def test_contains_h1():
 
 def test_contains_h2():
     h2_elements = [
-        FiniteElement("ARG", triangle, 5),
-        FiniteElement("MOR", triangle, 2),
+        FiniteElement("ARG", triangle, 5, (), "custom", H2),
+        FiniteElement("MOR", triangle, 2, (), "custom", H2),
     ]
     for h2_element in h2_elements:
         assert h2_element in H2
@@ -132,7 +116,7 @@ def test_contains_h2():
 
 def test_contains_hinf():
     hinf_elements = [
-        FiniteElement("R", triangle, 0)
+        FiniteElement("Real", triangle, 0, (), identity_pullback, HInf)
     ]
     for hinf_element in hinf_elements:
         assert hinf_element in HInf
@@ -148,16 +132,9 @@ def test_contains_hinf():
 
 def test_contains_hdiv():
     hdiv_elements = [
-        FiniteElement("RT", triangle, 1),
-        FiniteElement("BDM", triangle, 1),
-        FiniteElement("BDFM", triangle, 2),
-        # HDiv elements:
-        HDiv(TensorProductElement(FiniteElement("DG", triangle, 1),
-                                  FiniteElement("CG", interval, 2))),
-        HDiv(TensorProductElement(FiniteElement("RT", triangle, 1),
-                                  FiniteElement("DG", interval, 1))),
-        HDiv(TensorProductElement(FiniteElement("N1curl", triangle, 1),
-                                  FiniteElement("DG", interval, 1)))
+        FiniteElement("Raviart-Thomas", triangle, 1, (2, ), contravariant_piola, HDiv),
+        FiniteElement("BDM", triangle, 1, (2, ), contravariant_piola, HDiv),
+        FiniteElement("BDFM", triangle, 2, (2, ), contravariant_piola, HDiv),
     ]
     for hdiv_element in hdiv_elements:
         assert hdiv_element in HDiv
@@ -172,15 +149,8 @@ def test_contains_hdiv():
 
 def test_contains_hcurl():
     hcurl_elements = [
-        FiniteElement("N1curl", triangle, 1),
-        FiniteElement("N2curl", triangle, 1),
-        # HCurl elements:
-        HCurl(TensorProductElement(FiniteElement("CG", triangle, 1),
-                                   FiniteElement("DG", interval, 1))),
-        HCurl(TensorProductElement(FiniteElement("N1curl", triangle, 1),
-                                   FiniteElement("CG", interval, 1))),
-        HCurl(TensorProductElement(FiniteElement("RT", triangle, 1),
-                                   FiniteElement("CG", interval, 1)))
+        FiniteElement("N1curl", triangle, 1, (2, ), covariant_piola, HCurl),
+        FiniteElement("N2curl", triangle, 1, (2, ), covariant_piola, HCurl),
     ]
     for hcurl_element in hcurl_elements:
         assert hcurl_element in HCurl
@@ -191,79 +161,3 @@ def test_contains_hcurl():
         assert hcurl_element not in HDiv
         assert hcurl_element not in H2
         assert hcurl_element not in H2dx2dy
-
-
-def test_enriched_elements_hdiv():
-    A = FiniteElement("CG", interval, 1)
-    B = FiniteElement("DG", interval, 0)
-    AxB = TensorProductElement(A, B)
-    BxA = TensorProductElement(B, A)
-    C = FiniteElement("RTCF", quadrilateral, 1)
-    D = FiniteElement("DQ", quadrilateral, 0)
-    Q1 = TensorProductElement(C, B)
-    Q2 = TensorProductElement(D, A)
-    hdiv_elements = [
-        EnrichedElement(HDiv(AxB), HDiv(BxA)),
-        EnrichedElement(HDiv(Q1), HDiv(Q2))
-    ]
-    for hdiv_element in hdiv_elements:
-        assert hdiv_element in HDiv
-        assert hdiv_element in L2
-        assert hdiv_element in H0dx0dy
-        assert hdiv_element not in H1
-        assert hdiv_element not in H1dx1dy
-        assert hdiv_element not in HCurl
-        assert hdiv_element not in H2
-        assert hdiv_element not in H2dx2dy
-
-
-def test_enriched_elements_hcurl():
-    A = FiniteElement("CG", interval, 1)
-    B = FiniteElement("DG", interval, 0)
-    AxB = TensorProductElement(A, B)
-    BxA = TensorProductElement(B, A)
-    C = FiniteElement("RTCE", quadrilateral, 1)
-    D = FiniteElement("DQ", quadrilateral, 0)
-    Q1 = TensorProductElement(C, B)
-    Q2 = TensorProductElement(D, A)
-    hcurl_elements = [
-        EnrichedElement(HCurl(AxB), HCurl(BxA)),
-        EnrichedElement(HCurl(Q1), HCurl(Q2))
-    ]
-    for hcurl_element in hcurl_elements:
-        assert hcurl_element in HCurl
-        assert hcurl_element in L2
-        assert hcurl_element in H0dx0dy
-        assert hcurl_element not in H1
-        assert hcurl_element not in H1dx1dy
-        assert hcurl_element not in HDiv
-        assert hcurl_element not in H2
-        assert hcurl_element not in H2dx2dy
-
-
-def test_varying_continuity_elements():
-    P1DG_t = FiniteElement("DG", triangle, 1)
-    P1DG_i = FiniteElement("DG", interval, 1)
-    P1 = FiniteElement("CG", interval, 1)
-    P2 = FiniteElement("CG", interval, 2)
-    P3 = FiniteElement("CG", interval, 3)
-    RT1 = FiniteElement("RT", triangle, 1)
-    ARG = FiniteElement("ARG", triangle, 5)
-
-    # Tensor product elements
-    P1DGP2 = TensorProductElement(P1DG_t, P2)
-    P1P1DG = TensorProductElement(P1, P1DG_i)
-    P1DGP1 = TensorProductElement(P1DG_i, P1)
-    RT1DG1 = TensorProductElement(RT1, P1DG_i)
-    P2P3 = TensorProductElement(P2, P3)
-    ARGP3 = TensorProductElement(ARG, P3)
-
-    assert P1DGP2 in H1dz and P1DGP2 in L2
-    assert P1DGP2 not in H1dh
-    assert P1DGP1 in H1dy and P1DGP2 in L2
-    assert P1P1DG in H1dx and P1P1DG in L2
-    assert P1P1DG not in H1dx1dy
-    assert RT1DG1 in H000 and RT1DG1 in L2
-    assert P2P3 in H1dx1dy and P2P3 in H1
-    assert ARG in H2dx2dy
-    assert ARGP3 in H2dhH1dz
diff --git a/test/test_split.py b/test/test_split.py
index bce2dbf03..7a16e4a84 100755
--- a/test/test_split.py
+++ b/test/test_split.py
@@ -1,28 +1,31 @@
 __authors__ = "Martin Sandve Alnæs"
 __date__ = "2009-03-14 -- 2009-03-14"
 
-from ufl import (Coefficient, FiniteElement, FunctionSpace, Mesh, MixedElement, TensorElement, TestFunction,
-                 VectorElement, as_vector, product, split, triangle)
+from ufl import Coefficient, FunctionSpace, Mesh, TestFunction, as_vector, product, split, triangle
+from ufl.finiteelement import FiniteElement, MixedElement, SymmetricElement
+from ufl.pullback import identity_pullback
+from ufl.sobolevspace import H1
 
 
 def test_split(self):
     cell = triangle
-    domain = Mesh(VectorElement("Lagrange", cell, 1))
     d = cell.geometric_dimension()
-    f = FiniteElement("CG", cell, 1)
-    v = VectorElement("CG", cell, 1)
-    w = VectorElement("CG", cell, 1, dim=d+1)
-    t = TensorElement("CG", cell, 1)
-    s = TensorElement("CG", cell, 1, symmetry=True)
-    r = TensorElement("CG", cell, 1, symmetry={(1, 0): (0, 1)}, shape=(d, d))
-    m = MixedElement(f, v, w, t, s, r)
+    domain = Mesh(FiniteElement("Lagrange", cell, 1, (d, ), identity_pullback, H1))
+    f = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1)
+    v = FiniteElement("Lagrange", cell, 1, (d, ), identity_pullback, H1,
+                      sub_elements=[f for _ in range(d)])
+    w = FiniteElement("Lagrange", cell, 1, (d+1, ), identity_pullback, H1,
+                      sub_elements=[f for _ in range(d + 1)])
+    t = FiniteElement("Lagrange", cell, 1, (d, d), identity_pullback, H1,
+                      sub_elements=[f for _ in range(d ** 2)])
+    s = SymmetricElement({(0, 0): 0, (0, 1): 1, (1, 0): 1, (1, 1): 2}, [f for _ in range(3)])
+    m = MixedElement([f, v, w, t, s, s])
 
     f_space = FunctionSpace(domain, f)
     v_space = FunctionSpace(domain, v)
     w_space = FunctionSpace(domain, w)
     t_space = FunctionSpace(domain, t)
     s_space = FunctionSpace(domain, s)
-    r_space = FunctionSpace(domain, r)
     m_space = FunctionSpace(domain, m)
 
     # Check that shapes of all these functions are correct:
@@ -31,7 +34,6 @@ def test_split(self):
     assert (d+1,) == Coefficient(w_space).ufl_shape
     assert (d, d) == Coefficient(t_space).ufl_shape
     assert (d, d) == Coefficient(s_space).ufl_shape
-    assert (d, d) == Coefficient(r_space).ufl_shape
     # sum of value sizes, not accounting for symmetries:
     assert (3*d*d + 2*d + 2,) == Coefficient(m_space).ufl_shape
 
@@ -43,8 +45,8 @@ def test_split(self):
     assert s == 0
 
     # Mixed elements of non-scalar subelements are flattened
-    v2 = MixedElement(v, v)
-    m2 = MixedElement(t, t)
+    v2 = MixedElement([v, v])
+    m2 = MixedElement([t, t])
     v2_space = FunctionSpace(domain, v2)
     m2_space = FunctionSpace(domain, m2)
     # assert d == 2
@@ -58,13 +60,13 @@ def test_split(self):
 
     # Split twice on nested mixed elements gets
     # the innermost scalar subcomponents
-    t = TestFunction(FunctionSpace(domain, f*v))
+    t = TestFunction(FunctionSpace(domain, MixedElement([f, v])))
     assert split(t) == (t[0], as_vector((t[1], t[2])))
     assert split(split(t)[1]) == (t[1], t[2])
-    t = TestFunction(FunctionSpace(domain, f*(f*v)))
+    t = TestFunction(FunctionSpace(domain, MixedElement([f, [f, v]])))
     assert split(t) == (t[0], as_vector((t[1], t[2], t[3])))
     assert split(split(t)[1]) == (t[1], as_vector((t[2], t[3])))
-    t = TestFunction(FunctionSpace(domain, (v*f)*(f*v)))
+    t = TestFunction(FunctionSpace(domain, MixedElement([[v, f], [f, v]])))
     assert split(t) == (as_vector((t[0], t[1], t[2])),
                         as_vector((t[3], t[4], t[5])))
     assert split(split(t)[0]) == (as_vector((t[0], t[1])), t[2])
diff --git a/test/test_str.py b/test/test_str.py
index 96683ae38..df80d8215 100755
--- a/test/test_str.py
+++ b/test/test_str.py
@@ -1,6 +1,9 @@
-from ufl import (CellDiameter, CellVolume, Circumradius, FacetArea, FacetNormal, FiniteElement, FunctionSpace, Index,
-                 Mesh, SpatialCoordinate, TestFunction, TrialFunction, VectorElement, as_matrix, as_ufl, as_vector,
-                 quadrilateral, tetrahedron, triangle)
+from ufl import (CellDiameter, CellVolume, Circumradius, FacetArea, FacetNormal, FunctionSpace, Index, Mesh,
+                 SpatialCoordinate, TestFunction, TrialFunction, as_matrix, as_ufl, as_vector, quadrilateral,
+                 tetrahedron, triangle)
+from ufl.finiteelement import FiniteElement
+from ufl.pullback import identity_pullback
+from ufl.sobolevspace import H1
 
 
 def test_str_int_value(self):
@@ -12,7 +15,7 @@ def test_str_float_value(self):
 
 
 def test_str_zero(self):
-    domain = Mesh(VectorElement("Lagrange", triangle, 1))
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     x = SpatialCoordinate(domain)
     assert str(as_ufl(0)) == "0"
     assert str(0*x) == "0 (shape (2,))"
@@ -25,41 +28,41 @@ def test_str_index(self):
 
 
 def test_str_coordinate(self):
-    domain = Mesh(VectorElement("Lagrange", triangle, 1))
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     assert str(SpatialCoordinate(domain)) == "x"
     assert str(SpatialCoordinate(domain)[0]) == "x[0]"
 
 
 def test_str_normal(self):
-    domain = Mesh(VectorElement("Lagrange", triangle, 1))
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     assert str(FacetNormal(domain)) == "n"
     assert str(FacetNormal(domain)[0]) == "n[0]"
 
 
 def test_str_circumradius(self):
-    domain = Mesh(VectorElement("Lagrange", triangle, 1))
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     assert str(Circumradius(domain)) == "circumradius"
 
 
 def test_str_diameter(self):
-    domain = Mesh(VectorElement("Lagrange", triangle, 1))
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     assert str(CellDiameter(domain)) == "diameter"
 
 
 def test_str_facetarea(self):
-    domain = Mesh(VectorElement("Lagrange", triangle, 1))
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     assert str(FacetArea(domain)) == "facetarea"
 
 
 def test_str_volume(self):
-    domain = Mesh(VectorElement("Lagrange", triangle, 1))
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     assert str(CellVolume(domain)) == "volume"
 
 
 def test_str_scalar_argument(self):
-    domain = Mesh(VectorElement("Lagrange", triangle, 1))
-    v = TestFunction(FunctionSpace(domain, FiniteElement("CG", triangle, 1)))
-    u = TrialFunction(FunctionSpace(domain, FiniteElement("CG", triangle, 1)))
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
+    v = TestFunction(FunctionSpace(domain, FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1)))
+    u = TrialFunction(FunctionSpace(domain, FiniteElement("Lagrange", triangle, 1, (), identity_pullback, H1)))
     assert str(v) == "v_0"
     assert str(u) == "v_1"
 
@@ -72,21 +75,21 @@ def test_str_scalar_argument(self):
 
 
 def test_str_list_vector():
-    domain = Mesh(VectorElement("Lagrange", tetrahedron, 1))
+    domain = Mesh(FiniteElement("Lagrange", tetrahedron, 1, (3, ), identity_pullback, H1))
     x, y, z = SpatialCoordinate(domain)
     v = as_vector((x, y, z))
     assert str(v) == ("[%s, %s, %s]" % (x, y, z))
 
 
 def test_str_list_vector_with_zero():
-    domain = Mesh(VectorElement("Lagrange", tetrahedron, 1))
+    domain = Mesh(FiniteElement("Lagrange", tetrahedron, 1, (3, ), identity_pullback, H1))
     x, y, z = SpatialCoordinate(domain)
     v = as_vector((x, 0, 0))
     assert str(v) == ("[%s, 0, 0]" % (x,))
 
 
 def test_str_list_matrix():
-    domain = Mesh(VectorElement("Lagrange", triangle, 1))
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     x, y = SpatialCoordinate(domain)
     v = as_matrix(((2*x, 3*y),
                    (4*x, 5*y)))
@@ -98,7 +101,7 @@ def test_str_list_matrix():
 
 
 def test_str_list_matrix_with_zero():
-    domain = Mesh(VectorElement("Lagrange", triangle, 1))
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     x, y = SpatialCoordinate(domain)
     v = as_matrix(((2*x, 3*y),
                    (0, 0)))
@@ -113,5 +116,6 @@ def test_str_list_matrix_with_zero():
 
 
 def test_str_element():
-    elem = FiniteElement("Q", quadrilateral, 1)
+    elem = FiniteElement("Q", quadrilateral, 1, (), identity_pullback, H1)
+    assert repr(elem) == "ufl.finiteelement.FiniteElement(\"Q\", quadrilateral, 1, (), IdentityPullback(), H1)"
     assert str(elem) == "<Q1 on a quadrilateral>"
diff --git a/test/test_strip_forms.py b/test/test_strip_forms.py
index 3e8a77608..9a74ac506 100644
--- a/test/test_strip_forms.py
+++ b/test/test_strip_forms.py
@@ -1,11 +1,13 @@
 import gc
 import sys
 
-from ufl import (Coefficient, Constant, FiniteElement, FunctionSpace, Mesh, TestFunction, TrialFunction, dx, grad,
-                 inner, triangle)
+from ufl import Coefficient, Constant, FunctionSpace, Mesh, TestFunction, TrialFunction, dx, grad, inner, triangle
 from ufl.algorithms import replace_terminal_data, strip_terminal_data
 from ufl.core.ufl_id import attach_ufl_id
 from ufl.core.ufl_type import attach_operators_from_hash_data
+from ufl.finiteelement import FiniteElement
+from ufl.pullback import identity_pullback
+from ufl.sobolevspace import H1
 
 MIN_REF_COUNT = 2
 """The minimum value returned by sys.getrefcount."""
@@ -51,8 +53,9 @@ def test_strip_form_arguments_strips_data_refs():
     assert sys.getrefcount(const_data) == MIN_REF_COUNT
 
     cell = triangle
-    domain = AugmentedMesh(cell, data=mesh_data)
-    element = FiniteElement("Lagrange", cell, 1)
+    domain = AugmentedMesh(FiniteElement("Lagrange", cell, 1, (cell.geometric_dimension(), ),
+                                         identity_pullback, H1), data=mesh_data)
+    element = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1)
     V = AugmentedFunctionSpace(domain, element, data=fs_data)
 
     v = TestFunction(V)
@@ -88,8 +91,9 @@ def test_strip_form_arguments_does_not_change_form():
     const_data = object()
 
     cell = triangle
-    domain = AugmentedMesh(cell, data=mesh_data)
-    element = FiniteElement("Lagrange", cell, 1)
+    domain = AugmentedMesh(FiniteElement("Lagrange", cell, 1, (cell.geometric_dimension(), ),
+                                         identity_pullback, H1), data=mesh_data)
+    element = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1)
     V = AugmentedFunctionSpace(domain, element, data=fs_data)
 
     v = TestFunction(V)
diff --git a/test/test_tensoralgebra.py b/test/test_tensoralgebra.py
index d070a3dd4..bdb6ad406 100755
--- a/test/test_tensoralgebra.py
+++ b/test/test_tensoralgebra.py
@@ -2,9 +2,12 @@
 
 import pytest
 
-from ufl import (FacetNormal, Mesh, VectorElement, as_matrix, as_tensor, as_vector, cofac, cross, det, dev, diag,
-                 diag_vector, dot, inner, inv, outer, perp, skew, sym, tr, transpose, triangle, zero)
+from ufl import (FacetNormal, Mesh, as_matrix, as_tensor, as_vector, cofac, cross, det, dev, diag, diag_vector, dot,
+                 inner, inv, outer, perp, skew, sym, tr, transpose, triangle, zero)
 from ufl.algorithms.remove_complex_nodes import remove_complex_nodes
+from ufl.finiteelement import FiniteElement
+from ufl.pullback import identity_pullback
+from ufl.sobolevspace import H1
 
 
 @pytest.fixture(scope="module")
@@ -63,7 +66,7 @@ def test_inner(self, A, B, u, v):
 
 
 def test_pow2_inner(self, A, u):
-    domain = Mesh(VectorElement("Lagrange", triangle, 1))
+    domain = Mesh(FiniteElement("Lagrange", triangle, 1, (2, ), identity_pullback, H1))
     f = FacetNormal(domain)[0]
     f2 = f*f
     assert f2 == remove_complex_nodes(inner(f, f))