What's New in Python 3.11?(deepsource.io) |
What's New in Python 3.11?(deepsource.io) |
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'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.
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.
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.
I work in both Python and Go. I love both languages. Here are a few reasons why I chose Go over Python for a lot of things these days:
Alot faster, self-contained executables instead of virtualenv, scales natively, no GIL, linear code in goroutines instead of asyncio.
Especially considered the obligatory compilation step and lack of IPython.
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.
Type annotations, imho, are actually useful and not really hard to read.
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.
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 :).
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.
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.
But then yea, 1 line of python using an inbuilt function solves a 10 line go routine in a hackerrank.
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?
Most people thought otherwise, because they don't bother to read the reference manuals.
Which is why making an already complex language even more difficult a very big mistake.
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.
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.
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?
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)
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.
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.
For the same reasons that you can't "just" use C++11 and ignore C++20.
The ecosystem moves forward, and you can't just sit behind no matter how much you would like to.
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.
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.
Not specifically a new feature, but I remember wasting a good bit of time when coming across a line that was essentially:
return headers.get('x-foo') == settings.FOO_KEY is not None
and being extremely confused until discovering it was a use of comparison chaining[1] and was actually doing the right thing.[1] https://docs.python.org/3.10/reference/expressions.html#comp...
> 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.
* https://docs.python.org/3.11/whatsnew/3.11.html
* https://docs.python.org/3.11/whatsnew/3.11.html#faster-cpyth...
Very nice.
The gains are real.
Maybe I'm being too negative and a faster browser API will be developed.
Many a student turns away from a language because the developers underestimate the importance of error messages.
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!
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.
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.
Am I missing out?
TaskGroups should make things even less awkward, too.
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.
pool+map is useful but it is not general.
https://docs.python.org/3.11/library/typing.html#typing.reve...
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.
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.
For everything else, use a function.
It has been talked about for a long time, probably 4 years now.
> It is crystal clear, that data['profits']['yearly'] was None.
I wouldn't call that exactly "crystal clear" for someone not somewaht experienced with the language or software in general.
For example, the changes to TypedDict in 3.11 might actually get me to try them out again.
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.
But stay smug.
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 think "parameter Specification Variables"[0] come pretty close to solving that no? I'd love to hear your take if you've used them before.
> Type semantics aren't defined so there are multiple competing interpretations
> The most popular type checker - MyPy - is full of bugs and deliberate unsoundness.
100% agreed. I hope Pyright "wins" as the dominant type checker, because like you said, it's MUCH better in every aspect.
> A depressingly huge part of the Python community doesn't get static typing so a ton of your dependencies won't have types.
100%. I have so many half hearted attempts at typing my dependencies![1]
> I definitely wouldn't choose Python if I had a choice.
At this point, I'd have to agree, except of course for data science/ML things.
[0] https://peps.python.org/pep-0612/
[1] https://github.com/davidatbu?tab=repositories&q=stubs&type=&...
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!
That's a step further than I'm willing to go :D. For example, the other day, I was able to do
settingToToggle: KeysWithTypeOf<SomeObject, boolean>
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.
I really want a declarative solution like Rust's Clap library, but I haven't yet found anything like it out there.
I would actually consider this to be one of the weakest points of python types
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]
[0] https://github.com/patrick-kidger/torchtyping/issues/37#issu...
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.
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.
>>> def f(arr, elem):
... return arr[elem]
...
>>> arr=np.zeros((1, 1))
>>> f(arr, 0).shape
(1,)
>>> f(arr, ...).shape
(1, 1)
>>> f(arr, np.newaxis).shape
(1, 1, 1)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).
(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.)
However you prefer to work, your tools may now be easier to test and maintain. This change is good for everyone.
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.
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.
I'm a huge Python fanboy and this line is absolutely awful.
Comparison chains are great for stuff like "x < y < z" or "x == y == z", but the operators should never be mixed.
"Readability counts" is, IMO, the most important line in the Zen of Python, and that line is unreadable. I imagine it's equivalent to:
return headers.get('x-foo') == settings.FOO_KEY and settings.FOO_KEY is not None
Which, admittedly, feels like a clumsy line of code, but it's at least readable.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.
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.
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 systemLeaving 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.
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.)
Who knows, it could be a good feature to earn the 4.0 moniker.
We have a 15 year old setup.cfg in one project at work, many other .ini's... has never been an issue.
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.
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.
Easier said that done, of course.
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?
Toml is ill-defined as well. ini files work fine for these trivial uses.
https://hitchdev.com/strictyaml/why-not/toml/
So now you have two flawed ways to do it, congrats.
It's what your Terraform files are primarily written in
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 :)
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.
FYI: You can use strings as a workaround, or the
from __future__ import annotations
trick.> 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.
I’ll have to check it out.
[1] https://github.com/microsoft/pyright/blob/main/package-lock....
Regarding ease-of-install though, installing pyright is painless in comparison to installing mypy when dealing with virtual envs, IMO.
I think that's unrelated. Seems to be something that's only for Callables.
[0] https://peps.python.org/pep-0646/#args-as-a-type-variable-tu...
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.
The history of Python means that coders make functions whose return is usually quite clear, but when it's not, I'm really glad I have typing.
Also, some libraries like Pydantic make use of types in a very clever and productive way.
Really cool for parts of your code in which runtime type enforcing is important (eg. external interfaces)
Go made multiple decisions that make me face palm. It was so close to being a great systems programming alternative.
A pythonic C++ that doesn’t smell bad.
I wish Go had classes. I like those damn things. Extending a Dog from Animal with all the methods and data in one place is nice.
Obligatory...try Rust?
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.
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.
Guess that's what people mean by composability.
Closer to 50.
The fact that simple reusable utility functions were/are complicated does make it feel half baked to me too.
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.
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).
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
> 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 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.
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.
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.
>>> {1} | {1.0} | {True}
{1}
But the behavior for dicts is surprising even in that light: >>> {1: 1} | {1.0: 1.0} | {True: True}
{1: True} >>> {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.And as (almost) always, Pyright has already implemented the provisional PEP!
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)
Yep you're right, there's even an official effort to clear some of these out: https://peps.python.org/pep-0594/
If I’m at the point where I’m debugging 3rd party code, a little bit of unfamiliar Python syntax is the least of my problems.
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.
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).
This is far easier to work with than multiprocessing.
When doing, e.g. AWS work, multiprocessing is additional pain for little gain.
Maybe 3.11 will make threads less painful.
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.
OF course, you can use them together to get both.
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.
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.
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.
History has shown that worrying about an incompatibility with the moribund Python 2 that never affected trivial packaging config was a waste of time.
Meanwhile we still have a setup.cfg on a work project, has worked without issue for 15+ years.
Here it is:
>>> import bisect
>>> sorted_fruits = ['apple', 'banana', 'orange', 'plum']
>>> bisect.bisect_left(sorted_fruits, 'banana')
1(Of course, for a custom class you can just wrap it in a __contains__ method and then just use the “in” operator.)
For dict/set hash lookup, O(1):
x in s
For binary search on a sorted python list, it takes a standard library import (bisect), and the containment test is: (i:=bisect.bisect_left(a, x)) < len(a) and a[i] == xSo, yes, let's have a contains function on (unsorted) arrays.
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.
print("quux" in {"foo", "bar", "baz"})Hu, says who? Says the junior developer so as not to get overwhelmed?
If you've done any kind of expertise work, digging under third party layers is a very regular exercise.
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.
[0] https://star-history.com/#tiangolo/fastapi&django/django&pal...
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.
Give me a fucking break. Let’s not even talk about the fact that it has well over a thousand open “questions”.
Could you please elaborate?
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.
True. However, the hardest part of the binary search algorithm is implemented through bisect, so it still saves a lot of developer time.
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#.
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.
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.
Try using a walrus operator in a multi-line if/elif statement or a complex comprehension.
Try to understand the subtleties of the new dictionary merge operator.
And the best one, try learning the typing annotations.
All this on top of already complex OOP class system.
Good luck.
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.
>>> class C:
... def __len__(self):
... return 10
... def __getitem__(self, i):
... if i >= 10:
... raise IndexError('out of range')
... return i+1
...
>>> c = C()
>>> import bisect
>>> bisect.bisect_left(c, 1)
0
>>> bisect.bisect_left(c, 4)
3 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
At that point it's just syntactic sugar. class Dependant:
def __init__(
self,
*,
path_params: Optional[List[ModelField]] = None,
query_params: Optional[List[ModelField]] = None,
header_params: Optional[List[ModelField]] = None,
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.
https://github.com/tiangolo/fastapi/blob/master/fastapi/rout...
But yeah, code is documentation!!! We don’t need any actual API docs!!! All hail tiangolo!
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?
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.
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.
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.
Master Gorad asks his apprentice:
"How many statements does it take to call a function in Go ?
Apprentice replies puzzled "I can just call foo();" and I am done ?
Master Gorad beats his student with a cane. No, you silly, it takes at-least 3 statements to make a function call in Go:
r, err := foo();
if err != nil {
return nil, err;
}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.
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.
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.
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.
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.
Exceptions, aka faults, are a time-tested feature of CPUs that have been around for half a century:
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.
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
It's like stack unwinding is a new concept or something.
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?
Throwing an error is not "not handling it"!
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.
But error handling in Go is also obvious.
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!
How does this compare to error handling in Go?
I prefer this way, but there was one thing I didn't understand until Ned Batchelder taught me, how to layer them:
- https://nedbatchelder.com/text/exceptions-in-the-rainforest....
- https://nedbatchelder.com/text/exceptions-vs-status.html
I didn't have the full mental model until I read that.
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.
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].
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.
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
}
}Typically you just care to have one or two primary places where you actually care if something has gone 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.The old-fashioned
if (condition):
statement_1
else:
statement_2
now becomes try:
assert(condition)
statement_1
except AssertionFailed:
statement_2
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.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.
[0] https://en.wikipedia.org/wiki/Continuation
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.
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.
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."
No, they are not.
func couldFail() error { /*...*/ }
func ohNoes() {
couldFail()
}
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
try:
couldFail()
except:
passIf 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.
That depends whether the error condition is worth caring about, for a given application, in a given state.
eg. how often do programs check if `printf("something")` actually succeeded?
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.
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.
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.
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)
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.
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.
In Rust you would use an Enum (a discriminated Union) for this not an Union and you wouldn't need unsafe.
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.
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
def myfunc():
if foo():
do_a()
else:
do_b()
and def myfunc():
try:
cond = foo()
except:
raise
if cond:
do_a()
else:
do_b()
and func myfunc() err {
cond, err := foo()
if err != nil {
return err
}
if cond {
do_a()
} else {
do_b()
}
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!
https://gosamples.dev/check-error-type/
cond, err := foo()
if errors.Is(err, ErrFooHadTemporaryFailure) {
// retry
} else if errors.Is(err, ErrFooHostOffline) {
// switchFooHost()
} else if err != nil {
// propagate unhandled error upwards
return nil, fmt.Errorf("foo unhandled err: %w", err)
}
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")