diff --git a/doc/ref_kernel.rst b/doc/ref_kernel.rst
index 896388d2911a6d3c0e7783d7b1b3833b87c770d0..46063310917fe4c111bac96a5a15c7c588974617 100644
--- a/doc/ref_kernel.rst
+++ b/doc/ref_kernel.rst
@@ -553,6 +553,8 @@ Helper values
 
 .. autoclass:: UniqueName
 
+.. autoclass:: Optional
+
 .. }}}
 
 Libraries: Extending and Interfacing with External Functionality
diff --git a/doc/tutorial.rst b/doc/tutorial.rst
index 397f34a987ed336795d00e2770c2fbeadf089ae7..3c85060dacf03b52f6e0b1faf05ad4697b6a5d07 100644
--- a/doc/tutorial.rst
+++ b/doc/tutorial.rst
@@ -1869,7 +1869,7 @@ Now to make things more interesting, we'll create a kernel with barriers:
     ...     e[i,j,k] = c[i,j,k+1]+c[i,j,k-1]
     ...     """
     ...     ], [
-    ...     lp.TemporaryVariable("c", lp.auto, shape=(50, 10, 99)),
+    ...     lp.TemporaryVariable("c", dtype=None, shape=(50, 10, 99)),
     ...     "..."
     ...     ])
     >>> knl = lp.add_and_infer_dtypes(knl, dict(a=np.int32))
diff --git a/loopy/__init__.py b/loopy/__init__.py
index f50ce237c0170d2b9703f1760a334e9f39e7f7f1..d69a57bf1a5435adfb067df5cfb2080633cac765 100644
--- a/loopy/__init__.py
+++ b/loopy/__init__.py
@@ -149,6 +149,8 @@ from loopy.target.pyopencl import PyOpenCLTarget
 from loopy.target.ispc import ISPCTarget
 from loopy.target.numba import NumbaTarget, NumbaCudaTarget
 
+from loopy.tools import Optional
+
 
 __all__ = [
         "TaggedVariable", "Reduction", "LinearSubscript", "TypeCast",
@@ -275,6 +277,8 @@ __all__ = [
         "NumbaTarget", "NumbaCudaTarget",
         "ASTBuilderBase",
 
+        "Optional",
+
         # {{{ from this file
 
         "register_preamble_generators",
diff --git a/loopy/kernel/array.py b/loopy/kernel/array.py
index bae9d7d1fbc873076a84b933e5c78f5c9b19dbb5..3588f38af13479b127208c25735f1046eaa82706 100644
--- a/loopy/kernel/array.py
+++ b/loopy/kernel/array.py
@@ -693,8 +693,9 @@ class ArrayBase(ImmutableRecord):
 
         if dtype is lp.auto:
             from warnings import warn
-            warn("Argument/temporary data type should be None if unspecified, "
-                    "not auto. This usage will be disallowed in 2018.",
+            warn("Argument/temporary data type for '%s' should be None if "
+                    "unspecified, not auto. This usage will be disallowed in 2018."
+                    % name,
                     DeprecationWarning, stacklevel=2)
 
             dtype = None
diff --git a/loopy/kernel/creation.py b/loopy/kernel/creation.py
index c42db348234345a48efcb22b842fd114c5f65f14..2175b5f364796fed58d2373d82ae18ea7128ef2f 100644
--- a/loopy/kernel/creation.py
+++ b/loopy/kernel/creation.py
@@ -28,7 +28,7 @@ THE SOFTWARE.
 import numpy as np
 
 from pymbolic.mapper import CSECachingMapperMixin
-from loopy.tools import intern_frozenset_of_ids
+from loopy.tools import intern_frozenset_of_ids, Optional
 from loopy.symbolic import IdentityMapper, WalkMapper
 from loopy.kernel.data import (
         InstructionBase,
@@ -454,7 +454,6 @@ def parse_insn(groups, insn_options):
         and *inames_to_dup* is None or a list of tuples `(old, new)`.
     """
 
-    import loopy as lp
     from loopy.symbolic import parse
 
     if "lhs" in groups:
@@ -486,14 +485,11 @@ def parse_insn(groups, insn_options):
 
     for lhs_i in lhs:
         if isinstance(lhs_i, TypeAnnotation):
-            if lhs_i.type is None:
-                temp_var_types.append(lp.auto)
-            else:
-                temp_var_types.append(lhs_i.type)
-
+            assert isinstance(lhs_i.type, Optional)
+            temp_var_types.append(lhs_i.type)
             lhs_i = lhs_i.child
         else:
-            temp_var_types.append(None)
+            temp_var_types.append(Optional())
 
         inner_lhs_i = lhs_i
         if isinstance(inner_lhs_i, Lookup):
@@ -1138,9 +1134,9 @@ class ArgumentGuesser:
 
     def make_new_arg(self, arg_name):
         arg_name = arg_name.strip()
+        import loopy as lp
 
         from loopy.kernel.data import ValueArg, ArrayArg, AddressSpace
-        import loopy as lp
 
         if arg_name in self.all_params:
             return ValueArg(arg_name)
@@ -1191,7 +1187,7 @@ class ArgumentGuesser:
                 for assignee_var_name, temp_var_type in zip(
                         insn.assignee_var_names(),
                         insn.temp_var_types):
-                    if temp_var_type is not None:
+                    if temp_var_type.has_value:
                         temp_var_names.add(assignee_var_name)
 
         # }}}
@@ -1431,7 +1427,7 @@ def create_temporaries(knl, default_order):
                     insn.assignee_var_names(),
                     insn.temp_var_types):
 
-                if temp_var_type is None:
+                if not temp_var_type.has_value:
                     continue
 
                 if assignee_name in new_temp_vars:
@@ -1446,17 +1442,17 @@ def create_temporaries(knl, default_order):
 
                 new_temp_vars[assignee_name] = lp.TemporaryVariable(
                         name=assignee_name,
-                        dtype=temp_var_type,
+                        dtype=temp_var_type.value,
                         address_space=lp.auto,
                         base_indices=lp.auto,
                         shape=lp.auto,
                         order=default_order,
                         target=knl.target)
 
-                if isinstance(insn, Assignment):
-                    insn = insn.copy(temp_var_type=None)
-                else:
-                    insn = insn.copy(temp_var_types=None)
+            if isinstance(insn, Assignment):
+                insn = insn.copy(temp_var_type=Optional())
+            else:
+                insn = insn.copy(temp_var_types=(Optional(),) * len(insn.assignees))
 
         new_insns.append(insn)
 
diff --git a/loopy/kernel/data.py b/loopy/kernel/data.py
index 7877f8b939444bf3dc095037ffeaa1bb548c39d6..8103029dc5f86896b4e08dd9e277b81b01e9ca27 100644
--- a/loopy/kernel/data.py
+++ b/loopy/kernel/data.py
@@ -330,8 +330,9 @@ class KernelArgument(ImmutableRecord):
 
         import loopy as lp
         if dtype is lp.auto:
-            warn("Argument/temporary data type should be None if unspecified, "
-                    "not auto. This usage will be disallowed in 2018.",
+            warn("Argument/temporary data type for '%s' should be None if "
+                   "unspecified, not auto. This usage will be disallowed in 2018."
+                    % kwargs["name"],
                     DeprecationWarning, stacklevel=2)
 
             dtype = None
diff --git a/loopy/kernel/instruction.py b/loopy/kernel/instruction.py
index e9c7bde9fd937dd58b5fa44476280a4b0c793dec..d7784eabff6a61b2c65425e2ba9506aa854a0d6e 100644
--- a/loopy/kernel/instruction.py
+++ b/loopy/kernel/instruction.py
@@ -25,6 +25,7 @@ THE SOFTWARE.
 from six.moves import intern
 from pytools import ImmutableRecord, memoize_method
 from loopy.diagnostic import LoopyError
+from loopy.tools import Optional
 from warnings import warn
 
 
@@ -838,8 +839,9 @@ class Assignment(MultiAssignmentBase):
 
     .. attribute:: temp_var_type
 
-        if not *None*, a type that will be assigned to the new temporary variable
-        created from the assignee
+        A :class:`loopy.Optional`. If not empty, contains the type that
+        will be assigned to the new temporary variable created from the
+        assignment.
 
     .. attribute:: atomicity
 
@@ -893,7 +895,7 @@ class Assignment(MultiAssignmentBase):
             within_inames_is_final=None,
             within_inames=None,
             boostable=None, boostable_into=None, tags=None,
-            temp_var_type=None, atomicity=(),
+            temp_var_type=Optional(), atomicity=(),
             priority=0, predicates=frozenset(),
             insn_deps=None, insn_deps_is_final=None,
             forced_iname_deps=None, forced_iname_deps_is_final=None):
@@ -1006,8 +1008,9 @@ class CallInstruction(MultiAssignmentBase):
 
     .. attribute:: temp_var_types
 
-        if not *None*, a type that will be assigned to the new temporary variable
-        created from the assignee
+        A tuple of `:class:loopy.Optional`. If an entry is not empty, it
+        contains the type that will be assigned to the new temporary variable
+        created from the assigment.
 
     .. automethod:: __init__
     """
@@ -1079,7 +1082,7 @@ class CallInstruction(MultiAssignmentBase):
         self.expression = expression
 
         if temp_var_types is None:
-            self.temp_var_types = (None,) * len(self.assignees)
+            self.temp_var_types = (Optional(),) * len(self.assignees)
         else:
             self.temp_var_types = temp_var_types
 
@@ -1127,34 +1130,33 @@ class CallInstruction(MultiAssignmentBase):
 
 
 def make_assignment(assignees, expression, temp_var_types=None, **kwargs):
-    if len(assignees) > 1 or len(assignees) == 0:
-        atomicity = kwargs.pop("atomicity", ())
-        if atomicity:
-            raise LoopyError("atomic operations with more than one "
-                    "left-hand side not supported")
+    if temp_var_types is None:
+        temp_var_types = (Optional(),) * len(assignees)
 
-        from pymbolic.primitives import Call
-        from loopy.symbolic import Reduction
-        if not isinstance(expression, (Call, Reduction)):
-            raise LoopyError("right-hand side in multiple assignment must be "
-                    "function call or reduction, got: '%s'" % expression)
-
-        return CallInstruction(
-                assignees=assignees,
-                expression=expression,
-                temp_var_types=temp_var_types,
-                **kwargs)
-
-    else:
+    if len(assignees) == 1:
         return Assignment(
                 assignee=assignees[0],
                 expression=expression,
-                temp_var_type=(
-                    temp_var_types[0]
-                    if temp_var_types is not None
-                    else None),
+                temp_var_type=temp_var_types[0],
                 **kwargs)
 
+    atomicity = kwargs.pop("atomicity", ())
+    if atomicity:
+        raise LoopyError("atomic operations with more than one "
+                "left-hand side not supported")
+
+    from pymbolic.primitives import Call
+    from loopy.symbolic import Reduction
+    if not isinstance(expression, (Call, Reduction)):
+        raise LoopyError("right-hand side in multiple assignment must be "
+                "function call or reduction, got: '%s'" % expression)
+
+    return CallInstruction(
+            assignees=assignees,
+            expression=expression,
+            temp_var_types=temp_var_types,
+            **kwargs)
+
 
 # {{{ c instruction
 
diff --git a/loopy/symbolic.py b/loopy/symbolic.py
index f4d46854b8dd15c8c1e9a716017ce2724b4db2fc..ae0c8999eef2b4cfea2ff89eee28a85173916628 100644
--- a/loopy/symbolic.py
+++ b/loopy/symbolic.py
@@ -1131,14 +1131,15 @@ class LoopyParser(ParserBase):
 
     def parse_prefix(self, pstate):
         from pymbolic.parser import _PREC_UNARY, _less, _greater, _identifier
+        import loopy as lp
         if pstate.is_next(_less):
             pstate.advance()
             if pstate.is_next(_greater):
-                typename = None
+                typename = lp.Optional(None)
                 pstate.advance()
             else:
                 pstate.expect(_identifier)
-                typename = pstate.next_str()
+                typename = lp.Optional(pstate.next_str())
                 pstate.advance()
                 pstate.expect(_greater)
                 pstate.advance()
diff --git a/loopy/tools.py b/loopy/tools.py
index 7e9a8921477403a280f08c6c79fe380d5cb5b2f3..470921e31293192b6e96e90b02ec41968bc5fad9 100644
--- a/loopy/tools.py
+++ b/loopy/tools.py
@@ -587,6 +587,78 @@ class LazilyUnpicklingListWithEqAndPersistentHashing(LazilyUnpicklingList):
 # }}}
 
 
+# {{{ optional object
+
+class _no_value(object):  # noqa
+    pass
+
+
+class Optional(object):
+    """A wrapper for an optionally present object.
+
+    .. attribute:: has_value
+
+        *True* if and only if this object contains a value.
+
+    .. attribute:: value
+
+        The value, if present.
+    """
+
+    __slots__ = ("has_value", "_value")
+
+    def __init__(self, value=_no_value):
+        self.has_value = value is not _no_value
+        if self.has_value:
+            self._value = value
+
+    def __str__(self):
+        if not self.has_value:
+            return "Optional()"
+        return "Optional(%s)" % self._value
+
+    def __repr__(self):
+        if not self.has_value:
+            return "Optional()"
+        return "Optional(%r)" % self._value
+
+    def __getstate__(self):
+        if not self.has_value:
+            return _no_value
+
+        return (self._value,)
+
+    def __setstate__(self, state):
+        if state is _no_value:
+            self.has_value = False
+            return
+
+        self.has_value = True
+        self._value, = state
+
+    def __eq__(self, other):
+        if not self.has_value:
+            return not other.has_value
+
+        return self.value == other.value if other.has_value else False
+
+    def __neq__(self, other):
+        return not self.__eq__(other)
+
+    @property
+    def value(self):
+        if not self.has_value:
+            raise AttributeError("optional value not present")
+        return self._value
+
+    def update_persistent_hash(self, key_hash, key_builder):
+        key_builder.rec(
+                key_hash,
+                (self._value,) if self.has_value else ())
+
+# }}}
+
+
 def unpickles_equally(obj):
     from six.moves.cPickle import loads, dumps
     return loads(dumps(obj)) == obj
diff --git a/test/test_misc.py b/test/test_misc.py
index 05df0317a6a39823dc8ac0c6a51d992336bb81d1..7a834a6f5d393298e97df22d47a1de3b64354a42 100644
--- a/test/test_misc.py
+++ b/test/test_misc.py
@@ -287,6 +287,46 @@ def test_LazilyUnpicklingListWithEqAndPersistentHashing():
     # }}}
 
 
+def test_Optional():  # noqa
+    from loopy import Optional
+
+    # {{{ test API
+
+    opt = Optional()
+    assert not opt.has_value
+    with pytest.raises(AttributeError):
+        opt.value
+
+    opt = Optional(1)
+    assert opt.has_value
+    assert 1 == opt.value
+
+    assert Optional(1) == Optional(1)
+    assert Optional(1) != Optional(2)
+    assert Optional() == Optional()
+    assert Optional() != Optional(1)
+
+    # }}}
+
+    # {{{ test pickling
+
+    import pickle
+
+    assert not pickle.loads(pickle.dumps(Optional())).has_value
+    assert pickle.loads(pickle.dumps(Optional(1))).value == 1
+
+    # }}}
+
+    # {{{ test key builder
+
+    from loopy.tools import LoopyKeyBuilder
+    kb = LoopyKeyBuilder()
+    kb(Optional())
+    kb(Optional(None))
+
+    # }}}
+
+
 if __name__ == "__main__":
     if len(sys.argv) > 1:
         exec(sys.argv[1])