When reading this, I wrote most of it early and left a draft to bake.... Then deleted a ton of it after other people replied. I'm conscious that my terminology might be all over the map.  Keep that in mind before hitting reply.  It'll take me a while to digest and pedantically use Luciano's terms, they appear to be a great start. :)

On Tue, Apr 20, 2021 at 10:09 AM Mark Shannon <mark@hotpy.org> wrote:
Hi everyone,

Once upon a time Python was a purely duck typed language.

Then came along abstract based classes, and some nominal typing starting
to creep into the language.

If you guarded your code with `isinstance(foo, Sequence)` then I could
not use it with my `Foo` even if my `Foo` quacked like a sequence. I was
forced to use nominal typing; inheriting from Sequence, or explicitly
registering as a Sequence.

True.  Though in practice I haven't run into this often myself.  Do you have practical examples of where this has bitten users such that code they would've written pre-abc is no longer possible?  This audience can come up with plenty of theoretical examples, those aren't so interesting to me.  I'm more interested in observances of actual real world fallout due to something "important" (as defined however each user wants) using isinstance checks when it ideally wouldn't.

Practically speaking, one issue I have is how easy it is to write isinstance or issubclass checks. It has historically been much more difficult to write and maintain a check that something looks like a duck.

 `if hasattr(foo, 'close') and hasattr(foo, 'seek') and hasattr(foo, 'read'):`

Just does not roll off the figurative tongue and that is a relatively simple example of what is required for a duck check.

To prevent isinstance use when a duck check would be better, we're missing an easy builtin elevated to the isinstance() availability level behaving as lookslikeaduck() that does matches against a (set of) declared typing.Protocol shape(s). An implementation of this exists - https://www.python.org/dev/peps/pep-0544/#runtime-checkable-decorator-and-narrowing-types-by-isinstance - but it requires the protocols to declare runtime checkability and has them work with isinstance similar to ABCs...  technically accurate BUT via isinstance? Doh!  It promotes the use of isinstance when it really isn't about class hierarchy at all...

Edit: Maybe that's okay, isinstance can be read leniently to mean "is an instance of something that one of these things over here says it matches" rather than meaning "a parent class type is..."?  From a past experience user perspective I don't read "isinstance" as "looks like a duck" when I read code.  I assume I'm not alone.

I'd prefer something not involving metaclasses and __instancecheck__ type class methods.  Something direct so that the author and reader both explicitly see that they're seeing a duck check rather than a type hierarchy check.  I don't think this ship has sailed, it could be built up on top of what already exists if we want it.  Was this already covered in earlier 544 discussions perhaps?

As Nathaniel indicated, how deep do we want to go down this rabbit hole of checking?  just names?  signatures and types on those?  What about exceptions (something our type system has no way to declare at all)?  and infinite side effects?  At the end of the day we're required to trust the result of whatever check we use and any implementation may not conform to our desires no matter how much checking we do. Unless we solve the halting problem. :P

PEP 544 supports structural typing, but to declare a structural type you
must inherit from Protocol.
That smells a lot like nominal typing to me.

Not quite.  A Protocol is merely a way to describe a structural type.  You do not need to have your implementations of anything inherit from typing.Protocol.  I'd personally advise people do not inherit from Protocol in their implementation. Leave that for a structural type declaration for type description and annotation purposes only, even though Protocol appears to support direct inheritance. I understand why some don't like this separate shape declaration concept.

Luciano notes that it is preferred to define your protocols as narrow and define them in places *where they're used*, to follow a golang interface practice.  My thinking aligns with that.

That inheritance is used in the declaration of the protocol is an implementation detail because our language has never had a syntax for declaring an interface.  544 fit within our existing language syntax.

Then came PEP 563 and said that if you wanted to access the annotations
of an object, you needed to call typing.get_type_hints() to get
annotations in a meaningful form.
This smells a bit like enforced static typing to me.

I think useful conversations are ongoing here.  Enforced is the wrong word.  [rest of comment deleted in light of Larry's work in progress response]

Nominal typing in a dynamically typed language makes little sense. It
gains little or no safety, but restricts the programs you can write.

"makes little sense" is going further than I'd go, but overall agreed.  It's a reason why we discourage people from using isinstance.  When part of my job was reviewing a lot of new to the language folks code, the isinstance antipattern was a common thing to see from those who weren't yet comfortable with dynamic concepts.

An extreme example of that is this:

# Some magic code to mess with collections.abc.Sequence
 >>> match {}:
...     case []:
...        print("WTF!")
...
WTF!

With duck typing this would be impossible (unless you use ctypes to mess
with the dict object).

user: Doctor, it hurts when i do this.
doctor: So... don't do that.

Anyone who has code that messes with the definitions of things in collections.abc (or really any stdlib module) deserves all of the pain they cause their entire program.  That is the same sentiment we should all have for anyone using ctypes to mess with the underlying VM.  Some footguns can't be meaningfully prevented.

Python has always been a language that both supported duck typing and nominal typing "I said I'm a duck, trust me" at the same time. When the duck like thing or the thing that named itself a duck (goose?) turns out not to quack like a duck (honk) it's always been a problem.  I don't see anything new here.  Our language is built on trust.  (if that's interpreted as "hope is our strategy"... that is a reason static analyzers like pytype and mypy exist, to reduce the reliance on hope/trust)

If you don't want Nominal typing, a logical conclusion could be "just get rid of classes".  Or at least "get rid of inheritance".  Obviously impossible while still identifying as Python.

So, lets stick to our promise that type hints will always be optional,
and restore duck typing.

You word this as if it were a call to arms, but duck typing hasn't gone anywhere, and there are no concrete actions to take or specific problems cited. Just a vague feeling that some more recent designs are drifting away from letting things quack.  Fair enough as a sentiment, but not something I see as specific.  From my perspective nobody wants to throw the duckling out with the bath water and I don't see anybody intentionally trying to.

I'm not suggesting that we get rid type hints and abstract base classes.
They are popular for a reason.
But let's treat them as useful tools, not warp the rest of the language
to fit them.

Ultimately I agree with this sentiment.  Maybe because I read it differently than you wrote it.  It's one of the reasons PEP-484 was as limited as it was.  It did what could fit within the existing language.  Knowing that this wasn't enough to satisfy all needs.  Where you see language warping, others see natural evolution to support previously ignored concepts.  PEP 544 doesn't warp anything.  PEP 563 raised good questions that are being worked on and actively discussed in this and other threads.

The thing that concerns me most are not static analysis tools.   Those have for the most part all been communicating and coordinating (typing-sig FTW).  Runtime tools consuming annotations concern me more.  When those are used, they become non-optional in that they cannot be ignored by the end user.  We really all need to play in the same park here with regards to annotation meaning, so if anyone has runtime uses of annotations they really need to participate in typing-sig.  It looks like that has started to happen and our deciders now know more people to ensure are looped in if not.  Great!  =)

I care more about not painting ourselves into a corner blocking future improvements than about being perfect on the first try.

[enough editing and re-editing, if there are non-sequitur edits left above, sorry!]

-gps