[HN Gopher] Performance hacks for faster Python code
___________________________________________________________________
Performance hacks for faster Python code
Author : ashvardanian
Score : 70 points
Date : 2025-11-12 00:55 UTC (8 days ago)
(HTM) web link (blog.jetbrains.com)
(TXT) w3m dump (blog.jetbrains.com)
| tyingq wrote:
| Maybe also knowing when not to use python, or finding a solution
| in python that uses C/rust/etc underneath.
| knowitnone3 wrote:
| maybe you can skip C and just use assembly
| guhcampos wrote:
| Or fab your own chip.
| postexitus wrote:
| Or wire wrap your own transistors.
| lunias wrote:
| It's kinda funny how uv is written in Rust and many Python
| libraries where performance is expected to matter (NumPy,
| Pandas, PyTorch, re, etc.) are implemented in C. Even if you
| call into fast code from Python you still have to contend with
| the GIL which I find very limiting for anything resembling
| performance.
| woodruffw wrote:
| Python's strong native story has always been one of its
| biggest draws: people find it ironic that so much of the
| Python ecosystem is native code, but it plays to Python's
| strength (native code where performance matters, Python for
| developer joy/ergonomics/velocity).
|
| > Even if you call into fast code from Python you still have
| to contend with the GIL which I find very limiting for
| anything resembling performance.
|
| It depends. A lot of native extension code can run without
| the GIL; the normal trick is to "detach" from the GIL for
| critical sections and only reconnect to it once Python needs
| to see your work. PyO3 has a nice collection of APIs for
| holding/releasing the GIL and for detaching from it
| entirely[1].
|
| [1]: https://docs.rs/pyo3/0.27.1/pyo3/marker/struct.Python.ht
| ml#m...
| SAI_Peregrinus wrote:
| Python is the ultimate (for now) glue language. I'd _much_
| rather write a Python script to glue together a CLI utility
| & a C library with a remote database than try to do that
| all in C or Rust or BASH.
| lunias wrote:
| Yeah, it's great for stuff like that, but I find myself
| using Node more in that area.
| lunias wrote:
| I didn't know about detaching from the GIL... I'll look
| into that.
|
| > native code where performance matters, Python for
| developer joy/ergonomics/velocity
|
| Makes sense, but I guess I just feel like you can eat your
| cake and have it too by using another language. Maybe in
| the past there was a serious argument to be made about the
| productivity benefits of Python, but I feel like that is
| becoming less and less the case. People may slow down (a
| lot) writing Rust for the first time, but I think that
| writing JavaScript or Groovy or something should be just as
| simple, but more performant, do multi-threading out of the
| box, and generally not require you to use other languages
| to implement performance critical sections as much. The
| primary advantage that Python has in my eyes is: there are
| a lot of libraries. The reason why there are a lot of
| libraries written in Python? I think it's because Python is
| the number 1 language taught to people that aren't
| specifically pursuing computer science / engineering or
| something in a closely related field.
| woodruffw wrote:
| Yes, I think Python is _excellent_ evidence that
| developer ecosystems (libraries, etc.) are paramount.
| Developer ergonomics are important, but I think one of
| the most interesting lessons from the last decade is that
| popular languages /ecosystems will _converge_ onto
| desirable ergonomics.
| zahlman wrote:
| In my analysis, the lion's share of uv's performance
| improvement over pip is _not_ due to being written in Rust.
| Pip just has horrible internal architecture that can 't be
| readily fixed because of all the legacy cruft.
|
| And for numerical stuff it's absolutely possible to
| completely trash performance by naively assuming that
| C/Rust/Fortran etc. will magically improve everything. I saw
| an example in a talk once where it superficially seemed
| obvious that the Rust code would implement a much more
| efficient (IIRC) binary search (at any rate, some sub-linear
| algorithm on an array), but _making the data available_ to
| Rust; as a native Rust data structure, required O(N)
| serialization work.
| lunias wrote:
| > Pip just has horrible internal architecture that can't be
| readily fixed because of all the legacy cruft.
|
| Interesting... I didn't know that. So they should be able
| to get similar results in Python then?
|
| > absolutely possible to completely trash performance by
| naively assuming
|
| Yeah, of course we'd need a specific benchmark to compare
| results. It totally depends on the problem that you're
| trying to solve.
| zahlman wrote:
| > So they should be able to get similar results in Python
| then?
|
| I'm making PAPER (https://github.com/zahlman/paper) which
| is intended to prove as much, while also filling some
| under-served niches (and ignoring or at least postponing
| some legacy features to stay small and simple). Although
| I procrastinated on it for a while and have recently been
| distracted with factoring out a dependency... I don't
| want to give too much detail until I have a reasonable
| Show HN ready.
|
| But yeah, a big deal with uv is the caching it does. It
| can look up wheels by name and find already-unpacked
| data, which it hard-links into the target environment.
| Pip unpacks from the wheel each time (which also entails
| copying the data rather than doing fast filesystem
| operations, and its cache is an _HTTP_ cache, which just
| intercepts the attempt to contact PyPI (or whatever other
| specified index).
|
| Python offers access to hard links (on systems that
| support them) in the standard library. All the
| filesystem-related stuff is already implemented in C
| under the hood, and a lot of the remaining slowness of
| I/O is due to unavoidable system calls.
|
| Another big deal is that when uv is asked to precompile
| .pyc files for the installation, it uses multiple cores.
| The standard library also has support for this (and, of
| course, _all_ of the creation of .pyc files in CPython is
| done at the C level); it 's somewhat naive, but can still
| get most of the benefit. Plus, for the most part the
| precompiled files are also eligible for caching, and last
| time I checked even uv didn't do that. (I would not be at
| all surprised to hear that it does now!)
|
| > It totally depends on the problem that you're trying to
| solve.
|
| My point was more that even when you have a reasonable
| problem, you have to be careful about how you interface
| to the compiled code. It's better to avoid "crossing the
| boundary" any more than absolutely necessary, which often
| means designing an API explicitly around batch requests.
| And even then your users will mess it up. See: explicit
| iteration over Numpy/Pandas data in a Python loop,
| iterative `putpixel` with PIL, any number of bad ways to
| use OpenGL bindings....
| lunias wrote:
| > explicit iteration over Numpy/Pandas data in a Python
| loop
|
| Yeah, I get it. I see the same thing pretty often... The
| loop itself is slow in Python so you have APIs that do
| batch processing all in C. Eventually I think to myself,
| "All this glue code is really slowing down my C." haha
| fainpul wrote:
| People are making fun of this statement here / are being
| sarcastic. But it's a totally legit suggestion. If you know in
| advance, you are going to make something where performance
| matters, strongly consider using something other than one of
| the slowest languages of them all.
| woodruffw wrote:
| (2) surprised me a little. Not because of the performance
| consequences, but because I almost never see explicit calls to
| `copy()` in Python (and I read a _lot_ of Python).
|
| I think maybe a more realistic example there would be people
| using splatting without realizing/internalizing that it performs
| a full copy, e.g. xs = [1, *ys]
|
| Another one that stood out was (3). Slots are great, but >95% of
| the time I'd expect people would want to use `slots=True` with
| dataclasses instead of manually writing `__slots__` and a
| constructor like that. `slots=True` has worked since Python 3.10,
| so every non-EOL version of Python supports it.
| sgarland wrote:
| You can use __slots__ for normal classes; it's not limited to
| only dataclasses.
| woodruffw wrote:
| I know that; that's why I said "I'd expect" not "you can't."
| jessetemp wrote:
| I didn't find 2 surprising either, but I'm a little surprised
| you never see it. If you want to treat the args to a function
| as immutable, what can you do besides copy, modify, and return
| a new object?
| zahlman wrote:
| > what can you do besides copy, modify, and return a new
| object?
|
| You can directly produce a modified copy, rather than using a
| mutating operation to implement the modifications.
|
| It should be noted that "return a modified copy" algorithms
| can be much _more_ efficient than "mutate the existing data"
| ones. For example, consider the case of removing multiple
| elements from a list, specified by a predicate. The version
| of this code that treats the input as immutable, producing a
| modified copy, can perform a single pass: def
| without(source, predicate): return [e for e in
| source if not predicate(e)]
|
| whereas mutating code can easily end up with quadratic
| runtime -- and also be difficult to get right:
| def remove_which(source, predicate): i = 0
| while i < len(source): if predicate(source[i]):
| # Each deletion requires O(n) elements to shift position.
| del source[i] else: # The
| index increment must be conditional, #
| since removing an element shifts the next one
| # and that shifted element must also be considered.
| i += 1
| skribanto wrote:
| swap with last element then truncate at the end
| zahlman wrote:
| Yes, you can do this if you don't care about order, and
| avoid the performance degradation. But it's even more
| complex.
|
| Or if you do care about order, you can emulate the C++
| "erase-remove" idiom, by keeping track of separate "read"
| and "write" positions in the source, iterating until
| "read" reaches the end, and only incrementing "write" for
| elements that are kept; and then doing a single `del` of
| a slice at the end. But this, too, is complex to write,
| and very much the sort of thing one chooses Python in
| order to avoid. And you do all that work, in essence,
| just to emulate what the list comprehension does but in-
| place.
| woodruffw wrote:
| I think copying and modifying is normal! I just almost never
| see it with `copy()`. Apologies if I didn't say that clearly.
| automatoney wrote:
| There's some genuinely interesting tips in here, but #10 is for
| sure just padding so they could call the article "10 Hacks" haha.
| Everything else is at least somewhat Python specific, but "Hack
| 10: Avoid repeated function calls in loops" is just applicable to
| anything.
| knowitnone3 wrote:
| I agree. I can't imagine anybody would call a function that
| returns the same result in a loop like that. There are plenty
| more optimizations they could come up with. In fact, there are
| a couple of books https://www.oreilly.com/library/view/high-
| performance-python... for instance. Didn't want to link the one
| on Amazon
| FuckButtons wrote:
| Yeah, 10 felt like it was written by ai.
| zahlman wrote:
| A lot of it felt that way, to me.
| persedes wrote:
| Some helpful guidelines, but it's 2025 and people still use
| time.time and no stats with their benchmarks :(
|
| In general I feel like these kind of benchmarks might change for
| each python version, so some caveats might apply.
| igouy wrote:
| Perhaps you could suggest what should be used instead of
| time.time
| zem wrote:
| https://switowski.com/blog/how-to-benchmark-python-code/ has
| a decent overview of some benchmarking libraries
| LigmaBaulls wrote:
| a smart hack for performance is don't use python
| pansa2 wrote:
| > _Copying large objects like lists [...] can be costly in both
| time and memory._
|
| > _modify[ing] objects in place [...] improves performance by
| avoiding the overhead of allocating and populating new
| structures._
|
| AFAIK the poor performance of list copies (demonstrated in the
| article by a million-element list taking 10ms) doesn't come from
| memory allocation nor from copying the contents of the list
| itself (in this case, a million pointers).
|
| Rather it comes from the need to chase all of those pointers,
| accessing a million disparate memory locations, in order to
| increment each element's reference count.
| trollbridge wrote:
| Yeah. A more nuanced approach is that you should copy things
| that don't consist of lots and lots of references to other
| things, and you should mutate things that are mostly references
| to other structures.
|
| Which means, eventually, designing your data structures so you
| generally have two types of structures: one which isn't full of
| pointers, and one which mostly is.
| lunias wrote:
| What about using PyPy? You'll probably see a significant
| improvement in these benchmarks. You should also give it a shot
| in Node which I expect to be about on par with PyPy, but without
| the GIL.
| danudey wrote:
| If anyone wants to be surprised by optimization, a great way to
| do it is to look at all the cases where, even though Python is
| slower than C, the Python interpreter written in Python is
| faster than the Python interpreter written in C.
|
| Also, if we're going to suggest 'write it in another language'
| approaches, rewrite it in Golang. I detest writing in Golang
| but once you get the hang of things you can get to the point
| where your code only takes twice the time to write and 2% of
| the time (and memory) to run.
| lunias wrote:
| > rewrite it in Golang
|
| Totally, I'm a big fan of statically typed, compiled
| languages; especially when the codebase is large and/or there
| are a lot of contributors. I chose the Node example because I
| feel like it offers the same "ease-of-use" that draws people
| to Python.
|
| > get to the point where your code only takes twice the time
| to write and 2% of the time (and memory) to run.
|
| 100%. Sometimes this matters, sometimes it doesn't, but if
| we're talking about "smart performance hacks" this is
| definitely a top contender.
|
| I work on a Python project and I really wish that it
| supported multi-threading. If I rewrote it, I would
| prioritize that feature in the target language.
| ecshafer wrote:
| 1,2,5 are just kind of standard computer science knowledge things
| I would expect any CS2 student to know, and are pretty universal
| across most languages.
|
| Some of these are pretty nice python tricks though.
| qsort wrote:
| I'm sure this is plenty useful for less experienced people, but
| the "smart" hacks read a bit like:
|
| Hack 1: Don't Use The Obviously Wrong Data Structure For Your
| Problem!
|
| Hack 2: Don't Have The Computer Do Useless Stuff!
|
| Hack 3: Don't Allocate Memory When You Don't Need To!
|
| And now, a word from our sponsor: AI! Use AI to help AI build AI
| with AI, now with 15% more AI! Only with AI! Ask your doctor if
| AI is right for you!
|
| It's worth pointing out that a few of them are Python-specific.
| Compilers can inline code, there's usually no need to manually
| inline functions in most languages, that's Python being Python.
| Which scope the function is from being important is
| quintessentially Python being Python.
|
| The major gains in Python come from... not using Python.
| Essentially you have to rewrite your code around the fact that
| numpy and pandas are the ones really doing the work behind the
| curtain (e.g. aggressively vectorize, use algorithms that can use
| vectorization well rather than "normal" ones). Number 8 of the
| list hints at that.
| sevensor wrote:
| "Hacks" 4-10 could easily be replaced with "use numpy."
| Performance gains from doing math better in pure Python are
| minimal compared with numpy. It's not unusual for the numpy
| version of something to end up taking 0.01x as long to run.
| fluorinerocket wrote:
| Yes I didn't realize anyone actually did anything numerical
| without numpy. I don't think I've ever imported python's math
| module once. Who in their right mind is making a 1e6 long
| python list
| qsort wrote:
| Math is a lost cause, yeah, just use numpy (with the
| important caveat that you need to know what you're doing,
| it's easy to fumble badly).
|
| But Python has a few interesting features that can easily get
| you big wins, like generators, e.g.
| https://www.dabeaz.com/generators/Generators.pdf
| SatvikBeri wrote:
| A lot of interesting math can't be done in numpy, sadly. At
| that point you might be better off writing the initial
| version in Python and translating it to something else.
|
| A friend of mine asked me to translate some (well-written)
| number theory code a while back, I got about a 250x speedup
| just doing a line by line translation from Python to Julia.
| But the problem was embarrassingly parallel, so I was able
| to slap on an extra 40x by tossing it on a big machine for
| a few hours for a total of 10,000x. My friend was very
| surprised - he was expecting around a 10x improvement.
| danudey wrote:
| I 'wrote' (adapted from the Rich project's example code)
| a simple concurrent file downloader in Python; run
| 'download <any number of URLs>' and it goes and downloads
| each one, assuming that the URL has what looks like a
| filename at the end or the server response with a
| Content-Disposition header that contains a filename. It
| was very simple; spawn a thread for each file we're
| downloading, show a progress bar for each file we're
| downloading, update the progress bar as we download.
|
| I ended up rewriting the whole thing in Rust (my first
| Rust project) solely because I noticed that just that
| simple process - "get some bytes from the network, write
| them to this file descriptor, update the progress bar's
| value" was churning my CPU due to how intensive it was
| for the progress bar to update as often as it was - which
| wasn't often.
|
| Because of how ridiculous it was I opted to rewrite it in
| another language; I considered golang but all of the
| progress bar libraries in Golang are mediocre at best,
| and I liked the idea of learning more Rust. Surprise
| surprise, it's faster and more efficient; it even
| downloads faster, which is kind of ridiculous.
|
| An even crazier example: a coworker was once trying to
| parse some giant logfile and we ended up nerd-sniping
| ourselves into finding ways to speed it up (even though
| it finished while we were doing so). After profiling this
| very simple code, we found that 99% of the time in
| processing each line was simply parsing the date, and 99%
| of that was because Python's strptime is devoted to being
| able to parse timezones even if the input you're giving
| it doesn't include one. We played around with things like
| storing a hash map of "string date to python datetime"
| since there were a lot of duplicates, but the fastest
| method was to write an awful Python extension that
| basically just exposed glibc's strptime so you could
| bypass Python's (understandably) complex tz parsing. For
| the version of Python we were using it made parsing
| hundreds of thousands of dates 47x faster, though now in
| Python3 it's only about 17x faster? Maybe less.
|
| https://github.com/danudey/pystrptime
|
| I still use Python all the time because usually the time
| I save writing my code quickly more than outweighs the
| time I spend having slower code overall; still, if your
| code is going to live a while, maybe try running it
| through a profiler and see what surprises you can find.
| prinzmaus wrote:
| Use polars vs pandas. This alone saves me more time than any
| other "hack".
| danudey wrote:
| Not to mention that a lot of these performance improvements,
| while sane, are on the order of milliseconds of improvement.
| Unless you're doing one of these unoptimized approaches
| thousands or millions of times in a tight loop you're probably
| not saving a substantial amount of time/energy/computation.
| Premature optimization is still the root of all evil!
|
| If you want an _actual_ performance improvement in Python code
| that most people wouldn 't necessarily expect: consider using
| regexes for even basic string parsing if you're doing a lot of
| it, rather than doing it yourself (e.g. splitting strings, then
| splitting those strings, etc.); while regexes "feel" like they
| should be more complicated and therefore slower or less
| efficient, the regex engine in Python is implemented in C and
| there's a decent chance that, with a little tweaking, even
| simple string processing can be done faster with a regex. Again
| only important in a hot loop, but still.
| adammarples wrote:
| I remember learning not to use dot access in performance critical
| loops
| nodesocket wrote:
| While obvious, a huge performance improvement bump can had using
| cachetools on functions. Cachetools is much more feature rich
| than lru_cache with support for TTL.
| zahlman wrote:
| A previous submission of this
| (https://news.ycombinator.com/item?id=45937910) didn't take off;
| I'm posting this to cite my comment there
| (https://news.ycombinator.com/item?id=45940360) rather than
| copying and pasting it.
| yegle wrote:
| FWIW: the `x in foo` test calls the `foo.__contains__` magic
| method.
|
| For a list, the only way to implement it is by iterate through it
| (see the `list_contains` function in the CPython code).
|
| But for the special `range` object, it can implement the
| `__contains__` efficiently by looking at the start/stop/step (see
| the `range_contains` source code).
|
| Although the Hack 1 is for demonstration purpose, in most cases
| you can just do `999999 in range(1000000)`.
|
| In my test, the same `999999 in foo` is 59.1ns for the range
| object, 27.7ns for the set, 6.7ms for the list. The set is the
| fastest, except converting the range object to the set takes
| 21ms.
| cluckindan wrote:
| 6 runs two division operations in the "unoptimized" version and
| one in the "optimized" version. Gee, I wonder why it's exactly
| twice faster.
___________________________________________________________________
(page generated 2025-11-20 23:01 UTC)