Has anyone had to choose between Mypy and Pyright? Which is "better"?
A couple years ago I was in charge of choosing between the two, and I somewhat flippantly chose Pyright because it felt a lot faster (<5 second to do 30k lines) and had an easy integration with the editors people use at work (Pylance, coc-pyright).
Later, I realized that some popular libraries we use (django, numpy) have dedicated plugins for Mypy that you can't use with Pyright. So you have to look for Pyright-friendly type stubs or roll your own.
I've generally liked Pyright (as a side note, they have super responsive maintainers - ask a question on their GH, and they usually answer within a few hours), but I've been wondering if I am missing out with Mypy.
At least today, my experience is that Pyright is faster, flags more issues, provides better error messages, and (alas for Mypy) has fewer bugs. I hope this will change over time as Mypy matures, but there you have it.
When using Pyright in large Django projects, it's helpful to add explicit type annotations in a number of places (like reverse relations in Models) but I've found this tends to improve the clarity of the resulting code anyway.
Ruff is not a replacement for black[^1]. It's also currently complementary to pylint, not a replacement[^2]. I still use those tools with ruff.
Further, out of the box, ruff doesn't even replace isort. You have to specifically enable it to sort imports (`I`)[^3].
I personally think that ruff's defaults are much too conservative. I configure it with `ALL` and then ignore the rules I don't want. I _want_ upgrades of ruff which add new rules to find new things. I'll never know about the new rules if I have to follow its development. I fully expect `pre-commit autoupdate` to cause me some pain after I run it, and that's okay. Linters adding new rules are usually helping me make my code cleaner.
What does your pyproject.toml (or equivalent) look like? Which linters, formatters, and similar tools do you consider essential to your Python development workflow?
It is much better written, much more correct, has fewer bugs, and it is the default in VSCode so less friction to set up.
In my experience it isn't hugely faster so I wouldn't say that is a big factor. The main benefit is that it is just very correct and has very few bugs (and when there are bugs they are fixed very quickly).
I would avoid Mypy if at all possible. I once tried to fix a bug in it and the codebase was very very hairy. Partly that's because it predates official type hinting support, and partly because it supports a load of hacks to allow incorrect types. Sometimes even the way you write the type affects how it is interpreted (e.g. `a # type: foo` is different to `a: foo`).
The only times I would choose Mypy is if you really need one of those plugins you mentioned, or if you are adding types to a legacy codebase and can't face seeing how bad it really is.
Why would a typechecker written in rust be able to implement a different logic than one written in any other language?
And one written in python could just use python for the grammar so it would have the advantage of always loading the code the same exact way as python does.
We started with Mypy for thr colour-science.org projects but Pyright is so much faster that it is hard to look back. We have no particular issue with Numpy. As you pointed out the devs, especially Eric Traut, are really responsive.
Is anyone still using mypy, and if so why? I have replaced it by pyright [0] for a while now, and not looking back. It’s been a faster, more powerful replacement with (in my case), zero downside.
Pyright doesn't work with Django, as Django's so dynamic that it requires a plugin to infer all types correctly. Sadly, even mypy with plugins is a mess to get set up in vscode, especially if you want it to use the same config as you use for ci checks from the command line.
We use mypy + [django-stubs](https://github.com/typeddjango/django-stubs) (in a huge Django + drf project at day job) which includes a plugin for mypy allowing it to recognize all reverse relations and manager methods. Mypy is still really rough around the edges. The cli args are poorly documented, and how they correspond to declarations in a mypy.ini / pyproject.toml is mysterious. Match-statements still have bugs even a year after release. Exclusion of untyped / partially typed files and packages we've had to solve with grep filtering mypy's output for our whitelisted set of files, as it's been unable to separate properly between errors you care about (in your own codebase) and errors in others code (dependencies, untypable dynamic python packages etc).
The largest issue IMO is that mypy tried to adapt a java / OOP style way of type system onto python, instead of recognizing the language's real power within duck typing and passing structural types around. Typescript chose the right approach here, modelling javascript the way it is actually written, favoring structural over nominal typing, instead of the archaic and now left-behind way of Java-style OOP that has influenced mypy.
There was a recently accepted PEP which allowed for limited dataclass transforms, enough to cover the @attr.s usecase for both mypy and pyright, but nowhere near expressive enough to cover django's models and ORM sadly. It's probably impossible / undesirable to allow for such rich plugins, so i see the future for proper pluginless typing to be more akin to how pydantic / normal dataclasses solve typing, by starting with a specification of the types, deriving its runtime implementation, instead of plugins having to reverse the type representation of a custom DSL.
Did you manage to get MyPy + Django to work together usefully? I tried the plugin you mentioned but it still seemed stymied by the dynamic nature of Django (reverse relations, etc), so I gave up on it.
If it’s actually possible to live in typed nirvana with Django I’ll bang my head against the wall some more.
We’re using Django 3.2 btw + FactoryBoy.
Actually there’s a number of annoyingly dynamic Python libraries out there where methods are created dynamically that it feels a bit like playing whack-a-mole. Is the situation like TypeScript where you need a MyLibrary.types.ts for each library?
It's quite useful on ci, and as a `make mypy` can run before pushing up your code, but for interactive errors we all use pyright, which is a bit of a letdown because you don't get autocomplete for fields and models accessed through reverse relation managers etc, pyright doesn't know about them. Many packages ship types nowadays, so the type coverage isn't too bad for us right now.
Here's our plugin setup in mypy.ini in case this helps (Django 4.2 + drf 3.14)
with this horror of a regex in make (because you'll get drowned in wrong type errors in all of the untype files, and errors get shown from imports even if you don't care about that imported file), add more file targets as necessary:
FILES_TO_MYPY = $(shell ls mycompany/\*/validators.py mycompany/\*/services.py mycompany/\*/selectors.py mycompany/\*/managers.py | sort | uniq)
# 1)We have to grep out django-manager-missing like this until the following bug
# is fixed: https://github.com/python/mypy/issues/12987.
# 2) We grep out the line of `Found 95 errors in 16 files (checked 83 source
# files)` that now appears as we use follow-imports: silent, because there's a
# bug where errors from imported modules are counted against the total even
# though they aren't emitted. If any real errors appear we get them as a
# separate line anyways.
.PHONY: mypy
mypy:
@{ MYPY_FORCE_COLOR=${NOT_CI} $(VENV)/bin/mypy --config-file mypy.ini $(FILES_TO_MYPY) 2>&3 | grep -v 'django-manager-missing\|errors in'; } 3>&1 | tee $(HYRE_TESTS_OUTPUT_PATH)/mypy.stdout.txt
This allows you to get proper errors for things like
model = MyModel.objects.get()
othermodel = model.othermodel_set.first()
reveal_type(othermodel) # correctly revealed to note: Revealed type is "Union[mycompany.importpath.models.OtherModel None]"
and even errors on typos like
model = MyModel.objects.get()
othermodel = model.ooooothermodel_set.first() # revealed as MyModel has no attribute ooooothermodel_set, perhaps you ment othermodel_set
.
mypy is essentially the reference implementation. pyright probably is better as a type checker, but, for the referential aspect alone, I suspect many libraries will continue targeting mypy.
One potential benefit of mypy is that it comes with mypyc, a compiler that leverages mypy's evaluation of types. Since pyright and mypyc are not exactly equal, it makes sense to use mypy if you want to use mypyc.
I had started using mypy a couple years ago and simply never looked back at the decision. I perceived pyright as a Microsoft-specific tool, and waved it off, as mypy seemed the blessed and eventually converged-upon target.
Turns out that’s wrong so far. Tons of people advocating for the latter.
I quickly looked at the two repos. Pyright has 30 open issues. Mypy is at 2200. That is a simplistic metric, but an immense difference in any case.
I work on a monorepo and MyPy and Flake8 was such a PITA at first but after you use it for a while, you really see the value of type checking. Makes unit testing better and the code more robust in general. MyPy is seriously a great tool, especially with pydoc enforcements.
At that point, why not just use Go? It's designed to feel dynamic, provides concurrency primitives out of the box, and it's orders of magnitude faster for most tasks.
TCO is available in Python if you want it. Multiline lambdas can be had by abusing the walrus operator, but in a language that supports nested functions it's a bit pointless. Assuming you meant fluents, you can write most languages in that style if you want to, including Python.
TCO is available if I want it? How? Do you mean writing a slow decorator, that performs some stack magic and that I need to add everywhere? Does it cover all the cases? Like mutually tail recursive functions?
Abusing the walrus operator — Well, it is abuse then and hard to read.
It was suggested that neither Go nor Python would be chosen if language features are what you are after. Pointing out that both lack a certain feature only reiterates why one would not chose either of them.
If you don't consider static type and built in concurrency primitives features then ok, at what point does nice to have features outweigh performance gains and the robustness of static typing that directly impacts the quality of the end user experience and the readability of the code to other developers?
I disagree that Go's insistence on verbosity improves readability. Each individual line may simpler, but the larger purpose of the code is obscured in all the "if err != nil" weeds.
My unpopular opinion is that Dart should get a lot more attention. Strong typing with null-safety, compiles to native, batteries included, and nearly as expressive as Python.
> but the larger purpose of the code is obscured in all the "if err != nil" weeds.
That's the difference between script and system programming. When writing scripts, one is only concerned about carrying out a certain task and if anything goes wrong the whole thing can bail. Systems are more robust. When things go wrong they cannot just fail. They need to recover gracefully and do something meaningful in the failed state.
Handing failure conditions is the primary purpose of system code. Irrespective of Go, there is something to be said about some kind of identifier that says "take note: this is the most important code in the application!" If that comes across as being 'weedy', you know that you really needed a scripting language, not a systems language.
That is quite different. ? in Rust signifies "this code is not important"
Which even in systems is unavoidable, to be fair. Sometimes failure just isn't important. The division between systems and scripts is not perfect. But, that will be the uncommon case in systems. It is not certain that really needs a symbol in which to draw attention. (An implementation may need a symbol, but that's beyond this discussion)
I'm not sure why you think it's unimportant. You seem to be confusing its purpose, and making distinction between systems and scripts that doesn't apply here.
Rust's whole error handling, including the ? operator work great for embedded systems where Go isn't enough of a systems language to even run.
EDIT: I think you're confused. The ? operator says "I'm not handling this specially here". That doesn't mean the error is just propagated to the top like an uncaught exception in a Python script. It allows you any level of granular handling you'd like.
Go's exception handling (panic/recover) is very much like most other languages with exception handling, including Python. It is there to use when you have exceptions.
But we're talking about expected failure (network failure, hardware failure, etc.), not exceptional cases (programmer error). You would not want to carry 'expecteds' on exception handlers. Different tools for different jobs.
That said, systems transcend Go. The discussion isn't about Go, or Python, specifically.
Python has built in concurrency support, stuff like Mypy and IDE tooling make up for the dynamic typing.
And if that isn't enough, the answer is OCaml, F#, D, C#, Java, Scala, Haskell, Kotlin, C++,... definitely not going from horse to donkey in language features.
I've introduced mypy to a project in the past. It was part of taking an older, low quality code base and stabilizing it into something useful. Sometimes you can't just throw it all away and start over and need a gradual approach that will start paying some dividends immediately. mypy fits that bill.
Pyre, written in OCaml, is the closest thing that's stable. It has been discussed on HN a couple of times. Here is the 2021 discussion: https://hackertimes.com/item?id=27107647.
Note that Pyre does not support Windows (https://github.com/facebook/pyre-check/issues/554) and only provides wheels for x86_64 Linux and macOS (https://pypi.org/project/pyre-check/0.9.18/#files). While I don't use Windows, it makes me reluctant to adopt it. First, I want Windows users to be able to contribute to my projects that work on Windows. Mypy and (predictably) Pyright won't create an obstacle for Windows users. Second, the limited number of platforms makes Pyre seem a little internal-toolish.
It may still be worth it. As of today, actually, one of my projects has a Pyre branch. I want to see what it is like developing with it.
there's something a bit strange with pyre though, I feel like it makes it too difficult to check types in a subdirectory (like you have to provide a configuration or something)
it works well and fast for a whole codebase though
I want to like type hinted python, but there's always some corner case. Which is fine if I'm writing the rules, but there are teams that force things like "no adding ignore/disable for things you can't figure out".
Somewhat offtopic, does anyone have a link to a page that describes which extensions/tools I should be using with VSCode to author Python code?
Edited to add: when you read various recommendations, they're all discrete and don't have the overall context. e.g. I installed the VSCode suggested extensions, but then read a comment that I should really be using Ruff. OK, that's good, but if I have extensions X, Y, and Z, should I add extensions A, B, and C?
These pages are naturally out of date from the moment they're written.
Caveats aside, I use pyright, ruff and black (although ruff will probably one day consume `black`, entirely) – that's it.
`ruff` centralizes a lot of the linter/fixer ecosystem (isort, autoflake, pylint and more). `pyright` is for GoToDefinition and type-checking. `black` is for sanity-checking what `ruff` spits out, and should hopefully not be necessary (for me) as a standalone package at some point in the near future.
Ruff is not 1.0 yet, so I wouldn't use it at work – but I use it in all my own things.
the default extension is good enough to begin with. as you are adding more to your project (e.g. black/isort/django), you can naturally find more extensions.
One nuisance I found is that it's hard to get the VSCode Pyright to apply the same rules as the command-line one, which is very annoying when your IDE says the code is okay but it fails on CI. Not sure if that's fixed yet.
In the age of most projects having animated websites, videos on the homepages, ads for courses and maybe an online conference for launching a new version, I find this use of 2006-era blogspot strangely comforting.
For the past year I've worked with a large JS/TS project undergoing a slow, gradual transformation into TS - before that, for the better part of past 10 years I worked on a Python 3 + some typing, lightly enforced by mypy.
I must say I don't like JS, never have, likely never won't, but Typescript is a really pleasant language, even if JS underneath is more visible than I'd like. The mix of static analysis possible with TS vs added overhead of having to specify types (which can get very complex!) is just right. I welcome any and all efforts to make Python go this way - I'd be perfectly fine with transplanting TS type system as is.
Do you know about Pyright? It is a Python type checker implemented in, and apparently influenced by, TypeScript. It is from Microsoft, so the influence is not that surprising.
I use Pyright as my primary type checker and mypy as a secondary. (For example, Pyright won't run in Termux on my phone. I could likely fix this, but mypy just installs with pipx.) There is a document comparing Pyright and mypy in the Pyright repository: https://github.com/microsoft/pyright/blob/b09f35889a9cc9b262....
I find "dynamic during runtime + static during development" to be a highly potent combination because it largely prevents dumb programming mistakes and enables rich IDE hints while still allowing for enough magic to build beautiful interfaces.
While they are usually a great benefit and easy enough to so, sometimes getting types just right can be more of an effort than it's worth - it's beautiful that in those cases you can still go "fuck it, I know what I'm doing".
> I find "dynamic during runtime + static during development" to be a highly potent combination
Except you miss out on massive performance gains and your software runs 10x slower because of runtime type-checking. It's actually the worst of both worlds.
No, dynamically typed languages literally have runtime type checking built in because the interpreter does not know the type of any individual object until runtime.
Everything is stored on the heap, there is no cache locality for objects so every object property access has the performance of a linked list (or hash map, not good), and all method invocations require dynamic dispatch.
What this means is your code will be slow as fuck for no reason. If your code is 100% typed and you managed to find a fully compatible compiler that actually took advantage of the type annotations you could see massive speedups and significantly lower memory usage (we are talking order of magnitude improvements).
This was something nice about CL implementations like SBCL. When you needed fast you can fully type everything and unbox it effectivly, but it got out of your way when you didn't. Something approximating best of both worlds. Of course the fast code started to look like c, but that's not such a bad thing if you could localize it.
There are few dynamic languages that go that far. Python isn't currently one of them; the runtime is still doing low-level type-compliance checks under the hood even if the static typing layer "knows" those checks are wasteful. At least in every compiler / runtime I'm familiar with for Python; maybe someone has built one that uses the types to generate faster code that's unsound if the static type assertions are violated.
Other dynamic-typed-and-also-static-typed languages do allow for this. Many Common Lisp compiler implementations, for example, respect things like the (the) special form (https://www.cs.cmu.edu/Groups/AI/html/cltl/clm/node106.html), which acts as a static type assertion that allows the compiler to actually throw away the runtime sanity-checking (at the cost of producing actual unsafe code if the constraints of the form are violated).
I have seen people point out that this confusing discussion can be avoided by accepting that types are purely a static property of terms (not only identifiers and literals) in source code, and that what many insist on calling dynamic types are tags, not types.
At the machine layer, the machine is doing symbol manipulation on bit patterns. Whether those symbols ultimately mean anything is entirely up to the eventual observer of the end result.
The concept of "types" breaks down at this level much like the concept of "a dog" breaks down at the atomic level.
Python's typing ecosystem has come leaps and bounds, quickly. About 5 years ago I had to do an evaluation of Python+mypy and found that while mypy was a great idea, support by major libraries was abysmal and mypy itself was immature, so it wasn't suitable for real use. 2 years later, I took another look and the ecosystem had evolved massively, most libraries I cared about had type annotations added, PyCharm/VSCode had added support for type checking in the IDE, etc.
These days all of my use of Python is with type annotations that are validated by mypy as part of CI. It's very rare that I run into a Python library that doesn't have type annotations available. It's massively improved the stability of the Python code I'm working on. One indirect side effect is that static typing actively discourages people from poor design patterns such as **kwargs or passing around dicts/dataframes of data, since there's an obvious downside to doing so now.
I agree with avoiding *kwargs, but I wonder what we should do rather than to pass around dataframes if we have a program that works with dataframes? I get that there is a downside as the actual types are hidden inside the dataframes, but I am unsure if the tradeoff is worth it by unpacking the dataframe into arrays or objects for every function call and return, if the functions require them to be dataframes?
Sadly I haven't found any good ways to strongly type dataframes, so my solution is mainly cultural: just make people aware that they shouldn't use dataframes unless it makes sense, as opposed to Python codebases where dataframes are the default data type that gets passed around. I've tried to limit dataframe usage only to things like reading data from CSVs, then converting them to typed dataclasses before further processing.
It also depends on your use case. pandas is obviously very flexible and easy to use, so it might be worth the tradeoff to keep using it if your main use case is interactive Jupyter notebooks run by data scientists, or dealing with highly variable input data. But most of my Python is backend data pipelines that work on predictable input data, where reliability is important, so I default to dataclasses, only using pandas in rare situations with a lot of validation code surrounding it.
In older (< 3.10 ?) versions you have to import the classes from the typing module, but other than that it’s the same. You can also define types with the Generic class if you’d rather.
With live checks while you write, it’s pretty easy to do this as you go.
I have 90 PRs to Mypy configured repos this year, across 10+ python repos which are a mix of services and background jobs. All of them have some mix of pyright/jedi+ruff+black. I have implemented these tools either via CI or in my ide like experience in NVim.
TLDR; BE WARNED. this basket of bandaids is NOT a replacement for a strongly typed language. I would NOT recommend Python to teams or for services. No amount of mypy, pydantic, pyright, ruff, black, can convert python into a production language. This collection of bandaids is complex to maintain, upgrade, standardize, and has infinite edge cases where type annotation errors are just wrong.
The worst part about this basket is that you get all of the above features for free and are REQUIRED with go via gopls and gofmt. Gopls "is the official Go language server developed by the Go team." good luck achieving parity of this in python.
If you implement a service/app/anything production, use anything but a dynamically typed language like python.
It doesn't take an expert to notice that enforced types make a language more reliable in production, easier to debug, and faster to refactor.
You should be horrified by Dropbox's blog post on python in production: https://dropbox.tech/application/our-journey-to-type-checkin.... because this problem only existed and required $Ms of dollars to fix (even hiring the core team behind Mypy) because Python is being used instead of a typed language.
I'm rejecting all dynamically typed languages *for production*. Where production means anything that needs to exist for a long time and is supported by a team. A litmus test for whether an app is production is if you actually need mypy, pyright, jedi, to effectively work on the repo.
Python is great for single developer experience and going fast as a single developer. I use it every day, willingly, in jupyter notebooks. But at scale there are no success stories. You are zoomed in on the present success of "engineering miracles" and ignoring the big picture that the application would have moved faster if they had the foresight to choose a typed language.
Hence, why i'm saying be warned on following the path of mypy+black+pyright+ruff+etc., since you will be doomed to patching on top of a fundamental flaw of the language.
I highly recommend to avoid Python for production apps especially if it is your free will (and not a matter of refactoring or rewriting existing mistakes).
yup! along the dimensions of feature velocity and developer experience with regard to cost.
of course python/ruby/javascript have succeed in bringing value to the world in general -- but in this thread we're focusing on production reliability, debugging, refactoring.
as we see, dropbox is proud to admit that they hired Mypy core developers and fund the project, but guess how much they spent on the mistake of using python?
twitter, famously an advocate for Ruby, spent an ungodly amount of engineering to work around ruby, hit a wall, and switched to JVM.
Just the fact that mypy and typescript exist and have exploded in adoption is proof that eng. teams very quickly hit a sand pit when using dynamic typed languages. Mypy being funded by dropbox, Typescript funded by microsoft, shows the level of investment needed to support these dynamic languages.
So the thousands of developers using Python either doesn't know their own best or are just in perpetual pain in their use of python? There isn't the slight possibility that your view on what is a useable programming language in production is subjective and not objective?
Yep! I think all of those repos will either proverbially die a hero or live long enough to see themselves as a villain.
And thats because of the fundamental tradeoff of dynamic typing vs static typing. That part no one can disagree on.
The only disagreeable part I believe I'm saying is where does the dynamic typing scale limit happen. As a polyglot developer, working a lot with junior developers, I can say that dynamic typing hits a scale limit of feature velocity / production support / cost much faster than the general folk are aware of.
by no means is go/java/rust/c++ a silver bullet. it is a way to raise the bar for debugging, reliability, support. it is more effective than bundling python with 5 other type bandaids. in the medium/long term, lets guess at 2 KLoC or >3 person team, these typed langs will result in faster feature development and lower issues in production.
its comfortable, easy, appealing in the short term. just like why people overeat their junk food.
there's this common idea in management that new grads, SREs, "less experienced devs", can jump in and contribute. it's a falacy because the code becomes unmaintainable without a large investment (more unittests, refactoring the "buggy" code blocks with mypy type annotations, some SME who continues fixing prod bugs). a Python developer will shove their head in the sand with repl'ing, testing after deploy, and settle with this dev/ex while being completely unaware of the benefits of compiler errors and warnings.
Python has the same level of complexity as another language but you'll only see all of that dirty baggage in production as runtime exceptions.
The continued growth and maturity of typing and LLMs like GitHub copilot finally make IDEs powerful for python. Excited about the future of the language.
A couple years ago I was in charge of choosing between the two, and I somewhat flippantly chose Pyright because it felt a lot faster (<5 second to do 30k lines) and had an easy integration with the editors people use at work (Pylance, coc-pyright).
Later, I realized that some popular libraries we use (django, numpy) have dedicated plugins for Mypy that you can't use with Pyright. So you have to look for Pyright-friendly type stubs or roll your own.
I've generally liked Pyright (as a side note, they have super responsive maintainers - ask a question on their GH, and they usually answer within a few hours), but I've been wondering if I am missing out with Mypy.