What is the correct way of duplicating base class '__init__' signature in Python?

This answer is a description of how I figured out a reasonable solution, so please bear with me, or just scroll to the TL;DR'd decorator at the end.

Let's start by looking at what help is and does. help is added to the builtin namespace by site. The default implementation on my Ubuntu machine redirects everything to pydoc.help. This in turn uses inspect to get signatures and descriptions. You are only interested in functions, and more specifically __init__. Also, you only care about the signature, not the rest of the docs. This should make things simpler.

We can safely assume that the signature you see in help/pydoc is generated by inspect.signature. Looking at the source code of that function for Python 3.8.2, and tracing through inspect.Signature.from_callable -> inspect._signature_from_callable -> Line 2246, we see a possible solution.

The gist of it is is that if a function object has a __signature__ attribute, and that attribute is an instance of inspect.Signature, it will be used as the signature of the function, without recomputing it from the normal inspection of the __code__ and __annotation__ objects.

Another point in our favor is that functions are first-class objects with a __dict__ attribute that can have arbitrary keys assigned to it. Assigning __signature__ to your function will not affect its execution, since it is only used for inspection. The actual runtime signature is determined in the __code__ object through attributes like co_argcount, co_kwonlyargcount, co_varnames, etc.

You can therefore just do:

import inspect

Child2.__init__.__signature__ = inspect.signature(Base.__init__)

The result:

>>> help(Child1)
Help on class Child1 in module __main__:

class Child1(Base)
 |  Child1(arg1, arg2)
 |  
 |  Method resolution order:
 |      Child1
 |      Base
 |      builtins.object
 |  
 |  Methods defined here:
 |  
 |  foo(self)
 |  
 |  ----------------------------------------------------------------------
 |  Methods inherited from Base:
 |  
 |  __init__(self, arg1, arg2)
 |      Initialize self.  See help(type(self)) for accurate signature.
 |  
 |  ----------------------------------------------------------------------
 |  Data descriptors inherited from Base:
 |  
 |  __dict__
 |      dictionary for instance variables (if defined)
 |  
 |  __weakref__
 |      list of weak references to the object (if defined)

>>> help(Child2)
Help on class Child2 in module __main__:

class Child2(Base)
 |  Child2(arg1, arg2)
 |  
 |  Method resolution order:
 |      Child2
 |      Base
 |      builtins.object
 |  
 |  Methods defined here:
 |  
 |  __init__(self, arg1, arg2)
 |      Initialize self.  See help(type(self)) for accurate signature.
 |  
 |  foo(self)
 |  
 |  ----------------------------------------------------------------------
 |  Data descriptors inherited from Base:
 |  
 |  __dict__
 |      dictionary for instance variables (if defined)
 |  
 |  __weakref__
 |      list of weak references to the object (if defined)

Both constructors continue functioning as usual aside from that.

Since this does not modify the code object or even the annotations, the change is unlikely to affect anything with regards to the operation of the function.

TL;DR

Here is a decorator you can use to copy over function signatures without interfering with the function in any other way:

import inspect

def copy_signature(base):
    def decorator(func):
        func.__signature__ = inspect.signature(base)
        return func
    return decorator

And you could rewrite Child2 as

class Child2:

    @copy_signature(Base.__init__)
    def __init__(self, *args, **kwargs):
        ...