@fyrfaras @whitequark i just saw >:/
yay! im so excited to "sloppercharge my productivity" or whatever!!!
although i do love the irony that astral's repos use claude. really shows how full of shit they are when their announcement is sucking up to openai's codex but they're using a competitor's slop machine. definitely not just showing a complete lack of values lol
@whitequark @Elizafox @fyrfaras mmmmmm type-safe slop
ive used mypy enough to know that this is going to fail hilariously the first time it encounters wrong or poorly-typed interfaces >:3c
@whitequark @Elizafox @fyrfaras fully agree. mypy sucks but its what i learned and its got all the plugins, plus i never got pyright working
i was actually planning on switching to ty for my next project

@Elizafox @whitequark @imbl Alrighty, thank you for explaining it to me. ^^ Also I just read a toot thread, below, by @xgranade that mypy doesn't use “mathematical type checking” but rather “linting heuristics” which could lead to this bad reputation you've mentioned earlier I think.
As I mentioned earlier, it's a bit more difficult to have good exit strategies with ruff, given that the specs around linting are much more loose. It's even harder to have a good exit strategy for ty, even though there's good specs, because there's not a great type checker to use instead¹. ___ ¹As has been pointed out to me, mypy is, for all its strengths and weaknesses, not a type checker. It doesn't follow formal mathematical type checking rules, it follows linting heuristics.
@fyrfaras @Elizafox @whitequark @imbl In particular, I cannot predict the behavior of mypy given knowledge of Python's type system alone. I have to know about what is and is not likely a logic error in Python.
Which is fine if you want a somewhat more rigorous linter that has the benefit of being developed under the auspices of the PSF itself. But it's not what I want or would want out of a type checker.
@whitequark @fyrfaras @Elizafox @imbl Agree, absolutely.
I made the mistake of thinking of it as one for a long time, but I have been disavowed of that notion.
@whitequark @fyrfaras @Elizafox @imbl Ooof, yeah. Were I in the mood to start useless fights, I'd be tempted to PR a change to that page, but it'd just get people hurt for no reason.
I mean, it just isn't a type checker. That's not what it does.
@whitequark I guess part of it is that I'm trying to reconcile "cool people that I think well of work on mypy" and "mypy is manifestly unfit to serve as the thing it advertises itself as." For me, that comes down to trying to understand what mypy *is* for if it's not the thing it's billed as, if there's some kind of semantic drift there.
Were I to make that PR as a frequently loud critic, I don't think it'd come across in good faith, and, well, fair enough.
@xgranade @whitequark like "too much busywork for the amount of bugs it finds" is not an experience I have had, but we write very different kinds of code so I'm definitely not arguing with that; I could see that 'ty' or 'pylance' might do far better with your code, or might even have a more rigorous approach to certain aspects of their type algebra. but all three happily give this program (and many like it) the thumbs-up:
x = 1
y = 2
locals()['y'] = 'oops'
print(x + y)
@whitequark @xgranade this is actually why I prefer mypy, specifically because (via plugins) it can accept an extension to the type system implemented with runtime hackery, and handle Zope Interface, while none of the other ones can. pydantic also has a similar thing (and dataclasses were originally supported via the 'attrs' plugin, although everybody else eventually got on board with those).
but I guess when it comes to "helpful" warnings, mypy probably does produce a lot more of those
@xgranade @glyph a type-checker that rejects well-typed programs is by definition buggy. if it's not fixed then it is unfit for purpose
we could have a broader discussion of what "well-typed" means for Python (it is not clear to me at all) but if the above is an accurate assessment of the consensus in mypy development then it is not a(n even aspirationally correct) type-checker
```
def f() -> None:
return
y = f()
```
main.py:4: error: "f" does not return a value (it only ever returns None) [func-returns-value]
Found 1 error in 1 file (checked 1 source file)
@glyph @whitequark That program is well-typed by the documented rules. In fact it passes if I add a type parameter!
```
def f(x: None) -> None:
return x
def g[T](x: T) -> T:
return x
y = f(None) # errors
z = g(None) # passes
```
There's no underlying mathematical framework for that, it's "hey, we noticed that if you set y = f() and f can only return None, you probably did something wrong." Which is correct, I agree with that! But it's not a type checking failure.
@glyph @whitequark Perhaps even more stark is that the same thing happens even when I tell mypy *explicitly* that something can only return None, but I do so in two different ways.
```
class C:
def f(self, x: None) -> None:
return x
class D[T]:
def f(self, x: T) -> T:
return x
c = C()
y = c.f(None) # errors
d = D[None]()
z = d.f(None) # passes
```
That fails to be coherent under substitution of type parameters.
@xgranade @whitequark heh. fair enough I guess, although I am tempted to troll “that’s totally a valid type check once you realize that in certain contexts None means ‘null’ but in others it means ‘void’ “
(formally speaking, -> None is already lightly gibberish as it is an unprincipled pun for NoneType; but in support of your point, ty does accept NoneType but Mypy doesn’t)
@glyph @whitequark But that's my point... Python doesn't even have a void, but mypy acts like it does!
```
from typing import Any
def f() -> None:
pass
y: Any = f()
```
Assigning a value to a variable of type `Any` should always be allowed. That should be true in any gradually typed system.
@glyph @whitequark By way of getting at what I mean about expecting there to be rules I can follow, one of those is that if `x = y` typechecks, then `f = lambda: y; x = f()` should also type check.
That doesn't hold, though:
```
from typing import Callable
f: Callable[[], None] = lambda: None
y = f() # error
x: None = None
y = x # ok
```
That means I cannot in general refactor by extracting a value into a function that returns that value, which breaks the whole idea of a type system.
@glyph @whitequark Anyway, sorry for the long reply, but I guess the short version is that I don't think any of the above behavior is consistent with a type system, but it can be consistent with a linter.
That is, knowing the types of a program in Python is not sufficient to know if that program will pass mypy. I have to know about the context in which those types are used, which breaks the abstraction entirely. It makes mypy take on a *much* higher cognitive load.
@xgranade @whitequark or like… this? https://docs.astral.sh/ty/reference/rules/#ambiguous-protocol-member
that looks a lot like linting
@glyph @xgranade it is normal for a typechecker to include linting rules at the "warn" level. i mean even gcc has those
what is not normal / problematic for me in this context is having those be hard-rejects. "someone dreamed up a rule out of vibes and now i have to mangle perfectly well-formed code" is not something i accept
@whitequark @glyph Part of the ergonomic difference for me is that I do run ruff (sigh) with warnings as errors. Sometimes that results in false negatives (programs rejected that are valid). In those cases, I can leave a # noqa comment indicating why. 90% of the time that's an unused import that's actually used but it doesn't detect it, though I digress.
Declaring something as Any should be the polite way to spell "fuck off, type checker" but even *that* doesn't work in mypy... hence mangling.
@whitequark @glyph I think we have different philosophies on that, yeah... not even that I disagree, I just think of it differently? I'm happy working within what a tool wants me to do, it's something my mathy brain is OK with. Just tell me the goddamned rules.
Whereas that's something mypy does not do. The rules are, to your earlier point, vibed up.
@glyph @whitequark I don't think that's a linting at all? It's saying that type *inference* is disabled in a given context, but if you explicitly specify the types then it will work. That's fairly common (hell, even Rust has it, and it's got about the most formal type system I've seen in an imperative language) at interfaces between code.
Even then, to @whitequark's point (beat me to it), warnings are one thing but *rejecting* a well-typed program is one thing a type checker should never do.
@xgranade @whitequark okay perhaps a clearer "definitely not type checking" one would be https://docs.astral.sh/ty/reference/rules/#deprecated but yeah
mypy doesn't even have a concept of a "warning", I guess, is the problem? It has categories you can enable and disable but they're not grouped together like this
@glyph @whitequark That is type checking, though. Deprecation information is stored in the `__deprecated__` attribute, and the existence of protocols means you can have a protocol that matches the existence of that attribute (as another aside, it's the only existence of a negative type in Python, so that's weird, but a weird type is still a type).
The problem with the `y = None` vs `y = (lambda: None)()` is that in both cases, `y` has the exact same value but mypy only raises on the second one.
@xgranade @whitequark I'll have to think more about the shortcuts other checkers take to see if they can be convinced to do something similar but less obvious.
I understand now why this doesn't bother me: I came to Python from a combo of Perl and Lisp (sort of) and it drove me crazy for the first few years; it was too principled to really have Perl vibes but too loose to be a Lisp even though it seemed like it wanted to be one. Stuff like 'why isn't `if` an expression' seemed like a mistake
@xgranade @whitequark eventually I realized that it wants to be what it is, which is kind of mostly a lisp-1 with a bunch of weird caltrops to prevent technically-correct but bad-for-maintainability stuff in a way which is mathematically unprincipled but probably the right call. "We all know -> None means void even though : None doesn't" is one of those annoying "wrong" edge cases I just learned to accept forever ago.
But characterizing Mypy as a "type-aware linter" as a result, is fair
@glyph @whitequark "mathematically unprincipled but probably the right call" That's the core of what bothers me, yeah. A type system is, by definition, a system of mathematical rules. If it's mathematically unprincipled, no matter how well motivated it is, it's not a type system.
(As a side note, `-> None` being an idiom for `-> NoneType` doesn't bother me... having a useful shorthand is not inconsistent with formal reasoning. Accepting `-> None` and rejecting `-> NoneType` is just *weird*.)