Prettify function parameter default value representation (in pydoc or in inspect.Parameter)

Currently inspect.Parameter, that is used by inspect.signature and, as a consequence, in pydoc to represent function parameters, uses plain repr() to provide a string representation of its default value, which can be bulky and not very informative for some objects. Consider the following (not intended to be real use-case) example that tries to cover some cases (both realistic and added rather for concept demonstration):

import _random
import builtins
import re
import inspect

class A:
    class B:
        def bar(self):
            pass
    def foo(self):
        pass

class MyList(list):
    pass

def g():
    def h():
        pass
    return h
h = g()

def f(a=print, b=re.compile, c=_random.Random().seed, d=A,
      e=A(), f=A.foo, g=A().foo, h=A.B, i=A.B(), j=A.B.bar, k=A.B().bar,
      l=MyList([1, 2, 3]), m=re, n=builtins, o=SyntaxError, p=lambda x: x, q=h):
    pass

print(inspect.signature(f))
(a=<built-in function print>, b=<function compile at 0x7f6f490fd080>,
c=<built-in method seed of _random.Random object at 0x55998bdd4a20>,d=<class '__main__.A'>,
e=<__main__.A object at 0x7f6f491244d0>, f=<function A.foo at 0x7f6f4911ab60>,
g=<bound method A.foo of <__main__.A object at 0x7f6f49124510>>, h=<class '__main__.A.B'>,
i=<__main__.A.B object at 0x7f6f49124590>, j=<function A.B.bar at 0x7f6f4911ac00>,
k=<bound method A.B.bar of <__main__.A.B object at 0x7f6f491245d0>>,
l=[1, 2, 3], m=<module 're' from '/usr/local/lib/python3.11/re/__init__.py'>, n=<module 'builtins' (built-in)>,
o=<class 'SyntaxError'>, p=<function <lambda> at 0x7f6f4911ade0>, q=<function g.<locals>.h at 0x7f6f4911ad40>)

The function/object addresses, mentions of value kind (class, object, built-in method, bound method etc) and module paths doesn’t provide any useful information in context of interactive documentation, but make output much longer.
No information is provided about the nature of compile function.
Also no difference is made between the subclass that doesn’t implement its own __repr__ (MyList) and its base class, but maybe in this case the code that renders default value shouldn’t try to be smarter that programmer and just use __repr__ of base class as it does now.

Here is an output with the solution I tried to implement:

(a=builtins.print, b=re.compile,
c=_random.Random().seed, d=__main__.A,
e=__main__.A(), f=__main__.A.foo,
g=__main__.A().foo, h=__main__.A.B,
i=__main__.A.B(), j=__main__.A.B.bar,
k=__main__.A.B().bar, l=__main__.MyList(),
m=re, n=builtins, o=builtins.SyntaxError,
p=__main__.<lambda>, q=__main__.g.<locals>.h)

There are cases that may be more difficult to handle, like built-in types that provide their own __repr__ but do not belong to builtins:

import inspect
import re

m = re.compile(r"[_\w\d]+")
def f(x=m):
    pass

print(m)
print(inspect.signature(f))
re.compile('[_\\w\\d]+')
(x=re.Pattern())

The problem is that the two following code are equivalent:

class one():
    ...

def f(a=one()):
    return
o = one()
def f(a=o):
    return

Once the code has been executed and the functions have been defined, there is no way (short of accessing the code text file itself, traceback-style) to tell the two apart. That’s because the parameters’ default values are evaluated at function-definition time, and their expression is then obsolete therefore forgotten.
That’s why when receiving an object instance (your A() for example), you can’t tell whether it was created inside the signature, or before. So, when you receive an instance of class A, you can’t just print it as A() : maybe it had arguments passed to it which are relevant, you have no way of knowing that, and if it was created before function definition it may have received attribute changes, or method calls changing its state. In that case, printing A() for two objects is unclear : are they the same one defined before function definition, or are they two different ones called inside the function signature ? With the repr’s address, you would know :person_shrugging:

In what you propose I think we could handle the representation of classes (except if their metaclass defines its own class repr), and of functions and unbound methods. But I don’t think handling objects, their bound methods or other things like that.

Thank you for your feedback. Let me challenge some of your points.

That’s why when receiving an object instance (your A() for example), you can’t tell whether it was created inside the signature, or before.

It possibly matters in situations when inspect.signature is used to retrieve default values of function parameters to do some introspection in Python code. I’m not sure that it’s the same for the use case I’m covering here, that is when user browses docs on object/function (probably imported from some other module) using help() in interactive interpreter.

So, when you receive an instance of class A, you can’t just print it as A() : maybe it had arguments passed to it which are relevant, you have no way of knowing that, and if it was created before function definition it may have received attribute changes, or method calls changing its state.

This is also true for current representation of default values. You can’t get any information about object state changes from it, except in cases when object provides its own __repr__ that explicitly shows values of some attributes, but this is what I am trying to cover in this feature.
Also the A() syntax does not necessary indicate that object was created with no arguments passed to constructor (there is no easy way to find it out); this can be viewed as more short analogue of <... object at ...> for object instances that are not functions or types.

In that case, printing A() for two objects is unclear : are they the same one defined before function definition, or are they two different ones called inside the function signature ? With the repr’s address, you would know

If I get you right, the address of object that is default value can’t provide any information about its state change; it will always stay the same as when you binded it to argument, unless if you will change function’s __defaults__, which doesn’t look as a good practice for me and anyway will work on function signature just as same as it does now.
Though it makes sense for default values of the same type without __repr__ defined: there is no way to distinguish between them without their addresses present in repr. Maybe formatting objects like e. g. module.Class(<0x7ffffffbad0b>).bound_method will work in this case.

In what you propose I think we could handle the representation of classes (except if their metaclass defines its own class repr), and of functions and unbound methods. But I don’t think handling objects, their bound methods or other things like that.

It’s good that we agreed on this. Then I’ll wait for more feedback but will think at least in this direction.

When the type doesn’t have a repr, the object’s default repr shows the id, so you have some way of comparing, maybe between one argument an another, maybe between parameters of two different functions, maybe between a parameter and an object provided by the module, to know if they are the same object or not.
My point about objects ids is not about changing __defaults__ (which is terrible practice), it’s that when you only represent an instance by the (qualified) name of the type and empty parentheses, you erase those information, which may take some room and be intricate but are actually meaningful. I’m dubious about programming a whole new representation system, which will need to be maintained (be prepared to hear that one again), which is different from the mainstream way of repr-ing objects, and which would hide information from the caller.
And if you readd the id in your new repr as you shown near the end, then it takes just as much room as before (even more in the case of A().B().bar since there are two ids to be shown), and it’s not much more readable than the builtin repr.

Overall I think there’s a good idea in there, but for things other than types and builtin functions I think it’s an impasse because at the end of the day it’s the dev’s responsibility to make their repr meaningful, not ours.