HN user

_carljm

259 karma
Posts0
Comments43
View on HN
No posts found.

We provide this diagnostic in ty (https://docs.astral.sh/ty/reference/rules/#possibly-unresolv...), but it's disabled by default because it can have false positives in many scenarios where the code works at runtime (this is true also in the type checkers that enable it by default). A typical example is code like

  def _(flag: bool):
    if flag:
      x = True
    ...
    if flag:
      read(x)
But it's easy to turn the rule on if you want it:
  [tool.ty.rules]
  possibly-unresolved-reference = "error"

We'll be adding ourselves to that table soon. We'll have some work to catch up with pyright on conformance, but that's what the time between now and stable release is for.

There are ways to type invariant generics more precisely that still meet the gradual guarantee. E.g.:

  x = []  # list[Unknown]
  x.append(A())  # list[Unknown | A]
  takes_list_of_a_or_b(x)  # list[A | B]
We haven't decided yet if this is what we want to do, though. It's also possible that we may decide to compromise on the gradual guarantee in this area. It's not an ironclad rule for us, just something we're considering as a factor.

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 you special-case `none()` and error if it occurs?

Yes, we've talked; I know a number of the pyrefly devs well. Ty had already been months in development when pyrefly development started. We discussed collaboration, but they decided they needed to do their own thing in order to move quickly and ensure it would serve their needs, which is totally reasonable.

The current version can handle importing pydantic without error just fine, but it probably can't find your virtualenv, so it doesn't know what third-party dependencies you have installed. Ty will discover your venv if it is in `.venv` in the project directory; otherwise you can help it out with the `--python` CLI flag.

We're looking forward to hearing what your experience is! There's a certain amount of roughly-constant overhead (e.g. reading all the files), so generally ty will look relatively faster the larger the project is. For very large projects we've seen up to 50-60x faster or more. We haven't really put a lot of work into targeted optimization yet, so we aim for it to get faster in the future.

It will certainly be slower than Ruff, just because multi-file type analysis more complex and less embarrassingly parallel than single-file linting.

(ty developer here)

Currently we default to our oldest supported Python version, in which `datetime.UTC` really doesn't exist! Use `--python-version 3.12` on the CLI, or add a `ty.toml` with e.g.

``` [environment] python-version = "3.12" ```

And we'll find `datetime.UTC`.

We've discussed that this is probably the wrong default, and plan to change it.

We've been calling it Cinder internally for years, long before we thought about opening it up. If we wanted to make it a big branded thing, we'd work on finding a unique name for that, but we aren't trying to do that, we're just opening the code to make it easier to share work with upstream and other people working on CPython performance. We hope libcinder keeps the mindshare around the "cinder" name.

It’s definitely true that there’s stuff a JIT would love to know that the type system can’t tell you. But that doesn’t mean there isn’t useful information available in type annotations. In particular it is possible to speed up attribute access and method/function calls a lot when target types are statically known.

Our approach to “int fits in a machine word” in Static Python is to require machine ints to be explicitly annotated as such, and then you opt in to limited size ints with overflow etc, in exchange for getting really fast arithmetic.

With Static Python we use type annotations in compilation, and we require them to be correct (they are runtime checked at boundaries with non-Static code and throw TypeError if wrong types are passed in.)

We haven’t gone the hidden classes route so far because it’s simpler to just look at attributes assigned in __init__ and annotated on the class and lock those into slots. If you’re writing typed Python you probably don’t do a lot of tacking extra ad hoc attributes onto instances, since type checkers don’t like that either.

It was a few years ago and I didn’t work on it directly, so my knowledge is a bit fuzzy, and I think the person who did work on it is no longer at the company. As best I recall the slog to get it working was mostly about C extensions, but I don’t know details. And then once it was working the memory use was worse and it didn’t show perf improvements. This is speculation but I would guess the design of the pypy jit isn’t a great fit for a prefork workload, where the hot paths don’t get hot until postfork, but it’s pretty inefficient to do identical jit compilation in every worker instead of once in the parent process.

Static Python is a bit different in that it’s not a subset language. It makes a few semantic changes, most notably that classes are slotified, so you can’t tack random extra attributes into instances. If you’re writing typed Python you probably don’t do much of this anyway since all type checkers will complain about it. But the approach is “normal dynamic Python with optimizations where allowed by types,” not subset.

It’s worth noting that we’ve never targeted perf on any of these benchmarks, so these are just the results that fell out from optimizing for our production workload, which is a totally different beast.

Most of the hits are due to the default “JIT everything” behavior which is really bad if there are a lot of rarely called functions, but would be easy to fix. This is discussed in the README.