Comment by notatallshaw

3 months ago

As the author of that post, I'd like to point out the example was meant to be stupid.

The purpose was to show different ideologies and expectations on the same code don't work, such as strict backwards compatibilities, duck typing, and strictly following linting or type hinting rules (due to some arbitrary enforcement). Although re-reading it now I wish I'd spent more than an evening working on it, it's full of issues and not very polished.

> If you have a super-generic function like that and type hinting enforced, you just use Any and don't care about it.

Following the general stupidness of the post: they are now unable to do that because a security consultant said they have to enable and can not break RUFF rule ANN401: https://docs.astral.sh/ruff/rules/any-type/

> Following the general stupidness of the post: they are now unable to do that because a security consultant said they have to enable and can not break RUFF rule ANN401: https://docs.astral.sh/ruff/rules/any-type/

Okay, then your function which is extremely generic and needs to support 25 different use cases needs to have an insane type definition which covers all 25 use cases.

This isn't an indictment of the type system, this is an indictment of bad code. Don't write functions that support hundreds of input data types, most of which are unintended. Type systems help you avoid this, by the way.

  • > Don't write functions that support hundreds of input data types

    But by it's nature, duck typing supports an unbounded number of input types and is what Python was built on.

    You've already decided duck typing is wrong and strict type adherence is correct, which is fine, but that doesn't fit the vast history of Python code, or in fact many of the core Python libraries.

    • > But by it's nature, duck typing supports an unbounded number of input types and is what Python was built on.

      You're trying to shove a square peg into a round hole. It's not about right or wrong. Either you want your function to operate on any type, attempt to add the two values (or perform any operation which may or may not be supported, i.e. duck typing), and throw an runtime error if it doesn't work--in which case you can leave it untyped or use `Any`--or you want stronger type safety guarantees so you can validate before runtime that nobody is calling your method with incorrect arguments, in which case you have to represent the types which you accept somehow.

      If you want to have a method that's fully duck typed, you're supposed to use `Any`. That's exactly why it exists. Inventing contrived scenarios about how you can't use `Any` is missing the point. It's like complaining C doesn't work if you're not allowed to use pointers.

      You're right that historically Python code was written with duck typing in mind, but now even highly flexible libraries like Pandas have type definition support. The ecosystem is way different from even 5-6 years ago, I can't think of any well-known libraries which don't have good typing support by now.

      1 reply →

    • Duck typing is great, for example to support a range of numeric inputs - say fixed-precison integers, floats, dynamic precision integers, and numpy arrays, pandas series, tensorflow/pytorch tensor. That duck typing can support functions with unbounded types, dos not mean that it is necessary, not generally sensible, for a particular function to support unbounded types.

    • Duck typing is great and Python’s type system has powerful support for it. You can for instance restrict a function to only objects with a frobnicate() method, without in any way constraining yourself on which implementation you accept. Type checking plus duck typing is very precise and powerful, and it helps me sleep at night.

Stupid is okay. _Nonsense_ is not. Your example was nonsense, it was absurd. The moment I saw the first example I was like, this should be add_ints and should only take ints.

Imagine I say "the human body is dumb! Here's an example: if I stab myself, it bleeds!" Like is that stupid or absurd?

  • And yet, some Python users insistent on type hinting very dynamic Python code while trying to keep how dynamic it is.

But there was a conceivable way (maybe not in Python) to make a `slow_add` function very generic, yet only be defined over structures where any conceivable `+` operation is defined.

You just have to say the type implements Semigroup.

Yes, this would work if the arguments are lists, or integers, or strings. And it won't pass the typecheck for arguments that are not Semigroups.

It may not work with Python, but only because it's designers weren't initially interested in typechecking.

  • Challenge accepted.

        from dataclasses import dataclass
        from typing import Protocol, Self, TypeVar
    
        class Semigroup(Protocol):
            def __add__(self, other: Self) -> Self:
                ...
    
        T = TypeVar("T", bound=Semigroup)
        def join_stuff(first: T, *rest: T) -> T:
            accum = first
            for x in rest:
                accum += x
            return accum
    
        @dataclass
        class C:
            x: int
    
        @dataclass
        class D:
            x: int
            def __add__(self, other: Self) -> Self:
                return type(self)(self.x + other.x)
    
        @dataclass
        class E:
            x: int
            def __add__(self, other: Self) -> Self:
                return type(self)(self.x + other.x)
    
        _: type[Semigroup] = D
        _ = E
    
        def doit() -> None:
            print(join_stuff(1,2,3))
            print(join_stuff((1,), tuple(), (2,)))
            print(join_stuff("a", "b", "c"))
            print(join_stuff(D(1), D(2)))
            print(join_stuff(D(1), 3))
            print(D(1) + 3) # caught by mypy
            print(D(1) + E(3)) # caught by mypy
            print(join_stuff(1,2,"a")) # Not caught by mypy
            print(join_stuff(C(1), C(2))) # caught by mypy
        doit()
    
    
    

    Now, this doesn't quite work to my satisfaction. Mypy lets you freely mix and match values of incompatible types, and I don't know how to fix that. Basically, if you directly try to add a D and an int, mypy will yell at you, but there's no way I've found to insist that the arguments to join_stuff, in addition to being Semigroups, are all of the compatible types. It looks like mypy is checking join_stuff as if Semigroup were a concrete class, so once you're inside join_stuff, the actual types of the arguments become irrelevant.

    However, it will correctly tell you that it can't accept arguments that don't define addition at all, and that's better than nothing.

    • Pretty cool that you got this far though!

      I think at this point one starts to fight against Python, which wasn't designed with this in mind. But cool nonetheless.

      1 reply →