Live data from Hacker News

Pyrefly vs. Ty: Comparing Python's two new Rust-based type checkers

blog.edward-li.com

151–160 of 166 posts

Re: Pyrefly vs. Ty: Comparing Python's two new Rust-based type checkers

#151
post #92

Earlier quoted context omitted.

> 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 sy…

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

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.

Re: Pyrefly vs. Ty: Comparing Python's two new Rust-based type checkers

#152
post #115

Earlier quoted context omitted.

As a Redditor said: > The standard VC business model is to invest in stuff that FAANG will buy from them one day. The standard approach is to invest in stuff that's enough of a threat to FAANG that they'll buy it to kill it, but this seems more like they're gambling on an acqui-hire in the future.

I have never seen a FAANG company buy a pure programming-language based tooling startup.

Facebook acquired Monoidics in 2013; they were the startup that created Infer[0].

[0] https://en.wikipedia.org/wiki/Infer_Static_Analyzer

Re: Pyrefly vs. Ty: Comparing Python's two new Rust-based type checkers

#153
post #110

Earlier quoted context omitted.

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 issue…

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 bec…

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.

Re: Pyrefly vs. Ty: Comparing Python's two new Rust-based type checkers

#154
post #110

Earlier quoted context omitted.

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 bec…

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 + oth…

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.

Re: Pyrefly vs. Ty: Comparing Python's two new Rust-based type checkers

#155

Earlier quoted context omitted.

> Gradual typing means that an implicit "any" (unknown type) anywhere in your code base is not an error or even a warning. That depends on the implementation of gradual typing. Elixir implements gradual set-theoretic types where dynamic types are a range of existing types and can be refined for typing violations. Here is a trivial example: def example(x) do {Integer.to_string(x), Atom.to_string(x)} end Since the func…

How is this function definition (or maybe just its parameter x) "untyped"? There is enough information to deduce that the type of parameter x is empty and the type of the function doesn't matter because there is an error. If the body of the function contained only the first or the second call, the verdict would have been that x is respectively an Integer or an Atom and the type of the function is the type of the cont…

For us type inference is the same as type checking where all parameters are given the dynamic type. So even if you explicitly added a signature that said dynamic, we would still find a violation, where others would not. The point is that dynamic does not have to mean “anything goes”.

Re: Pyrefly vs. Ty: Comparing Python's two new Rust-based type checkers

#156

Earlier quoted context omitted.

> Gradual typing means that an implicit "any" (unknown type) anywhere in your code base is not an error or even a warning. That depends on the implementation of gradual typing. Elixir implements gradual set-theoretic types where dynamic types are a range of existing types and can be refined for typing violations. Here is a trivial example: def example(x) do {Integer.to_string(x), Atom.to_string(x)} end Since the func…

ty also implements gradual set-theoretic types, and can represent "ranged" dynamic types (as intersections or unions with Any/Unknown). We don't currently refine dynamic type based on all uses, as suggested here, though we've considered something very much like this for invariant generics. In your example, wouldn't `none()` be a type for `x` that satisfies both `Integer.to_string(x)` and `Atom.to_string(x)`? Or do yo…

Oh, that’s exciting to hear! I would love to exchange notes and I know one of the lead researchers of set theoretic types would love to learn more about your uses too. If that sounds fun to you, you can find me on Gmail (same username).

In our case, we implement a bidirectional system where before applying x to Integer.to_string, we compute the domain of Integer.to_string (which is integer) and pass it up. If x is a dynamic type, then we refine it. So on the first call, x refines to `dynamic & integer`, then we apply it.

The second refinement fails because it becomes none, so we discard it, but it means the application on Atom.to_string will fail anyway. So yes, we check for emptiness and discard none.

Re: Pyrefly vs. Ty: Comparing Python's two new Rust-based type checkers

#157

Earlier quoted context omitted.

beartype is great, but I only find it useful at the edges. Runtime checks aren't needed if you have strict typing throughout the project. On a gradually-typed codebase, you can use beartype (e.g. is_bearable) to ensure the data you're ingesting has the proper type. I usually use it when I'm dealing with JSON types.

Why isn’t it necessary? Do you mean that with edit-time type checking, you can catch all errors, so no need for runtime verification the edit-time type decls match? What about interacting with other libraries?

If you have strict static type checking, type errors can't creep in, so you don't need runtime checking. pyright (type checker) will tell you when a runtime check is redundant. For example, if you already have `var` annotated (or inferred) as `str`, then `if isinstance(var, str)` is statically guaranteed to be true.

Of course, that's only if you trust all the types in your code. You still have escape hatches, such as Any and cast, that can break this guarantee. There are lints (from ruff) and pyright options to help with this. Concerning external libraries, I either use libraries that are 100% typed (which is common these days), or write my own type-safe wrappers around the others.

Re: Pyrefly vs. Ty: Comparing Python's two new Rust-based type checkers

#158
post #154

Earlier quoted context omitted.

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 + oth…

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.

Thanks! My approach is to stop once it starts to hurt, and figure out what I should expect the type checker to miss. The type system and I are both getting better at it as time goes by. It’s not perfect, but it’s way better than not having it.

Re: Pyrefly vs. Ty: Comparing Python's two new Rust-based type checkers

#159
post #92

Earlier quoted context omitted.

> 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 sy…

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

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.

Re: Pyrefly vs. Ty: Comparing Python's two new Rust-based type checkers

#160
post #149

Earlier quoted context omitted.

> 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 cas…

> You're trying to shove a square peg into a round hole

Yeah, that's literally the point of my Reddit post

Post reply on HN