diff --git a/Tools/PyUtils/CMakeLists.txt b/Tools/PyUtils/CMakeLists.txt
index cdeb97162f24e768bd38d5d82bd509a0cdc96b29..48f93dfc0068f0755a0c989f6b1fb60232e740a7 100644
--- a/Tools/PyUtils/CMakeLists.txt
+++ b/Tools/PyUtils/CMakeLists.txt
@@ -60,5 +60,9 @@ atlas_add_test( RootUtils
    EXTRA_PATTERNS "Ran 1 test in |CheckABICompatibility|standard library" )
 
 atlas_add_test( flake8_OutputLevel
-   SCRIPT flake8 --enable-extensions=ATL100 --ignore=E,F,W --stdin-display-name=flake8_OutputLevel.py
-   --exit-zero --isolated - < ${CMAKE_CURRENT_SOURCE_DIR}/test/flake8_OutputLevel.py )
+   SCRIPT flake8 --enable-extensions=ATL900 --select=ATL --stdin-display-name=flake8_OutputLevel.py
+   --exit-zero --isolated - < ${CMAKE_CURRENT_SOURCE_DIR}/python/flake8_atlas/test/flake8_OutputLevel.py )
+
+atlas_add_test( flake8_logging
+   SCRIPT flake8 --enable-extensions=ATL901 --select=ATL --stdin-display-name=flake8_logging.py
+   --exit-zero --isolated - < ${CMAKE_CURRENT_SOURCE_DIR}/python/flake8_atlas/test/flake8_logging.py )
diff --git a/Tools/PyUtils/python/flake8_atlas/README.md b/Tools/PyUtils/python/flake8_atlas/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..1baf12ccce746328e6493063687581ec86e7f36a
--- /dev/null
+++ b/Tools/PyUtils/python/flake8_atlas/README.md
@@ -0,0 +1,17 @@
+# ATLAS plugins for flake8
+
+This module contains ATLAS-specific `flake8_atlas` plugins. It is built by default in the
+athena release and available after running `asetup`. To verify if the plugin is found
+run:
+```
+> flake8 --version
+3.6.0 (flake8_atlas: 1.0, mccabe: 0.6.1, pycodestyle: 2.4.0, pyflakes: 2.0.0) CPython 2.7.15 on Linux
+```
+
+Plugins with codes `>=ATL900` are disabled by default. To enable them use
+```
+flake8 --enable-extension=ATL901
+```
+In addition to ATLAS specific plugins most of the `python23.py` plugins from
+the [hacking](https://github.com/openstack-dev/hacking) plugin by OpenStack
+were imported (and some modified).
\ No newline at end of file
diff --git a/Tools/PyUtils/python/flake8_atlas/checks.py b/Tools/PyUtils/python/flake8_atlas/checks.py
index 6d5a7a0df6f808cdc17937c9efffd28bf60cfdfb..49a26cd83c60903d6703964e78ffccc1f9654a14 100644
--- a/Tools/PyUtils/python/flake8_atlas/checks.py
+++ b/Tools/PyUtils/python/flake8_atlas/checks.py
@@ -4,15 +4,38 @@
 Documentation: http://flake8.pycqa.org/en/latest/plugin-development
 """
 
-from utils import flake8_atlas, off_by_default
+from PyUtils.flake8_atlas import utils
 import ast
+import re
 
-@flake8_atlas
-@off_by_default
-class OutputLevel(object):
-   """Check if an explicit OutputLevel is set"""
+# Inspired by: https://github.com/openstack-dev/hacking/blob/master/hacking/checks/other.py
+RE_LOGGING = re.compile(r".*\.(?:error|warn|warning|info|debug)"
+                        r"\([^,]*(%)[^,]*[,)]")
+@utils.flake8_atlas
+def delayed_string_interpolation(logical_line):
+    r"""String interpolation should be delayed at logging calls.
+    ATL101: log.debug('Example: %s' % 'bad')
+    Okay:   log.debug('Example: %s', 'good')
+    """
+    msg = ("ATL100: use lazy string formatting in logging calls (',' instead of '%')")
+
+    m = RE_LOGGING.match(logical_line)
+    if m is not None:
+       col = m.start(1)
+       yield (col-1, msg)
+
+
+######################################################################
+# ATL9xy: Specialized plugins (disabled by default)
+######################################################################
 
-   code = ('ATL100: Do not assign an explicit OutputLevel', 'ATL100')
+@utils.flake8_atlas
+@utils.off_by_default
+class OutputLevel(object):
+   """Check if an explicit OutputLevel is set
+   ATL900: myalg.OutputLevel = DEBUG
+   """
+   msg = ('ATL900: Do not assign an explicit OutputLevel', 'ATL900')
 
    def __init__(self, tree):
       self.tree = tree
@@ -24,10 +47,21 @@ class OutputLevel(object):
          if isinstance(node, ast.Assign):
             for t in node.targets:
                if isinstance(t,ast.Attribute) and t.attr=='OutputLevel':
-                  yield (node.lineno, node.col_offset) + self.code
+                  yield (node.lineno, node.col_offset) + self.msg
 
          # Find: setattr(c,'OutputLevel',DEBUG)
          if isinstance(node, ast.Call) and isinstance(node.func, ast.Name) and node.func.id=='setattr':
             a = node.args[1]
             if isinstance(a, ast.Str) and a.s=='OutputLevel':
-               yield (node.lineno, node.col_offset) + self.code
+               yield (node.lineno, node.col_offset) + self.msg
+
+
+RE_PRINT = re.compile(r"\bprint")
+@utils.flake8_atlas
+@utils.off_by_default
+def print_for_logging(logical_line):
+    """Check for occurences of plain 'print'"""
+
+    for match in RE_PRINT.finditer(logical_line):
+        yield match.start(0), (
+            "ATL901: use 'AthenaCommon.Logging' instead of 'print'")
diff --git a/Tools/PyUtils/python/flake8_atlas/python23.py b/Tools/PyUtils/python/flake8_atlas/python23.py
new file mode 100644
index 0000000000000000000000000000000000000000..42d804630816297fe90ea207fe363a85aa39156f
--- /dev/null
+++ b/Tools/PyUtils/python/flake8_atlas/python23.py
@@ -0,0 +1,224 @@
+# Copyright (C) 2002-2019 CERN for the benefit of the ATLAS collaboration
+#
+# This is a modified version of the 'hacking' flake8 plugin:
+#   https://github.com/openstack-dev/hacking
+#
+# The unmodified original checks are kept with the "hacking_" prefix.
+# For modified versions (including bugfixes) the prefix was removed.
+#
+
+# Original licence:
+# -----------------
+#  Licensed under the Apache License, Version 2.0 (the "License"); you may
+#  not use this file except in compliance with the License. You may obtain
+#  a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#  WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#  License for the specific language governing permissions and limitations
+#  under the License.
+
+from PyUtils.flake8_atlas import utils
+import re
+import tokenize
+
+
+def import_normalize(line):
+    # convert "from x import y" to "import x.y"
+    # handle "from x import y as z" to "import x.y as z"
+    split_line = line.split()
+    if ("import" in line and line.startswith("from ") and "," not in line and
+            split_line[2] == "import" and split_line[3] != "*" and
+            split_line[1] != "__future__" and
+            (len(split_line) == 4 or
+             (len(split_line) == 6 and split_line[4] == "as"))):
+        return "import %s.%s" % (split_line[1], split_line[3])
+    else:
+       return line
+
+
+#def hacking_python3x_except_compatible(logical_line, noqa):
+#Deleted as this is covered by pycodestyle E722
+
+RE_OCTAL = re.compile(r"0+([1-9]\d*)")
+
+@utils.flake8_atlas
+def hacking_python3x_octal_literals(logical_line, tokens, noqa):
+    r"""Check for octal literals in Python 3.x compatible form.
+
+    As of Python 3.x, the construct "0755" has been removed.
+    Use "0o755" instead".
+
+    Okay:   f(0o755)
+    Okay:   f(0755)
+    Okay:   f(755)
+    Okay:   f(0)
+    Okay:   f(000)
+    Okay:   MiB = 1.0415
+    ATL232: f(0755)
+    Okay:   f(0755)  # noqa
+    """
+    if noqa:
+        return
+
+    for token_type, text, _, _, _ in tokens:
+        if token_type == tokenize.NUMBER:
+            match = RE_OCTAL.match(text)
+            if match:
+                yield 0, ("ATL232: Python 3.x incompatible octal %s should be "
+                          "written as 0o%s " %
+                          (match.group(0)[1:], match.group(1)))
+
+
+RE_PRINT = re.compile(r"\bprint(?:$|\s+[^\(])")
+
+@utils.flake8_atlas
+def hacking_python3x_print_function(logical_line, noqa):
+    r"""Check that all print occurrences look like print functions.
+
+    Check that all occurrences of print look like functions, not
+    print operator. As of Python 3.x, the print operator has
+    been removed.
+
+    Okay:   print(msg)
+    Okay:   print (msg)
+    Okay:   print msg  # noqa
+    Okay:   print()
+    ATL233: print msg
+    ATL233: print >>sys.stderr, "hello"
+    ATL233: print msg,
+    ATL233: print
+    """
+    if noqa:
+        return
+    for match in RE_PRINT.finditer(logical_line):
+        yield match.start(0), (
+            "ATL233: Python 3.x incompatible use of print operator")
+
+
+@utils.flake8_atlas
+def hacking_no_assert_equals(logical_line, tokens, noqa):
+    r"""assert(Not)Equals() is deprecated, use assert(Not)Equal instead.
+
+    Okay:   self.assertEqual(0, 0)
+    Okay:   self.assertNotEqual(0, 1)
+    ATL234: self.assertEquals(0, 0)
+    ATL234: self.assertNotEquals(0, 1)
+    Okay:   self.assertEquals(0, 0)  # noqa
+    Okay:   self.assertNotEquals(0, 1)  # noqa
+    """
+
+    if noqa:
+        return
+    for token_type, text, start_index, _, _ in tokens:
+
+        if token_type == tokenize.NAME:
+            if text == "assertEquals" or text == "assertNotEquals":
+                yield (start_index[1],
+                       "ATL234: %s is deprecated, use %s" % (text, text[:-1]))
+
+
+@utils.flake8_atlas
+def hacking_no_assert_underscore(logical_line, tokens, noqa):
+    r"""assert_() is deprecated, use assertTrue instead.
+
+    Okay: self.assertTrue(foo)
+    ATL235: self.assert_(foo)
+    Okay: self.assert_(foo)  # noqa
+    """
+    if noqa:
+        return
+    for token_type, text, start_index, _, _ in tokens:
+
+        if token_type == tokenize.NAME and text == "assert_":
+            yield (
+                start_index[1],
+                "ATL235: assert_ is deprecated, use assertTrue")
+
+
+@utils.flake8_atlas
+def hacking_python3x_metaclass(logical_line, noqa):
+    r"""Check for metaclass to be Python 3.x compatible.
+
+    Okay:   @six.add_metaclass(Meta)\nclass Foo(object):\n    pass
+    Okay:   @six.with_metaclass(Meta)\nclass Foo(object):\n    pass
+    Okay:   class Foo(object):\n    '''docstring\n\n    __metaclass__ = Meta\n'''
+    ATL236: class Foo(object):\n    __metaclass__ = Meta
+    ATL236: class Foo(object):\n    foo=bar\n    __metaclass__ = Meta
+    ATL236: class Foo(object):\n    '''docstr.'''\n    __metaclass__ = Meta
+    ATL236: class Foo(object):\n    __metaclass__ = \\\n        Meta
+    Okay:   class Foo(object):\n    __metaclass__ = Meta  # noqa
+    """
+    if noqa:
+        return
+    split_line = logical_line.split()
+    if(len(split_line) > 2 and split_line[0] == '__metaclass__' and
+       split_line[1] == '='):
+        yield (logical_line.find('__metaclass__'),
+               "ATL236: Python 3.x incompatible __metaclass__, "
+               "use six.add_metaclass()")
+
+
+# NOTE(guochbo): This is removed module list:
+# http://python3porting.com/stdlib.html#removed-modules
+removed_modules = [
+    'audiodev', 'Bastion', 'bsddb185', 'bsddb3',
+    'Canvas', 'cfmfile', 'cl', 'commands', 'compiler'
+    'dircache', 'dl', 'exception', 'fpformat',
+    'htmllib', 'ihooks', 'imageop', 'imputil'
+    'linuxaudiodev', 'md5', 'mhlib', 'mimetools'
+    'MimeWriter', 'mimify', 'multifile', 'mutex',
+    'new', 'popen2', 'posixfile', 'pure', 'rexec'
+    'rfc822', 'sha', 'sgmllib', 'sre', 'stat'
+    'stringold', 'sunaudio' 'sv', 'test.testall',
+    'thread', 'timing', 'toaiff', 'user'
+]
+
+
+@utils.flake8_atlas
+def hacking_no_removed_module(logical_line, noqa):
+    r"""Check for removed modules in Python 3.
+
+    Examples:
+    Okay:   from os import path
+    Okay:   from os import path as p
+    Okay:   from os import (path as p)
+    Okay:   import os.path
+    ATL237: import thread
+    Okay:   import thread  # noqa
+    ATL237: import commands
+    ATL237: import md5 as std_md5
+    """
+    if noqa:
+        return
+    line = import_normalize(logical_line.strip())
+    if line and line.split()[0] == 'import':
+        module_name = line.split()[1].split('.')[0]
+        if module_name in removed_modules:
+            yield 0, ("ATL237: module %s is "
+                      "removed in Python 3" % module_name)
+
+
+RE_NEW_STYLE_CLASS = re.compile(r"^class[^(]+\(.+\):")
+
+@utils.flake8_atlas
+def no_old_style_class(logical_line, noqa):
+    r"""Check for old style classes.
+
+    Examples:
+    Okay:   class Foo(object):\n    pass
+    Okay:   class Foo(Bar, Baz):\n    pass
+    Okay:   class Foo(object, Baz):\n    pass
+    Okay:   class Foo(somefunc()):\n    pass
+    ATL238: class Bar:\n    pass
+    ATL238: class Bar():\n    pass
+    """
+    if noqa:
+        return
+    line = logical_line.replace(' ','')
+    if line.startswith("class") and not RE_NEW_STYLE_CLASS.match(line):
+       yield (0, "ATL238: old style class declaration, "
+              "use new style (inherit from `object`)")
diff --git a/Tools/PyUtils/python/flake8_atlas/setup.py b/Tools/PyUtils/python/flake8_atlas/setup.py
index 51847beed3c4c879e84115a70cfa469c0515982d..ad4c4bb09b593d493c3ecf3ced55617e7cbe3feb 100644
--- a/Tools/PyUtils/python/flake8_atlas/setup.py
+++ b/Tools/PyUtils/python/flake8_atlas/setup.py
@@ -14,7 +14,16 @@ setuptools.setup(
    description="ATLAS plugins for flake8",
    entry_points={
       'flake8.extension': [
-         'ATL100 = PyUtils.flake8_atlas.checks:OutputLevel'
+         'ATL100 = PyUtils.flake8_atlas.checks:delayed_string_interpolation',
+         'ATL232 = PyUtils.flake8_atlas.python23:hacking_python3x_octal_literals',
+         'ATL233 = PyUtils.flake8_atlas.python23:hacking_python3x_print_function',
+         'ATL234 = PyUtils.flake8_atlas.python23:hacking_no_assert_equals',
+         'ATL235 = PyUtils.flake8_atlas.python23:hacking_no_assert_underscore',
+         'ATL236 = PyUtils.flake8_atlas.python23:hacking_python3x_metaclass',
+         'ATL237 = PyUtils.flake8_atlas.python23:hacking_no_removed_module',
+         'ATL238 = PyUtils.flake8_atlas.python23:no_old_style_class',
+         'ATL900 = PyUtils.flake8_atlas.checks:OutputLevel',
+         'ATL901 = PyUtils.flake8_atlas.checks:print_for_logging',
       ],
    }
 )
diff --git a/Tools/PyUtils/test/flake8_OutputLevel.py b/Tools/PyUtils/python/flake8_atlas/test/flake8_OutputLevel.py
similarity index 100%
rename from Tools/PyUtils/test/flake8_OutputLevel.py
rename to Tools/PyUtils/python/flake8_atlas/test/flake8_OutputLevel.py
diff --git a/Tools/PyUtils/python/flake8_atlas/test/flake8_logging.py b/Tools/PyUtils/python/flake8_atlas/test/flake8_logging.py
new file mode 100644
index 0000000000000000000000000000000000000000..e4f5fe5d425d0be89b1f88c1aac94f72b20299c7
--- /dev/null
+++ b/Tools/PyUtils/python/flake8_atlas/test/flake8_logging.py
@@ -0,0 +1,24 @@
+#
+# Copyright (C) 2002-2019 CERN for the benefit of the ATLAS collaboration
+#
+# Test for logging related plugins
+
+import logging
+logging.basicConfig(level=logging.INFO)
+log = logging.getLogger()
+myfunnyloggername = log
+
+log.info('This is %s logging practice' % 'bad')
+log.info('This is %s logging practice', 'good')
+myfunnyloggername.warning('Hello %s' % 'world')
+
+log.info('This is %s logging practice: %d' % ('bad',42))
+log.info('This is %s logging practice: %d' % ('bad',42)) # noqa
+log.info('This is %s logging practice: %d', 'good', 42)
+
+print("Hello world")
+print("Hello world") # noqa
+print "Hello world"
+
+def myprint(s): pass
+myprint("Function that ends with print is OK")
diff --git a/Tools/PyUtils/python/flake8_atlas/utils.py b/Tools/PyUtils/python/flake8_atlas/utils.py
index 730c2a24edc93f4653122252bc25a13e10e3c9b0..a914d2a4669030dd27ab9a77ef9d4576489401e8 100644
--- a/Tools/PyUtils/python/flake8_atlas/utils.py
+++ b/Tools/PyUtils/python/flake8_atlas/utils.py
@@ -5,6 +5,8 @@ def flake8_atlas(f):
    """Default decorator for flake8 plugins"""
    f.name = 'flake8_atlas'
    f.version = '1.0'
+   if not hasattr(f, 'off_by_default'):
+      f.off_by_default = False
    return f
 
 def off_by_default(f):
diff --git a/Tools/PyUtils/share/flake8_OutputLevel.ref b/Tools/PyUtils/share/flake8_OutputLevel.ref
index b5aafa3f51436d8e2003dbc83d62be44a395e95c..e48d4980e4e4795ae1911a6ee313887ccc7d6366 100644
--- a/Tools/PyUtils/share/flake8_OutputLevel.ref
+++ b/Tools/PyUtils/share/flake8_OutputLevel.ref
@@ -1,3 +1,3 @@
-flake8_OutputLevel.py:10:7: ATL100: Do not assign an explicit OutputLevel
-flake8_OutputLevel.py:14:1: ATL100: Do not assign an explicit OutputLevel
-flake8_OutputLevel.py:16:1: ATL100: Do not assign an explicit OutputLevel
+flake8_OutputLevel.py:10:7: ATL900: Do not assign an explicit OutputLevel
+flake8_OutputLevel.py:14:1: ATL900: Do not assign an explicit OutputLevel
+flake8_OutputLevel.py:16:1: ATL900: Do not assign an explicit OutputLevel
diff --git a/Tools/PyUtils/share/flake8_logging.ref b/Tools/PyUtils/share/flake8_logging.ref
new file mode 100644
index 0000000000000000000000000000000000000000..7cc15218e18c06ffb3d3f0bf101120b680ffa968
--- /dev/null
+++ b/Tools/PyUtils/share/flake8_logging.ref
@@ -0,0 +1,6 @@
+flake8_logging.py:11:39: ATL100: use lazy string formatting in logging calls (',' instead of '%')
+flake8_logging.py:13:37: ATL100: use lazy string formatting in logging calls (',' instead of '%')
+flake8_logging.py:15:43: ATL100: use lazy string formatting in logging calls (',' instead of '%')
+flake8_logging.py:19:1: ATL901: use 'AthenaCommon.Logging' instead of 'print'
+flake8_logging.py:21:1: ATL233: Python 3.x incompatible use of print operator
+flake8_logging.py:21:1: ATL901: use 'AthenaCommon.Logging' instead of 'print'