Is it just me or do more people find that Python is becoming more and more illegible with these type annotations? I understand why they are introduced, but to me they cause a lot of line noise in the relatively clean signal that is Python code.
To add insult to injury, type hints are not enforced nor even verified by the runtime. I find that if I want type safety, I'd rather branch out to other languages that were designed for it from the start. In particular, I find that rewriting some of my Python modules in Rust is a joy (using PyO3, but keenly following RustPython development too).
Having worked at a company with a million-lines-plus of Python monolith, gradual typing annotations made all the difference. Why do you need it enforced at runtime? It's static types!
They said "by the runtime", perhaps they mean that the python interpreter should throw errors at compilation time, since its not a classic compiled language.
> They said "by the runtime", perhaps they mean that the python interpreter should throw errors at compilation time, since its not a classic compiled language.
Why waste time doing static analysis each time you run the code? If you don't change the code, its not going to change.
I draw the line, when satisfying the type checker requires writing ugly uselessly complicated code, like writing classes just to use them as type annotations, because the type checker does not get it otherwise, and things like that. Things that introduce code inflexibility at a later point, or encourage bad designs of using too much inheritance. Writing type annotations for procedure arguments and return type can be quite helpful. I just don't want to overdo it all. If the tradeoff is that far into the safety direction, then why did anyone choose Python for that project and not say for example Rust? Or even Java?
Lets not make Python the new Java. People already start to translate design patterns 1 to 1 from Java examples to Python and I have to tell them, that there are other ways in Python. Lets not choke Python programs with design patterns that are needlessly implemented in complicated ways, where the language already provides other means! (singleton, visitor, factory, decorators, etc.)
The factory pattern in Java exists because a type / a class is not a "first-class" concept. You can't pass around a class so that it gets constructed at some other point. Python (which I believe inherits from Smalltalk) has a different object system, which makes the factory pattern irrelevant.
> I find that if I want type safety, I'd rather branch out to other languages that were designed for it from the start.
Those other languages don't have pandas, scikit-learn, pytorch. If you are building on top of Python's data science and machine learning ecosystem, it's nice to have both the wide range of libraries available in Python, and type safety. Even if the annotations get a bit ugly looking.
It becomes more helpful as projects and teams mature.
I’d say that types aren’t the last step though, I’m starting to prefer protocols and those are proving to be even more loosely coupled and more pythonic to me. They let you “type” the behavior rather than the actual type. Say there is a case when you don’t care if it’s a set, tuple, or a list because all you’re going to do is Iterate, we’ll you’d go the behavior route and say you want an Iterator. You can create your own protocols as well and it has the effect that you document what you intend to do with the objects passed to you such that it doesn’t matter what you pass as long as it adheres to the protocol you defined. It’s documented duck typing. Here’s a great video that explains how it can simplify your code https://m.youtube.com/watch?v=xvb5hGLoK0A
Try Common Lisp. Unless you tell it otherwise, Steel Bank Common Lisp treats all type declarations as assertions. They are checked at runtime. The compiler also does a great deal of compile-time checking too.
But if you don’t like to use any of it you don’t have to. So you can annotate just some code, either because you want the types checked or for performance reasons.
One caveat though: SBCL’s treatment of type declarations in this manner is implementation specific (well, SBCL’s predecessor, CMUCL, also did this.) The Common Lisp standard simply says the result is undefined if a value’s type does not match the declaration. So a more portable way to do this is to use the CHECK-TYPE macro and use declarations only for performance reasons.
mypy and Python typing have really only picked up pace once Typescript has shown that it can be done, and well. Typescript also doesn't give any runtime safety but the compile time safety gains are very much worth it. There is no reason Python + mypy couldn't get the same guarantees when you keep to the less adventurous parts of it.
For readability, I'd quite like to see a design that looks a bit more like K&R-era C, where the function signature just has the parameter names, and each parameter's type annotation gets a line of its own somewhere further down.
Annotations can come in after you've cracked the architecture. Once you are in that place you really shouldn't be getting bothered by the annotation because you are building on top of them and its helping your code completion.
They are useful if you program in a somewhat object oriented way. They are deeply unhelpful if they're all either "method(self) -> dict" or "method(input: pd.DataFrame)"
To me this seems like a solution in search of a problem. Checking for "None" is not that bad and doesn't require nested "if" statements if you apply this pattern instead:
if user is None:
return
balance = user.get_balance()
if balance is None:
return
credit = balance.credit_amount()
# etc...
Mypy also correctly detects that user/balance/etc. can't be null after each check. Of course it's better to design functions/methods that never return "None", but that's not always an option. This approach also has the advantage that you can easily log at what stage the sequence of method calls failed. Stuffing this into a bunch of lambdas makes that much harder.
Imperative styles like yours are readable, and more importantly, hackable.
Business processes are rarely set in stone and the above code can be easily modified, as opposed to a "beautiful" "pure" "correct" ball of functional yarn that must be carefully unwound and rearchitected.
discount_program: Maybe['DiscountProgram'] = Maybe.from_optional(
user,
).bind_optional( # This won't be called if `user is None`
lambda real_user: real_user.get_balance(),
)
Ugh. I don't care that this is shorter – just use a bloody if statement!
Yes, even if you have to nest them sometimes (not nearly as often as this page suggests in my experience).
But the Maybe monad does have its beauty, which the in-built Optional doesn't really provide. If this package makes people more accepting, then let them.
Just because somebody mentioned the term "monad" doesn't mean that suddenly the code should be written in Haskell.
Python allows recursion too. Are you using recursion? Why not just write it in Haskell?
Python allows function calls too. Are you using function calls? Why not just write it in a functional language like Haskell?
A monad is just a general programming concept, like recursion and function calls. One particular language pioneering the concept's heavy use doesn't mean other languages can't adapt it. To the contrary, languages get influences from one another all the time. Especially Python.
Do you like list comprehensions? This super-pythonic thing? Goes back to functional languages in the 70s and the name was coined by Philip Wadler, one of the main Haskell gurus. If you like list comprehensions, this feels like trying to write Haskell code in Python. Why not just write it in Haskell?
If I have the choice between a clean and simple to read codebase and a convoluted mess, I can tell you which one I choose. If the term "beauty" for that rubs you the wrong way then maybe we are just having a terminology disconnect.
Here's why I dislike it. There's a lot of libraries out there in Python that appear to be DSLs or have custom syntaxes that heavily use decorators etc e.g. Flask or FastAPI. In exchange for learning these custom syntaxes they offer tangible value in terms of eliminating boilerplate or solving a particular use case really well. In case of Flask or FastAPI, they make writing http/rest servers a piece of cake.
This library however offers not much benefit over the following example from Effective Python and in return I have to learn a whole new Python.
def add_ints(a, b) -> Tuple[str, bool]:
if isinstance(a, int) and isinstance(b, int):
return a + b, True
else:
return None, False
result, status = add_ints(1, 1)
Basically it's is a lot of syntactic noise for very little.
I’m not a fan of the Tuple return type. This is how golang does things and while it works there I don’t like seeing this in Python code because the Tuple tells you nothing unless you follow this convention and you have to add the Tuple annotation on top of every return type making it harder to read. I don’t see a need for it here since you’re already returning an Optional[int] (int | None in Python 3.10+). You could also raise a TypeError. Also your type checker should catch this if a or b are not an int, so you might feel safe returning a+b at this point and having the return type be simply int.
I see the point in the maybe type, but trying to introduce that into a language that only allows 1 single expression in anonymous functions has to be one of the most unergonomic things I can imagine.
from returns.result import Result, Success, Failure
def find_user(user_id: int) -> Result['User', str]:
user = User.objects.filter(id=user_id)
if user.exists():
return Success(user[0])
return Failure('User was not found')
user_search_result = find_user(1)
# => Success(User{id: 1, ...})
user_search_result = find_user(0) # id 0 does not exist!
# => Failure('User was not found')
Python is deeply incompatible with this style. No do-notation, crippled lambdas, slow function calls. If you want to write Rust/OCaml maybe use Rust or OCaml?
Current Python is. Languages evolve. Nothing wrong in importing good ideas from other languages, bonus points if long standing warts get fixed at the same time.
It was not designed to those things and there are many other languages that are way better in many aspects of software engineering than Python ever will be with type checks and other relatively minor improvements.
I consider Python as the lingua francua that gets the job done, easy to train new programmers to use it and there are several C/C++/Fortran/Rust libraries written so it can do things performant enough to be relevant. There was nothing new introduced by Python that other languages took from it which shows that there is nothing groundbreaking about it.
I think Python is the quick and dirty, not too smart glue languages with a mediocre module system and many footguns. I think Sussman pointed out that the world needs more library gluers than it needs very smart software engineer who create those libraries. Python is the proof of this theory.
> There was nothing new introduced by Python that other languages took from it which shows that there is nothing groundbreaking about it.
Not strictly true, Python's approach to readability (syntax) inspired languages like Kotlin, GDScript, to name a couple.
But the bigger picture is that Python has spawned prominent frameworks/DSLs in virtually every domain, so to suggest that it is a dead-end of innovation is not quite correct.
Very many languages only have single-expression lambdas, Python isn’t unusual in that, its just that nontrivial single expressions in Python are extremely ugly (to the point where most people think they are much more limited than they are.)
The lambda issue in Python isn’t really about lambdas, but the readability and expressiveness of expressions.
> Very many languages only have single-expression lambdas
Which languages are those? Java, C#, C++, Rust, JS, Swift, Kotlin, Scala, Ruby, PHP, and Go all allow defining multi-line anonymous functions that can contain any statement normally allowed in a function.
I prefer having failures in the type signature so the type checker makes sure they're handled, although it depends on the failure, occasionally I just raise an error like you suggest.
I see a few alternatives for the first example here already, but none that take advantage of the walrus operator:
# Assuming credit can't be negative.
if user and (balance := user.get_balance()) and (credit := balance.credit_amount()):
discount_program = choose_discount(credit)
This seems like something that the language itself should provide, or not. I would not add a third party dependency to give me these functional programming features and then write a bunch of code tied to it.
All these concepts are great and I use them all the time... but just not in Python.
Every language has tradeoffs in terms of features, available libraries, existing codebases you need to work within, etc. Python has a tradeoff of not having these functional features.
I appreciate the huge amount of effort this must have been, but these types of projects don't usually work out or stick.
To add insult to injury, type hints are not enforced nor even verified by the runtime. I find that if I want type safety, I'd rather branch out to other languages that were designed for it from the start. In particular, I find that rewriting some of my Python modules in Rust is a joy (using PyO3, but keenly following RustPython development too).