Repository navigation
The help function shows incorrect signature for subclass #105080
Description
Activity
- addedtype-bugAn unexpected behavior, bug, or errorAn unexpected behavior, bug, or error
on May 30, 2023 - addedstdlibStandard Library Python modules in the Lib/ directoryStandard Library Python modules in the Lib/ directory
on May 30, 2023 This is due to the behavior of
inspect.signature(). To be more specific,_signature_from_callable()ininspect.py.The current behavior to get a signature for a
type:- Check if the type has its own
__new__ - Check if the type has its own
__init__ - Find the user-defined
__new__in__mro__ - Find the user-defined
__init__in__mro__ - Non-related searches to this issue
As
A1has a direct defined__init__, it is shown as the signature. ForA2, no__init__or__new__is defined, but there's a__new__method defined in its__mro__, so that method was used.This feels a bit weird to me - like inheritance, the derived class should take the bahavior of closer bases, not further ones. If we can agree this is a bug, I can fix it by going through
__mro__and search for user-defined__new__and__init__. This would break the old behavior though(obviously).We can also make it safer - only change this in main and not backport. I guess it's a decision call.
- Check if the type has its own
From the description this seems to happen because it favors
__new__over__init__when looking for the signature.In my experience it's usually
__init__that has the more specific signature, while__new__has a generic signature, only because it has to accept whatever__init__accepts, but it doesn't usually care about the actual arguments when building the instance, while__init__cares about the arguments because it has to use them to initialize the instance.Wouldn't it make more practical sense to prefer
__init__over__new__, given that it usually has the more specific signature? As I understand the search algorithm mentioned above, it means any class that defines both a generic__new__and a specific__init__will still have the same problem, even after the proposed fix and even in the absence of any inheritance.From the description this seems to happen because it favors
__new__over__init__when looking for the signature.In my experience it's usually
__init__that has the more specific signature, while__new__has a generic signature, only because it has to accept whatever__init__accepts, but it doesn't usually care about the actual arguments when building the instance, while__init__cares about the arguments because it has to use them to initialize the instance.Wouldn't it make more practical sense to prefer
__init__over__new__, given that it usually has the more specific signature? As I understand the search algorithm mentioned above, it means any class that defines both a generic__new__and a specific__init__will still have the same problem, even after the proposed fix and even in the absence of any inheritance.What do you mean by "generic"
__new__? You should not define a__new__method for a class unless you need to do something very specific when creating an instance. The__new__method in your example code should not be defined(I took it as a proof of concept). For most use cases,__new__is not defined and__init__is the only method defined. For the cases where__new__is defined, it should be critical(for example, singleton class) and should take precedence before__init__.The
__new__method in my example was there as part of a minimal test case that shows the problem.Consider this example for a "generic"
__new__that has a purpose:class XMLSimpleElement: _xml_value_type = None # To be defined in subclass def __new__(cls, *args, **kw): if cls._xml_value_type is None: raise TypeError(f"The {cls.__name__} class cannot be instantiated because it doesn't define the _xml_value_type attribute") return super().__new__(cls) def __init__(self, value): super().__init__() self.value = value ... class XMLStringElement(XMLSimpleElement): _xml_value_type = strIn this case the
__new__method is meant to prevent instantiation for classes that do not define their value type, since they are a kind of an abstract base class that cannot work without knowing the value type, but at the same time it doesn't care about the arguments it receives, nor does it want to match its signature with that of__init__since a subclass may have an__init__with a slightly different signature, like:class XMLSimpleElementWithAttributes(XMLSimpleElement): def __init__(self, value, *attributes): super().__init__(value) self.attributes = attributesSimilarly there are many examples out there of base classes that have a test in their
__new__method that if the class being instantiated is the base class itself, it will raise a TypeError because the base class is not supposed to be instantiated.In the example above both
help(XMLSimpleElement)andhelp(XMLStringElement)show the wrong signature, whilehelp(XMLSimpleElementWithAttributes)shows the correct signature.I don't think the example code is the correct way to go.
Your
XMLSimpleElementis an abstract class. It feels a bit weird to have an__init__method on an abstract base class that defines a specific way(that can be overwritten) to initialize.I think the correct way to do it is to have a pure abstract base class
class XMLBaseElement: _xml_value_type = None # To be defined in subclass def __new__(cls, *args, **kw): if cls._xml_value_type is None: raise TypeError(f"The {cls.__name__} class cannot be instantiated because it doesn't define the _xml_value_type attribute") return super().__new__(cls)
Then a simple based on that:
class XMLSimpleElement: def __init__(self, value): super().__init__() self.value = value
Having a
__new__method that is very generic and an__init__that is very specific just do not add together to me. Maybe others have different opinions.If the implementation is like this, then all the signatures would be correct with my proposal.
Again, it was just an example to show that a generic
__new__and a specific__init__can coexist. I'm sure it can be refactored to work around the problem. My point is that I'd like something that works without me having to work around the problem.In the ideal case, the signature lookup should traverse the
__mro__looking for the first method between__new__and__init__that has the most specific signature. However it's unclear to me if we can properly define what "the most specific" signature is and how to identify it, let alone the fact that this would probably add a great deal of complexity to the solution.Anyway, my follow up comments were more about stating my opinion that from my experience preferring
__init__over__new__when picking the signature seems to work better in more cases and I find the current choice of preferring__new__over__init__less practical and more likely to run into issues.That being said your proposal would definitely improve things and I'd be thankful for it. As for the cases that it would not cover, I guess I can always define
__signature__at the class level to override the choice for signature if I'm not happy with it.I guess my point is - you can't prove a point with problematic code. Not saying your code is wrong, but if it's not preferable, then changing existing behavior based on that would be a bad idea. That's being said - do you know any real code in a popular repo that has similar issues?
In the docs it mentioned that:
__new__()is intended mainly to allow subclasses of immutable types (like int, str, or tuple) to customize instance creation. It is also commonly overridden in custom metaclasses in order to customize class creation.I think if you can do something in
__init__, you should do it in__init__, not__new__. In your example, you can check the class member in__init__and it works perfectly fine - so no__new__is needed. You'll probably say that's a "workaround", but that's probably - probably the correct way to achieve the feature.I guess one of the reason to favor
__new__over__init__is that -__new__is guaranteed to execute, but__init__is not. I can easily create some meaningful code to create instances of different classes based on the argument then the__init__method would be from the class of these instances.class XMLStrElement: def __init__(self, val): self.val = val class XMLIntElement: def __init__(self, val): self.val = val class XMLMultiElement: def __init__(self, *args): self.vals = args class XMLElement: def __new__(cls, *args): if len(args) == 1: if isinstance(args[0], str): return XMLStrElement(args[0]) elif isinstance(args[0], int): return XMLIntElement(args[0]) return super().__new__(cls) else: return XMLMultiElement(args) def __init__(self, val): self.val = val i = XMLElement(1) s = XMLElement("s") m = XMLElement(1, "s") e = XMLElement(None) print(i, s, m, e)
Instead of personal experience,
__new__is favored over__init__on a language level.I think overall, I feel weird about having a generic
__new__and a specific__init__on the same class - it does not quite make sense to me.Reacted by sunmy2019 and Erlend E. AaslandWouldn't it make more practical sense to prefer
__init__over__new__, given that it usually has the more specific signature?No.
__new__can change the object type. You even cannot tell the object type until__new__is executed, so you cannot decide which__init__to call.- removedtype-bugAn unexpected behavior, bug, or errorAn unexpected behavior, bug, or error
on May 31, 2023 I do think the original request is valid - if a derived class
Ddefined__init__method but not__new__, we should use that for signature. And for all the derived classes based onDtoo.The existing behavior feels wrong to me - at least we should not have different behavior on class
DandD1(D).I do think the original request is valid - if a derived class
Ddefined__init__method but not__new__, we should use that for signature. And for all the derived classes based onDtoo.The existing behavior feels wrong to me - at least we should not have different behavior on class
DandD1(D).I cannot follow. What is
D? Can you provide a code snippet?I do think the original request is valid - if a derived class
Ddefined__init__method but not__new__, we should use that for signature. And for all the derived classes based onDtoo.
The existing behavior feels wrong to me - at least we should not have different behavior on classDandD1(D).I cannot follow. What is
D? Can you provide a code snippet?It's the original code snippet.
class A0: def __new__(cls, *args, **kw): return super().__new__(cls) def __init__(self, *args, **kw): super().__init__() class A1(A0): def __init__(self, a, b): super().__init__() self.a = a self.b = b class A2(A1): c = None
It does not make sense that
A2andA1have different signatures. I think the signature for bothA1andA2should be(a, b). Maybe someone would argue that they should both be(*args, **kw)- we can discuss that. However, I don't think in any case they should be different, that's just wrong.I think the signature for both
A1andA2should be(a, b).Not exactly.
With customized metaclass, there are use cases like
class A0: def __new__(cls, *args, **kw): class D: def __init__(self): super().__init__() return D() class A1(A0): def __init__(self, a, b): super().__init__() self.a = a self.b = b class A2(A1): c = NoneA2(...)is an instance ofD. (You can call with any args/kwargs).they should both be (*args, **kw)
I think so.
8 remaining items
- added a commit that references this issue
on Jun 2, 2023 Thank you. I really appreciate the expedite handling.
May I ask what is the intention with this bug fix? Will it be backported, or is it just going to exist in 3.12 going forward?
This is considered a bug fix I believe so it was fixed in
mainand3.12(notice that3.12is already a backport as we are on 3.13 alpha now). Not sure why it was not merged back to3.11, that's a question for @carljm .3.11is still taking bug fixes right?We won't port it back further because
3.10only takes security fix now. So the only version that might be affected at this point is3.11.That's fine. 3.11 is what I'm interested in. If this could land there it would be great.
Sorry, that was my oversight; it should be backported to 3.11. Kicked that off now.
Looks like it does not backport cleanly. I will try to get to the manual backport soon but it may be a few days. @gaogaotiantian if you want to prepare the backport PR sooner (using the cherry_picker tool) I will be happy to review and merge it.
- added a commit that references this issue
on Jun 4, 2023
Bug report
With the following class hierarchy:
help(A2)shows the wrong signature for instantiating A2 asA2(*args, **kw)instead of the expectedA2(a, b), despite the fact that is shows the correct signature for__init__:Note that
help(A1)works correctly and shows the correct signature asA1(a, b):This doesn't seem to be an issue if
__new__is not defined on A0, or if A1 redefines__new__with the same signature as__init__.Your environment
Linked PRs