Hacker News new | past | comments | ask | show | jobs | submit login

Very good discussion from my wife's alma mater!

One thing I didn't see mentioned that is a particularly annoying false positive in mypy is the enforcement of Liskov Substitution for equality checks. All types have the common bottom type of object and object itself defines the __eq__ magic method as accepting any other object, which means all custom Python classes that want to define an equality check have to either define the '==' operator as valid accepting anything as a second operand, or tell mypy to ignore the error, which is the obviously saner thing to do since you want a type error, not a return of False, when you check equality against a completely unrelated type. Unfortunately, mypy provides no way to give you that statically.

I'm glad they mentioned the default parameter None to avoid the classic footgun of empty collections, though. That has to be the single most annoying false positive, when you declare a parameter as Optional and the type checker then complains it was set to None in the parameter definition but later set to an actual value, which you have to do because of the totally insane and unintuitive fact that all invocations of a Python function share the same parameter, so filling in the empty list with something else upon calling it means everything else that calls it now gets the changed default value instead of the empty list.

This is probably the single greatest rookie mistake made in Python because of how unexpected it is if you don't deeply understand the object model, but doing the literally recommended by the book solution to get around it results in the official type checker endorsed by Guido himself to report a type error.




It's possible to work around without forcing wrong type on parameter

    def f(l: List[int]=cast(Any, None)) -> int:
        if l is None:
            l = []
        ...
    
    f()

Using casts is useful for all sorts of things like this when sentinel values/objects are involved. People often feel it's somehow 'wrong', but it's fine to trick the type checker as long as it's localized to neighbouring lines of code :)


You learn something every day! I didn't even know about the cast function.


> you want a type error, not a return of False, when you check equality against a completely unrelated type

What? No I don't?

    foo = if x: Foo else 1
    foo == 1 # why would I want this to throw? I just want False!
Disclaimer that I haven't done much Python recently (or Java, ever – I wonder if raise-on-== is a java-ism)


Yup, that's true. In Python, generally if you want to only compare equal to the same type, you would use the NotImplemented value to implement:

    class MyClass:
        def __eq__(self, other):
            if not isinstance(other, MyClass):
                return NotImplemented
            else:
                # whatever your comparison is
When NotImplemented is returned, there is a fallback sequence described in https://docs.python.org/3/library/numbers.html#implementing-... - where basically, in the expression `A == B`, if `A.__eq__(B)` returns NotImplemented, then `B.__eq__(A)` is checked as well (falling back to an `is` identity comparison if both return NotImplemented).

This is neat because it allows you to define a class B that can compare equal to an existing class A with the expression A == B, even though class A has no knowledge of B.


You don't want it to raise an exception at run-time, but you probably want it to be a static type error: two things that aren't the same type can never be equal, so if you're bothering to compare them you're at best wasting cycles and at worst making a dumb mistake.

In your example, `foo` could be marked (in mypy) a `Union[Foo, int]`, which would be a reasonable value to compare against an `int` and not a type error.


But there are times you want heterogenous types to compare with ==. Your approach isn't compatible with ad-hoc polymorphism.

For an example, consider `np.eye(4) == 1`. In this, you're comparing a doubly nested array of floats with a scalar int, but the operation is well defined (vectorized equality comparison).

And yes, it should be feasible to typecheck that operation.


Yep. This is what I meant. I don't want to throw a runtime error. I want the type checker to be able to actually type check this statically, which mypy can't do.


>two things that aren't the same type can never be equal

Well, actually as long as they implement the right dundermethods, they can totally be.


Besides, there is a difference between the actual type of the objects `a` and `b` in `a == b` and the structural type you specify in an annotation.

For instance:

  from typing import Protocol


  class MyClass:
      def method_a(self) -> None:
          ...

      def method_b(self) -> None:
          ...


  class ProtocolA(Protocol):
      def method_a(self) -> None:
          ...


  class ProtocolB(Protocol):
      def method_b(self) -> None:
          ...


  def compare(a: ProtocolA, b: ProtocolB):
      if a == b:
          print("a == b")
      else:
          print("a != b")


  a = MyClass()
  b = MyClass()

  compare(a, b)

Clearly, you would expect the output to be "a == b".


> two things that aren't the same type can never be equal

You're saying is that an 8-bit number can never be equal to a 16-bit number, or a 32-bit number, or a...

I hope you agree that's ridiculous.


I hope you agree that's ridiculous.

No comparison without explicit casts is a valid design choice. I think Ada and Rust work that way, though I've never used either language and might be mistaken...


> No comparison without explicit casts is a valid design choice.

Sure, but that wasn't the claim.


Yeah, there was some interpretation of the intent behind the comment on my part (cf "you probably want it to be a static type error").

How you view the claim itself depends on your thoughts about the level at which equality should operate (should type tags be considered part of a value's identity, is it about the bit representation, or the abstract value encoded by that representation, ...)


> I wonder if raise-on-== is a java-ism

There are two forms of equality in Java:

- 'x == y' is an identity check, like 'x is y' in Python (are x and y referencing the same memory?). These must be of the same type, or else it won't compile.

- 'x.equals(y)' is an overridable method, like 'x == y' in Python. Its argument is of type 'Object', so it will accept anything.

I don't do much Java these days, but I do write a lot of Scala (which runs on the JVM). One of the first things I do in a Scala project is load the 'cats' package so I can use 'x.eqv(y)' which refuses to compile if x and y are of incompatible types. This problem also propagates up through different APIs, e.g. 'x.contains(y)' checks whether y appears in the collection x (which could be a list, hashmap, etc.); this doesn't check the types, and I've ended up with bugs that would have been caught if it were stricter. I now prefer to define a safer alternative 'x.has(y)' which uses 'x.exists(z => z.eqv(y))'.


The Python stdlib all seems to disagree with the GP and provide False for comparisons with values of different types. Personally, I didn't expect this, because Python tends to not like mixing types in any way, but it's the one place where it makes sense.


>which you have to do because of the totally insane and unintuitive fact that all invocations of a Python function share the same parameter

I think it does make sense if you consider the function signature an expression scoped one level above the function definition. I agree it's initially not intuitive, and it's tripped me up a few times before as a beginner, like probably most other beginners.

As a more experienced Python developer, I think I'd actually now find it unintuitive if they changed it, due to everything I've internalized about Python scope, values, assignment, expressions, etc. Same for always requiring "self" as the first method argument. Overall, I'd prefer the changed versions, but I think they (or someone else) would basically need to make a whole new spiritual successor language.


You want equality to return False for unrelated types for when you have dictionaries with heterogeneous types as keys.


On the other hand, you probably don't want to have dictionaries with heterogeneous types as keys in the first place...




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: