diff --git a/python/core/__init__.py.in b/python/core/__init__.py.in
index 94925cf61e5..0a5de109653 100644
--- a/python/core/__init__.py.in
+++ b/python/core/__init__.py.in
@@ -37,6 +37,7 @@ from .additions.qgsgeometry import _geometryNonZero, mapping_geometry
 from .additions.qgssettings import _qgssettings_enum_value, _qgssettings_set_enum_value, _qgssettings_flag_value
 from .additions.qgstaskwrapper import QgsTaskWrapper
 from .additions.readwritecontextentercategory import ReadWriteContextEnterCategory
+from .additions.validitycheck import check
 
 # Injections into classes
 QgsFeature.__geo_interface__ = property(mapping_feature)
diff --git a/python/core/additions/validitycheck.py b/python/core/additions/validitycheck.py
new file mode 100644
index 00000000000..25fdd1586f0
--- /dev/null
+++ b/python/core/additions/validitycheck.py
@@ -0,0 +1,95 @@
+# -*- coding: utf-8 -*-
+
+"""
+***************************************************************************
+    validitycheck.py
+    ---------------------
+    Date                 : January 2019
+    Copyright            : (C) 2019 by Nyall Dawson
+    Email                : nyall dot dawson at gmail dot com
+***************************************************************************
+*                                                                         *
+*   This program is free software; you can redistribute it and/or modify  *
+*   it under the terms of the GNU General Public License as published by  *
+*   the Free Software Foundation; either version 2 of the License, or     *
+*   (at your option) any later version.                                   *
+*                                                                         *
+***************************************************************************
+"""
+from qgis._core import (
+    QgsAbstractValidityCheck,
+    QgsApplication)
+
+
+class CheckFactory:
+    """
+    Constructs QgsAbstractValidityChecks using a decorator.
+
+    To use, Python based checks should use the decorator syntax:
+
+    .. highlight:: python
+    .. code-block:: python
+        @check.register(type=QgsAbstractValidityCheck.TypeLayoutCheck)
+        def my_layout_check(context, feedback):
+            results = ...
+            return results
+
+    """
+
+    def __init__(self):
+        # unfortunately /Transfer/ annotation isn't working correct on validityCheckRegistry().addCheck(),
+        # so we manually need to store a reference to all checks we register
+        self.checks = []
+
+    def register(self, type, *args, **kwargs):
+        """
+        Implements a decorator for registering Python based checks.
+
+        :param type: check type, e.g. QgsAbstractValidityCheck.TypeLayoutCheck
+        """
+
+        def dec(f):
+            check = CheckWrapper(check_type=type, check_func=f)
+            self.checks.append(check)
+            QgsApplication.validityCheckRegistry().addCheck(check)
+
+        return dec
+
+
+class CheckWrapper(QgsAbstractValidityCheck):
+    """
+    Wrapper object used to create new validity checks from @check.
+    """
+
+    def __init__(self, check_type, check_func):
+        """
+        Initializer for CheckWrapper.
+
+        :param check_type: check type, e.g. QgsAbstractValidityCheck.TypeLayoutCheck
+        :param check_func: test function, should return a list of QgsValidityCheckResult results
+        """
+        super().__init__()
+        self._check_type = check_type
+        self._results = []
+        self._check_func = check_func
+
+    def create(self):
+        return CheckWrapper(check_type=self._check_type, check_func=self._check_func)
+
+    def id(self):
+        return self._check_func.__name__
+
+    def checkType(self):
+        return self._check_type
+
+    def prepareCheck(self, context, feedback):
+        self._results = self._check_func(context, feedback)
+        if self._results is None:
+            self._results = []
+        return True
+
+    def runCheck(self, context, feedback):
+        return self._results
+
+
+check = CheckFactory()
diff --git a/tests/src/python/test_qgsvaliditychecks.py b/tests/src/python/test_qgsvaliditychecks.py
index 3ff7c2f85aa..c974a0f3c64 100644
--- a/tests/src/python/test_qgsvaliditychecks.py
+++ b/tests/src/python/test_qgsvaliditychecks.py
@@ -19,7 +19,8 @@ from qgis.core import (QgsApplication,
                        QgsValidityCheckRegistry,
                        QgsValidityCheckResult,
                        QgsValidityCheckContext,
-                       QgsFeedback)
+                       QgsFeedback,
+                       check)
 from qgis.testing import start_app, unittest
 
 app = start_app()
@@ -53,12 +54,37 @@ class TestContext(QgsValidityCheckContext):
         return 0
 
 
+# register some checks using the decorator syntax
+@check.register(type=QgsAbstractValidityCheck.TypeLayoutCheck)
+def my_check(context, feedback):
+    assert context
+
+
+@check.register(type=QgsAbstractValidityCheck.TypeLayoutCheck)
+def my_check2(context, feedback):
+    res = QgsValidityCheckResult()
+    res.type = QgsValidityCheckResult.Warning
+    res.title = 'test'
+    res.detailedDescription = 'blah blah'
+    return [res]
+
+
 class TestQgsValidityChecks(unittest.TestCase):
 
     def testAppRegistry(self):
         # ensure there is an application instance
         self.assertIsNotNone(QgsApplication.validityCheckRegistry())
 
+    def testDecorator(self):
+        # test that checks registered using the decorator have worked
+        self.assertEqual(len(QgsApplication.validityCheckRegistry().checks()), 2)
+
+        context = TestContext()
+        feedback = QgsFeedback()
+        res = QgsApplication.validityCheckRegistry().runChecks(QgsAbstractValidityCheck.TypeLayoutCheck, context, feedback)
+        self.assertEqual(len(res), 1)
+        self.assertEqual(res[0].title, 'test')
+
     def testRegistry(self):
         registry = QgsValidityCheckRegistry()
         self.assertFalse(registry.checks())