diff --git a/README.rst b/README.rst
index 14a8b6b..3fa08d2 100644
--- a/README.rst
+++ b/README.rst
@@ -290,6 +290,14 @@ using ``filter_adapt(objects)``::
 
    list(ISpeaker.filter_adapt([None, Talker(), a_speaker, 'text']) --> [TalkerToSpeaker, a_speaker]
 
+To adapt an object only if it is not ``None`` then use::
+
+    ISpeaker.optional_adapt(optional_talker)
+
+This is equivalent to::
+
+    ISpeaker.adapt(optional_talker) if optional_talker is not None else None
+
 By default the adaption functions will return an object which provides **only**
 the functions and properties specified by the interface.  For example given the
 following implementation of the ``ISpeaker`` interface above::
@@ -485,6 +493,10 @@ Classes
     **adapt_or_none** *(obj, allow_implicit=False, interface_only=None)*
         As per **adapt()** except returns ``None`` instead of raising a ``ValueError``
 
+    **optional_adapt** *(obj, allow_implicit=False, interface_only=None)*
+        Adapts obj to this interface if it is not ``None`` returning ``None`` otherwise.
+        Short-cut for ``adapt(obj) if obj is not None else None``
+
     **can_adapt** *(obj, allow_implicit=False)*
         Returns ``True`` if ``adapt(obj, allow_implicit)`` will succeed.  Short-cut for
         ``adapt_or_none(obj) is not None``
diff --git a/pure_interface.py b/pure_interface.py
index 9ecb7fd..4ecb744 100644
--- a/pure_interface.py
+++ b/pure_interface.py
@@ -38,7 +38,7 @@ else:
             super(abstractstaticmethod, self).__init__(callable)
 
 
-__version__ = '3.2.0'
+__version__ = '3.3.0'
 
 
 is_development = not hasattr(sys, 'frozen')
@@ -616,6 +616,14 @@ class PureInterface(ABC):
             adapted = cls.interface_only(adapted)
         return adapted
 
+    @classmethod
+    def optional_adapt(cls, obj, allow_implicit=False, interface_only=None):
+        # type: (Type[PI], Any, bool, Optional[bool]) -> Optional[PI]
+        """ Adapt obj to to_interface or return None if adaption fails """
+        if obj is None:
+            return None
+        return cls.adapt(obj, allow_implicit=allow_implicit, interface_only=interface_only)
+
     @classmethod
     def adapt_or_none(cls, obj, allow_implicit=False, interface_only=None):
         # type: (Type[PI], Any, bool, Optional[bool]) -> Optional[PI]
diff --git a/setup.py b/setup.py
index af5d976..e4ef140 100644
--- a/setup.py
+++ b/setup.py
@@ -3,7 +3,7 @@ from setuptools import setup
 
 setup(
     name='pure_interface',
-    version='3.2.0',
+    version='3.3.0',
     py_modules=['pure_interface', 'pure_contracts'],
     url='https://github.com/aranzgeo/pure_interface',
     install_requires=['six', 'typing'],

Add optional_adapt() function
This idiom is quite common
`obj = Interface.adapt(obj) if obj is not None else None`

create `optional_adapt(obj)` to replace it.

`optional_adapt(obj)` should adapt obj if it is not None.


diff --git a/tests/test_adaption.py b/tests/test_adaption.py
index d9541cc..5611957 100644
--- a/tests/test_adaption.py
+++ b/tests/test_adaption.py
@@ -4,6 +4,7 @@ from __future__ import absolute_import, division, print_function, unicode_litera
 import pure_interface
 
 import unittest
+import mock
 
 
 class ISpeaker(pure_interface.PureInterface):
@@ -310,3 +311,15 @@ class TestAdaptionToInterfaceOnly(unittest.TestCase):
         a = IA.adapt_or_none(4, interface_only=False)
         self.assertIsInstance(a, IntToA)
 
+    def test_optional_adapt(self):
+        a_speaker = Speaker()
+        allow = object()
+        interface_only = object()
+        with mock.patch('pure_interface.PureInterface.adapt') as adapt:
+            # act
+            s = ISpeaker.optional_adapt(a_speaker, allow_implicit=allow, interface_only=interface_only)
+            none = ISpeaker.optional_adapt(None, allow_implicit=allow, interface_only=interface_only)
+            # assert
+            adapt.assert_called_once_with(a_speaker, allow_implicit=allow, interface_only=interface_only)
+            self.assertIs(s, adapt.return_value)
+            self.assertIsNone(none, 'optional_adapt(None) did not return None')

pip install -e .[dev]
parse_log_pytest
requirements.txt
pytest mock pycontracts
apt-get update apt-get install -y gcc
3.9
requirements/base.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
decorator==5.2.1
exceptiongroup==1.2.2
future==1.0.0
iniconfig==2.1.0
mock==5.2.0
packaging==24.2
pluggy==1.5.0
-e git+https://github.com/seequent/pure_interface.git@3c26b3ac10ed81fbd49eab936142498269d3b810#egg=pure_interface
PyContracts==1.8.12
pyparsing==3.2.3
pytest==8.3.5
six==1.17.0
tomli==2.2.1
typing==3.7.4.3

name: pure_interface
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - pip=25.0=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - decorator==5.2.1
      - exceptiongroup==1.2.2
      - future==1.0.0
      - iniconfig==2.1.0
      - mock==5.2.0
      - packaging==24.2
      - pluggy==1.5.0
      - pycontracts==1.8.12
      - pyparsing==3.2.3
      - pytest==8.3.5
      - six==1.17.0
      - tomli==2.2.1
      - typing==3.7.4.3
prefix: /opt/conda/envs/pure_interface

tests/test_adaption.py::TestAdaptionToInterfaceOnly::test_optional_adapt
tests/test_adaption.py::TestAdaption::test_adapt_on_class_works tests/test_adaption.py::TestAdaption::test_adapt_to_interface_or_none tests/test_adaption.py::TestAdaption::test_adapt_to_interface_raises tests/test_adaption.py::TestAdaption::test_adapter_call_check tests/test_adaption.py::TestAdaption::test_adapter_check tests/test_adaption.py::TestAdaption::test_adaption_passes tests/test_adaption.py::TestAdaption::test_callable_adapter_passes tests/test_adaption.py::TestAdaption::test_filter_adapt tests/test_adaption.py::TestAdaption::test_from_type_check tests/test_adaption.py::TestAdaption::test_implicit_adapter tests/test_adaption.py::TestAdaption::test_implicit_filter_adapt tests/test_adaption.py::TestAdaption::test_no_interface_on_class_raises tests/test_adaption.py::TestAdaption::test_to_interface_check tests/test_adaption.py::TestAdaptionToInterfaceOnly::test_adapt_to_interface_or_none tests/test_adaption.py::TestAdaptionToInterfaceOnly::test_adapter_preference tests/test_adaption.py::TestAdaptionToInterfaceOnly::test_adapter_to_sub_interface_used tests/test_adaption.py::TestAdaptionToInterfaceOnly::test_callable_adapter_passes tests/test_adaption.py::TestAdaptionToInterfaceOnly::test_filter_adapt tests/test_adaption.py::TestAdaptionToInterfaceOnly::test_implicit_adapter_passes tests/test_adaption.py::TestAdaptionToInterfaceOnly::test_implicit_filter_adapt tests/test_adaption.py::TestAdaptionToInterfaceOnly::test_wrapping_works tests/test_adaption.py::TestAdaptionToInterfaceOnly::test_wrapping_works2
README.rst pure_interface.py setup.py
README.rst pure_interface.py setup.py