Python 3.13, what didn't make the headlines
bitecode.dev
bitecode.dev
For example, 3.12 changed the byte code list comprehensions use, citing faster performance. In my tests, this is only true with small lists, and as soon as you get to a large number of items (~10K), it falls off a cliff. While large lists are probably not the norm, to have that completely ignored is maddening.
The 3.13 REPL is nice, I’ll give them that.
https://gist.github.com/thomasaarholt/2e4d42cbf3f0de60c811bb...
Running on Python 3.12.7 (main, Oct 2 2024, 15:45:55) [Clang 18.1.8 ]
11.32ns on N=100
9.42ns on N=1000
7.32ns on N=10000
5.23ns on N=100000
5.87ns on N=1000000
Running on Python 3.13.0 (main, Oct 16 2024, 08:05:40) [Clang 18.1.8 ]
7.30ns on N=100
5.91ns on N=1000
5.09ns on N=10000
4.31ns on N=100000
5.89ns on N=1000000Now when you are a professional dev, you see them less and less, because you make less mistakes, but also because you heavily use tooling to prevent errors or diagnose them.
But if you observe beginners with modern versions of Python, it's quite magical really. The autonomy and confidence they get from those are huge.
The energy that Pablo Galindo Salgado injected into the culture of good error message has been a boon to the language.
Running on Python 3.12.7 (main, Oct 2 2024, 15:45:55) [Clang 18.1.8 ] 16.69ns on N=100 18.84ns on N=1_000 15.88ns on N=10_000 14.26ns on N=100_000 14.70ns on N=1_000_000
Running on Python 3.13.0 (main, Oct 16 2024, 08:05:40) [Clang 18.1.8 ] 9.54ns on N=100 17.88ns on N=1_000 15.81ns on N=10_000 14.14ns on N=100_000 14.46ns on N=1_000_000
[1] https://gist.github.com/thomasaarholt/2e4d42cbf3f0de60c811bb...
Unsurprising. Attention to internals has been split by the GILectomy - I guess some people expected automatic gains from that, but that seemed unwarranted to me. It's normal for performance improvements in Python to be incremental (also in terms of memory efficiency - e.g. https://github.com/zahlman/python-dict-stats/), with here and there a big gain for a specific task (e.g. `math.factorial` getting a better algorithm in 3.x - https://stackoverflow.com/questions/9815252).
There also isn't generally a huge amount of pressure for these kinds of optimizations, given typical Python use cases. People who need performance are often already using a Python binding to code written in another language anyway.
> Ditto for zipfile.Path, a pathlib-compatible wrapper for traversing zip files you didn't know existed since 3.8. And you didn't know it because it sucked, frankly. But 3.13 brings many QoL patches to it, and improves a lot on how it handles directories, which used to require a lot of manual labor. So I expect it will see more use from now on.
It'd be nice if the various archive-handling libraries offered more of a common interface, including that sort of wrapper. BTW, Barney Gale (the dev primarily responsible for `pathlib`) is relatively active on https://discuss.python.org and generally a joy to talk with.
You’re of course not wrong, it’s just disappointing to see regressions. A side project I have started as pure Python and has gradually shifted to Python calling various C functions via ctypes. Before anyone jumps in with “why not numpy” etc., I wanted to see how fast I could make Python using only the stdlib. Calling C code is only kind of cheating, IMO.
That aside, there do remain some surprising ways to gain performance in Python that aren’t usually looked at. For example, its string mini-format language (you know, before f-strings, and before .format()) is significantly faster than either alternative. Not surprising, since it’s essentially just printf. This doesn’t matter at all unless it’s in a hot loop, but most of my usage is, so I’ve spent an extremely long time in cProfile, perf, and memray doing optimizations.
Anyway, f-strings can be faster since they're "pre-compiled", especially for simple cases:
$ python -m timeit --setup 'foo="bar"' -- 'f"Is the {foo}tender here?"'
5000000 loops, best of 5: 61.1 nsec per loop
$ python -m timeit '"Is the %(foo)stender here?" % {"foo":"bar"}'
1000000 loops, best of 5: 246 nsec per loop
$ python -m timeit '"Is the {foo}tender here?".format(foo="bar")'
1000000 loops, best of 5: 345 nsec per loop
Similarly with positional rather than named substitutions: $ python -m timeit 'f"""Is the {"bar"}tender here?"""'
5000000 loops, best of 5: 60.4 nsec per loop
$ python -m timeit '"Is the %stender here?" % "bar"'
2000000 loops, best of 5: 124 nsec per loop
$ python -m timeit '"Is the {}tender here?".format("bar")'
2000000 loops, best of 5: 181 nsec per loop def test_with_cstr(epoch: float) -> str:
return "%04d-%02d-%02d %02d:%02d:%02d" % (time.gmtime(epoch)[:6])
def test_with_fstr(dt: datetime) -> str:
return f"{dt.year:04d}-{dt.month:02d}-{dt.day:02d} {dt.hour:02d}:{dt.minute:02d}:{dt.second:02d}"
EDIT: bad example, there’s other function overhead here as well.