← Back to context

Comment by mjr00

3 months ago

The top comment in that post shuts down the whole nonsense pretty quickly and firmly:

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

It's a stupid example, but even within the context of a `slow_add` function in a library: maybe the author originally never even thought people would pass in non-numeric values, so in the next version update instead of a hardcoded `time.sleep(0.1)` they decide to `time.sleep(a / b)`. Oops, now it crashes for users who passed in strings or tuples! If only there were a way to declare that the function is only intended to work with numeric values, instead of forcing yourself to provide backwards compatibility for users who used that function in unexpected ways that happened to work.

IMO: for Python meant to run non-interactively with any sort of uptime guarantees, type checking is a no-brainer. You're actively making a mistake if you choose to not add type checking.

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.

      4 replies →

  • 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.

      2 replies →

One thing that post does do though is very clearly highlight the difference between Python's type system and say ... TypeScript's.

TypeScript's goal is to take a language with an unhinged duck type system that allows people to do terrible things and then allow you to codify and lock in all of those behaviours exactly as they're used.

Mypy (and since it was written by GVM and codified in the stdlib by extension Python and all other typecheckers)'s goal is to take a language with an unhinged duck type system that allows people to do terrible things and then pretend that isn't the case and enforce strict academic rules and behaviours that don't particularly care about how real people write code and interact with libraries.

If you include type hints from the very beginning than you are forced to use the very limited subset of behaviours that mypy allow you to codify and everything will be "fine".

If you try to add type hints to a mature project, you will scream with frustration as you discover how many parts of the codebase literally cannot be represented in the extremely limited type system.