Simple generic auto __repr__ Preface

At some point I was tired of writing &amp; supporting __repr__ methods, so I've decided to write it once and reuse everywhere in my classes.

Since I'm trying to write classes as simple as possible, most of them ends in something like

class MyClass:
     def __init__(self, param, other_param, ...):
         self.param = param
         self.other_param = other_param
         ...


i.e. initializer parameters names corresponds to fields names, so we can get initializer arguments by simply getattr'ing corresponding fields.

Main ideas are:


I want to have informative string which includes parameters involved in instance creation. For simple cases it should be possible to copy string &amp; paste in some place (e.g. REPL session) and have similar object definition with as less work as possible. This helps a lot during debugging sessions, logging, especially in failed test cases with randomly generated data.

Great examples are Counter &amp; OrderedDict from collections standard library:

&gt;&gt;&gt; from collections import Counter
&gt;&gt;&gt; Counter(range(2))
Counter({0: 1, 1: 1})
&gt;&gt;&gt; Counter(range(2)) == Counter({0: 1, 1: 1})
True
&gt;&gt;&gt; from collections import OrderedDict
&gt;&gt;&gt; OrderedDict.fromkeys(range(2))
OrderedDict([(0, None), (1, None)])
&gt;&gt;&gt; OrderedDict.fromkeys(range(2)) == OrderedDict([(0, None), (1, None)])
True

Once signature change, __repr__ should handle this automatically for simple cases like renaming/removing/changing order of parameters.


Attempt

Note: following code requires Python3.5+ due to yield from statement

import inspect
from collections import (OrderedDict,
                         abc)
from typing import (Any,
                    Callable,
                    Iterable,
                    TypeVar,
                    Union)

Domain = TypeVar('Domain')
Range = TypeVar('Range')
Map = Callable[[Domain], Range]

Constructor = Callable[..., Domain]
Initializer = Callable[..., None]


def generate_repr(constructor_or_initializer: Union[Constructor, Initializer],
                  *,
                  field_seeker: Callable[[Domain, str], Any] = getattr
                  ) -&gt; Map[Domain, str]:
    signature = inspect.signature(constructor_or_initializer)
    parameters = OrderedDict(signature.parameters)
    # remove `self`
    parameters.popitem(0)
    to_positional_argument_string = repr
    to_keyword_argument_string = '{}={!r}'.format

    def __repr__(self: Domain) -&gt; str:
        return (type(self).__qualname__
                + '(' + ', '.join(to_arguments_strings(self)) + ')')

    def to_arguments_strings(object_: Domain) -&gt; Iterable[str]:
        for parameter_name, parameter in parameters.items():
            field = field_seeker(object_, parameter_name)
            if parameter.kind == inspect._VAR_POSITIONAL:
                if isinstance(field, abc.Iterator):
                    yield '...'
                else:
                    yield from map(to_positional_argument_string, field)
            elif parameter.kind == inspect._VAR_KEYWORD:
                yield from map(to_keyword_argument_string,
                               field.keys(), field.values())
            elif parameter.kind in {inspect._POSITIONAL_ONLY,
                                    inspect._POSITIONAL_OR_KEYWORD}:
                yield to_positional_argument_string(field)
            else:
                yield to_keyword_argument_string(parameter_name, field)

    return __repr__


Test

Let's define our class like

class A:
    def __init__(self, positional, *variadic_positional, keyword_only,
                 **variadic_keyword):
        self.positional = positional
        self.variadic_positional = variadic_positional
        self.keyword_only = keyword_only
        self.variadic_keyword = variadic_keyword

    __repr__ = generate_repr(__init__)


After that

&gt;&gt;&gt; A(1, 2, 3, keyword_only='some', a={'sample': 42}, b={1, 2})
A(1, 2, 3, keyword_only='some', b={1, 2}, a={'sample': 42})
Honestly it looks like most of your code can't be simplified. However I found:


to_positional_argument_string is a mouth full, and people would understand your code easier if you just use repr.
to_keyword_argument_string is another mouth full and so you could call it kw_repr.
You may want to add a comment explaining what isinstance(field, abc.Iterator) is for. This is because at first it confused me.
[Controversial] You can change your code so that you don't need to be passed constructor_or_initializer. To do this you'd create parameters when __repr__ is first used.