mirror of
https://github.com/Smaug123/static-site-pipeline
synced 2025-10-06 00:38:39 +00:00
Basic article about surprising things about Python
This commit is contained in:
71
hugo/content/posts/2025-06-21-python-pitfalls.md
Normal file
71
hugo/content/posts/2025-06-21-python-pitfalls.md
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
---
|
||||||
|
lastmod: "2025-06-21T00:00:00.0000000+01:00"
|
||||||
|
author: patrick
|
||||||
|
categories:
|
||||||
|
- programming
|
||||||
|
date: "2025-06-21T00:00:00.0000000+01:00"
|
||||||
|
title: Some Python surprises
|
||||||
|
summary: "I am forever astonished that people describe Python as a simple language. Here are some of the things I found very surprising about it."
|
||||||
|
---
|
||||||
|
|
||||||
|
# An assertion that can pass
|
||||||
|
|
||||||
|
The following code can succeed without throwing, although it's structurally very unintuitive why:
|
||||||
|
|
||||||
|
```python
|
||||||
|
try:
|
||||||
|
with Foo():
|
||||||
|
result = blah
|
||||||
|
except ValueError:
|
||||||
|
result.baz()
|
||||||
|
else:
|
||||||
|
assert False
|
||||||
|
```
|
||||||
|
|
||||||
|
This is an entry in the "you need to know how everything is implemented before you can understand it" registry: desugaring this makes it clear how it could pass (if the `__exit__` method on the context manager `Foo` throws `ValueError`), but the syntax is practically purpose-built to make it hard to see that it can.
|
||||||
|
|
||||||
|
# You can't monkey-patch `__add__` on an instance
|
||||||
|
|
||||||
|
Special methods like `__add__` are looked up on the *class*, not the instance.
|
||||||
|
(See [Special method lookup](https://docs.python.org/3/reference/datamodel.html#special-lookup).)
|
||||||
|
|
||||||
|
Again, `a + b` *looks* simple, but is in fact weirdly complex and its semantics are highly unintuitive.
|
||||||
|
|
||||||
|
# Integers are sometimes ref-equal and sometimes not
|
||||||
|
|
||||||
|
```python
|
||||||
|
a = 10000 # big enough to escape the small integer cache
|
||||||
|
b = 10000
|
||||||
|
a is b # False
|
||||||
|
10000 is 10000 # True
|
||||||
|
```
|
||||||
|
|
||||||
|
We're seeing a cpython optimisation in the `10000 is 10000` check: instances of literals in the same statement may be reused within that statement.
|
||||||
|
|
||||||
|
Personally I expected some amount of referential transparency around the `is` statement, but it turns out not!
|
||||||
|
|
||||||
|
# Dataclasses are implemented with strings and `eval`
|
||||||
|
|
||||||
|
I was astonished - it's literally [just weaving some strings together](https://github.com/python/cpython/blob/4eab9da960d6944546baa76e3eed56b809ea8ec0/Lib/dataclasses.py#L496) and [calling `exec`](https://github.com/python/cpython/blob/4eab9da960d6944546baa76e3eed56b809ea8ec0/Lib/dataclasses.py#L498)!
|
||||||
|
I don't know it to be *incorrect*, but the sheer bluntness of the instrument amazed me.
|
||||||
|
|
||||||
|
# The six builtins
|
||||||
|
|
||||||
|
There are [six builtins in Python](https://docs.python.org/3/library/constants.html).
|
||||||
|
`True`, `False`, and `None` are unsurprising; `__debug__` is a bit odd to be one of the six privileged constants, but fine, I guess.
|
||||||
|
|
||||||
|
`NotImplemented` is bizarre and suggests whole areas that you should ignore to maintain your sanity.
|
||||||
|
|
||||||
|
Then there's `Ellipsis`, which is the object for which `...` is sugar.
|
||||||
|
It can, for some reason, be reassigned (when `True` etc can't)?
|
||||||
|
(This reassignment doesn't actually have an effect, because the syntax `...` somehow manages to evaluate to `Ellipsis` even when the name `Ellipsis` itself is being used for a global. I still don't really understand what's happening here.)
|
||||||
|
|
||||||
|
# Multiprocessing
|
||||||
|
|
||||||
|
Oh *lord* the number of problems I've seen with the passing of objects across `multiprocessing` boundaries.
|
||||||
|
|
||||||
|
# Annoying wart: violation of Liskov substitution principle with keyword args
|
||||||
|
|
||||||
|
When overriding a method on a class, you can't rename the arguments - even to underscores - because argument names are part of a method's usable API surface.
|
||||||
|
(Pyright warns about this.)
|
||||||
|
The safe way to mark a variable as unused in an overridden method is to have the first line of the method be a useless assignment, e.g. `_ = arg_name`.
|
Reference in New Issue
Block a user