I did not expect to wake up at 4am seeing my post on front page HN, but here we are nevertheless :D
As the intro mentioned, these started off as 14 small tweets I wrote a month prior to starting my blog. When I finally got that set up, I just thought, "hey, I just spent the better part of two weeks writing these nifty Python tricks, might as well reuse them as a fun first post!"
That's why the flow might seem a little weird (as some pointed out, proxy properties are not really a Python "feature" in of itself). They were just whatever I found cool that day. I tried to find something more esoteric if it was a Friday, and something useful if it was a Monday. I was also kinda improving the entire series as it was going on, so that was also a factor.
Same goes with the title. These were just 14 feature I found interesting while writing Python both professionally and as a hobby. Some people mentioned these are not very "advanced" per se, and fair enough. I think I spent a total of 5 second thinking of a title. Oh well!
Hey, I really enjoyed reading your post. It's straight to the point, the list is very rich and well illustrated with examples. I have been coding in python for the last 20 years and I nowadays rarely like reading these posts as much as I did for yours. Kudos.
The tricks are more advanced than what I've seen in most people's code at some jobs before! The post is refreshing because it sits in the neglected middle ground between the tons of Python-for-beginners content out there and the arcane "I'm a core developer writing about very specific things" content.
My own opinion is that Python shall remain Python, and golang, Rust and Typescript each should be whichever they are with their unique philosophy and design.
I am coding in all 4, with some roughly 28 years now, and I don't like what is becoming of Python
There is a reason why python become that popular and widely adapted and used, and it is not the extra layers of type checking, annotations and the likes.
this looks familiar to me, but from other languages.
response := get_user_input()
I am aware of the factI am in minority, and not trying to change anyone's mind, simply what this voice to be heard from time to time.
All in all, a very comprehensive list of some of the recent introduced features.
There is an older list on SO which readers might also find useful:
Extra stuff like type checking and annotations are definitely not the reason why python became that popular and widely adapted and used, but it certainly doesn't hurt to add and use them.
To be clear, I'm not expecting people to start adding generics to their quick hacked together Python scripts (in fact please don't do that). Instead, if you're building a library or maintaining a larger Python codebase, a lot of these start becoming very useful. A lot of the typing features I mentioned are already used by Python under the hood, and that a lot of Python developers just take for granted.
> There is a reason why python become that popular and widely adapted and used, and it is not the extra layers of type checking, annotations and the likes.
Python got popular and widely adopted for the same reason PHP did: 1) it was readily available everywhere and 2) it's an easy language for beginners to pick up and get real applications working quickly.
But it turns out that the language features desirable for a quick prototype done by your fresh-out-of-college founding engineer aren't the same as the language features desirable for your team of 100 engineers trying to work together on the same codebase.
Python is at an odd intersection where it's used by everything from large teams building robust backend applications (particularly in the data/ML space), to data scientists hacking on scripts interactively in Jupyter, to ops people writing deployment scripts. You don't need type checking and annotations if you're writing a few one-off scripts, but you'd be crazy to not take advantage of them for a larger application.
A very good piece, that I enjoyed. But, I do get the impression that many of these more esoteric features, when reading a code-base, actually end up obscuring the developer's intention, and impair understanding. Without very competent IDE tooling, trying to work out what's happening in a piece of code is extremely difficult. It's turtles all the way down, and there's a lot more turtles now. A number of the new features, appreciated by many for good reason, tend to work against the Zen of Python which at some level explained the meteoric rise of Python as a bona fide implementation language over the last 30 years - simple, obvious, effective and practical.
Up to now python emphasizes to be and remain the dynamically typed monkey patch happy core language it is, with the completely volunteer option to provide type hints and use them to your advantage as you see fit.
So you can hack away and monkey patch to your heart's content and nothing is taken from you. no rust borrow checker. no need to use a type checker. ducks everywhere.
and I'm not aware of features that have to be used, aka incompatible changes to the core language.
I love Guido's balance. You don't need type checking and annotations if you don't want to use them. They are nice when you're playing in more complex systems, however, completely optional.
I mean it's completely optional. Even if you are to use python as a glue language, with absolutely 0 typing annotations on your side, it's still very very useful to have them in the libraries that you inevitably end up using with python. It's not even like it requires an additional build step, so it's really just free.
Every time I try to use Python I get this mixed feeling of liking how few guard rails there are between me and the logic I am making and this lingering worry that my code looks like fools attempt to use Python. So much exists as convention or loosely followed rules. Whenever I read articles like this I am wowed by the depth of things I didn't know about Python or how much has changed. It makes something like Go feel like a comfort blanket because I have reasonable certainty code I write won't feel deprecated or left behind a year or two later. Excellent article showing off even more I didn't know.
Instead of comfort I feel constantly annoyed that the language gives me so little that I have to produce more lines and very little in the way of convenience. It's a language that benefits a lot from AI completion, at least. Yeah, old code still looks the same as new code but that is due to a lack of progress on the language though.
I write an unfortunate amount of Go code.. because Go supports 2(?) of the features in this post (structural typing and generics, sort of, if you're feeling generous).
For example, circular imports aren't supported in Go at all. I run into this from time to time even if using an interface sometimes consts or other types are defined in the package as well and the whole thing has to be refactored. No way around it, has to be done.
Circular imports aren't encouraged in Python but Python never leaves you without options. Code can import types only while type checking or move imports out of the module level are quick workarounds. (I hope for typescripts `import type` someday.) That's what gives me a "comfort" feeling in Python, knowing that the language isn't likely to force me to work around the language design.
There are plenty of language features which let you re-use or build your own guardrails in python.
I do with the concept of truthiness and falsiness would be taken out and shot though. It's been responsible for far too many nasty bugs IME and it only cuts out a few extra characters. Not a great trade off.
Yeah, everybody uses it. That's partly how it ends up causing so many bugs - e.g. some user puts 0 in a text box and instead of the program reacting as if you put 0 in it reacts the same way as if no value was supplied.
Then everybody loses their minds because the system suddenly started doing something it was never supposed to.
The most common case is when people use `if not s` to test for None in an otherwise string value (e.g. from JSON) without realizing that empty strings are also false.
Keep programming in python long enough and you'll see it eventually. It might be hard to recognize at first.
On the python bug tracker somewhere a while back there was a guy who wrote "if time_variable:" which resolved to false but only when it was called at midnight. It was initally resolved as wontfix.
(midnight being the datetime's conceptual equivalent to empty string, 0, empty list, etc.)
The only things I’d have probably change about this list is the inclusion of some of the collections.abc containers for type annotations, TypedDict and how it can make working with strictly structured dictionaries not terrible (but if it looks like an class and quacks like a class, make it a class if you can), and Counter (only because I forget it exists every single time I should have used it).
Several comments disliking the walrus operator, like many of the features on this list I also hated it… until I found a good use for it. I almost exclusively write strictly typed Python these days (annotations… another feature I originally hated). The walrus operator makes code so much cleaner when you’re dealing with Optionals (or, a Union with None). This comes up a lot with regex patterns:
if (match := pattern.search(line)) is not None:
print(match.group())
Could you evaluate match on a separate line before the conditional? Sure. But I find this is a little clearer that the intended life of match is within the conditional, making it less tempting to reuse it elsewhere.
Not a Python feature specifically, but I’d also love to see code that uses regex patterns to embrace named capturing groups more often. .group(“prefix”) is a lot more readable than .group(1).
As someone coming from Javascript/Typescript & now working full time in Python, this is a lovely - mostly fantastically useful - little resource. Some choice observations:
1. Typing overloads: TS has typed overloads I think largely as an affordance to an unfortunate feature of Javascript. In my experience overloads are an anti-pattern or at best code smell. It's nice that you can type them if you're cleaning up an existing codebase that uses them but I would consider them tech debt.
2. Keyword-only and Positional-only Arguments: This is the opposite of the 1st feature (ability to make method signatures more strict) but man is the syntax cryptically terse. I'd love to use this everywhere but I'd be concerned about readability.
3. Future Annotations: Thank you for this section - forward references have been a real pain for me recently & this is the first explanation that's scratches the service of the "why" (rather than just focusing on case-by-case solutions), which is much more helpful. Bring on PEP 649.
4. Generics: Cries in legacy 3.10 codebase
5. Protocols: As a Typescript guy, this seems very cosy & familiar. And not very Pythonic. I'm not sure how to feel.
14. Metaclasses:
> if you are that 1% which has a unique enough problem that only metaclasses can solve, they are a powerful tool that lets you tinker with the internals of the Python object system.
OR if you're one of the many devs that believes their problem is unique & special & loves to apply magical overengineered solutions to simple common problems, then the next person who inherits your code is going to really love your metaclasses. They sure make tracing codepaths fun.
> 5. Protocols: As a Typescript guy, this seems very cosy & familiar. And not very Pythonic. I'm not sure how to feel.
Isn't it super pythonic? One of the first things you learn about Python is that "everything is duck typed", but then the type system is primarily nominally typed. It seems like Protocols should have been there from the start, like Typescript interfaces.
I'm new to Python & what is or isn't "pythonic" doesn't seem very intuitive to me (beyond just reading PEPs all day) - I guess I'm speaking from using the current type system & the idea of nominal types coexisting with structural seems a little disjointed.
For me, the biggest benefit to python is that it feels like executable pseudocode. The language gets out of the way of your domain level instructions. This is probably why most non-programmers have the easiest time with python.
The more fancy stuff you add to it, the less attractive it becomes. Sure, most of these things have some sort of use, but I reckon most people do not get deep enough into python to understand all these little things.
Lua is like this only moreso, and there's little fear of "progress" as large parts of the community have stuck with Lua5.1.
Of course, Lua is batteries-not-included so there may be the problem of "progress" in external libraries; in practice things like Penlight barely change though.
I disagree. Adding "fancy stuff" by definition does not remove what made it attractive to you in the first place. You even acknowledge this in your second sentence. Something you don't know of is not capable to influence your opinion in any way. And when you know of it -- and dislike it -- just keep doing it the way you did before knowing it.
Nitpick about 9.3 Short Circuit Evaluation: both things evaluate differently if you have empty strings. The if-else clause treats empty strings as valid while the or operator will treat them equivalent with None.
I would argue that most of these features (basically everything except metaclasses) are not advanced features. These are simple, but for some reason less well known or less used features.
Metaclasses are however quite complex (or at least lead to complex behavior) and I mostly avoid them for this reason.
And 'Proxy Properties' are not really a feature at all. Just a specific usage of dunder methods.
I enjoyed reading the article. I'm far from a Python expert, but as an observation, most of these features are actually just typing module features. In particular, I wasn't sold on Generics or Protocols as I would have just used duck typing in both cases... Does modern, production-level python code use types everywhere? Is duck typing frowned upon?
> Does modern, production-level python code use types everywhere?
I don't know about code inside companies, but most new open source projects I encounter use typing. Many old ones have been converted too.
> Is duck typing frowned upon?
No. You can use Protocols to define what shape you expect your duck to be. There's some discussion about whether you should use abstract classes or protocols, though.
Structural typing is a static equivalent of dynamic duck typing. It is compatible with type checkers. Protocols mentioned in the article work without inheritance. A duck doesn't need to know that there is SupportsQuack protocol. If it quacks, it passes the type checks.
That feature I'm using regularly and can recommend doing so, e.g. when passing boolean flags to functions, to clarify their meaning at the call-site by ensuring they have to be passed by name.
Turns out I managed to use almost all of these during a refactor of a project at work, even metaclasses... (Metaclass usage is justified in my case: we have a sort of language evaluator and using a metaclass lets us define function arguments with their types and validators in a very coincise and obvious way similar to Pydantic.)
I think this list should also include descriptors[0]: it's another metaprogramming feature that allows running code when accessing or setting class attributes similar to @property but more powerful. (edit: nvm, I saw that they are covered in the proxy properties section!)
I think the type system is quite good actually, even if you end up having to sidestep it when doing this kind of meta-programming. The errors I do get are generally the library's fault (old versions of SQLAlchemy make it impossible to assign types anywhere...) and there's a few gotchas (like mutable collections being invariant, so if you take a list as an argument you may have to type it as `Sequence[]` or you'll get type errors) but it's functional and makes the language usable for me.
I stopped using Ruby because upstream would not commit on type checking (yes I know you have a few choices if you want typing, but they're a bit too much overhead for what I usually use Ruby for, which is writing scripts), and I'm glad Python is committing here.
for server in servers:
if server.check_availability():
primary_server = server
break
else:
primary_server = backup_server
deploy_application(primary_server)
As it is shorter to do this:
primary_server = backup_server
for server in servers:
if server.check_availability():
primary_server = server
break
deploy_application(primary_server)
for server in servers:
if server.check_availability():
primary_server = server
break
else:
logger.warning("Cannot find a valid server") # <---
primary_server = backup_server
deploy_application(primary_server)
If you replace the assignment in the else clause with something side-effectful it does make sense. But even then it harms the readability of the code to such a ridiculous extent. I've never not regretted adding an else clause to a for loop. Any cognitive benefits from Python's approach to for loops closely mirroring natural language just go out the window.
If the team is familiar with Functional Programming concepts, I'd actually suggest
available_servers = (server for server in servers if server.check_availability())
primary_server = next(available_servers, backup_server)
deploy_application(primary_server)
which IMHO better conveys the intent to the reader (i.e. focus on "what to do" over "how to do it")
This kind of search can be done a variety of different ways, and is worth abstracting, e.g.:
def first(candidates, predicate, default):
try:
return next(c for c in candidates if predicate(c))
except StopIteration:
return default
deploy_application(first(servers, Server.check_availability, backup_server))
Indeed. I was thinking of ways to generalize for the case where a default isn't desired, then decided against introducing that complexity in the example, then forgot that I could re-simplify further.
I would say that the main benefit of for/else is readability and ease of writing since nothing needs to be front-loaded. (The downside is an arguably redundant addition to the basic syntax, of course.)
The shorter version that saves the else: line assigns primary_server twice: it is OK for an assignment but a potential problem if doing something less trivial, and very ugly style in any case.
I do like the else clause with for loops. However most people are not familiar with it, and also `else:` as a keyword is confusing. I always remember it as `no break:`.
That's still confusing though. The problem here is that `else` is semantically attached to `break`, but syntactically attached to the body of the loop. The latter makes it look like it executes if the loop body didn't, if you interpret it in the most straightforward way.
IMO a better design would be to have a block that always executes at the end of the loop - there's even a reasonable keyword for it, `finally` - but gets a boolean flag indicating whether there was a break or not:
for server in servers:
if server.check_availability():
primary_server = server
break
finally did_break:
if not did_break:
primary_server = backup_server
Or better yet, make `break` take an optional argument (which defaults to `True` if unspecified), and that's what you get in `finally`. So this could be written:
for server in servers:
if server.check_availability():
break server
finally server:
primary_server = server if server is not None else backup_server
I thought the typing overload was interesting because I thought it was going to be like overloading in C# and Java but it's really just useful for documentation and intellisense tools in IDEs.
Working in the ML field, I can't hate Python. But the type system (pre-3.12, of course) cost me a lot of nerves. Hoping for a better post-3.12 experience once all libraries are usable in 3.12+. After that experience, I’ve come to truly appreciate TypeScript’s type system. Never thought I’d say that.
One of the most frustrating bugs I had encountered was when I was using memory mapped CSC format sparse arrays.
I needed the array indices to be int64 and specified them as such during initialization.
Downstreams, however, it would look at the actual index values and dynamically cast them to int32 if it judged there would be no loss in precision. This would completely screw up the roundtrip through a module implemented in C.
Same experience here, Python’s typing experience is awful compared to TypeScript, even post-3.12. Mypy’s type inference is so dumb you have to write arguments like `i: int = 0`; `TypedDict`s seems promisable at first and then end up as a nightmare where you have to `cast` everything. I miss TypeScript’s `unknown` as well.
It still has a special case for dataclass-like things. I don't see how Python type checking (I haven't tried Red Knot) could let you do semi-magical things like Zod schema validation from TypeScript.
How would a typing system know if the right type is `int` or `Optional[int]` or `Union[int, str]` or something else? The only right thing is to type the argument as `Any` in the absence of a type declaration.
def f(i=0) -> None:
if i is None:
do_something()
else:
do_something_else()
Yeah, I know it's retarded. I don't expect high quality code in a code base missing type annotation like that. Assuming `i` is `int` or `float` just makes incrementally adoption of a type checker harder.
I want a typing system with a good inference that doesn’t require me to type each and every variable, just like in any good statically-typed language like OCaml or Typescript. Strong typing and explicit typing are two very different things.
It's guaranteed to be correct if you use different operators for ints and floats, which is what at least some ML dialects (notably, OCaml) do precisely so that types can be inferred from usage.
That's the downside of operator overloading - since it relies on types to resolve, they need to be known and can't be inferred.
I was merely giving an example that strong typing has nothing to do with having to write the types. (and, obviously, the inferred type (int -> int) is correct. )
I believe mypy infers i as an integer in i = 0. I remember I had to do i = 0.0 to make it accept i += someFloat later on. Or of course i:float = 0 but I preferred the former.
Because it shouldn’t in function arguments. The one defining the function should be responsible enough to know what input they want and actually properly type it. Assuming an int or number type here is wrong (it could be optional int for example).
In TypeScript arguments with a default value "inherit" the type of that value, unless you explicitely mark it otherwise. I believe this is how Pyright works as well.
Mypy was designed to enable gradual adoption. There is definitely Python code out there with `def f(i=0)` where `i` could be any numeric type including floats, complex, numpy etc.. This is called duck typing. It's wrong for a type checker to assume `i: int` in such a case.
Pyright probably works if you use it for a new project from the start or invest a lot of time "fixing" an existing project. But it's a totally different tool and it's silly to criticise mypy without understanding its use case.
Python has evolved into quite a complex language considering the amount of features that come built-in. The documentation, while complete, does not facilitate discovery of many of those features.
The quality of documentation is a known, highlighted concern in the community. There is particular interest in adopting Diataxis principles to improve documentation, both for Python itself and for packaging (see e.g. the user guide at https://packaging.python.org/en/latest/ , or the PyPA main site https://www.pypa.io/en/latest/ ).
Just like Microsoft maintains the Typescript handbook, I think the Python Foundation needs a "Idiomatic Modern Python Handbook" which always shows the most modern and idiomatic way to use Python features.
It's more complex that decade ago, but still a relatively simple language. I can understand the article without much effort, while I scratch my head really hard when read about advance feature of Typescript or Scala.
If only. I suspect very few Python programmers can even fully explain what `a + b` does.
If `a` and `b` are instances of classes, many would say it's equivalent to `a.__add__(b)` or `type(a).__add__(a, b)`, but in fact it's much more complex.
I once broke some Python by changing a = a + b to a += b.
If a and b are lists, the latter modifies the existing list (which may be referenced elsewhere) instead of creating a new one.
I think Python is the only language I've encountered that uses the + operator with mutable reference semantics like this. It seems like a poor design choice.
This is one of the absolute worst design mistakes in Python. The example you give (that `a = a + b` and `a += b` aren't equivalent) is bad enough:
>>> a = b = [1, 2, 3] >>> a = b = [1, 2, 3]
>>> a = a + [4] >>> a += [4]
>>> a, b >>> a, b
([1, 2, 3, 4], [1, 2, 3]) ([1, 2, 3, 4], [1, 2, 3, 4])
What's worse is that sometimes, they are equivalent:
>>> a = b = (1, 2, 3) >>> a = b = (1, 2, 3)
>>> a = a + (4,) >>> a += (4,)
>>> a, b >>> a, b
((1, 2, 3, 4), (1, 2, 3)) ((1, 2, 3, 4), (1, 2, 3))
And even worse, in order to support a version of `a += b` that sometimes modifies `a` (e.g. with lists), and sometimes doesn't (with tuples), the implementation of the `+=` operator is convoluted, which can lead to:
>>> t = ([1, 2, 3], ['a'])
>>> t[0] += [4]
TypeError: 'tuple' object does not support item assignment
>>> t
([1, 2, 3, 4], ['a'])
The operation raises a TypeError, despite having succeeded!
Yes, the next level of complexity is that `a + b` will sometimes fall back to `b.__radd__(a)` if `a.__add__(b)` returns `NotImplemented`. But also:
- There are situations where `__radd__` takes priority over `__add__`. The rules for determining that priority are complex (and IIRC subtly different from the rules that determine whether `a < b` prioritises `a.__lt__(b)` or `b.__gt__(a)`).
- The lookup of `__add__` etc uses a special form of attribute lookup that's neither equivalent to `a.__add__` nor `type(a).__add__`. This special lookup only searches `type(a)` whereas the first would find an `__add__` function on `a`, and the second on `type(type(a))`.
I doubt many 4gl language developers could explain what a + b does in their respective language. That rabbit hole goes all the way down to the physical instruction execution on silicon.
> @overload is a decorator from Python’s typing module that lets you define multiple signatures for the same function.
I'll be honest, I've never understood this language feature (it exists in several languages). Can someone honestly help me understand? When is a function with many potential signatures more clear than just having separate function names?
It's an implementation of "ad-hoc polymorphism", where, for example, it may make sense to "add" (+) together numbers of various types: integers, floating points, rational numbers, etc.
Thus the (+) operator for addition is "overridden" or "polymorphic" in the types of numbers that can be added together.
The argument for having a polymorphic signature rather than just multiple separate "monomorphic" functions is similar to that for "generics," otherwise known as "parametric polymorphism": why not just have a function `forEachInt` for iterating over lists of ints, a separate function `forEachChar` for iterating over lists of characters, and so on?
Higher levels of abstraction and generality, less boilerplate and coupling to any particular choice of data structure or implementation.
You could of course go the route of golang which indeed just had you write "monomorphized" versions of everything. Several years later generics were added to the language.
Alternatively, you throw everything out and never have to worry about typing or polymorphism, at the cost of static safety.
I would recommend reading the article example once more. Going from the example, without the overload, the function would return a union type which means any time you use the function, you have to put a type check to the result to know if the output is a list or not. With overload, as soon as an argument to the function is a certain type, the output is determined, so you won't need to type check.
I think that GP's point is that you could accomplish the same thing by simply having separate functions with different names, signatures, and return types.
Some of these newer features don't seem like improvements to the language (e.g. the Walrus operator):
'''
# ===== Don't write this =====
response = get_user_input()
if response:
print('You pressed:', response)
else:
print('You pressed nothing')
# ===== Write this instead =====
if response := get_user_input():
print('You pressed:', response)
else:
print('You pressed nothing')
'''
The first implementation is immediately clear, even if you're not familiar with Python syntax. If you don't know what the ":=" operator does, the code becomes less readable, and code clarity is traded away in favor of being slightly more concise.
First of all, it takes a minute to search "python :=", and the construct itself is pretty simple. It's been part of the language since 2018[0]. I don't think "not knowing the language" is a good reason to avoid it.
Second, the walrus operator limits the variable's scope to the conditional, which can reduce certain bugs. It also makes some scenarios (like if/elif chains) clearer.
I recommend checking out the PEP for some real-world examples.
That's exactly my point. Having to search for the meaning of the operator at all makes the code less readable. I recommend reading the Zen of Python, which covers the design principles of the language.
I don’t write code to the level of someone who has just finished Hello, World. This isn’t something esoteric, it’s an extremely basic and useful part of the language. I’ve seen this argument used against multiple languages, and it has never made sense to me.
“I don’t want to use windowing functions in SQL, because most people don’t know what they are.” So you’d rather give up an incredibly powerful part of your RDBMS, and dramatically increase the amount of bandwidth consumed by your DB?
It’s as if the industry is embracing people who don’t want to read docs.
Couldn't you extend this line of thinking to any language-specific syntax on any programming language?
Don't use `match`, macros, lifetimes, ... in rust, someone coming from another language without them might not get what it means. Instead write the equivalent C-looking code and don't take advantage of any rust specific things.
Don't use lisp, someone coming from another language might not be able to read it.
Etc..
At one point if you write code and want to be productive, you need to accept that maybe someone that is not familiar with the language you're using _might_ have to look up syntax to understand what's going on.
In that example I gave, I totally agree with you. One area where I find walrus operators kinda useful is with dealing with iterators. For example,
'''
iterable = iter(thing)
while val := next(iterable, None):
print(val)
'''
is a lot cleaner in my opinion compared to
'''
iterable = iter(thing)
val = next(iterable, None)
while val is not None:
print(val)
val = next(iterable, None)
'''
Reason why I did not use this example outright was because I wasn't sure if people were familiar with the iter api, so I just chose a simpler example for the blog.
Yeah, I think the walrus operator is occasionally useful in `while` statements, but that's about it. IMO it wasn't worth the additional language complexity for an operator that's so rarely useful.
Parent's example had a direct `iter(thing)`. Even if you had to use `iter(thing, sentinel)`, you'd still use `for val in iter(thing, sentinel)`, not while.
Good article! I found the typing bits particularly interesting. This part of the language has been evolving rapidly recently, which is cool if you like static typing.
Although I think the example of type alises in section 4 is not quite right. NewType creates a new "subtype" which is not equivalent to the original type. That's different to TypeAlias, which simply assigns a name to an existing type. Hence NewType is still useful in Python 3.12+.
I don't believe any reasonable person would call Python statically typed, it just now has a pathway though which one can send additional documentation, with all of its caveats
The way the language is evolving, it seems likely to me that people in the applications camp (ML, simple web-dev, etc.) will soon need a "simple Python" fork or at least an agreed-upon subset of the language that doesn't have most of these complications (f-strings are a major success, though).
Good list. Some of these i knew already, but the typing overloading and keyword/positional-only arguments were new to me.
One personal favorite of mine is __all__ for use in __init__.py files. It specifies which items are imported whenever uses from x import *. Especially useful when you have other people working on your codebase with the tendency to always import everything, which is rarely a good idea.
It’s never a good idea. I use `__all__` to explicitely list my exports in libraries, so that when one write `from mylib import `, the IDE auto-completes only the public classes and functions.
This is a nice list of "things you might not know" that is worth skimming to add to your toolkit.
If you are really interested in "advanced Python", though, I would recommend the book Fluent Python by Ramalho. I have the first edition which is still highly relevant, including the async bits (you just have to translate the coroutines into async syntax). There is a second edition which is more up to date.
I would also recommend checking out the functools[0] and itertools[1] modules in the standard library. Just go and read the docs on them top to bottom.
It's also worth reading the first few sections of Python Data Model[2] and then bookmarking this page.
+1 to itertools especially. Absurdly powerful, and the recipes at the bottom of the doc page are terrific.
The only problem I’ve found is that for interviews, people often aren’t familiar with it, which can lead to you solving whatever puzzle they had in far less time than they intended, and without manually building whatever logic it was they assumed you would need.
> The only problem I’ve found is that for interviews, people often aren’t familiar with it, which can lead to you solving whatever puzzle they had in far less time than they intended, and without manually building whatever logic it was they assumed you would need.
My favourite example of a similar thing happening to me was when I was asked to reverse the digits in a number. I somewhat jokingly asked if I was assuming base 10, which got some awkward looks so I knew something was up. They weren't impressed at all with my answer of `"".join(reversed(str(123456789)))`. I didn't get the job.
It has an effect, and is usually worth including anyway. I used to omit it by default; now I include it by default. Also, you say "nowadays" but it's been almost 13 years now (https://peps.python.org/pep-0420/).
> since 3.13 there is a @deprecated decorator that does what you think it does
Nice find. Probably worth mentioning it comes from the `warnings` standard library.
> the time package has functions for monotonic clocks and others not just time()
There's quite a bit in there, but I question how many people need it.
Anyway, it's always surprising to me how when other people make these lists, such a large fraction is taken up by tricks with type annotations. I was skeptical of the functionality when the `typing` standard library was introduced; I've only grown more and more wary of it, even as people continue to insist to me that it's somehow necessary.
This is all fun and games unless you have to debug someone elses code and they use a new feature that you didnt know about.
Speaking for myself, I would be glad if there were a python_light version of the interpreter that has a simple syntax only like the early 3.x versions.
You underestimate the social complexity of a simplified language. Everyone would disagree about the design (what is worth including because it is "simple" or necessary?), everyone would avoid using an handicapped language where features they need are gratuitously missing, and nobody would care about it enough to work on implementation and maintenance.
This sounds fun if you have 10x programmers or at least IQ > 140 programmers in charge. Last place I worked, I was told never use "smart" tricks if you can do the same thing in a simpler way. For-else and f-strings and := sound like harmless enough (though the last one is controversial); "with" is useful for resources that need to be deallocated but "yield"? Really? "more readable" when you slap a decorator on the thing? Why are we packing the enter and exit operations into a single function?
Playing with the typesystem and generics like this makes me worry I'm about to have a panic attack.
Give me code that I can understand and debug easily, even when I didn't write it; don't do implicit magical control flow changes unless you have a very good excuse, and then document both the how and the why - and you'll get a product that launches earlier and has fewer bugs.
Sometimes, a few more if statements here and there make code that is easier to understand, even if there's a clever hack that could cut a line or two here and there.
I don’t really see "smart tricks" here, just features of the language that you may not be aware of. I use most of them in my daily work without thinking about it: `@overload` and other typing features are useful to document the code, prevent mistakes, and help IDEs generate more helpful suggestions; keyword-only arguments force users to pass options as keywords, so you avoid calls like `foo(x, z, z, true, true, false, true)` and ensure the code is more explicit; for-else can sometimes clarify some complicated code; the walrus operator, short-circuiting with `or`, and chaining operators are just normal Python.
overload is very useful, but I find some people mistakenly think it actually modifies the runtime function signature to do multiple dispatch, so I do try to avoid it in favour of simpler functions whenever possible
too many overloads can be a code smell IMHO, it's ok when you are implementing a very common pattern and want proper types (decorators come to mind, so that both @decorator and @decorator() work, where the decorator might also have optional args) but I think in cases like the example in the article it should almost always be two separate functions
The simple contextlib.contextmanager example doesn't really sell the benefits.
The main one is that it makes error handling and clean-up simpler, because you can just wrap the yield with a normal try/catch/finally, whereas to do this with __enter__ and __exit__ you have to work out what to do with the exception information in __exit__, which is easy to get wrong:
Suppressing or passing the exception is also mysterious, whereas with contextlib you just raise it as normal.
Another is that it makes managing state more obvious. If data is passed into the context manager, and needs to be saved between __enter__ and __exit__, that ends up in instance variables, whereas with contextlib you just use function parameters and local variables.
Finally, it makes it much easier to use other context managers, which also makes it look more like normal code.
Here's a more real-world-like example in both styles:
You can describe it in english as "open a file, then try to write a header, run the code inside the with statement, write a footer, and if anything fails truncate the file and pass the exception to the caller". This maps exactly to the lines in the contextlib version, whereas this logic is all spread out in the other one.
It's also more correct, as the file will be closed if any operation on it fails -- you'd need to add two more try/catch/finally blocks to the second example to make it as good.
When I code, I try to make everything I write not clever. I am not saving a byte here and there because I am not typing in a program from a magazine in the 1980s (I was there, I am not going back). No code golf. I did my time on the Timex-Sinclair with its miserable two kilobytes of memory and reserved keywords as special-function strokes at a character each.
Each line should do one thing, in general, and it ought to be obvious. Cleverness is held in reserve for when it is truly needed, namely data structures and algorithms.
One of my accomplishments which seems to have mattered only to me is when my apartment-finding/renting system, written entirely in Perl, was transferred into the hands of students, new programming students. Perl is famously "write-once, read-never" and seems to have a culture favoring code golf and executable line noise. Still, the students got back to me and told me how easily-ported everything was, because I had done just one thing per line, avoided $_ and other such shortcuts, and other practices. They were very happy to take it over because I had avoided being cryptic and terse.
My favorite hack is to use a single threaded ThreadPoolExecutor as my main thread. When another thread wants to communicate with it, it just queues the callback function into main using submit. This cleans up the architecture and avoids the use of Queue which is a bit slower.
Yes, please. Given I'm forced to use Python, I'd welcome any "compile time" tools I can get. These days, it's using pyright with strict mode, which is pretty good, but there's still a long way to go.
Hey yall! Original author of the blog here!
I did not expect to wake up at 4am seeing my post on front page HN, but here we are nevertheless :D
As the intro mentioned, these started off as 14 small tweets I wrote a month prior to starting my blog. When I finally got that set up, I just thought, "hey, I just spent the better part of two weeks writing these nifty Python tricks, might as well reuse them as a fun first post!"
That's why the flow might seem a little weird (as some pointed out, proxy properties are not really a Python "feature" in of itself). They were just whatever I found cool that day. I tried to find something more esoteric if it was a Friday, and something useful if it was a Monday. I was also kinda improving the entire series as it was going on, so that was also a factor.
Same goes with the title. These were just 14 feature I found interesting while writing Python both professionally and as a hobby. Some people mentioned these are not very "advanced" per se, and fair enough. I think I spent a total of 5 second thinking of a title. Oh well!
Very good job. I bet most python developers will learn something here. Python has changed a lot; for the better, judging by your examples.
Hey, I really enjoyed reading your post. It's straight to the point, the list is very rich and well illustrated with examples. I have been coding in python for the last 20 years and I nowadays rarely like reading these posts as much as I did for yours. Kudos.
The tricks are more advanced than what I've seen in most people's code at some jobs before! The post is refreshing because it sits in the neglected middle ground between the tons of Python-for-beginners content out there and the arcane "I'm a core developer writing about very specific things" content.
I appreciate you writing the post and I enjoyed reading it.
Congratulations for ending up on the front page! (I hope the server hosting your blog is okay!)
I’ve coded python for 15 years and I’ve not seen many of these, so I’d consider them reasonably advanced personally.
One I think you missed is getters and setters on attributes!
My own opinion is that Python shall remain Python, and golang, Rust and Typescript each should be whichever they are with their unique philosophy and design.
I am coding in all 4, with some roughly 28 years now, and I don't like what is becoming of Python
There is a reason why python become that popular and widely adapted and used, and it is not the extra layers of type checking, annotations and the likes.
this looks familiar to me, but from other languages.
I am aware of the factI am in minority, and not trying to change anyone's mind, simply what this voice to be heard from time to time.All in all, a very comprehensive list of some of the recent introduced features.
There is an older list on SO which readers might also find useful:
https://stackoverflow.com/questions/101268/hidden-features-o...
Extra stuff like type checking and annotations are definitely not the reason why python became that popular and widely adapted and used, but it certainly doesn't hurt to add and use them.
To be clear, I'm not expecting people to start adding generics to their quick hacked together Python scripts (in fact please don't do that). Instead, if you're building a library or maintaining a larger Python codebase, a lot of these start becoming very useful. A lot of the typing features I mentioned are already used by Python under the hood, and that a lot of Python developers just take for granted.
Case in point, the python-opencv (https://github.com/opencv/opencv-python) library has basically no types and it's an absolute pain to work with.
BTW thats a really good SO thread, thanks for linking it!
> There is a reason why python become that popular and widely adapted and used, and it is not the extra layers of type checking, annotations and the likes.
Python got popular and widely adopted for the same reason PHP did: 1) it was readily available everywhere and 2) it's an easy language for beginners to pick up and get real applications working quickly.
But it turns out that the language features desirable for a quick prototype done by your fresh-out-of-college founding engineer aren't the same as the language features desirable for your team of 100 engineers trying to work together on the same codebase.
Python is at an odd intersection where it's used by everything from large teams building robust backend applications (particularly in the data/ML space), to data scientists hacking on scripts interactively in Jupyter, to ops people writing deployment scripts. You don't need type checking and annotations if you're writing a few one-off scripts, but you'd be crazy to not take advantage of them for a larger application.
A very good piece, that I enjoyed. But, I do get the impression that many of these more esoteric features, when reading a code-base, actually end up obscuring the developer's intention, and impair understanding. Without very competent IDE tooling, trying to work out what's happening in a piece of code is extremely difficult. It's turtles all the way down, and there's a lot more turtles now. A number of the new features, appreciated by many for good reason, tend to work against the Zen of Python which at some level explained the meteoric rise of Python as a bona fide implementation language over the last 30 years - simple, obvious, effective and practical.
please help me understand.
Up to now python emphasizes to be and remain the dynamically typed monkey patch happy core language it is, with the completely volunteer option to provide type hints and use them to your advantage as you see fit.
So you can hack away and monkey patch to your heart's content and nothing is taken from you. no rust borrow checker. no need to use a type checker. ducks everywhere.
and I'm not aware of features that have to be used, aka incompatible changes to the core language.
So what is the critique, exactly?
I love Guido's balance. You don't need type checking and annotations if you don't want to use them. They are nice when you're playing in more complex systems, however, completely optional.
I mean it's completely optional. Even if you are to use python as a glue language, with absolutely 0 typing annotations on your side, it's still very very useful to have them in the libraries that you inevitably end up using with python. It's not even like it requires an additional build step, so it's really just free.
Every time I try to use Python I get this mixed feeling of liking how few guard rails there are between me and the logic I am making and this lingering worry that my code looks like fools attempt to use Python. So much exists as convention or loosely followed rules. Whenever I read articles like this I am wowed by the depth of things I didn't know about Python or how much has changed. It makes something like Go feel like a comfort blanket because I have reasonable certainty code I write won't feel deprecated or left behind a year or two later. Excellent article showing off even more I didn't know.
Instead of comfort I feel constantly annoyed that the language gives me so little that I have to produce more lines and very little in the way of convenience. It's a language that benefits a lot from AI completion, at least. Yeah, old code still looks the same as new code but that is due to a lack of progress on the language though.
I write an unfortunate amount of Go code.. because Go supports 2(?) of the features in this post (structural typing and generics, sort of, if you're feeling generous).
For example, circular imports aren't supported in Go at all. I run into this from time to time even if using an interface sometimes consts or other types are defined in the package as well and the whole thing has to be refactored. No way around it, has to be done.
Circular imports aren't encouraged in Python but Python never leaves you without options. Code can import types only while type checking or move imports out of the module level are quick workarounds. (I hope for typescripts `import type` someday.) That's what gives me a "comfort" feeling in Python, knowing that the language isn't likely to force me to work around the language design.
There are plenty of language features which let you re-use or build your own guardrails in python.
I do with the concept of truthiness and falsiness would be taken out and shot though. It's been responsible for far too many nasty bugs IME and it only cuts out a few extra characters. Not a great trade off.
Truthiness and falsiness is one of my more used features. ``if not collection`` comes up in nearly every project multiple times.
Yeah, everybody uses it. That's partly how it ends up causing so many bugs - e.g. some user puts 0 in a text box and instead of the program reacting as if you put 0 in it reacts the same way as if no value was supplied.
Then everybody loses their minds because the system suddenly started doing something it was never supposed to.
I've never had this behavior cause a bug.
The most common case is when people use `if not s` to test for None in an otherwise string value (e.g. from JSON) without realizing that empty strings are also false.
Keep programming in python long enough and you'll see it eventually. It might be hard to recognize at first.
On the python bug tracker somewhere a while back there was a guy who wrote "if time_variable:" which resolved to false but only when it was called at midnight. It was initally resolved as wontfix.
(midnight being the datetime's conceptual equivalent to empty string, 0, empty list, etc.)
+1 — and it only saves characters when someone really knows what they’re doing. Most of is end up with `if (not result) or (result == “”)` and worse.
The only things I’d have probably change about this list is the inclusion of some of the collections.abc containers for type annotations, TypedDict and how it can make working with strictly structured dictionaries not terrible (but if it looks like an class and quacks like a class, make it a class if you can), and Counter (only because I forget it exists every single time I should have used it).
Several comments disliking the walrus operator, like many of the features on this list I also hated it… until I found a good use for it. I almost exclusively write strictly typed Python these days (annotations… another feature I originally hated). The walrus operator makes code so much cleaner when you’re dealing with Optionals (or, a Union with None). This comes up a lot with regex patterns:
Could you evaluate match on a separate line before the conditional? Sure. But I find this is a little clearer that the intended life of match is within the conditional, making it less tempting to reuse it elsewhere.Not a Python feature specifically, but I’d also love to see code that uses regex patterns to embrace named capturing groups more often. .group(“prefix”) is a lot more readable than .group(1).
As someone coming from Javascript/Typescript & now working full time in Python, this is a lovely - mostly fantastically useful - little resource. Some choice observations:
1. Typing overloads: TS has typed overloads I think largely as an affordance to an unfortunate feature of Javascript. In my experience overloads are an anti-pattern or at best code smell. It's nice that you can type them if you're cleaning up an existing codebase that uses them but I would consider them tech debt.
2. Keyword-only and Positional-only Arguments: This is the opposite of the 1st feature (ability to make method signatures more strict) but man is the syntax cryptically terse. I'd love to use this everywhere but I'd be concerned about readability.
3. Future Annotations: Thank you for this section - forward references have been a real pain for me recently & this is the first explanation that's scratches the service of the "why" (rather than just focusing on case-by-case solutions), which is much more helpful. Bring on PEP 649.
4. Generics: Cries in legacy 3.10 codebase
5. Protocols: As a Typescript guy, this seems very cosy & familiar. And not very Pythonic. I'm not sure how to feel.
14. Metaclasses:
> if you are that 1% which has a unique enough problem that only metaclasses can solve, they are a powerful tool that lets you tinker with the internals of the Python object system.
OR if you're one of the many devs that believes their problem is unique & special & loves to apply magical overengineered solutions to simple common problems, then the next person who inherits your code is going to really love your metaclasses. They sure make tracing codepaths fun.
> 5. Protocols: As a Typescript guy, this seems very cosy & familiar. And not very Pythonic. I'm not sure how to feel.
Isn't it super pythonic? One of the first things you learn about Python is that "everything is duck typed", but then the type system is primarily nominally typed. It seems like Protocols should have been there from the start, like Typescript interfaces.
Maybe you're right.
I'm new to Python & what is or isn't "pythonic" doesn't seem very intuitive to me (beyond just reading PEPs all day) - I guess I'm speaking from using the current type system & the idea of nominal types coexisting with structural seems a little disjointed.
For me, the biggest benefit to python is that it feels like executable pseudocode. The language gets out of the way of your domain level instructions. This is probably why most non-programmers have the easiest time with python.
The more fancy stuff you add to it, the less attractive it becomes. Sure, most of these things have some sort of use, but I reckon most people do not get deep enough into python to understand all these little things.
Lua is like this only moreso, and there's little fear of "progress" as large parts of the community have stuck with Lua5.1.
Of course, Lua is batteries-not-included so there may be the problem of "progress" in external libraries; in practice things like Penlight barely change though.
I disagree. Adding "fancy stuff" by definition does not remove what made it attractive to you in the first place. You even acknowledge this in your second sentence. Something you don't know of is not capable to influence your opinion in any way. And when you know of it -- and dislike it -- just keep doing it the way you did before knowing it.
> And when you know of it -- and dislike it -- just keep doing it the way you did before knowing it.
That breaks down as soon as you need to work with anyone else's code that uses the "fancy stuff".
Nitpick about 9.3 Short Circuit Evaluation: both things evaluate differently if you have empty strings. The if-else clause treats empty strings as valid while the or operator will treat them equivalent with None.
Similarly with 9.2, assignment using a walrus operator will also fail if the value is 0 (or anything falsy: https://docs.python.org/3/library/stdtypes.html#truth-value-...)
You can use `if (response := get_user_input()) is not None` if that's important. IME, empty strings and None would be treated the same way.
I would argue that most of these features (basically everything except metaclasses) are not advanced features. These are simple, but for some reason less well known or less used features.
Metaclasses are however quite complex (or at least lead to complex behavior) and I mostly avoid them for this reason.
And 'Proxy Properties' are not really a feature at all. Just a specific usage of dunder methods.
Simple doesn’t make something basic or beginner level.
I enjoyed reading the article. I'm far from a Python expert, but as an observation, most of these features are actually just typing module features. In particular, I wasn't sold on Generics or Protocols as I would have just used duck typing in both cases... Does modern, production-level python code use types everywhere? Is duck typing frowned upon?
> Does modern, production-level python code use types everywhere?
I don't know about code inside companies, but most new open source projects I encounter use typing. Many old ones have been converted too.
> Is duck typing frowned upon?
No. You can use Protocols to define what shape you expect your duck to be. There's some discussion about whether you should use abstract classes or protocols, though.
Structural typing is a static equivalent of dynamic duck typing. It is compatible with type checkers. Protocols mentioned in the article work without inheritance. A duck doesn't need to know that there is SupportsQuack protocol. If it quacks, it passes the type checks.
This is wild and something I didn't know about:
https://blog.edward-li.com/tech/advanced-python-features/#2-...
And you can use * for the reverse (every parameter from here on needs to be keyword-only).
https://docs.python.org/3.12/reference/compound_stmts.html#f...
That feature I'm using regularly and can recommend doing so, e.g. when passing boolean flags to functions, to clarify their meaning at the call-site by ensuring they have to be passed by name.
Turns out I managed to use almost all of these during a refactor of a project at work, even metaclasses... (Metaclass usage is justified in my case: we have a sort of language evaluator and using a metaclass lets us define function arguments with their types and validators in a very coincise and obvious way similar to Pydantic.)
I think this list should also include descriptors[0]: it's another metaprogramming feature that allows running code when accessing or setting class attributes similar to @property but more powerful. (edit: nvm, I saw that they are covered in the proxy properties section!)
I think the type system is quite good actually, even if you end up having to sidestep it when doing this kind of meta-programming. The errors I do get are generally the library's fault (old versions of SQLAlchemy make it impossible to assign types anywhere...) and there's a few gotchas (like mutable collections being invariant, so if you take a list as an argument you may have to type it as `Sequence[]` or you'll get type errors) but it's functional and makes the language usable for me.
I stopped using Ruby because upstream would not commit on type checking (yes I know you have a few choices if you want typing, but they're a bit too much overhead for what I usually use Ruby for, which is writing scripts), and I'm glad Python is committing here.
[0]: https://docs.python.org/3/howto/descriptor.html
TFA's use-case for for/else does not convince me:
As it is shorter to do this:What about this?
Yes, that might be a better example.
If you replace the assignment in the else clause with something side-effectful it does make sense. But even then it harms the readability of the code to such a ridiculous extent. I've never not regretted adding an else clause to a for loop. Any cognitive benefits from Python's approach to for loops closely mirroring natural language just go out the window.
If the team is familiar with Functional Programming concepts, I'd actually suggest
which IMHO better conveys the intent to the reader (i.e. focus on "what to do" over "how to do it")This kind of search can be done a variety of different ways, and is worth abstracting, e.g.:
Instead of catching the `StopIteration` exception, you can simply provide a default case to `next` :
Indeed. I was thinking of ways to generalize for the case where a default isn't desired, then decided against introducing that complexity in the example, then forgot that I could re-simplify further.
Great, first() comes part of more_itertools !
I would say that the main benefit of for/else is readability and ease of writing since nothing needs to be front-loaded. (The downside is an arguably redundant addition to the basic syntax, of course.)
The shorter version that saves the else: line assigns primary_server twice: it is OK for an assignment but a potential problem if doing something less trivial, and very ugly style in any case.
I do like the else clause with for loops. However most people are not familiar with it, and also `else:` as a keyword is confusing. I always remember it as `no break:`.
I think of it as "otherwise:"
That's still confusing though. The problem here is that `else` is semantically attached to `break`, but syntactically attached to the body of the loop. The latter makes it look like it executes if the loop body didn't, if you interpret it in the most straightforward way.
IMO a better design would be to have a block that always executes at the end of the loop - there's even a reasonable keyword for it, `finally` - but gets a boolean flag indicating whether there was a break or not:
Or better yet, make `break` take an optional argument (which defaults to `True` if unspecified), and that's what you get in `finally`. So this could be written:I thought the typing overload was interesting because I thought it was going to be like overloading in C# and Java but it's really just useful for documentation and intellisense tools in IDEs.
Working in the ML field, I can't hate Python. But the type system (pre-3.12, of course) cost me a lot of nerves. Hoping for a better post-3.12 experience once all libraries are usable in 3.12+. After that experience, I’ve come to truly appreciate TypeScript’s type system. Never thought I’d say that.
One of the most frustrating bugs I had encountered was when I was using memory mapped CSC format sparse arrays.
I needed the array indices to be int64 and specified them as such during initialization.
Downstreams, however, it would look at the actual index values and dynamically cast them to int32 if it judged there would be no loss in precision. This would completely screw up the roundtrip through a module implemented in C.
Being an intermittent bug it was quite a hell.
Hey kiddo… did you ever try something nastier? I’ve got something that will blow your mind and you’ll keep coming back
You don’t know but you are addicted to types
Come to the light - Haskell!
That's basically where I am coming from :). I know about my addiction.
Which is despite, a decade of attempts, still missing dependent types. Time to embrace Idris.
Or embrace logic + functional programming: Curry. https://curry-language.org/
Same experience here, Python’s typing experience is awful compared to TypeScript, even post-3.12. Mypy’s type inference is so dumb you have to write arguments like `i: int = 0`; `TypedDict`s seems promisable at first and then end up as a nightmare where you have to `cast` everything. I miss TypeScript’s `unknown` as well.
I'm just waiting for the astral's people (uv, ruff) type checker at this point. On large projects mypy is often unreliable and slow.
You really should check out pyright/pylance/basedpyright. Just an all around better type checker. Even has the "unknown" from typescript (kinda).
It still has a special case for dataclass-like things. I don't see how Python type checking (I haven't tried Red Knot) could let you do semi-magical things like Zod schema validation from TypeScript.
100%. Python typing is nowhere near as powerful as TS, and the example you gave demonstrates that.
I mentioned pyright because (some of) the specific concerns by OP are addressed by it.
0 can be inferred as a float too, so doesn’t it make sense to type numbers?
Try:
The inferred type is not `float` nor `int`, but `Any`. Mypy will happily let you call `f("some string")`.How would a typing system know if the right type is `int` or `Optional[int]` or `Union[int, str]` or something else? The only right thing is to type the argument as `Any` in the absence of a type declaration.
`Any` is the correct call.
It could be:
Yeah, I know it's retarded. I don't expect high quality code in a code base missing type annotation like that. Assuming `i` is `int` or `float` just makes incrementally adoption of a type checker harder.That's a mypy issue.
Pyright correctly deduces the type as int.
In any case it's a bad example as function signatures should always be typed.
So you want strong typing, but then are to lazy to properly type your function definitions?
I want a typing system with a good inference that doesn’t require me to type each and every variable, just like in any good statically-typed language like OCaml or Typescript. Strong typing and explicit typing are two very different things.
no need to explicitly write the type if you have type inference:
1) the code you wrote isn’t Python.
2) inferring the type is int isn’t guaranteed to be correct in this case
It's guaranteed to be correct if you use different operators for ints and floats, which is what at least some ML dialects (notably, OCaml) do precisely so that types can be inferred from usage.
That's the downside of operator overloading - since it relies on types to resolve, they need to be known and can't be inferred.
I was merely giving an example that strong typing has nothing to do with having to write the types. (and, obviously, the inferred type (int -> int) is correct. )
Only if reveal_type only accepts an int. Just because the default value of i is 0 doesn't mean anything about what could be passed in.
not my fault python is broken.
I believe mypy infers i as an integer in i = 0. I remember I had to do i = 0.0 to make it accept i += someFloat later on. Or of course i:float = 0 but I preferred the former.
Yes, but not in arguments:
Output:Because it shouldn’t in function arguments. The one defining the function should be responsible enough to know what input they want and actually properly type it. Assuming an int or number type here is wrong (it could be optional int for example).
In TypeScript arguments with a default value "inherit" the type of that value, unless you explicitely mark it otherwise. I believe this is how Pyright works as well.
Mypy was designed to enable gradual adoption. There is definitely Python code out there with `def f(i=0)` where `i` could be any numeric type including floats, complex, numpy etc.. This is called duck typing. It's wrong for a type checker to assume `i: int` in such a case.
Pyright probably works if you use it for a new project from the start or invest a lot of time "fixing" an existing project. But it's a totally different tool and it's silly to criticise mypy without understanding its use case.
I’d be very happy if `def f(i=0)` would type i as a number-like, but right now it’s not typed at all: mypy "infers" its type as `Any`.
I tried Pyright but as you say on an existing project you need a looot of time to "fix" it.
Python has evolved into quite a complex language considering the amount of features that come built-in. The documentation, while complete, does not facilitate discovery of many of those features.
The quality of documentation is a known, highlighted concern in the community. There is particular interest in adopting Diataxis principles to improve documentation, both for Python itself and for packaging (see e.g. the user guide at https://packaging.python.org/en/latest/ , or the PyPA main site https://www.pypa.io/en/latest/ ).
If you want to help, there's a section on the Python forum (https://discuss.python.org/c/documentation/26) and a Discord server, and issues with documentation can also be reported on the main Python GitHub issue tracker (https://github.com/python/cpython/labels/docs).
Just like Microsoft maintains the Typescript handbook, I think the Python Foundation needs a "Idiomatic Modern Python Handbook" which always shows the most modern and idiomatic way to use Python features.
Python has always been quite complex, people get deceived by how simple Hello World looks in Python.
Using it since version 1.6.
It's more complex that decade ago, but still a relatively simple language. I can understand the article without much effort, while I scratch my head really hard when read about advance feature of Typescript or Scala.
> still a relatively simple language
If only. I suspect very few Python programmers can even fully explain what `a + b` does.
If `a` and `b` are instances of classes, many would say it's equivalent to `a.__add__(b)` or `type(a).__add__(a, b)`, but in fact it's much more complex.
I once broke some Python by changing a = a + b to a += b.
If a and b are lists, the latter modifies the existing list (which may be referenced elsewhere) instead of creating a new one.
I think Python is the only language I've encountered that uses the + operator with mutable reference semantics like this. It seems like a poor design choice.
This is one of the absolute worst design mistakes in Python. The example you give (that `a = a + b` and `a += b` aren't equivalent) is bad enough:
What's worse is that sometimes, they are equivalent: And even worse, in order to support a version of `a += b` that sometimes modifies `a` (e.g. with lists), and sometimes doesn't (with tuples), the implementation of the `+=` operator is convoluted, which can lead to: The operation raises a TypeError, despite having succeeded!Care to elaborate? Where is the complexity hidden?
1. operators use special lookup for dunder methods--on class, not on instance:
2. There is __radd__ that is called if __add__ doesn't support given types (for different types).the full story is in the language reference, but the short answer is: __radd__ (unless it's more complicated than I realize)
Yes, the next level of complexity is that `a + b` will sometimes fall back to `b.__radd__(a)` if `a.__add__(b)` returns `NotImplemented`. But also:
- There are situations where `__radd__` takes priority over `__add__`. The rules for determining that priority are complex (and IIRC subtly different from the rules that determine whether `a < b` prioritises `a.__lt__(b)` or `b.__gt__(a)`).
- The lookup of `__add__` etc uses a special form of attribute lookup that's neither equivalent to `a.__add__` nor `type(a).__add__`. This special lookup only searches `type(a)` whereas the first would find an `__add__` function on `a`, and the second on `type(type(a))`.
I've also heard of further complications caused by implementation details leaking into the language semantics - for example, see Armin Ronacher's blog post: https://lucumr.pocoo.org/2014/8/16/the-python-i-would-like-t...
I doubt many 4gl language developers could explain what a + b does in their respective language. That rabbit hole goes all the way down to the physical instruction execution on silicon.
Meanwhile some 3GL language developers, think they can explain.
> @overload is a decorator from Python’s typing module that lets you define multiple signatures for the same function.
I'll be honest, I've never understood this language feature (it exists in several languages). Can someone honestly help me understand? When is a function with many potential signatures more clear than just having separate function names?
It's an implementation of "ad-hoc polymorphism", where, for example, it may make sense to "add" (+) together numbers of various types: integers, floating points, rational numbers, etc.
Thus the (+) operator for addition is "overridden" or "polymorphic" in the types of numbers that can be added together.
The argument for having a polymorphic signature rather than just multiple separate "monomorphic" functions is similar to that for "generics," otherwise known as "parametric polymorphism": why not just have a function `forEachInt` for iterating over lists of ints, a separate function `forEachChar` for iterating over lists of characters, and so on?
Higher levels of abstraction and generality, less boilerplate and coupling to any particular choice of data structure or implementation.
You could of course go the route of golang which indeed just had you write "monomorphized" versions of everything. Several years later generics were added to the language.
Alternatively, you throw everything out and never have to worry about typing or polymorphism, at the cost of static safety.
I would recommend reading the article example once more. Going from the example, without the overload, the function would return a union type which means any time you use the function, you have to put a type check to the result to know if the output is a list or not. With overload, as soon as an argument to the function is a certain type, the output is determined, so you won't need to type check.
I think that GP's point is that you could accomplish the same thing by simply having separate functions with different names, signatures, and return types.
In other languages it's an alternate way to do default or optional values in function arguments. Python instead usually uses kwargs for this.
That's what you get when you add a typing system and come full circle to doing OOP like Java.
Here is another one, list and expression comprehensions shared with ML languages (not that AI one), apparently many aren't aware of them.
The itertools package.
and maybe https://more-itertools.readthedocs.io/en/stable/
never used it but it seems worth a try
Some of these newer features don't seem like improvements to the language (e.g. the Walrus operator):
'''
# ===== Don't write this =====
response = get_user_input()
if response:
else: # ===== Write this instead =====if response := get_user_input():
else: '''The first implementation is immediately clear, even if you're not familiar with Python syntax. If you don't know what the ":=" operator does, the code becomes less readable, and code clarity is traded away in favor of being slightly more concise.
I disagree.
First of all, it takes a minute to search "python :=", and the construct itself is pretty simple. It's been part of the language since 2018[0]. I don't think "not knowing the language" is a good reason to avoid it.
Second, the walrus operator limits the variable's scope to the conditional, which can reduce certain bugs. It also makes some scenarios (like if/elif chains) clearer.
I recommend checking out the PEP for some real-world examples.
[0] https://peps.python.org/pep-0572/
Edit: my point about walrus scoping is incorrect. The new variable is function-scoped.
> the walrus operator limits the variable's scope to the conditional
Nope! It's still function-scoped.
In Python, walrus or no walrus, the body of a conditional is never a separate scope.
That's exactly my point. Having to search for the meaning of the operator at all makes the code less readable. I recommend reading the Zen of Python, which covers the design principles of the language.
I don’t write code to the level of someone who has just finished Hello, World. This isn’t something esoteric, it’s an extremely basic and useful part of the language. I’ve seen this argument used against multiple languages, and it has never made sense to me.
“I don’t want to use windowing functions in SQL, because most people don’t know what they are.” So you’d rather give up an incredibly powerful part of your RDBMS, and dramatically increase the amount of bandwidth consumed by your DB?
It’s as if the industry is embracing people who don’t want to read docs.
Couldn't you extend this line of thinking to any language-specific syntax on any programming language?
Don't use `match`, macros, lifetimes, ... in rust, someone coming from another language without them might not get what it means. Instead write the equivalent C-looking code and don't take advantage of any rust specific things.
Don't use lisp, someone coming from another language might not be able to read it.
Etc..
At one point if you write code and want to be productive, you need to accept that maybe someone that is not familiar with the language you're using _might_ have to look up syntax to understand what's going on.
One of the main objectives of Python is readability. IMO, features that take away from this objective become a detriment to the language.
In that example I gave, I totally agree with you. One area where I find walrus operators kinda useful is with dealing with iterators. For example,
'''
iterable = iter(thing)
while val := next(iterable, None): print(val)
'''
is a lot cleaner in my opinion compared to
'''
iterable = iter(thing)
val = next(iterable, None) while val is not None: print(val) val = next(iterable, None)
'''
Reason why I did not use this example outright was because I wasn't sure if people were familiar with the iter api, so I just chose a simpler example for the blog.
Yeah, I think the walrus operator is occasionally useful in `while` statements, but that's about it. IMO it wasn't worth the additional language complexity for an operator that's so rarely useful.
You can get code formatting by indenting with 2 spaces: https://news.ycombinator.com/formatdoc
Isn't this equivalent to just `for val in thing`?
If `thing` already supports iteration, then yes, but sometimes you need to build your own [0].
[0]: https://docs.python.org/3/library/functions.html#iter
Parent's example had a direct `iter(thing)`. Even if you had to use `iter(thing, sentinel)`, you'd still use `for val in iter(thing, sentinel)`, not while.
Good article! I found the typing bits particularly interesting. This part of the language has been evolving rapidly recently, which is cool if you like static typing.
Although I think the example of type alises in section 4 is not quite right. NewType creates a new "subtype" which is not equivalent to the original type. That's different to TypeAlias, which simply assigns a name to an existing type. Hence NewType is still useful in Python 3.12+.
> which is cool if you like static typing.
I don't believe any reasonable person would call Python statically typed, it just now has a pathway though which one can send additional documentation, with all of its caveats
And, yes, I am aware of the chorus of "just sprinkler more linters"Nice!
I would add assert_never to the pattern matching section for exhaustiveness checks: https://typing.python.org/en/latest/guides/unreachable.html#...
The way the language is evolving, it seems likely to me that people in the applications camp (ML, simple web-dev, etc.) will soon need a "simple Python" fork or at least an agreed-upon subset of the language that doesn't have most of these complications (f-strings are a major success, though).
Nothing stops you from simply not using features, and even having CI checks that forbid them. Not sure why you’d need a fork.
The only newish thing on this list appears to be structural pattern matching. Other than that it's all typing stuff which is all optional.
Good list. Some of these i knew already, but the typing overloading and keyword/positional-only arguments were new to me.
One personal favorite of mine is __all__ for use in __init__.py files. It specifies which items are imported whenever uses from x import *. Especially useful when you have other people working on your codebase with the tendency to always import everything, which is rarely a good idea.
It’s never a good idea. I use `__all__` to explicitely list my exports in libraries, so that when one write `from mylib import `, the IDE auto-completes only the public classes and functions.
This is a nice list of "things you might not know" that is worth skimming to add to your toolkit.
If you are really interested in "advanced Python", though, I would recommend the book Fluent Python by Ramalho. I have the first edition which is still highly relevant, including the async bits (you just have to translate the coroutines into async syntax). There is a second edition which is more up to date.
I would also recommend checking out the functools[0] and itertools[1] modules in the standard library. Just go and read the docs on them top to bottom.
It's also worth reading the first few sections of Python Data Model[2] and then bookmarking this page.
[0] https://docs.python.org/3/library/functools.html
[1] https://docs.python.org/3/library/itertools.html
[2] https://docs.python.org/3/reference/datamodel.html
+1 to itertools especially. Absurdly powerful, and the recipes at the bottom of the doc page are terrific.
The only problem I’ve found is that for interviews, people often aren’t familiar with it, which can lead to you solving whatever puzzle they had in far less time than they intended, and without manually building whatever logic it was they assumed you would need.
> The only problem I’ve found is that for interviews, people often aren’t familiar with it, which can lead to you solving whatever puzzle they had in far less time than they intended, and without manually building whatever logic it was they assumed you would need.
My favourite example of a similar thing happening to me was when I was asked to reverse the digits in a number. I somewhat jokingly asked if I was assuming base 10, which got some awkward looks so I knew something was up. They weren't impressed at all with my answer of `"".join(reversed(str(123456789)))`. I didn't get the job.
Good list, its one of these things you either never heard of, or used for years and think everyone knew that. I'll add a few:
* did you know __init__.py is optional nowadays?
* you can do relative imports with things like "from ..other import foo"
* since 3.13 there is a @deprecated decorator that does what you think it does
* the new generics syntax also works on methods/functions: "def method[T](...)" very cool
* you can type kwargs with typeddicts and unpack: "def fn(*kwargs: Unpack[MyKwargs])"
* dataclasses (and pydantic) support immutable objects with: "class MyModel(BaseModel, frozen=True)" or "@dataclass(frozen=True)"
* class attributes on dataclasses, etc. can be defined with "MY_STATIC: ClassVar[int] = 42" this also supports abstract base classes (ABC)
* TypeVar supports binding to enforce subtypes: "TypeVar['T', bound=X]", and also a default since 3.13: "TypeVar['T', bound=X, default=int]"
* @overload is especially useful for get() methods to express that the return can't be none if the default isn't None
* instead of Union[a, b] or Optional[a] you can write "a | b" or "a | None" nowadays
* with match you can use assert_never() to ensure exhaustive matching in a "case _:" block
* typing has reveal_type() which lets mypy print the type it thinks something is
* typing's "Self" allows you to more properly annotate class method return types
* the time package has functions for monotonic clocks and others not just time()
anyone know more things?
> did you know __init__.py is optional nowadays?
It has an effect, and is usually worth including anyway. I used to omit it by default; now I include it by default. Also, you say "nowadays" but it's been almost 13 years now (https://peps.python.org/pep-0420/).
> since 3.13 there is a @deprecated decorator that does what you think it does
Nice find. Probably worth mentioning it comes from the `warnings` standard library.
> the time package has functions for monotonic clocks and others not just time()
There's quite a bit in there, but I question how many people need it.
Anyway, it's always surprising to me how when other people make these lists, such a large fraction is taken up by tricks with type annotations. I was skeptical of the functionality when the `typing` standard library was introduced; I've only grown more and more wary of it, even as people continue to insist to me that it's somehow necessary.
> did you know __init__.py is optional nowadays?
It's not optional. Omitting it gets you a namespace package, which is probably not what you want.
> TypeVar supports binding to enforce subtypes: "TypeVar['T', bound=X]",
Using the new generics syntax you mentioned above you can now do:
This is all fun and games unless you have to debug someone elses code and they use a new feature that you didnt know about. Speaking for myself, I would be glad if there were a python_light version of the interpreter that has a simple syntax only like the early 3.x versions.
just my 2 ct
> This is all fun and games unless you have to debug someone elses code and they use a new feature that you didnt know about.
What’s wrong about learning things by looking at code from more experienced people?
You underestimate the social complexity of a simplified language. Everyone would disagree about the design (what is worth including because it is "simple" or necessary?), everyone would avoid using an handicapped language where features they need are gratuitously missing, and nobody would care about it enough to work on implementation and maintenance.
This sounds fun if you have 10x programmers or at least IQ > 140 programmers in charge. Last place I worked, I was told never use "smart" tricks if you can do the same thing in a simpler way. For-else and f-strings and := sound like harmless enough (though the last one is controversial); "with" is useful for resources that need to be deallocated but "yield"? Really? "more readable" when you slap a decorator on the thing? Why are we packing the enter and exit operations into a single function?
Playing with the typesystem and generics like this makes me worry I'm about to have a panic attack.
Give me code that I can understand and debug easily, even when I didn't write it; don't do implicit magical control flow changes unless you have a very good excuse, and then document both the how and the why - and you'll get a product that launches earlier and has fewer bugs.
Sometimes, a few more if statements here and there make code that is easier to understand, even if there's a clever hack that could cut a line or two here and there.
I don’t really see "smart tricks" here, just features of the language that you may not be aware of. I use most of them in my daily work without thinking about it: `@overload` and other typing features are useful to document the code, prevent mistakes, and help IDEs generate more helpful suggestions; keyword-only arguments force users to pass options as keywords, so you avoid calls like `foo(x, z, z, true, true, false, true)` and ensure the code is more explicit; for-else can sometimes clarify some complicated code; the walrus operator, short-circuiting with `or`, and chaining operators are just normal Python.
overload is very useful, but I find some people mistakenly think it actually modifies the runtime function signature to do multiple dispatch, so I do try to avoid it in favour of simpler functions whenever possible
too many overloads can be a code smell IMHO, it's ok when you are implementing a very common pattern and want proper types (decorators come to mind, so that both @decorator and @decorator() work, where the decorator might also have optional args) but I think in cases like the example in the article it should almost always be two separate functions
The simple contextlib.contextmanager example doesn't really sell the benefits.
The main one is that it makes error handling and clean-up simpler, because you can just wrap the yield with a normal try/catch/finally, whereas to do this with __enter__ and __exit__ you have to work out what to do with the exception information in __exit__, which is easy to get wrong:
https://docs.python.org/3/reference/datamodel.html#object.__...
Suppressing or passing the exception is also mysterious, whereas with contextlib you just raise it as normal.
Another is that it makes managing state more obvious. If data is passed into the context manager, and needs to be saved between __enter__ and __exit__, that ends up in instance variables, whereas with contextlib you just use function parameters and local variables.
Finally, it makes it much easier to use other context managers, which also makes it look more like normal code.
Here's a more real-world-like example in both styles:
https://gist.github.com/tomjnixon/e84c9254ab6d00542a22b7d799...
I think the first is much more obvious.
You can describe it in english as "open a file, then try to write a header, run the code inside the with statement, write a footer, and if anything fails truncate the file and pass the exception to the caller". This maps exactly to the lines in the contextlib version, whereas this logic is all spread out in the other one.
It's also more correct, as the file will be closed if any operation on it fails -- you'd need to add two more try/catch/finally blocks to the second example to make it as good.
I don't fully disagree, but the yield context manager approach is very common when writing test fixtures, you do:
Not that it makes it any less magical, but at least it's a consistent python patternI strongly agree. The walrus operator ... argh.
When I code, I try to make everything I write not clever. I am not saving a byte here and there because I am not typing in a program from a magazine in the 1980s (I was there, I am not going back). No code golf. I did my time on the Timex-Sinclair with its miserable two kilobytes of memory and reserved keywords as special-function strokes at a character each.
Each line should do one thing, in general, and it ought to be obvious. Cleverness is held in reserve for when it is truly needed, namely data structures and algorithms.
One of my accomplishments which seems to have mattered only to me is when my apartment-finding/renting system, written entirely in Perl, was transferred into the hands of students, new programming students. Perl is famously "write-once, read-never" and seems to have a culture favoring code golf and executable line noise. Still, the students got back to me and told me how easily-ported everything was, because I had done just one thing per line, avoided $_ and other such shortcuts, and other practices. They were very happy to take it over because I had avoided being cryptic and terse.
My favorite hack is to use a single threaded ThreadPoolExecutor as my main thread. When another thread wants to communicate with it, it just queues the callback function into main using submit. This cleans up the architecture and avoids the use of Queue which is a bit slower.
If context managers are considered advanced I despair at the code you're writing.
As a UD, I didn't know what a context manager was before reading this article, so you are not really far from the truth, tbh.
Trying so hard to make it a typed language
Yes, please. Given I'm forced to use Python, I'd welcome any "compile time" tools I can get. These days, it's using pyright with strict mode, which is pretty good, but there's still a long way to go.
Another blog with no feed :(
[dead]
[flagged]
[flagged]
Those are basic, you can decompile a function and change its bytecode, you can completely revamp the parser using codecs, etc
Yeah I only worked professionally with python for about 6 months and I knew about most of them.