diff --git a/Control/AthToolSupport/AsgServices/AsgServices/AsgServiceConfig.h b/Control/AthToolSupport/AsgServices/AsgServices/AsgServiceConfig.h
index 1a1ac37804604a5048d97d178dc70757be7e0ada..62be805d08d09d189f822b9b0c450e76e2379d07 100644
--- a/Control/AthToolSupport/AsgServices/AsgServices/AsgServiceConfig.h
+++ b/Control/AthToolSupport/AsgServices/AsgServices/AsgServiceConfig.h
@@ -45,6 +45,15 @@ namespace asg
     explicit AsgServiceConfig (const std::string& val_typeAndName);
 
 
+    /// \brief Virtual destructor, to make PyROOT happy
+    ///
+    /// Without it ROOT 6.22+ does not allow Python classes to inherit from this
+    /// type.
+    ///
+  public:
+    virtual ~AsgServiceConfig () = default;
+
+
     /// \brief make a service with the given configuration
     ///
     /// \warn This is mostly meant as a low level interface to be used
diff --git a/Control/AthToolSupport/AsgServices/python/AsgServiceConfig.py b/Control/AthToolSupport/AsgServices/python/AsgServiceConfig.py
new file mode 100644
index 0000000000000000000000000000000000000000..4343c1e2b997deca8cd369ecb5bccfbcb953d74f
--- /dev/null
+++ b/Control/AthToolSupport/AsgServices/python/AsgServiceConfig.py
@@ -0,0 +1,475 @@
+# Copyright (C) 2002-2021 CERN for the benefit of the ATLAS collaboration
+
+# Import(s):
+import ROOT
+import unittest
+import copy
+
+# This file is (mostly) a copy of AnaAlgorithmConfig.py, with
+# algorithms replaced with services.  Ideally those two classes should
+# be merged at some point, and the public tools be added via
+# AsgToolConfig.  However, given that this class inherits from a
+# type-specific Config class that is (probably) a non-trivial change
+# to the class, and postponed to a future update.  Nils Krumnack 19
+# Mar 21
+
+
+class AsgServiceConfig( ROOT.asg.AsgServiceConfig ):
+    """Standalone Analysis Service Configuration
+
+    This class is used to describe the configuration of an analysis service
+    (a C++ class inheriting from asg::AsgService) in Python. It behaves
+    similar to an Athena configurable, but is implemented in a much simpler
+    way.
+
+    An example of using it in configuring an EventLoop job could look like:
+
+       job = ROOT.asg.Job()
+       ...
+       from AsgServices.AsgServiceConfig import AsgServiceConfig
+       service = AsgServiceConfig( "asg::UnitTestService1/TestService",
+                                 property = 1.23 )
+       service.string_property = "Foo"
+       job.servicesAdd( service )
+
+    Note that the python code doesn't know what properties can actually be set
+    on any given C++ service. Any mistake made in the Python configuration
+    (apart from syntax errors) is only discovered while initialising the
+    analysis job.
+    """
+
+    # Class/static variable(s):
+    printHeaderWidth = 80
+    printHeaderPre   = 3
+
+    def __init__( self, typeAndName, **kwargs ):
+        """Constructor for an service configuration object
+
+        Keyword arguments:
+          typeAndName -- The type/instance name of the service
+
+        Note that you can pass (initial) properties to the constructor like:
+
+           service = AsgServiceConfig( "asg::UnitTestService1/TestService",
+                                     property = 1.23 )
+        """
+
+        # Call the base class's constructor. Use the default constructor instead
+        # of the one receiving the type and name, to avoid ROOT-10872.
+        super( AsgServiceConfig, self ).__init__()
+        self.setTypeAndName( typeAndName )
+
+        # Initialise the properties of the service:
+        self._props = {}
+
+        # Set the properties on the object:
+        for key, value in kwargs.items():
+            self.setPropertyFromString( key, stringPropValue( value ) )
+            self._props[ key ] = copy.deepcopy( value )
+            pass
+
+        pass
+
+    def getName( self ):
+        """Get the instance name of the service
+
+        This is for compatibility with the getName() function of Athena
+        configurables.
+        """
+
+        return self.name()
+
+    def getType( self ):
+        """Get the type name of the service
+
+        This is for compatibility with the getType() function of Athena
+        configurables.
+        """
+
+        return self.type()
+
+    def __getattr__( self, name ):
+        """Get a previously set property value from the configuration
+
+        This function allows us to retrieve the value of a property that was
+        already set for the service, to possibly use it in some configuration
+        decisions in the Python code itself.
+
+        Keyword arguments:
+          name -- The name of the property
+        """
+
+        # Fail if the property was not (yet) set:
+        if not name in self._props:
+            raise AttributeError( 'Property \'%s\' was not set on \'%s/%s\'' %
+                                  ( name, self.type(), self.name() ) )
+
+        # Return the property value:
+        return self._props[ name ]
+
+    def __setattr__( self, key, value ):
+        """Set an service property on an existing configuration object
+
+        This function allows us to set/override properties on an service
+        configuration object. Allowing for the following syntax:
+
+           service = ...
+           service.IntProperty = 66
+           service.FloatProperty = 3.141592
+           service.StringProperty = "Foo"
+
+        Keyword arguments:
+          key   -- The key/name of the property
+          value -- The value to set for the property
+        """
+
+        # Private variables should be set directly:
+        if key[ 0 ] == '_':
+            return super( AsgServiceConfig, self ).__setattr__( key, value )
+
+        # Set the property, and remember its value:
+        super( AsgServiceConfig,
+               self ).setPropertyFromString( key, stringPropValue( value ) )
+        self._props[ key ] = copy.deepcopy( value )
+        pass
+
+    def __eq__( self, other ):
+        """Check for equality with another object
+
+        The implementation of this is very simple. We only check that the type
+        and the name of the services would match.
+        """
+
+        # First check that the other object is also an AsgServiceConfig one:
+        if not isinstance( other, AsgServiceConfig ):
+            return False
+
+        # Now check whether the type and the name of the services agree:
+        return ( ( self.type() == other.type() ) and
+                 ( self.name() == other.name() ) )
+
+    def __ne__( self, other ):
+        """Check for an inequality with another object
+
+        This is just defined to make the '!=' operator of Python behave
+        consistently with the '==' operator for such objects.
+        """
+        return not self.__eq__( other )
+
+    def __str__( self ):
+        """Print the service configuration in a user friendly way
+
+        This is just to help with debugging configurations, allowing
+        the user to get a nice printout of their job configuration.
+        """
+
+        name = 'Service %s/%s' % ( self.type(), self.name() )
+        result = AsgServiceConfig._printHeader( name )
+        result += '\n'
+        for key, value in sorted( self._props.items() ):
+            if isinstance( value, str ):
+                printedValue = "'%s'" % value
+            else:
+                printedValue = value
+                pass
+            result += "|- %s: %s\n" % ( key, indentBy( printedValue, "| " ) )
+            pass
+        result += AsgServiceConfig._printFooter( name )
+        return result
+
+    def addPrivateTool( self, name, type ):
+        """Create a private tool for the service
+
+        This function is used in 'standalone' mode to declare a private tool
+        for the service, or a private tool for an already declared private
+        tool.
+
+        Can be used like:
+          config.addPrivateTool( 'tool1', 'ToolType1' )
+          config.addPrivateTool( 'tool1.tool2', 'ToolType2' )
+
+        Keyword arguments:
+          name -- The full name of the private tool
+          type -- The C++ type of the private tool
+        """
+
+        # First off, tell the C++ code what to do.
+        self.createPrivateTool( name, type ).ignore()
+
+        # And now set up the Python object that will take care of setting
+        # properties on this tool.
+
+        # Tokenize the tool's name. In case it is a subtool of a tool, or
+        # something possibly even deeper.
+        toolNames = name.split( '.' )
+
+        # Look up the component that we need to set up the private tool on.
+        component = self
+        for tname in toolNames[ 0 : -1 ]:
+            component = getattr( component, tname )
+            pass
+
+        # Check that the component doesn't have such a (tool) property yet.
+        if hasattr( component, toolNames[ -1 ] ):
+            raise RuntimeError( "Tool with name '%s' already exists" % name )
+            pass
+
+        # Now set up a smart object as a property on that component.
+        component._props[ toolNames[ -1 ] ] = PrivateToolConfig( self, name,
+                                                                 type )
+        pass
+
+    @staticmethod
+    def _printHeader( title ):
+        """Produce a nice header when printing the configuration
+
+        This function is used for printing the header of both services
+        and tools.
+
+        Keyword arguments:
+          indentString -- String used as indentation
+          title        -- The title of the service/tool
+        """
+
+        preLength = AsgServiceConfig.printHeaderPre
+        postLength = AsgServiceConfig.printHeaderWidth - 3 - preLength - \
+            len( title )
+        return '/%s %s %s' % ( preLength * '*', title, postLength * '*' )
+
+    @staticmethod
+    def _printFooter( title ):
+        """Produce a nice footer when printing the configuration
+
+        This function is used for printing the footer of both services
+        and tools.
+
+        Keyword arguments:
+          indentString -- String used as indentation
+          title        -- The title of the service/tool
+        """
+
+        preLength = AsgServiceConfig.printHeaderPre
+        postLength = AsgServiceConfig.printHeaderWidth - 12 - preLength - \
+            len( title )
+        return '\\%s (End of %s) %s' % ( preLength * '-', title,
+                                         postLength * '-' )
+
+    pass
+
+
+class PrivateToolConfig( object ):
+    """Standalone Private Tool Configuration
+
+    This class is used to mimic the behaviour of Athena tool configurable
+    classes. To be able to set the properties of private tools used by
+    dual-use services in a way that's valid for both Athena and EventLoop.
+    """
+
+    def __init__( self, service, prefix, type ):
+        """Constructor for an private tool configuration object
+        """
+
+        self._service = service
+        self._prefix = prefix
+        self._type = type
+        self._props = {}
+
+        pass
+
+    def __getattr__( self, name ):
+        """Get a previously set property value from the configuration
+
+        This function allows us to retrieve the value of a tool property that
+        was already set for an service's private tool, to possibly use it in
+        some configuration decisions in the Python code itself.
+
+        Keyword arguments:
+          name -- The name of the property
+        """
+
+        # Fail if the property was not (yet) set:
+        if not name in self._props:
+            raise AttributeError( 'Property "%s" was not set on "%s/%s.%s"' %
+                                  ( name, self._service.type(),
+                                    self._service.name(), self._prefix ) )
+
+        # Return the property value:
+        return self._props[ name ]
+
+    def __setattr__( self, key, value ):
+        """Set a tool property on an existing configuration object
+
+        This function allows us to set/override properties on a private tool
+        of an service configuration object. Allowing for the following syntax:
+
+           service = ...
+           service.Tool.IntProperty = 66
+           service.Tool.FloatProperty = 3.141592
+           service.Tool.StringProperty = "Foo"
+
+        Keyword arguments:
+          key   -- The key/name of the property
+          value -- The value to set for the property
+        """
+
+        # Private variables should be set directly:
+        if key[ 0 ] == '_':
+            return super( PrivateToolConfig, self ).__setattr__( key, value )
+
+        # Construct the full name, used in the C++ code:
+        fullName = self._prefix + "." + key
+
+        # Set the property, and remember its value:
+        self._service.setPropertyFromString( fullName,
+                                               stringPropValue( value ) )
+        self._props[ key ] = copy.deepcopy( value )
+        pass
+
+    def __str__( self ):
+        """Print the private tool configuration in a user friendly way
+
+        This is just to help with debugging configurations, allowing
+        the user to get a nice printout of their job configuration.
+        """
+
+        name = 'Private Tool %s/%s' % ( self._type, self._prefix )
+        result = ' \n'
+        result += AsgServiceConfig._printHeader( name )
+        result += '\n'
+        for key, value in sorted( self._props.items() ):
+            if isinstance( value, str ):
+                printedValue = "'%s'" % value
+            else:
+                printedValue = value
+                pass
+            result += "|- %s: %s\n" % ( key, indentBy( printedValue, "| " ) )
+            pass
+        result += AsgServiceConfig._printFooter( name )
+        return result
+
+    pass
+
+
+def stringPropValue( value ):
+    """Helper function producing a string property value"""
+
+    stringValue = str( value )
+    if isinstance( value, bool ):
+        stringValue = str( int( value ) )
+        pass
+    return stringValue
+
+
+def indentBy( propValue, indent ):
+    """Helper function used in the configuration printout"""
+
+    stringValue = str( propValue )
+    result = ""
+    for stringLine in stringValue.split( '\n' ):
+        if len( result ):
+            result += "\n" + indent
+            pass
+        result += stringLine
+        pass
+    return result
+
+
+#
+# Declare some unit tests for the code
+#
+
+## Test case for the service type/name handling
+class TestServiceTypeAndName( unittest.TestCase ):
+
+    ## Test that the type and name are set correctly when using a single
+    #  argument
+    def test_singletypename( self ):
+        config1 = AsgServiceConfig( "TypeName" )
+        self.assertEqual( config1.type(), "TypeName" )
+        self.assertEqual( config1.name(), "TypeName" )
+        config2 = AsgServiceConfig( "NS::SomeType" )
+        self.assertEqual( config2.type(), "NS::SomeType" )
+        self.assertEqual( config2.name(), "NS::SomeType" )
+        pass
+
+    ## Test that specifying the type and name separately in the same string
+    #  works as expected.
+    def test_typeandname( self ):
+        config1 = AsgServiceConfig( "TypeName/InstanceName" )
+        self.assertEqual( config1.type(), "TypeName" )
+        self.assertEqual( config1.name(), "InstanceName" )
+        config2 = AsgServiceConfig( "NS::SomeType/Instance" )
+        self.assertEqual( config2.type(), "NS::SomeType" )
+        self.assertEqual( config2.name(), "Instance" )
+        pass
+
+## Test case for the service property handling
+class TestServiceProperties( unittest.TestCase ):
+
+    ## Common setup for the tests
+    def setUp( self ):
+        self.config = AsgServiceConfig( "Type/Name" )
+        pass
+
+    ## Test that properties that got set, can be read back
+    def test_propaccess( self ):
+        self.config.Prop1 = "Value1"
+        self.config.Prop2 = [ "Value2" ]
+        self.assertEqual( self.config.Prop1, "Value1" )
+        self.assertEqual( self.config.Prop2, [ "Value2" ] )
+        self.assertNotEqual( self.config.Prop1, "Foo" )
+        self.assertNotEqual( self.config.Prop2, "Value2" )
+        pass
+
+    ## Test that an unset property can't be accessed
+    def test_nonexistentprop( self ):
+        with self.assertRaises( AttributeError ):
+            value = self.config.Prop3
+            pass
+        pass
+
+## Test case for using private tools
+class TestServicePrivateTool( unittest.TestCase ):
+
+    ## Set up the main service object to test
+    def setUp( self ):
+        self.config = AsgServiceConfig( "ServiceType/ServiceName" )
+        pass
+
+    ## Test setting up and using one private tool
+    def test_privatetool( self ):
+        self.config.addPrivateTool( "Tool1", "ToolType1" )
+        self.config.Tool1.Prop1 = "Value1"
+        self.config.Tool1.Prop2 = [ 1, 2, 3 ]
+        self.assertEqual( self.config.Tool1.Prop1, "Value1" )
+        self.assertEqual( self.config.Tool1.Prop2, [ 1, 2, 3 ] )
+        pass
+
+    ## Test setting up and using a private tool of a private tool
+    def test_privatetoolofprivatetool( self ):
+        self.config.addPrivateTool( "Tool1", "ToolType1" )
+        self.config.addPrivateTool( "Tool1.Tool2", "ToolType2" )
+        self.config.Tool1.Tool2.Prop3 = "Foo"
+        self.config.Tool1.Tool2.Prop4 = [ "Bar" ]
+        self.assertEqual( self.config.Tool1.Tool2.Prop3, "Foo" )
+        self.assertEqual( self.config.Tool1.Tool2.Prop4, [ "Bar" ] )
+        pass
+
+    ## Test that unset properties on the tools can't be used
+    def test_nonexistentprop( self ):
+        self.config.addPrivateTool( "Tool1", "ToolType1" )
+        with self.assertRaises( AttributeError ):
+            value = self.config.Tool1.BadProp
+            pass
+        self.config.addPrivateTool( "Tool1.Tool2", "ToolType2" )
+        with self.assertRaises( AttributeError ):
+            value = self.config.Tool1.Tool2.BadProp
+            pass
+        pass
+
+    ## Test that private tools can't be set up on not-yet-declared tools
+    def test_nonexistenttool( self ):
+        with self.assertRaises( AttributeError ):
+            self.config.addPrivateTool( "BadTool.Tool4", "BadToolType" )
+            pass
+        pass
diff --git a/Control/AthToolSupport/AsgServices/python/__init__.py b/Control/AthToolSupport/AsgServices/python/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..8afb1e2299ab1e3afe02659e2bed277cce48062b
--- /dev/null
+++ b/Control/AthToolSupport/AsgServices/python/__init__.py
@@ -0,0 +1,3 @@
+# Copyright (C) 2002-2021 CERN for the benefit of the ATLAS collaboration
+
+__version__ = '1.0.0'