You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
I have implemented a stack-based version of ast.literal_eval and conducted benchmarks to compare its performance with the existing implementation. The results indicate that the stack-based version may improve performance on non-nested expressions, although it is slightly slower on nested expressions.
Benchmark Results
The benchmarks were conducted on an Intel(R) Core(TM) Ultra 9 185H. Below are the results comparing ast.literal_eval and stack_literal_eval:
Generally, performance changes like this add too much complexity to the code to be desirable -- there's also nothing stopping you from just using your implementation on your own. With that being said, maybe this is a good idea, but I think this needs to be discussed on DPO.
I've just had a look at the benchmarks but something feels wrong. The benchmarks say that ast.literal_eval takes for parsing a constant:
constant
literal_eval : 0.6472s
stack_literal_eval : 0.5110s
I don't understand how it can be that slow. Could you test your implementation using the command-line, e.g., ./python -m timeit -s 'import ast' 'ast.literal_eval("42")' (for instance, on a debug build, I get "50000 loops, best of 5: 4.53 usec per loop")? Or explain how those numbers were obtained? In addition, could you indicate whether the build is a debug, release, or a PGO(+LTO maybe) build please?
Ok, so the numbers are better. I'm still wondering whether the cost of maintaining a stack-based implementation is better. Usually literal_eval is used for relatively flat statements I think but is it possible to also see the memory usage?
By the way, there is a comment saying that "a complex expression can overflow the C stack and cause a crash." Does your implementation also suffer from a similar drawback?
Finally, I'd prefer having benchmarks using pyperf or hyperfine so that we also know the standard deviation of the benchmarks (if you're willing to push for this feature, we like having those kind of benchmarks).
The issue of "a complex expression can overflow the C stack and cause a crash" occurs with the compile function. You can reproduce it with ast.literal_eval("+0" * 10**4), so it does suffer from this drawback.
I just want to complain that pyperf is really not easy to use when generating this table. Here it is:
That's why I do it only with the text table and not the markdown one :D (if you're talking about something else, like how to generate all rows, I agree that it's a bit painful to do. I use a bash script for that though).
Overall the improvement are only interesting for constants or empty objects. I don't think this is sufficient enough compared to the maintenance cost (also, lists and sets are quite important IMO). Someone else's opinion would be welcomed (maybe @JelleZijlstra?)
Thanks for this contribution. The current implementation of ast.literal_eval relies on nested functions while your stack-based version does not; I wonder if that makes a significant difference. Your version also seems to be missing a special case for ast.Expression, which is present in the current implementation.
Like some previous commenters, I'm concerned that more complex cases (such as lists) apparently get slower. I don't know how most people use literal_eval(), but I suspect that most users pass something more complex than a single constant.
I found that the overhead of creating the local functions was quite high when the input was not very large. Moreover, these local functions are not de-allocated using reference counting - which triggers GC more often.
At Odoo, we often use it to parse small inputs (usually 20 to 200 characters).
gc.set_threshold(1000000)
gc.freeze()
ast.literal_eval('["anything", 1]')
ast.literal_eval('["anything", 1]')
col0=list(gc.get_objects(0)) # you will notice a few local objects per call
The problem with isinstance here is that bool is a subclass of int, so your code will fail on inputs involving Booleans.
>>>isinstance(True, int)
True
@JelleZijlstra, you seem to have missed my previous comment. Could you please take a look at this approach? I believe it's more robust and has already been tested. Thanks.
Activity
ZeroIntensity commentedon Sep 25, 2024
Generally, performance changes like this add too much complexity to the code to be desirable -- there's also nothing stopping you from just using your implementation on your own. With that being said, maybe this is a good idea, but I think this needs to be discussed on DPO.
cc @Eclips4, as you're now an AST expert :)
picnixz commentedon Sep 25, 2024
I've just had a look at the benchmarks but something feels wrong. The benchmarks say that
ast.literal_evaltakes for parsing a constant:I don't understand how it can be that slow. Could you test your implementation using the command-line, e.g.,
./python -m timeit -s 'import ast' 'ast.literal_eval("42")'(for instance, on a debug build, I get "50000 loops, best of 5: 4.53 usec per loop")? Or explain how those numbers were obtained? In addition, could you indicate whether the build is a debug, release, or a PGO(+LTO maybe) build please?Stanley5249 commentedon Sep 25, 2024
@picnixz
Sorry for the confusion. The benchmark results I provided represent the total time for 300,000 runs across both methods and all test cases.
Additionally, I'm using Python 3.12.6 release build.
picnixz commentedon Sep 25, 2024
Ok, so the numbers are better. I'm still wondering whether the cost of maintaining a stack-based implementation is better. Usually
literal_evalis used for relatively flat statements I think but is it possible to also see the memory usage?By the way, there is a comment saying that "a complex expression can overflow the C stack and cause a crash." Does your implementation also suffer from a similar drawback?
Finally, I'd prefer having benchmarks using
pyperforhyperfineso that we also know the standard deviation of the benchmarks (if you're willing to push for this feature, we like having those kind of benchmarks).Stanley5249 commentedon Sep 25, 2024
The issue of "a complex expression can overflow the C stack and cause a crash" occurs with the compile function. You can reproduce it with
ast.literal_eval("+0" * 10**4), so it does suffer from this drawback.I'll proceed with the benchmarks.
Stanley5249 commentedon Sep 25, 2024
@picnixz
I just want to complain that pyperf is really not easy to use when generating this table. Here it is:
For reproduction, see here.
picnixz commentedon Sep 25, 2024
That's why I do it only with the text table and not the markdown one :D (if you're talking about something else, like how to generate all rows, I agree that it's a bit painful to do. I use a bash script for that though).
Overall the improvement are only interesting for constants or empty objects. I don't think this is sufficient enough compared to the maintenance cost (also, lists and sets are quite important IMO). Someone else's opinion would be welcomed (maybe @JelleZijlstra?)
JelleZijlstra commentedon Sep 25, 2024
Thanks for this contribution. The current implementation of
ast.literal_evalrelies on nested functions while your stack-based version does not; I wonder if that makes a significant difference. Your version also seems to be missing a special case forast.Expression, which is present in the current implementation.Like some previous commenters, I'm concerned that more complex cases (such as lists) apparently get slower. I don't know how most people use literal_eval(), but I suspect that most users pass something more complex than a single constant.
Stanley5249 commentedon Oct 19, 2024
I've switched to a recursion-based approach, similar to the original implementation but much cleaner, resulting in increased performance.
The
ast.Expressionspecial case has also been fixed.You can check out the changes in this branch: Recursion Branch.
Timeit Results
Pyperf Results
kmagusiak commentedon Jul 22, 2025
Hello,
I found that the overhead of creating the local functions was quite high when the input was not very large. Moreover, these local functions are not de-allocated using reference counting - which triggers GC more often.
At Odoo, we often use it to parse small inputs (usually 20 to 200 characters).
Here's an implementation that sticks closer to current implementation:
https://gist.github.com/kmagusiak/d9a0b4612d11aacb2fba65620eb73dc6
I wrote a stack-based version, but it was slightly slower and not worth the complexity.
Is this something I should create a PR for?
JelleZijlstra commentedon Jul 22, 2025
That seems worth doing, yes.
pythongh-124503: simplified ast.literal_eval
Stanley5249 commentedon Jul 22, 2025
Hi @kmagusiak,
There's a specific reason I wrote my implementation this way, compared to your version.
My implementation uses
type(real) in [int, float]:Your implementation uses
isinstance(left, (int, float)):The problem with
isinstancehere is thatboolis a subclass ofint, so your code will fail on inputs involving Booleans.@JelleZijlstra, you seem to have missed my previous comment. Could you please take a look at this approach? I believe it's more robust and has already been tested. Thanks.
kmagusiak commentedon Jul 22, 2025
Indeed, I missed it when writing the POC, but I have updated the code before opening the PR (there is a test covering this).
JelleZijlstra commentedon Jul 22, 2025
If it's faster on all or virtually all input and it doesn't change any behavior, I'm happy. Feel free to send a PR!
pythongh-124503: simplified ast.literal_eval
pythongh-124503: simplified ast.literal_eval
pythongh-124503: update news
gh-124503: Optimize ast.literal_eval() for small input (GH-137010)
pythongh-124503: Optimize ast.literal_eval() for small input (pythonG…