Comment by purplehat_

2 days ago

Could someone explain just what's so bad about this?

My best guess is that it adds complexity and makes code harder to read in a goto-style way where you can't reason locally about local things, but it feels like the author has a much more negative view ("crimes", "god no", "dark beating heart", the elmo gif).

Maybe I have too much of a "strongly typed language" view here, but I understood the utility of isinstance() as verifying that an object is, well, an instance of that class - so that subsequent code can safely interact with that object, call class-specific methods, rely on class-specific invariants, etc.

This also makes life directly easier for me as a programmer, because I know in what code files I have to look to understand the behavior of that object.

Even linters use it to that purpose, e.g. resolving call sites by looking at the last isinstance() statement to determine the type.

__subclasshook__ puts this at risk by letting a class lie about its instances.

As an example, consider this class:

  class Everything(ABC):

    @classmethod
    def __subclasshook__(cls, C):
      return True

    def foo(self):
      ...

You can now write code like this:

  if isinstance(x, Everything):
    x.foo()

A linter would pass this code without warnings, because it assumes that the if block is only entered if x is in fact an instance of Everything and therefore has the foo() method.

But what really happens is that the block is entered for any kind of object, and objects that don't happen to have a foo() method will throw an exception.

  • You _can_ write pathological code like the Everything example, but I can see this feature being helpful if used responsibly.

    It essentially allows the user to check if a class implements an interface, without explicitly inheriting ABC or Protocol. It’s up to the user to ensure the body of the case doesn’t reference any methods or attributes not guaranteed by the subclass hook, but that’s not necessarily bad, just less safe.

    All things have a place and time.

    • > It essentially allows the user to check if a class implements an interface, without explicitly inheriting ABC or Protocol.

      Protocols don't need to be explicit superclasses for compile time checks, or for runtime checks if they opt-in with @runtime_checkable, but Protocols are also much newer than __subclass_hook__.

      1 reply →

    • I don't think so. I think the other code should just stop using isinstance checks and switch to some custom function. I personally think isinstance checks benefit from having its behavior simpler and less dynamic.

      > check if a class implements an interface, without explicitly inheriting ABC or Protocol

      This really doesn't sound like a feature that belongs in the language. Go do something custom if you really want it.

    • But the moment you use a third party library, you cannot use it “responsibly” because that library, too, might use it “responsibly”, and then, you can easily get spooky interaction at a distance, with bugs that are hard or even impossible to fix.

    • A good example being stuff like isinstance(x, Iterable) and friends. Figuring out if something is iterable is a bit of a palaver otherwise.

I took the memes as largely for comedic effect, only?

I do think there is a ton of indirection going on in the code that I would not immediately think to look for. As the post stated, could be a good reason for this in some things. But it would be the opposite of aiming for boring code, at that point.

TL;DR having a class that determines if some other class is a subclass of itself based off of arbitrary logic and then using that arbitrary logic to categorize other people's arbitrary classes at runtime is sociopathic.

Some of these examples are similar in effect to what you might do in other languages, where you define an 'interface' and then you check to see if this class follows that interface. For example, you could define an interface DistancePoint which has the fields x and y and a distance() method, and then say "If this object implements this interface, then go ahead and do X".

Other examples, though, are more along the lines of if you implemented an interface but instead of the interface constraints being 'this class has this method' the interface constraints are 'today is Tuesday'. That's an asinine concept, which is what makes this crimes and also hilarious.

  • You better not find out about Protocols in Python then. The behavior you describe is exactly how duck typing / "structural subtyping" works. Your class will be an instance of Iterable if you implement the right methods having never known the Iterable class exists.

    I don't find using __subclasshook__ to implement structural subtyping that you can't express with Protocols/ABCs alone to be that much of a crime. You can do evil with it but I can perform evil with any language feature.

    • > You better not find out about Protocols in Python then. The behavior you describe is exactly how duck typing / "structural subtyping" works. Your class will be an instance of Iterable if you implement the right methods having never known the Iterable class exists.

      Conforming to an interface is a widely accepted concept across many popular languages. __subclasshook__ magic is not. So there is a big difference in violating the principle of least surprise.

      That said, I'd be curious to hear a legitimate example of using it to implement "structural subtyping that you can't express with Protocols/ABCs alone".

      2 replies →