I’ll just say. Since about 3.6/3.8 the language has been growing huge. Like C++ huge.
I liked f-strings. Asyncio has its place but is waaay overused. Typing was a cool idea, but so awkward in practice.
All of it has really turned me off, personally. It’s getting to where reading libraries is just painful. The simplicity of the language was really beautiful and when I wanted type safety, etc. I’d use something else. I can still write simple Python, but it’s more all the other code I need to grok.
I find myself going to Go more and more for stuff I used to use Python for. It’s easier to set up a dev environment for other people, easier to distribute code, and gives me type safety and concurrency as first class citizens while being a very small language.
I miss dictionary comprehensions and other shortcuts from time to time. But it actually feels more ergonomic now.
All personal opinion. I’m fine shifting languages. I still write C from time to time to bang bits. I play with various LISPs to exercise my brain. I never used Python for performance critical code. I guess ML is changing that need.
But I know others that want Java to have functional features and Python to have this stuff.
I’m not saying I’m “right”, just uncomfortable in a place I used to love to hang out in.
I never understood how people consider go an alternative to python. I tried it myself for a while, and I couldn't stand the verbosity. So much code needed to do simple things, it almost felt like C again. And the error handling... Shudder.
I'm very happy python survived the 2-to-3 migration and is now thriving more than ever! Looking forward to the 3.11 speed improvements.
Every other major programming language is cargo culting as much "functional programming" as it can bolt on to its chassis. Which is then WAY overused by a small number of self-styled "10x-ers", to dump a bunch of unmaintainable write-only code into your codebase before jumping ship and abandoning its support a few months later.
Go and Python code may have significant difference. But there is spiritual overlap in the sense that Go really encourages a straightforward imperative programming style, which makes its code more accessible to a wider range of developers.
Never understood this. I've been doing coding for a quarter century. There is a reason we moved away from manual error handling to exceptions. It's easier to reason about, it's less code, it's better.
The Go fans keep insisting that this error handling in Go is new and fresh. I looked at my Go code, I saw that 2/3 of it is basically checking if a function returned an error, and I moved on. I've done it this way - I am not going there again.
And no, it doesn't force you to handle errors. You can simply not check for errors, just as you would swallow an exception.
My impression of the Go culture is that of the hip kids discovering vinyl.
As my former boss used to say - "there is an easy way, and there is the cool way".
As an anecdotal data point... I like Python and prefer it to Go for the most part. However, in the case of error handling, Go seems to get it right and Python does it very wrong. Exceptions are extremely confusing, I just can't wrap my head around them. I don't even accept, philosophically speaking, the concept of exception: there are no "exceptions" when running a program, only conditions that you dislike.
Strongly disagree with your conclusions. There are definitely arguments against exceptions in cases where you always want to understand and handle failure conditions, but Go has not got it right. The need to manually handle returned errors adds too much noise to the code, and in my subjective opinion, the code looks far cleaner when you skip the error checking. Additionally, if the manual error handling code isn't written, failures will generally be silent.
My example of error handling done right would be Rust's `Result<T, E>`. There's the ? operator for syntactic sugar to propagate errors upwards. Otherwise, to skip error handling, you're generally forced to unwrap() the result, which is a noticeable smell, and it will crash your program if the operation failed (as opposed to continuing despite the error). Finally, it maintains the error code advantage of keeping the control flow explicit and visible.
We've had decades of erroneous error handling from C codebases to learn from. Frankly, it's insane that we have a modern language that still uses error codes in essentially the same way.
Rust absolutely nails the error handling department.
Python style exceptions, pros:
- rapidly drop through layers when needed to get to a stable entry point
- easy to add errors deep in the stack
Cons:
- basically a limited GOTO, therefore side-effect, therefore not composable
- no way to know if what you are calling is gonna throw on you
Java pros:
- have to declare exception types, so at least you know what you are dealing with
Cons:
- you have to always declare exception types, which can be tedious
- same non-composable GOTO-lite behavior
- adding an exception deep in the stack is tricky
Go pros:
- errors are values, therefore typed and technically composable
- no surprises
Cons:
- tedious, verbose
- can't rapidly unwind unless you panic
- rarely composable in practice, requires manual unwrapping in most cases
- adding an exception deep in the stack is tricky
Rust Result pros:
- strongly, statically typed
- no surprises
- higly composable
- can map over it
- rapidly unwind in a composable way with ? operator
Cons:
- adding an exception deep in the stack is tricky (but often amenable to programmatic refactoring)
It's no wonder Rust is the SO most loved language 7 years running.
Python actually has a Result type library which I really like, but it's been hard selling my team, and you really need buy in. But I'd give it a swing.
Sorry, but Rust's error handling is pretty clumsy compared to what you can do in some languages.
Example:
You write a function that calls 2 thirdparty libraries, both of which can fail. The typesystem in Rust is unable to express that the resulting error with be libAError or libBError. It is lacking anonymous union-types. Even if union-types have been added, you'd have to define one first and you'd have to use unsafe (at least from my understanding).
This also impacts user-defined error-types of course, but it makes errorhandling when using other libraries very annoying. You always have to define new types and do wrapping.
Another example:
You have a function that calls 2 thirdparty libraries, both of which can fail. The two libraries have decided to use a customized error-type (and not the built-in one) because they need to carry around some extra context or need some other feature, but they still support everything that Result does but in a slightly. Now you need to manually convert those or learn the interface of this specific error-type, because there is no way to abstract over different "Result"-types. Why? Because Rust has no support for Higher Kinded Types, which would be needed to do so.
There are more examples, but these are two that are immediately relevant to pretty everyone using Rust. And yes, Rust has done a lot of things right and especially better than C or C++. But when looking at it as someone who has used other high-level-languages I can say it definitely did not "absolutely nail" it.
I agree with your first point, but it's worth noting that if someone is willing to pay a small runtime cost, they can just return a Box<dyn Error> or anyhow::Error in these cases ergonomically. Additionally, there is the thiserror crate which makes defining the union type you described trivial.
Your second point is dubious; I've never seen any crate use anything other than Result. Afaik, it works everywhere and is so idiomatic that if somebody suggested reimplementing it, I'd immediately question their credentials.
> Box<dyn Error> or anyhow::Error in these cases ergonomically
This comes at the cost of losing the precise information what error-types could occur and makes it harder to read the code compared to a language that can use its typesystem to model that.
> Your second point is dubious; I've never seen any crate use anything other than Result.
Maybe. And you also barely see something like Result being used in Python. Is that because Result is bad? No. It is because it is _hard_ to use Result in Python, both of the lack of ergonomy compared to e.g. exceptions and also because it's not standard and people will look at you funny.
So why is pretty much everyone in Rust using Result? Because using your own error-type causes exactly the problems that I mentioned (and more). So no matter how you look at it - this is a shortcoming of Rust and hopefully something that the Rust team will improve in the future. (E.g. https://github.com/rust-lang/rfcs/issues/324)
> Sorry, but Rust's error handling is pretty clumsy compared to what you can do in some languages.
I agree with clumsy part, if that means error handing being more explicit.
I've personally never faced 2nd example, so I'll limit my discussion to 1st one. If you don't want to use an extra crate (anyhow/thiserror), you could always do a `map_err` which in my opinion is a superior choice as you get to know what kind of error you're dealing with and how to map it to your domain error.
> I agree with clumsy part, if that means error handing being more explicit.
Yeah no, that is not what I meant. It is possible to make error-handling even more ergonomic while staying fully explicit. I agree that implicit/magical/runtime error-handling is not more ergonomic.
> If you don't want to use an extra crate (anyhow/thiserror), you could always do a `map_err` which in my opinion is a superior choice as you get to know what kind of error you're dealing with and how to map it to your domain error.
I briefly looked at those crates, but while they make some parts better, they make others (for instance the explicitness) worse. On top of that, they are built using macros, which is totally okay from the perspective of the library and comes at a cost.
Overall it would simply be better if Rust's typesystem were capable enough so that those macros were not even needed.
All of that being said, Rust's error-handling is still good. I'm just bothered by the "Rust nailed it perfectly" attitude. It's better to be aware that there are even better solutions out there, so that we can go and improve Rust even more.
That's exactly my point: you should not have to define and use an enum here. The compiler should be smart enough to figure out that the result will be `Result<T, Error1 | Error2>` without the user doing anything here.
I also much prefer explicit error handling and really disklike exceptions because every function can fail in a myriad of ways and you have no idea how. Rust does it much, much better than Go though, thanks to sum types and syntax sugar for early returns (`?`).
But the flipside is that encoding every failure condition in the type system quickly becomes unfeasible. Every allocation can fail. Every array/slice indexing can be out of bounds. Some errors might just not be recoverable at all while maintaining consistent state. Go has the null pointer problem...
That's why both Go and Rust hide certain error conditions and have a fallback mechanism (panic/recover).
There is a balance between the two idioms, and which one is right depends on the language and the facilities the type system provides.
> Exceptions are extremely confusing, I just can't wrap my head around them.
Interesting. For me it's very much the opposite. What about them do you find confusing?
> I don't even accept, philosophically speaking, the concept of exception: there are no "exceptions" when running a program [...]
I think you're looking at it wrong. Exceptions, as implemented in the languages I know, are more about having a framework of allowing inner/library code to drop the ball and run away screaming, without making more of a mess.
> are more about having a framework of allowing inner/library code to drop the ball and run away screaming, without making more of a mess.
It also allows the calling function to ignore the mess and hope that something further up the call-chain will deal with it.
Exceptions circumvent the normal control-flow of a program, and that makes code less obvious to follow. When I see foo calling bar() in go, I know where, and if, errors returned by the function are being handled, just by looking at foo.
When I see foo calling bar() in Python, and there is no try-except block in foo, I have no idea what happens to any errors that bar may return. I now have to check everyting that calls foo, which may be multiple other places, each of which could have a different Exception handling, or none, in which case I have to also check all the callers of that caller of foo, etc.
And even if there is a try-except, I have to check whether except catches the correct TYPE, so now I also need to be aware of the different types of Exceptions bar may raise ... which in turn depends on everything that bar calls, and how bar itself handles these calls, etc.
Yes, error handling in Go is verbose. Yes, it would be nice if it offeres a little syntactic sugar to improve that.
> And even if there is a try-except, I have to check whether except catches the correct TYPE, so now I also need to be aware of the different types of Exceptions bar may raise ... which in turn depends on everything that bar calls, and how bar itself handles these calls, etc.
Why? Keep in mind in go, you almost certainly don't check the type of the error. Why hold python to a hire standard (or the opposite: why don't you pass errors with types in golang, and handle errors of only a particular type or set of types?)
The answer, in both cases, is of course the same: errors are almost always handled in one of two cases: basically at the callsite (either at the callsite or in the parent function), or they are handled in "main" in some form of catchall.
Exceptions are really great for this. You very much don't worry about the things that might be thrown by a child of a child of a child of foo() that you call, because the abstraction shows that you shouldn't care (unless perhaps foo documents explicitly a particular function it throws). You don't need to waste code or mindshare on something you really don't care about. In go however, you do!
Because unless the caller of foo uses a catchall, it may not actually catch the exception raised by bar. Lets say bar opens a file, and callerOfFoo says `except FileNotFoundError:` ... what if bar opens a file that exists with insufficient Permissions? Then it's `PermissionError`, and callerOfFoo won't catch it.
Sure, its possible that callerOfFoo is prepared for that, but my point is, I don't know that unless I check its code.
Because I can visualize Go errors completely. They're simple links from one place to another. They are straight fibers connecting my modules and functions to each other. They always work exactly like I expect them to.
With python and other exception based languages? I have made hundreds of commits in my lifetime after being surprised that some random 3rd party library throws a nonstandard exception and breaks all sensible control flow that everybody assumed was happening.
This is the reverse of the issue that the parent mentioned though!
Consider a go function foo which returns some value and an error. What can you do with that error? You mention control flow being broken by python and others, but the control flow of
Are all the same! And you can't do anything different in them, because in go, you have no knowledge about the error. Is it aa temp error you should retry? Who knows, are you going to parse the error string to figure it out? The only think you can do in go to an error from some library function is pass the error, possibly adding some additional context, because otherwise you may be mishandling the error.
In exception based languages the "pass the error" case is done for you, but if you do want to retry retriable errors, you can actually use a language feature to know that. In go, you have to hope the library creator created an interface (and that all of the possible errors that could percolate up have compatible error-extension interfaces!) that includes some kind of type information, which almost no one does.
You're talking about "sensible" control flow, but go doesn't have it!
I argue that errors.Wrap and errors.Is from the Go stdlib solves this problem.
Libraries can export their own error types and you can check if they threw that error type. I use this pattern all the time to handle special errors differently. Used it to implement optimistic locking which does ~not~ simply propagate all errors upwards!
Of course it is up to the package author (or yourself) to write functions that return specific wrapped error types. Otherwise we're stuck in the situation your comment describes.
What do you mean by creating an error interface? It's a one liner to make a new error case:
var ErrFooHadTemporaryFailure = errors.New("temporarily failure, retry later")
Please indulge an ignorant question from a Go-curious outsider: are ignored error returns flagged/grumbled-about? One of the enduring PITAs with C-error handling is that AFAIK there's no good way to highlight slipshod code. For example printf() returns "int number_chars_written", system() returns "int status_code": ignoring the first case is ubiquitous and having to cast it to (void) is noisy, ignoring the second is a bad smell and warrants at least a comment.
ohNoes() can call couldFail() and ignore the error return completely, without the compiler complaining.
This is intended. Error returns are not special; they are just another return value. Error handling by the caller includes "I don't care if there is an error". Yes, in some situations that's a bad code smell. In others, it really doesn't matter. Go puts the responsibility to decide which applies in the hands of the programmer.
To get the same effect in python, I'd have to wrap the call in
I'd hope it's a pretty non-controversial thing by now that software shouldn't silently fail without indicating something's wrong?
If I saw the latter Python code in a code review I would squirm. I would scrutinize it very seriously and if there's some really odd case where you really want this, I'd require at least a logger line or a comment to explain why in the world this is desired behavior.
> eg. how often do programs check if `printf("something")` actually succeeded?
Probably not very often? But they probably should?
If you're printing something, there's probably a case where you care about that being printed, and if it's not in that case, that's bad?
This also applies to logs too. Just because you're not always looking at the logs doesn't mean they're not important when you need them.
Sure, if you have some chatty logs going through a distributed logging system you might not want to crash the whole thing, and in that case you might want to ignore the exception. But this is a very fringe case.
Error handling for an API etc usually make the most sense in a central location (some middleware etc) so why would I check that each and every function call was successful.
I do like go, but the error handling is hardly the best thing about it. It’s just different. Neither is wrong.
Yes, but panics are intended to represent an actually exceptional situation, like dereferencing nil, or the machine running out of memory ... things that normally shouldn't happen, and from which the logic cannot easily recover.
> Error handling for an API etc usually make the most sense in a central location (
Which is doable:
if err := couldFail(); err != nil {
return err
}
I can let errors bubble up the call stack as far as I want, I just have to be explicit about it.
They are non-local jumps. They make reasoning about the code very difficult. When you read code, you have to treat all conditions symmetrically and equally likely (e.g., whether a file exists or it doesn't). Using exceptions for control flow, as is done sometimes in python, forces an unnatural asymmetry between cases that I find confusing. And this is just when there is a single exception at stake. Typically, several exceptions fly invisibly over the same code and it becomes impossible to understand (unless you assume that no exceptions occur, which is the wrong stance to take when analyzing an exception-riddled code).
TL;DR: raise and except are a corporate-friendly renaming of goto [0] and comefrom [1].
> When you read code, you have to treat all conditions symmetrically and equally likely
Pretty sure I don't do that when I read code. When I see "list.add()" I don't consider running out of memory and the operation failing equally likely to the list being added to. And if it did, in 99.99% of the cases I'm fine with it just bailing, because there's not much else to do at that point.
I agree that using exceptions for what could be considered normal conditions is not great. Trying to open a file that doesn't exist isn't by itself an exceptional incident. The calling code might consider it an exception and decide to raise, but the IO library shouldn't.
> Trying to open a file that doesn't exist isn't by itself an exceptional incident. The calling code might consider it an exception and decide to raise, but the IO library shouldn't.
This is a way of thinking that I don't get. Yes it's fine that an IOLibrary has to be able to handle a FileDoesNotExistError condition. But from the caller POV: I clearly instructed you to open a file of some description. I expect you to return to me a handle for said file. Everything that does not match that expectation is exceptional (to the caller).
And it is that violation of expectations that is communicated by (Python's) exceptions.
> Everything that does not match that expectation is exceptional (to the caller).
No. If you want to read some data from a file if it exists, and continue merrily along if it doesn't, then you cannot simply check if the file exists and then try to open it. That would lead to a race condition. The file could for example be deleted in between the two calls.
The only proper way to handle that is to try to open the file, and if the result of that is FileDoesNotExistError then you continue along merrily.
If the file the caller tries to open is a settings override file, say, then it's not exceptional that the file does not exist.
> Everything that does not match that expectation is exceptional (to the caller).
Sure, but saying "open this file" and having the response "nothing here, boss" is not exceptional. It's pretty normal. Isn't that just one of the two obvious options when you try and open a file? To me, exceptional would be something like "this file was open and now it's deleted."
I am convinced that a mixed goto-comefrom model would work best; consider this pseudo-syntax:
try {
some_code();
} catch (e) {
var fixed = handle(e);
if(fixed){
continue; // goes back to where the error was trown
} else {
beak;// gives up and continues from here
}
}
In fact, if we want to go all-in with this style, we could remove the if/else construction altogether (from the language!) and do everything using exceptions. Since they are good for control flow, why stop here?
This is much clearer, since the "normal" flow of execution is emphasized, and it avoids using an ad-hoc legacy if/else construct. Moreover, it has the advantage that statement_2 can be hidden into another part of your program, wherever you want to catch this exception.
I was thinking more of making a parallel with effect handler system currently in development for Ocaml and other research languages, you do something similar to throwing a continuations[0][1][2].
It is proposed as an alternative to both the current monad-oriented approach used in Haskell and the procedural side-effects of OCaml.
To my understanding the base idea is that you can register "handlers" for (user defined) "effects" that can be emitted from normal code.
Once an effect is emitted the handler receives a continuation and can decide what to do with it (call it immediately, register it in a async/await pool, repeat it 5 times).
It would offer a type safe way to implement async/await as library code, which sounds quite cool.
The proposed try/catch/continue was a silly bastardization of this idea.
If people overuse try/catch, the code is close to impossible to read… but try/catch isn’t meant to be applied liberally throughout the code. Don’t let someone’s misunderstandings and bad code ruin it for you.
Typically you just care to have one or two primary places where you actually care if something has gone wrong.
"Exception" is arguably a historical term in Python - it comes from languages where exceptions really are exceptional and very rarely handled; but when they're used for regular control flow, yeah, that's definitely a misnomer.
But our vocabulary is full of such things. A "method" is not a particularly descriptive term given what we use it for these days, either. At the end of the day, so long as everybody knows what it is, it's not a big deal.
Conceptually, though, it can be treated as an error monad.
> Conceptually, though, it can be treated as an error monad.
Exceptions are very much not a monad, that's one of the biggest pain points about them. You can't map over exceptions. They are a control flow statement.
They're a monad with an implicit hardcoded mapping function that's basically equivalent to Rust's "try". Yes, the fact that you can't change the function is a pain, but it doesn't preclude reasoning about them in this manner.
Go's error handling is one of its single worst features. It would be much better if if had supported algebraic data types (which would also eliminate two other abominations: `iota` and `nil`, with the former being replaced by type constructors as sum types can act as enums and the latter would be replaced with an `Option` type) along with some kind of pattern matching.
If you're not familiar with algebraic data types, they're well worth learning about, and not a difficult concept. Once you use a language with them, heading back to a language without them feels like developing with one hand tied behind your back.
There are indeed exceptions when running a program. Exceptions, as a construct, are baked in at the hardware level. When a program page faults, or tries to divide by zero, or runs an invalid opcode, the CPU will jump to an exception handler in much the same way as the higher-level constructs in Python and elsewhere.
Sure, but people here are talking about exceptions as a control-flow feature of high-level programming languages like Python. It has nothing to do with cpu exceptions or interrupts.
I don't think that's a good argument... otherwise we would like to honor the prominence of JNZ, JNE, JL, LOOP and other "spaghetti code" constructs used deep in CPU models but that are just not appropriate for human readable code.
I don’t know if this helps or just makes it more unacceptable to you, but exceptions in Python is more of a second channel of communication between parts of programs.
Since messages in this channel propagates up the call stacks they are very handy to stop an application and therefore used for error handling.
But they can just as well be used for all sorts of other messaging. In base Python they are used to communicate that you’ve reached the end of an iteration.
If you’ve ever made a function that returns both a value and a status in as a tuple, chances are you are better off using exceptions to communicate the status, especially if there is a “stop” status.
This is true, that's exactly what exceptions are. One could argue (I would) that this is a very powerful part of Python, but the indeterminate nature of the exception (where is it coming from, how is it defined, what should I provide to my client) makes it easier to catch exceptions as they percolate upward, but more difficult to correctly know how to handle them (especially as the permutations approach infinity as underlying libraries get upgraded).
Golang actually has a back-channel communication path that is somewhat similar as a second channel of communications. (Actually they're called channels!) They're a first-class and extremely powerful feature of the language. You can even run a for loop over them, block or not block while waiting for an incoming message, etc.
Here's a great video that talks about use cases and the patterns in using them, and they pair great with goroutines (and you don't have to sprinkle async before every function, either): https://www.youtube.com/watch?v=f6kdp27TYZs
In most of my code, exception handling happens in one file, one function. You throw an exception, and you let it propagate until it is handled. Why is this so hard?
Because where the code that handles it is not the problem (in fact, optimisting that for simplicity could cause other problems). The problem is what state is everything left in when the exception is thrown? What if you're halfway through writing to a file/network socket/screen/shared resource when an exception happens?
That should be cleanly handled in a language like Python and others, where the context managers clean up the resources when exiting a scope, normally or after an exception.
It's like stack unwinding is a new concept or something.
You're missing the point. The OS resources might clean up, but that doesn't mean the application's world (some of which may be a file on a server on another continent) has been left in a known good state.
Trying to understand your point. If you try to open a file or a network connection or divide by zero, you don't handle any exception in that calling function, but instead let them propagate all the way to the top?
What are you going to do with that? Keep returning errors until the calling function gets the error? That's what exceptions do for you. Do you have any guarantee that there is no bug in the error-handling code somewhere?
And what will be the final result of the error? An error screen, right? That's presentation layer. Any fatal error should halt execution, be thrown to the top, and manifest itself as a message in the presentation layer, right?
This really is the only practical area where Go is superior to Python. No messing with 3rd-party libs, no compromises, no hacks... Everything else is not really critical for most people, and largely not worth losing all the nicer features of Python for; if they could just fix it once and for all in stdlib, just blessing one compilation format and making it well-supported on all the big platforms, it would be a big step forward for the ecosystem.
Agreed. If python had a sensible self contain executable where I don’t have to fight and figure out how I’m going to share this program with it’s dependencies and imports I would use it over go in non speed needed applications.
I'm not associated with the nuitka folks, but I want to mention that I've successfully used it to make self-contained executables for python projects. I haven't used it for anything huge, but so far it's worked flawlessly.
The new type system goes pretty much against the idea of duck typing, I'd consider that a pretty big fundamental change. And it's pretty half baked on top (e.g. `a: int = "foo"` goes through the python interpreter without even a warning).
And while there it certainly does provide a lot of benefits, it does make the language look more and more like a huge pile of patchwork and afterthoughts. It neither feels simple nor consistent and the tooling is a mess as well, both overly complex (e.g. half a dozens tools to do basic type checking) and lacking really basic important features (e.g. easy way to compile ship able binaries).
Especially after all the pain of the python3 transition, it feels just frustrating how much of a mess it all is. The "only one way to do it" philosophy seems to have been thrown overboard a long while ago.
> The new type system goes pretty much against the idea of duck typing
That's impossible, because it's not a type system at all.
It's type hinting, effectively a documentation feature, and sure, you can misdocument stuff. If people started building tools that rely on it to hack an actual type system on top of it, well, it's their problem. Duck typing is not affected by it in any way.
> And it's pretty half baked on top (e.g. `a: int = "foo"` goes through the python interpreter without even a warning).
Well, yeah, because that's a compile-time check, not a runtime check. Use Pydantic if you want runtime validations.
> The new type system goes pretty much against the idea of duck typing,
It's very much orthogonal, due to the aforementioned distinction. Type annotations are compile-time. Duck-typing is run-time. You can still type-hint something and then call an attribute that isn't on the interface, and you can suppress the type error with `# type: ignore`. But also, if you know you are using quack(), you probably want foo(duck: Duck).
> The new type system goes pretty much against the idea of duck typing
How so? Python’s Protocol and TypedDict have their limitations but are structurally typed.
> `a: int = "foo"` goes through the python interpreter without even a warning
As long as you’re validating inputs then you don’t need type validation at runtime. Input validation ensures that static types are representative of runtime types
Once you start adding Protocol and TypedDict you are (almost) back where C++ and Java are with virtual classes and interfaces. The point of duck typing was that you don't have to write all that boilerplate and stuff just works as long as the right functions are there. That's lost once you have to declare what you want to use.
> As long as you’re validating inputs then you don’t need type validation at runtime.
The issue is that type validation is done by a different tool, that follow similar, but not identical rules to the runtime. So it's very easy to create code that passes the type checker, but fails at runtime or vice versa. Type validation is something Python should be able to do itself, it's part of the language after all.
I'd heavily argue against "stuff just works". Without type annotations it's this wild west where stuff fails in prod because you don't have tests that cover every possible code path. With static type analysis, you're able get automatic test coverage for a huge swath of your codebase.
I can't count the number of times I've seen None-related errors in prod because some function deep in the business logic can't handle a None parameter.
Another huge benefit of type annotations is that it adds friction for clever code. As I've retroactively added type annotations to old code, I've discovered wild polymorphism. Some functions required upwards of 10 type signature overloads to account for all the polymorphism -- engineers have to remember all that polymorphism! Engineers won't write extremely polymorphic functions if they also have to write the convoluted overloads.
> The point of duck typing was that you don't have to write all that boilerplate and stuff just works as long as the right functions are there.
As long as your definition of "just works" is "run, crash, fix something, run, repeat". I remember the pre-type-annotation days, and still live it with some libraries. I find it quite unpleasant, but hey I guess some folks like the "freedom".
> So it's very easy to create code that passes the type checker, but fails at runtime or vice versa.
In theory, yes, in practice, not as much as you might think. If you actually use mypy with strict, which is actually really hard to do, you will find very, very few surprises.
This typing annotation system is just as half-baked as Python's class system. No, having syntax sugar for instancing "objects" that amount to new dictionaries is not an alternative to proper OOP.
And even having a "proper OOP system" means little when you have to dig knee deep in class hiararchy. Source: just had to debug a Java application today. Not my idea of fun.
Sometimes, planning stuff from the ground-up goes a long way to reducing future "code debt". Otherwise, leaving it at basics is sound enough.
I feel the same, but about more subtle features like the walrus operator, the dictionary merge operator, or proposals that fortunately weren't accepted [1] which I feel do not add much to the language.
Type annotations, imho, are actually useful and not really hard to read.
For me, type annotations has been the single biggest ergonomics improvement to come to python. It's a very simple type system compared to many other languages (definitely simpler than rust, haskell, scala, java, and c++, I find it easier than go's generics but I am not practiced). I don't see how they add complexity to the language. In fact I find it limiting if anything.
Walrus is...fine. I don't use it much, but I defo see how it's one more thing.
Python's biggest complexity, imho, is its almost unrivaled dynamicism. There's just too many degrees of freedom at times.
The import system continues to be a nuclear footgun. I consider myself a python expert with 15 years under my belt and today I was still fighting with imports in a pytest suite. Like wtf.
my main issue with the typing is circular imports, and how ultimately for a lot of configurations the best solution is to just skip the typing that one time. Even "if type_checking:" often simply can't cut it
I want to be able to use typing everywhere if I'm going to use it, it really grinds when I have to selectively not use it.
That being said, im glad typing is in the language :)
Configuration hoisting and library layout to avoid circulars is tricky. The way I structure my libs, lets say ./foo is my main lib, I'll have ./foo/types/.py with most of my primary schema models (I use Pydantic heavily). Then ./foo/config/.py manages application config. Basically everything is pulled in with env vars / config files - I rarely use CLI flags for application-level global configuration - the rest are passed in to entry point functions.
What this means is I can import my AppConfig class anywhere (except types and config submodules) without circular imports, construct them, and they will be initialized with the configs from the env.
Occasionally I have to bust out ForwardRef annotations and model.update_forward_refs() but that's pretty rare. Basically, the imports are structured so that it's as close to a DAG as possible.
Definitely check out pydantic, it makes it really easy to do 12-factor style application config.
I haven't done a lot with CLI-flag global config, but if I had to, I'd use something like dependency-injectors to wire it all together.
Dictionary merge is a weird one to single out to me, it's an existing operator (|) and functions the same way as a set union over the keys (which uses that operator) in concept, it's a very natural thing that doesn't feel like an expansion of the language, just behaviour you'd expect from the datatype.
A problem with dictionary merge that doesn’t exist with sets is what to do with the values if you have a key in both dictionaries.
Do you keep the value on the left, the value on the right, error out, try merging the values (if so: how?), add a parameter allowing the user to make that choice, require the user to pass a lambda?
I don’t think there’s a ‘best’ answer here, but if you want to make one of them easier to write as a|b, you can only support one.
At the end of the day people were doing {**a, **b} or dict(d1.items() | d2.items()) (which is subtly different in that set order is arbitrary so which "wins" in the duplicate key case is arbitrary) in the wild, so I think supporting a natural dict-level union is the better option, even if it can't be all things to all people.
But that is not really the `|` operators fault. You get the exact same behaviour when you create a dictionary.
>>> {1: 1, 1.0: 1.0, True: True}
{1: True}
So `|` between dictionaries behaves just as you'd expect, if your expectations about creating a dictionary are correct. I think that's very reasonable.
Honestly the set operators too are just screwed: I end up using `set` surprisingly much yet I'm always going back to the docs to figure out what the "or" symbol was, etc.
I love types. But as first class citizens. I started in Pascal and C. And they’re one of the reasons I’m enjoying Go for the kind of work I did with Python. Usually, if it compiles, its right.
I’d be cool with a statically typed flavor that wouldn’t allow for the type gymnastics.
But that’s gross with the None situation where you’d have to fall back to magic values vs ‘nil’ or similar concepts. None was one of my favorite things back when other languages didn’t address it.
And a lot of my opinion is probably based on the fact that I came to the language in the late 90s from a Pascal/C background. Probably just getting to old :).
I felt the same way years ago, but in my case, that frustration was born out of lack of familiarity. With familiarity, I came to like the changes and directions the language was going. Maybe it's Stockholm syndrome, though.
In the end, I'm glad that Python is changing to meet the changing developer zeitgeist, even if I might have to play catch up with new features sometimes. It means Python will stick around and won't be considered "old and outdated" and fall out of use because it failed to keep up with what the developer community wants and expects. It's also the same reason why I'm glad that Go got generics.
For me Python feels like an accelerating train, as long as you are already on and don't get off then its great but it gets harder and harder to catch up for everyone else.
I love stuff like 'match', the walrus operator, Typing, etc. but it definitely feels like a lot to grok and it will be increasingly hard for new programmers to read arbitrary python code without grokking all of the new stuff.
It's really not a lot to grok, at least by most other language's standards.
PHP has a long history of doing things in a "right way" that subscribes to copying whatever one language is doing for 5 years. It first did things the C way, then the Java way, then got inspired by Ruby and Javascript, etc. The features are there, but executed in very awkward ways.
Ruby as a language explicitly blesses many ways to do the same thing.
Every new release in C# seems to bring in as many features as you'd get in 4 or 5 major Python or Java releases.
I look at the Python I wrote 8 years ago and it's _fine_. Even Python 2.6 code written with large frameworks looks similar enough to modern Python. The same can't be said for Javascript, Java (Java's best practices have changed a lot since then), C++, or C#.
> It's really not a lot to grok, at least by most other language's standards.
Yes, but people have been attracted to Python largely because it's not like a lot of other languages. It is/was concise, simple, dynamic and fairly easy to learn. I think some of the new features, even if they don't make it a worse language, make it less "Pythonic", and so tend to undermine its comparative advantage. For experienced programmers the new features might not seem complicated, but python is used by a lot of people who are not in that category, including people for whom software development isn't their primary job.
I agree for the most part; I have not seen the use of the walrus operator or many new features of the last two major Python releases.
Nontheless, as a user of the language I just don't see people trying to contort their code to use these things. The community has less attraction to flashy features than other languages, so I don't see people getting compelled to use things they don't care for.
I started learning python last year. There really isn't that much to the language even with the new features. It's nowhere near Rust or C++ for example.
I use all of those! I used to program Perl and Rust though, so maybe my preference for complexity in languages is higher than yours.
I understand your argument though. If you picked Python because of its simplicity, and now it has a ton of new syntax and features, I can see how that would be difficult to tolerate.
match/case is similar to other languages. Walrus is just assignment-as-an-expression. The subtleties of dict mergining don't matter 99% of the time. Type annotations are great and not hard to learn.
I don't think most people who are using Go really want to write their own contains method for slices every time: it seems antithetical to being productive and doing what you actually want to do and it is most definitely a shortcoming of the language. Fortunately with generics people can hopefully need to do that a lot less!
Just in case you forgot: Contains is a (nearly) standard lib function these days. Go did get generics, which are type safe (unlike some other languages I could mention). Before that, you had to write a slightly more clunky function yourself, but it was still a one-liner.
Second: it's not a good function to use. Checking if something is present in an array is not something you should do often and thoughtlessly. It has its uses, I agree, but in general a "dict", or if you really need an array, binary search, should be used. Where's the one liner for that in python? O wait, there isn't one. Go does have it, though, since a long time.
But it's a good thing we no longer use LoC as a measure.
Note that that finds the index to the left of the insertion point for the value given, but doesn’t perform a containment test; you have to also check that the index is in the list (instead of off the end) and that the value at the resulting index equals the value of interest to test containment. Which can be a one-liner without redundant calls (with walrus), but is a bit ugly.
(Of course, for a custom class you can just wrap it in a __contains__ method and then just use the “in” operator.)
> you have to also check that the index is in the list (instead of off the end) and that the value at the resulting index equals the value of interest to test containment
True. However, the hardest part of the binary search algorithm is implemented through bisect, so it still saves a lot of developer time.
You can do that too, using the bisect, provided you have the __len__ and __getitem__ implemented:
class C:
def __len__(self): ...
def __getitem__(self, i): ...
def __contains__(self, x):
i = bisect.bisect_left(self, x)
return i < len(self) and self[i] == x
The first two lines are setup, real code would already have done that.
You could maybe count the import, but you'd have to do that once and could do multiple bisections, so it's amortized.
Setting the variable doesn't count because you do, of course, need a variable to perform an operation on a variable. Sorting the array also wouldn't count because you can't use binary search if the array isn't sorted.
I don't know Go, however checking if an element is in an array, even unsorted, is likely faster than anything involving a linked list or a hash map unless the equality test is very expensive (maybe) if the array is small enough. And it is often small enough. I don't know how small, I guess it depends on the computer and the implementation, but 100 is probably still fine.
So, yes, let's have a contains function on (unsorted) arrays.
Are you really shaming us for wanting to check the contents of an array? god damn.. how can you get anything useful done, or even meet the requirements? What you are proposing goes beyond having your hands tied and being blindfolded.
Loc is a bad code metric when measuring at the scale of codebases. However, when measuring the succinctness of a language it has its place.
And, contains is a perfectly cromulent function to use unless there's a reason not to. At a million items it would be a bad use of contains if you were to lookup up multiple items, but modern day programming requires both knowledge of the code structure and the data.
Yes, python's type hinting is pants.
Go's goals are simplicity, python wants to be the working man's language.
How does that make the language half-baked? Go didn't have generics until recently which made utility functions like that complicated. C doesn't have them either.
But actually, try python with mypy and mypyc. It's still a little experimental, but if you stick to simple constructs it works well, and you can compile straight python to fast, static libraries, without using goofy cython syntax.
If by extending you mean that you want to have a type that includes ALL the functionality of another type, without needing to fill in each method, that's already supported.
In your new struct definition, include the desired base type as a member - without giving it a name. That embeds the unnamed member directly, and your new type automatically gets all the interfaces and their underlying implementations. As a python-person I always found this a somewhat odd mechanism, but from a functionality perspective it satisfies what I have wanted to do.
I'm not sure I get this. Asyncio is a serious commitment. You don't really want it unless you really want it. And even then in the libraries you may want both the sync and async path. I feel like most asyncio libraries would just not exist without it - so if you're not interested in that area, can't you ignore them?
What modern mainstream language is smaller than Python? Ruby? Javascript? Go is surely smaller but then your library surface area as well as code since increase correspondingly.
JS's standard library is smaller than Python's for sure. I... want to say Rust's standard library is as well, but the traits that are present tend to have a looooooot of methods (check out all the methods on Result!).
Python has a lot of things in the standard library, though I think the tendancy has been to stop adding things (case in point: I believe there is very little argument against putting requests into the standard library. I get why they don't but I disagree with the reasoning)
Personally I wonder if all this is the reason that it seems like Ruby is having an uptick again. Seems like I hear more and more about people coming back to Ruby.
Since you mention Go it seems like you’re mostly using Python for webdev. Webdev has always sucked in Python, unless you just want to make a CRUD app in Django. The GIL, the lack of static typing make it painful. Python is also not particularly efficient.
These days Python is targeted towards general scripting, data processing, scientific computation, etc. It’s really good as a glue language or for anything involving numpy. I use Python every day but I would not use it to build applications.
Web development in Python is a lot of fun nowadays and has scaled just fine (we serve a few billion requests/ month). FastAPI makes writing APIs easy as well (it powers https://internetdb.shodan.io) and for deployment check out uWSGI or uvicorn.
A billion requests per month is 400 requests per second. My toaster can do better than that :)
Not to say that your product isn’t capable for many use cases, but the fact that the 1krps range is something impressive in Python world is indicative of the issue: Python is not built for speed, it’s built for convenience, and increasingly it’s competing against more modern languages that offer both.
It can do a lot more and I didn't say it was impressive. What I am saying is that Python won't be the bottleneck for the vast majority of websites and that it will scale. And the raw performance of the underlying language is rarely the thing you need to worry about when writing a website. If you like Python then there are a lot of robust options available to create a product that scales.
I'm curious, how is FastAPI "relegated to the backwater of Python frameworks" when it is the fastest growing (by popularity) Python web framework[0], and it has the endorsement of the folks behind some of the most used Python frameworks, as well as adoption by everyone ranging from small startups to the largest corporations[1]?
> as long as Tiangolo doesn’t bother allowing the community to improve it.
I don't see it that way. I see a rapidly growing library still in pre-1.0 and a BDFL trying to wade through a mountain of "contributions", a lot of them garbage, trying to keep code quality high in one of the most pristine and readable source code repos I've ever seen.
Also they have crowdsourced 17 languages for documentation translation. And it's up to date. That's bonkers. You're lucky if english documentation is accurate and up to date in many open source codebases.
Lol @ “keeping code quality high”. Have you even looked through his source code? You know, the one with functions spanning dozens of parameters that literally have no actual documentation?
Give me a fucking break. Let’s not even talk about the fact that it has well over a thousand open “questions”.
Yeah, actually, I'm super well acquainted with it, are you? Normally I hate the "self-documenting code" trope, but in this case, actually think the vast majority of these parameters are actually self-documenting, and also, everything is type-annotated. Let's take a snippet from the Dependant class:
What are path_params? They're params parsed from the URL path. What are query params? The same, from URL query component. Header params? Params pulled from the request headers. Shocking. If you actually sit down and read through FastAPI it's remarkably easy to get your bearings and start hacking around, despite the "no actual documentation" (I think type annotations count as documentation).
Compare that to code bases like Django, Celery, Matplotlib, where it's kwargs, kwargs, everywhere and nary a type hint to speak of.
The number of open issues is due to the massive popularity of the library (46K stars) compared to the number of actual maintainers. Some actual questions on the Issues tab right now:
- How to add admin page?
- Is safe to remove value from list in asyncio
- Best practice to run FastAPI on Cloud Run with server port as $PORT
These are all n00b questions that could readily be answered with general-purpose resources, but this is the blight that super popular FOSS libs and their maintainers have to deal with.
I'm in fact super acclimated with that file in particular. I've had zero problem jumping in and hacking around with it (been trying to cook up a FastAPI-like endpoint interface for async jobs/tasks a la Celery). Not a single line is surprising if you slow down and actually read the code - and I consider myself less-than-great at actually reading other people's code. It took me all of ... 15 minutes to find my way around, figure out the machinery I needed to hijack, and maybe another 45 to get a crude MVP of my concept working.
Honestly I prefer it over reading documentation and then having to mentally map to the codebase. I wish I could write this code well.
You sure you've actually read any of it, or did you just skim, see no docstrings, and decide it's crap?
Actually my primary use is CLIs, integrations, and such. It used to be a great systems language (1.6-1.8, 2.x). But now I can’t rely on an OS having a reasonable version, or of libraries, so there’s the distribution problem.
It’s really only good for apps or things I (or other Python people) are going to run.
Part of it is that I don’t do analytics work. Now that PERL is niche, my use cases are no longer the target.
As someone who mainly sits in the C++ and not the Python space - why?
Can't you still use all those old features just as you did before? If you don't like the complexity of newer libraries and features, they're still optional aren't they?
Or did something fundamental shift (aside from the obvious P2->P3 switch) that has made existing features harder to use?
As somebody who's comfortable with Python and C++... ugh. Types are frankly kinda silly. They don't do anything, the language doesn't even ship a blessed typechecker, they don't speed up your code, they don't make it safe, they just add useless notation that some mutually-incompatible third-party tools grok. And the mutually-incompatible thing is what really rubs me wrong. They didn't sit down and define a bombproof type system; they just added an optional notation and let the community figure it out (like packaging). Years later, and the community has not figured it out (like packaging).
Compared to C++, where you can do incredible zero-overhead magic with the type system; for example duck-typing, once The Way, is now an absolute nightmare in Python's type system. Python typing is little but baggage and warts unless you've drunk the cool-aid and then you're horrified that people like me haven't.
(and, all that said, I'm quite excited about py3.11, it's great on performance and ergonomics)
They are not useless at all. Because they are introspectable new types of extremely ergonomic libraries have sprung up (FastAPI, Pydantic), as well as the developer experience improving.
Three responses now, recommending the same third party stuff, which I already acknowledged the existence of. Kinda missing the point of what I had to say, isn't it?
> If you don't like the complexity of newer libraries and features, they're still optional aren't they?
Chances are you're not programming in a vacuum. These features will be used by third party libraries and you will have to read/understand this code.
Even in your in house codebase, there will always be co-workers that want to use these features.
The optionality of language features is not real. If they exist, they will be used, you will have to understand them, they will leak from other libraries, etc.
Ideally if it's a library you just need to know how to use it, not how it works internally right? Maybe that's too optimistic, but that's certainly how I use libraries in C++.
That's not even the case in C++ (I find myself having to step through Boost and even the standard library once in a while) but it's about 10x worse in a language that has so little static verification. If you use the library wrong then you find out via a runtime error you'll have to track down. Instead of, say, a type checker complaining about the type mismatch of your template parameter.
It’s not as much my code as reading my dependencies, troubleshooting others’ code, and the ecosystem sprawl. I keep up to date and pick and choose what I want.
A lot of these issues just aren’t there in C++, and you probably wouldn’t even try to share a codebase for something with someone who’s not another expert. …and you’re most likely working on something big.
The big thing that’s plagued Python since 3.x is the distribution problem. You can’t just hand someone a script and maybe another one to user-install some requirements. You need to BYO Python, dependencies, etc.
Things like types, library management, etc. are being handled by external tools. If I want to onboard someone to collaborate with me they really have to have my version of Python (so probably pyenv), Pipenv or Poetry, mypy or whatever, pytest or whatever, black, and have the same config for some of them.
The verbosity overhead of Go (or Nim, etc.) is worth the trade off to me for most of my uses of Python. I can just hand someone a binary and the dev just needs the language installed and maybe like goimports.
I’ve never been married to one language. I like to use them for their wheelhouse. I’ll still use Python for 1-offs or some things I can ship in a container, etc. that I’m only working by myself or with experienced Python people on.
A lot of this is that it’s really turning into more of an Analytics language. The output isn’t expected to be shared and everyone around you is a Python dev.
And I’m old. I started when Python was so pretty to my Pascal/C self compared to PERL (shudder). The documentation is still amazing for the language and standard library. (No Google, I want the actual docs.) And you could onboard almost anyone to work on it if they had any programming background because it was such a simple language.
The 2->3 shift was really only bad for complex legacy codebases and when you primarily deal with bytes vs Unicode. The latter is really painful anywhere. It didn’t bother me much. I usually just had to fix a few small things or it gave me a good excuse to kill something that should have been retired anyway.
Initially I wanted to counter all your points but then I realized if this is how you feel then who am I to say otherwise. Personally I don't find all the feature creep/additional features bad per se but it sure has grown and evolved as a language when I first started getting serious about Python in the early 3.x days.
What I did want to mention was the improvements to the errors where you can see exactly what line, function call, etc., is causing the error will be a huge game changer in my opinion for the language and for adoption. It's ergonomic changes like that that make me bullish on the language going forward.
Typing is good, but when you have long function signatures with types and they're not wrapped to one parameter per line, it does become painful to read.
Keep in mind for a long time many people just used 2.7, so they'd never see feature changes. I'd argue that Python has always had a stead drumbeat of features
This is my problem. I’ve gotten to where the extra work to use a language that generates a binary is lower than the distribution problem with Python.
I’ve rewritten 3 CLIs because it’s too much of a pain for people to get them set up.
I still use it for myself to whip through a CSV, collate data from APIs. Sometimes a tiny API or web app (I really don’t like webdev) where I can deploy a container or pipeline deployment.
And I don’t have to use all the new bells and whistles. But so many do.
The type hinting system in particular is so much more painful to read (to me) than in a language with actual types.
You can generate a binary of your script (and all dependencies) with pyinstaller[1], and then your users don't have to worry about anything other than running the binary.
> PyInstaller bundles a Python application and all its dependencies into a single package. The user can run the packaged app without installing a Python interpreter or any modules. PyInstaller supports Python 3.7 and newer, and correctly bundles many major Python packages such as numpy, matplotlib, PyQt, wxPython, and others.
> Python 3.11 is up to 10-60% faster than Python 3.10. On average, we measured a 1.25x speedup on the standard benchmark suite. See Faster CPython for details.
Among other things: "In Python 3.11, the core modules essential for Python startup are “frozen”. This means that their code objects (and bytecode) are statically allocated by the interpreter."
I'd prefer that they had done something like unexec to allow loading some arbitrary set of modules into a bootstrapping image, then dumping a runnable executable that would launch with all those modules already present. The old Emacs approaches are now mostly busted, but there are alternatives.
Ye olde Lisp dump image. A neat approach, but results in an absolutely horrific cross compilation experience.
As someone who worked briefly on a Common Lisp implementation, including trying to get it to cross-compile, I'm very glad this technique hasn't caught on in the broader community.
Yeah, it is pretty unworkable now, and Emacs uses an object serialization approach that still works pretty well, but there are other methods too. There is some OpenVZ-derived way to make process snapshots in Linux whose name I've forgotten, but I've been wanting to look into it for this.
Yeah that might have been it, or something closely related. I do remember looking at that page and a bunch of similar ones. This was after there was a huge thread in the python.org bug tracker about speeding up Python start times, and they ended up doing it in a somewhat painful and module-specific manner. I wished they had done something like this instead. Linux really would benefit from some kind of kernel support for snapshotting executables, since there are all kinds of programs like libreoffice that are currently slow to start.
I checked out 3.11b3, simply by swapping 3.10->3.11 interpreter I measured a clock time improvement of 19% on a basic data processing and file output task.
This is what I was mentioning in the PyScript thread: CPython gets faster with every release, and WASM gets more efficient with every release; sooner or later the sum of these efforts will deliver a solution that is fast enough even in the browser, without us having to faff with the underlying JS at all.
bottleneck compared to what, native desktop drawing? Obviously, but that's not really an option anymore.
Compared to JS performance over the same tasks - that's where WASM improvements should deliver. It doesn't have to be as fast as JS, just fast enough to become an acceptable alternative for the average case.
Better error messages is something that literally every single language/framework/library should prioritize. It improves users lives by more than almost everything else besides correctness. It’s been great to see the improvements in python.
I agree that the exact error locations in tracebacks seem like the most impactful change here. Particularly in the light that Python is often used as an introductory language and unhelpful error messages are incredibly frustrating. Like this it feels like they can become more of a tool to improve!
Totally agree! It's hard to fathom why so many languages leave you to be some kind of detective trying to determine exactly where the issue is rather than just telling you.
I'm really excited for where Python's static typing is heading!
I doubt that typing Python code will ever become as ergonomic as typescript, simply because typescript doesn't have the constraint of modifying the syntax of it's "target language" (JS) when it wants to add a feature or whatever, whereas typed python must still be Python, and syntactic changes therefore need to be added much more conservatively.
But still, the typing story is improving before my very eyes with every release, and given that Python types are actually available at runtime (allowing for things like pydantic and FastAPI), and that Python has massive adoption in the ML space, all this is making Python a very enticing language to get back to using!
I'm not. There are still huge issues with it that Typescript and other statically typed languages don't have:
1. No way to fully type `args` and `kwargs` which are sadly common in Python code.
2. Type semantics aren't defined so there are multiple competing interpretations. Not great if your dependencies use one type checker and you use a different one. Not that that will matter because...
3. A depressingly huge part of the Python community doesn't get static typing so a ton of your dependencies won't have types.
4. The most popular type checker - MyPy - is full of bugs and deliberate unsoundness. Pyright is much better but also newer so lots of projects don't use it.
I definitely wouldn't choose Python if I had a choice.
I love that Python has types available at runtime! The ability to have those type-generated CLIs or APIs is fantastic. I learned from this article about functools.singledispatch, and can't wait to use it. I've always wanted something like in typescript! But `functools.singledispatch` is just not possible in TS.
I think working with dicts is still a little clunky in python, and there are some rough edges (eg mypy being a little weaker), but I genuinely think I like types in python better than TS!
which (after properly defining `KeysWithTypeOf`) allowed me to specify that `settingToToggle` should be a string literal that is a key on `SomeObject` whose corresponding value is a boolean.
Typescript comes closest to fully letting me express my intentions statically and curtly, and while I hope Python's static typing will grow to be as expressive as Typescript, I think that day is still afar off.
For me the remaining pieces for Python are much more attainable than the missing pieces for typescript. I think basically all the helper TS things (eg KeyOf, etc) are addable to Python. Runtime types are not addable to TS. It goes against some of the core principles of TS.
But I agree there are loads of missing features in Python :)
The guy behind it made a PR to typescript proposing runtime availability of types[0], got rejected, and got going on it by himself. And he already has PoC libraries with the (early) equivalents of Pydantic/FastAPI/SQLModel (using runtime types for (de)serialization).
I really hope this experiment works out and that it gains traction, because it's a massive value add to TS IMO.
Oh wow! You are correct, I am very interested in it! Thanks for sharing! Honestly on most of my personal projects, I only use typescript via the jsdoc interface. I just never felt the need to get to 100% type safety for most projects. And I never liked how the types lend themselves to a style of programming which typescript can't support due to lack of runtime typing (eg multiple dispatch, function overloading, serialisation, etc). This project looks very cool, and shifts those tradeoffs for me! I'm going to need to give it a whirl!
What do you use for type-generated CLIs? I've been using Pydantic for config parsing and Cleo for CLI, but I don't yet have a good solution for hoisting/injection of CLI arguments alongside env/file configs.
I really want a declarative solution like Rust's Clap library, but I haven't yet found anything like it out there.
Unfortunately I couldn't find a good package either, so ended up rolling my own script, FnToCLI https://github.com/internetarchive/openlibrary/blob/ccf10246... . It wasn't too hard to get the bones of it working, so if you want to build something similar with built-in env handling, that might not be too bad! Under the hood it converts everything to argparse, so you can always use any complicated argparse options/etc if you want to.
The closest package was Typer, by the creator of FastAPI, but it looked unmaintained, and didn't support Literal types! Or at least that was the case when I was investigating ~year ago.
Python types are available at runtime? Talking about __orig_class__? There are also breaking changes coming to annotations that may not work with pydantic
I would actually consider this to be one of the weakest points of python types
Variadic Generics has the potential to enable all the scientific computing libraries to provide really nice type hints. One step closer to eliminating insidious broadcasting bugs… or running training for 10 hrs only to have a validation loop fail because of a shape mismatch.
I disagree. I've had a serious attempt at array typing using variadic generics and I'm not impressed. Python's type system has numerous issues... and now they just apply to any "ArrayWithNDimensions" type as well as any "ArrayWith2Dimensions" type.
Variadic protocols don't exist; many operations like stacking are inexpressible; the synatx is awful and verbose; etc. etc.
I've written more about this here as part of my TorchTyping project: [0]
Thanks for linking this! And I (though not OP) totally agree with your points!
I hope that these things could be solved with future iterations. For example:
Variadic protocols don't exist.
Hopefully they are added sometime.
the syntax is awful and verbose.
Hopefully we can settle on allowing `1` instead of `Literal[1]` as a type (and other similar improvements).
many operations like stacking are inexpressible.
These could be expressed if things like "multiplying"/"adding" literal numeric types would be supported.
The variadic generics PEP was partly motivated by the ML use-case (and took input from maintainers of numpy ..etc), so I hope future iterations will also improve the usage in the ML space.
Partly motivated? The primary focus was ML space. The creators of PEP 646 work at Facebook with goal of supporting pytorch tensor dimension tracking. That was main motivation and many of the features/design of that pep were based on what's needed to type hint tensor dimensions.
Pep was originally even longer and there's planned follow up peps for other tensor related type features like literal arithmetic to allow type hinting function like np.concatenate. I expect 2/3 more peps in that area in the next year or two.
Absolutely right about ML being the main motivation. The PEP says:
The main case this PEP targets - concerns typing in numerical libraries.
However, despite the authors of the PEP working at facebook, the Pytorch team, at facebook, wasn't interested at all in the PEP. This is also from the PEP:
For the sake of transparency - we also reached out to folks from a third popular numerical computing library, PyTorch, but did not receive a statement of endorsement from them. Our understanding is that although they are interested in some of the same issues - e.g. static shape inference - they are currently focusing on enabling this through a DSL rather than the Python type system
> running training for 10 hrs only to have a validation loop fail because of a shape mismatch.
I agree that tracking array shapes at build time could catch certain classes of bugs (and I would love to have that capability). However, it's not as easy as it may seem. Consider the following example. It shows that even a simple slicing operation can change the number of array dimensions in ways that are only known at run time.
The only issue now is that pytorch has their own "shapes" solution[0], and last I checked, were kinda reserved about participating in the standardization of variadic generics because they don't expect to use it.
I really really hope that the ML community comes together to use variadic generics because I believe it will save researchers and devs so many debug cycles (as well as compute resources, tbh).
Sure, there are of course runtime checks you can do (and I now do after having been bitten by this before), but that’s not necessarily better than having a static analyzer which can guarantee correctness without executing a single line of code.
Yes, the example @beisner gave here (the val loop crashing after the training is done) is kind of a bad example, and some frameworks (like Pytorch lightning) do do a "full workflow check" before going into training, but his overall point that variadic generics could massively impact dev/researcher productivity stands, I think.
i would say it makes sense for a typed language, not sure if you can have it both in python. Why do they keep adding features upon features to the syntax of poor python? do they want to turn it into another kind of c++?
It's an optional typechecking tool. Speaking as an ML researcher who has authored my fair share of linear algebra bugs due to broadcasting, being able to optionally typecheck specific critical regions of the codebase would be a killer feature.
The addition of a TOML parser to the standard library is really welcome. I've always wanted to use TOML in my Python scripts but having to import an external library made me use JSON instead, which was awkward due to the lack of comments.
But why tomllib rather than toml? That’s just weird. I thought they’d long stopped using the -lib suffix as pointless.
(Judging by https://docs.python.org/3/library/, there are 17 of them already, disregarding zlib, mostly niche and of ancient lineage. Comparing with https://docs.python.org/2/library/, it looks like three have been added in the py3k era. reprlib in 3.0 (with unpythonic naming conventions, dunno what’s up with that), pathlib in 3.4, and graphlib in 3.9 (though the version it was added in is missing from the page; I invite someone else to fix this or file a bug report)—so I guess -lib suffixes aren’t quite as dead as I’d thought.)
If ever there was a bikeshed to bikeshed all other bikesheds it’s people creating hierarchies where none is needed. Toml is fine, it’s perfect, it’s exactly what people would expect and remember. But instead you’d have them need to remember that it’s hiding in std. “but if everything’s in std it’s easy to remember!” Yes but then also it’s not needed. And if everything isn’t in std you leave people having to remember if toml was in std, sys, or was it found in tools? Utils? Or did they put it in parsertools? And for what reason? The user knows they want toml. And it’s not like you need to have multiple separate tomls in different places in the hierarchy.
Leaving users having to guess where in the hierarchy you decided to hide something, that they know they’re looking for doesn’t add value. No, just keep it flat and simple.
> creating hierarchies where none is needed. Toml is fine, it’s perfect, it’s exactly what people would expect and remember. [...] And it’s not like you need to have multiple separate tomls in different places in the hierarchy.
Yes, "toml" would be perfect, but we're getting "tomllib" instead, due to the lack of namespacing that the parent comment is lamenting.
The problem with the current situation is that (1) every project has to keep all the standard library modules in mind and make sure to never name a module "io", "site" or "email", etc. and (2) once a non-conflicting name has been picked (e.g. "toml") it will break if the standard library later introduces a module with the same name.
(I'm not advocating for Java's endless chain of single-child directories though.)
PEP 518 lists some reasons not to use configparser and I agree with them wholeheartedly.[1] Personally I don't want to use ill-defined formats, even for simple projects.
The only reason offered is that there are (or might be) differences between version of Python, mainly 2 and 3, as far as I can see.
Python 2 is EOL, so that's no longer a concern.
As for differences between Python 3 releases, isn't there a fairly large difference in TOML support as well, since in versions before 3.11 it doesn't work at all? Wouldn't specifying the behaviour of the INI parser as whatever 3.11 is doing (and raising an exception on earlier versions) amount to essentially the same thing?
Newer packaging-related PEPs require TOML, and many developer tools use TOML by choice, but a fresh install of Python couldn't actually read it. That required workarounds from tool and library maintainers, making it more complicated to support as many users' preferences and ways of working as possible.
However you prefer to work, your tools may now be easier to test and maintain. This change is good for everyone.
These files are already in toml, so it's only "unnecessary" if you ask for them to switch the file format.
Otherwise, adding tools to the standard library to read file formats required by the ecosystem is a good idea, regardless of whether you agree with the particular format.
Ini files aren’t a standard though, so every language can handle them differently. As a result they often bite people using them in multiple languages in the foot
These are dev written and Python3 read files, the parser of which hasn't changed in a decade. Everyone knows how to write them. The parser is also lenient and handles both kinds of comment. That's enough for simple metadata.
We have a 15 year old setup.cfg in one project at work, many other .ini's... has never been an issue.
It seems to me that HSL is a better fit than TOML. I use TOML because it is okay and seems better suited than YAML in some situations, but if I had a choice HCL would be the defacto standard.
HCL is also oodles more complex than TOML since it has expression evaluation, whereas TOML is just a static format. It really depends on your use case and I think there's a lot of cases where TOML makes a lot more sense than HCL (e.g. metadata formats).
HCL also has expression evaluation however: TOML parsing is guaranteed linear over the size of the file, but HCL isn't necessarily because you can trivially do things like dynamically create a n*n-sized list from n-sized input list, which may be an undesirable property!
I only do this for configurations that aren't meant to be used by non-programmers. I don't want to have to explain for the billionth time that something like the path "C:\New Folder" isn't going to work as-written in a string literal, or that the name "type" is already taken.
This unblocks or simplifies so many of my projects. Variadic generics, better exceptions, and TOML in the standard library are huge, self-types are great, and extra speed is always appreciated. Thanks to everyone involved.
I wouldn't quite say they're gamechangers, but they're exactly what I happened to need.
Variadic generics will (I hope) be very nice for typing pre-2.0 SQLAlchemy code. `Query[*Ts]` or similar would be a natural type for many values from the ORM API, and hopefully allow expressing types for query transformations like adding or removing columns, joining to specific query shapes, adding arbitrary subqueries, etc.
The self-type might unblock a testing tool I haven't shipped because the DX isn't where I want it. IIRC: normal TypeVar binding plus the TypeVar hack for self-types, interacting with Mypy's particular way of detecting the type of a descriptor, interacting with the way the project uses mixins, sometimes produce types that technically aren't wrong but lead to spurious type errors in user-defined data fixtures. So I'm probably the only one on earth with this particular problem.
I don't know, it's been a while. Maybe it won't work. But the self-type hack is noisy anyway and getting rid of it is nice.
Shipping TOML in the standard library means I can drop a bunch of Tox gymnastics in projects that support pyproject.toml or TOML-based configuration in addition to the older standards, and now I'm not stuck either forcing certain TOML libraries on people or working out a way to plug in their choice. It's not that hard, but complexity multiplies, and now it's one less thing to deal with.
For the longest time I was pretty meh about following newer Python releases, instead preferring to stick with a "python--" subset that would work across all the machines in my fleet. But recently I've really been enjoying the new features and have decided to make 3.10 my standard. In particular, the type annotations make my editor (LunarVim) a LOT smarter and let me find many errors before I even run the code.
Part of that has been that Ubuntu 22.04 has 3.10 in it. So, while I'd like to go up to 3.11, I'm not sure there's a good story for adding 3.11 to a 22.04 system.
Does anyone actually use asyncio for processing stuff? It's been around for a while now but I still find it very awkward compared to just creating a pool and calling map or starmap.
All the time now for IO-bound workloads. It was very awkward a few years ago, but things like asyncio.run(), asyncio.gather(), asyncio.to_thread(), and asyncio.create_task() have made things less awkward.
TaskGroups should make things even less awkward, too.
It can use less resources and can be faster than passing data between processes. There's really no reason to use multiple processes for IO-bound workloads, either, and even for some IO-bound workloads, a thread pool can be faster than processes.
The second you aren't just passing raw bytes around, you have to take into consideration what can and can't be sent between processes in Python, as some objects can't be pickled and thus can't be passed between processes.
You can concurrently load a lot more coroutines than processes and threads, as well.
If your task is io bound, aka, lots of network stuff, multi process is overkill. Also, asyncio can handle a _lot_ more tasks 100,000's as opposed to 10s. So it really shines in heavy io things.
Also, multiprocessing can not share memory, and that can be a pretty busy g disadvantage depending on the task.
One reason is that you often don't need to use locks. Between lines containing await (or async for or async with), you can be sure that this task won't be pre-empted to run another async task.
Another reason, if you're using the Trio async library, is that managing and cancelling multiple tasks is really easy, and you can be sure that none get lost. This update to Python brings some of that to core asyncio (but I'll stick with Trio for now, thanks).
It is, and it's a subinterpreters thing. Node has separate interpreters that run in worker threads but also share memory, which is the route Python is planning on taking.
One reason is if you need to launch a subprocess with a timeout but don't want to use up CPU in the python script while that subprocess runs. The regular subprocess module will busy-loop in such cases, consuming CPU, while asyncio's does not.
The docs even warn about this for subprocess and suggest using asyncio to avoid it, although the docs are misleading - it only busy-loops if the timeout is not None, and only when running on Mac/Linux not Windows.
Multiprocessing provides parallelism up to what the machine supports, but no additional degree of concurrency, asyncio provides a fairly high degree of concurrency, but no parallelism.
Asyncio actually plays really nicely with multiprocessing, too. The concurrent.futures.ProcessPoolExecutor can handle running tasks in child processes and handles the communication seemlessly for you. I've used it quite a bit. Can easily use all 32 cores on my server this way without the GIL getting in the way.
Single-threaded execution of IO-bound work can be faster than breaking it up between threads or processes, and it can use a lot less resources. Then there are the preemptive vs cooperative multitasking concerns and the pros and cons of processes/threads vs light-weight threads/coroutines/etc.
Some IO-bound workloads are suited really well by the asyncio model, while other workloads might be better suited for processes and threads. They're three separate tools whose use cases might be similar, but they're not necessarily replacing one another. Multiple processes still have their place even while asyncio exists and vice versa.
Explicit (await) vs. implicit (anything that uses patched I/O deep down) switch. Essentially, it makes the reasoning about the code almost as hard as with preemptive threading.
It depends...are you looking for concurrency or parallelism? The two are similar, but not the same.
Using multiprocessing creates a LOT of overhead and is really intended for parallelism and is only the better solution if you're CPU-bound. If you're I/O-bound, then what you're really looking for is concurrency, and using asyncio will be more performant.
Embedding more information isn't really the problem: alternatives have always existed, it's just that none of them were as brief and good as this one. E.g. normally you'd catch then re-raise another exception with a new message with extra information and the original one as inner exception. The issue with that is that the resulting message/stacktrace isn't as readable as these notes.
To be clear, this feature has forever been supported by the type checkers, it's just that running python code with `reveal_type` would fail since it's not defined at run-time. IIUC, this change makes it so that it doesn't fail at run time.
Previously, if you had to define a class method that returned an object of the class itself, adding types for it was a bit weird, it would look something like this...
To be able to say that a method returns the same type as the class itself, you had to define a TypeVar, and say that the method returns the same type T as the current class itself.
This kind of stuff is so crazy to me. I see how types are useful for defining general categories of data (int, string, float), but isn't it better to have as few types as possible? It just makes reading and using code more confusing to keep multiplying "types" like this.
"Types" beyond the basic ones, in general, are needed only when you have to define complex data structures that can hold multiple variables in one.
What Python really needs is an easy, built-in way to create mutable structs, without an overhead of creating classes. I think it's a failure of language design to miss such a useful and basic feature.
Glad to see they ported Anyio's TaskGroups (or was it from trio?) to the stdlib. Starting tasks so they run concurrently was a huge pain in vanilla Python.
A few of the decisions made over the last few years are questionable, imo, but on the whole, most of the changes have been very very welcome, and I just don't use the parts I don't like.
For example, the changes to TypedDict in 3.11 might actually get me to try them out again.
regarding task groups: can this be used in places where we want to run several tasks until any one (or more) of the tasks exit? (i.e. instead of asyncio.gather, asyncio.wait with return_when=asyncio.FIRST_COMPLETED)
Consider the statement "I would like to learn C, but the lack of significant indentation is a huge turn-off for me". It has the exact same validity as your statement.
Actually, it doesn't. In C, you can use indentation as you see fit; braces allow you to define code blocks and scope as necessary. That gives you both worlds. Python apparently doesn't.
Or put another way, you're forced to specify the same thing twice: with whitespace for the human and with braces for the compiler, creating the possibility for these specifications to get out of sync. In Python, the compiler reads the same information you already put there for the human.
It's like saying you'd like to be able to read and write Spanish, and you'd like to like it, but using the upside down question-mark ¿ is a deal-breaker.
I liked f-strings. Asyncio has its place but is waaay overused. Typing was a cool idea, but so awkward in practice.
All of it has really turned me off, personally. It’s getting to where reading libraries is just painful. The simplicity of the language was really beautiful and when I wanted type safety, etc. I’d use something else. I can still write simple Python, but it’s more all the other code I need to grok.
I find myself going to Go more and more for stuff I used to use Python for. It’s easier to set up a dev environment for other people, easier to distribute code, and gives me type safety and concurrency as first class citizens while being a very small language.
I miss dictionary comprehensions and other shortcuts from time to time. But it actually feels more ergonomic now.
All personal opinion. I’m fine shifting languages. I still write C from time to time to bang bits. I play with various LISPs to exercise my brain. I never used Python for performance critical code. I guess ML is changing that need.
But I know others that want Java to have functional features and Python to have this stuff.
I’m not saying I’m “right”, just uncomfortable in a place I used to love to hang out in.