8 ms·
Show HN: Tacopy – Tail Call Optimization for Python
- anilakar 10mo ago> This eliminates the risk of stack overflow errors When you get stack overflows anywhere from a thousand down to fifty(!) frames in the stack it's not a risk, it's an inevitability in anything more complex than a programming tutorial. Yeah, I've been bitten by this in production. Writing the functionality in a clean iterative style was just too much of a hassle.
- phplovesong 10mo agoTCO can be implemented easily in non TC optimized langauges with a trampoline wrapper. Why do i need a fully fledged library for something that is basically a few lines of code?
- srean 10mo agoThere's quite a bit of overhead. I believe Clojure does it with trampoline as JVM does not (as far as I know) does not support tail call optimization. Ironic, given Guy Steele.
- ndr 10mo agoYes, Clojure doesn't have TCO either. You get `(loop ... (recur ...))` or `trampoline`. Naive recursion will consume the stack. https://clojuredocs.org/clojure.core/loop https://clojuredocs.org/clojure.core/loop https://clojuredocs.org/clojure.core/recur https://clojuredocs.org/clojure.core/recur https://clojuredocs.org/clojure.core/trampoline https://clojuredocs.org/clojure.core/trampoline
- pfdietz 10mo agoNor can you count on Common Lisp to have TCO. People who are new to CL and treat it like Scheme run into this constantly. Basically never recur down a list, since the list could be long. This problem also shows up in cases where TCO is not possible. For example, suppose we have a syntax tree for some program and need to traverse the tree. Nodes that represent lists of things might have a long list of children, and in a sufficiently large program recursing down that list can blow out the stack. Just recurse on list elements, which one can reasonably assume don't nest too deeply.
- srean 10mo ago> Tacopy is a Python library that provides a decorator to optimize tail-recursive functions by transforming them into iterative loops. Can this handle mutually recursive calls ? Because those are mostly the only place I use tail calls, rest I translate to iterative loops, list comprehension, maps and reduces.
- javierbg95 10mo agoReally cool project, fairly succinct and to the point :) I would love to see support for arbitrarily nested functions, as it is common to wrap these into a public API function without the iteration parameters.
- ndr 10mo agoYes, it is quite surprised they're not allowed. I wonder what's the likely limitation, any ideas?
- ndr 10mo agoFound out > Nested function definitions with @tacopy decorators are not supported. Functions decorated with @tacopy must be defined at module level. This constraint exists because inspect.getsource() on nested functions returns the source of the entire enclosing function, making it impossible to reliably extract and transform just the nested function's code. The decorator detects nested functions by checking for '<locals>' in the function's __qualname__ attribute and raises a clear error message instructing users to extract the function to module level. https://github.com/raaidrt/tacopy/blob/8f5db70da2b7bbfafc41b55abea44c0a65a807ba/DESIGN.md#:~:text=Nested%20function%20definitions,to%20module%20level. https://github.com/raaidrt/tacopy/blob/8f5db70da2b7bbfafc41b...
- raaid-rt 10mo agoHave some ideas on getting around this - stay tuned...
- dkersten 10mo agoOnce upon a time I tried to write such a decorator too in python 2.x and the byteplay bytecode disassembler library. I was trying to do the conversion at the bytecode level instead of transforming the AST. I believe I got as far as detecting simple self recursive functions, but never actually managed to implement the actual transformation.
- lioeters 10mo agoThat is an ambitious idea, and I applaud anyone who attempts such heights even if it turned out to be impractical. OP's approach is surprisingly straight forward, only a few hundred lines. https://github.com/raaidrt/tacopy/blob/main/src/tacopy/transformer.py https://github.com/raaidrt/tacopy/blob/main/src/tacopy/trans...
- jagged-chisel 10mo agoTaco Py. No Ta Copy. Took my brain a minute or so …
- upghost 10mo agoReally impressive! For anyone who's not a pythonista, trying to implement TCO is something akin to solving the Collatz conjecture but for Python. It's often just an exercise in madness. So seeing an elegant solution to this is really cool, I myself was a victim of this madness and was unable to do it so very cool to see someone nail it! This will be a goto tool for sure.
- busfahrer 10mo agoFun fact: In Scheme, TCO is required by the spec
- debugnik 10mo agoEcmaScript too, but most browser JS runtimes don't (didn't?) support it. It's also part of .NET CIL but only F# uses it so support for it has historically been flakey outside the CLR's main JIT mode.
- monster_truck 10mo agoSafari's JSC (and much more recently, WebAssembly) are the only ones that actually implement it. In practice I don't think it actually ends up being any better than V8, which I believe has some amount of logic to replace them with iterators or trampolines when it can. IIRC ES6 introduced PTC (Proper Tail Calls), Chrome had an experimental flag for a while but it introduced more issues than it solved (stack traces became a mess and the stack inspection changes came with realistic security concerns). Microsoft and Firefox refused to implement it. Safari refused to un-implement it, and also refused to adopt an opt-in per function flag. It's crazy how fast javascript has gotten. Ported a classic game earlier this year using all of the new stuff in ES5/6/onwards, the benchmarks are within a couple of percent of what the perf would be were it a standalone game. Runs with 250x monsters at 30x the original tick rate, or >1000x as many monsters at the original tick rate.
- artemonster 10mo agoCan someone give some real world examples for TCO? Every time I see this I only see fibonacci and gcd and I really want to encounter this one in the wild on something real and applicable
- threeducks 10mo agoTail calls can be used for parsing very efficiently: https://news.ycombinator.com/item?id=41289114 https://news.ycombinator.com/item?id=41289114
- spapas82 10mo agoFor tco to be really useful you need to think in a non procedural way. Imagine that you don't have loops in your language so you need recursion to do stuff multiple times. Also even in procedural languages there are some problems that are easier to understand and model if you use recursion, for example tree or graph like structures.
- artemonster 10mo agotraversing graph or a tree is not a TCO case because it would involve a stack/queue for DFS/BFS, whatever. I dont want to think in non procedural way, I reserve this nonsense to haskellers, please provide me a valid python use case for TCO :)
- bfffbgfdcb 10mo agoTraversing a graph and inspecting each node can definitely make good use of tail call optimization. For instance: you have a large graph and you are traversing a particular path through it — say a R/B tree seeking a node. You can write it iteratively or recursively. Neither needs to hold more than 1 node reference at a time, the choice is which you prefer to read and write. I prefer to write that recursively. Sounds like you may not. Observing “well I can write it iteratively so why do I need TCO” is obvious and uninteresting; that’s the point.
- upghost 10mo ago
- lukasnxyz 10mo agowhat is this, why is 95% of the file useless comments: https://github.com/raaidrt/tacopy/blob/main/src/tacopy/unparser.py https://github.com/raaidrt/tacopy/blob/main/src/tacopy/unpar... you're calling a built in function, why make obfuscate this in an "llm way"
- stingraycharles 10mo agoI also don’t think it’s actually tail call optimization, but rather an “unrecurser”. I’m also not convinced that this actually is worth the effort, considering it’s doing runtime rewriting of Python using Python.
- deleted 10mo ago[deleted]
- throwaway81523 10mo agoIt's a suboptimal implementation but it's interesting for the purpose of transpiling something like Scheme or ML or maybe Purescript to Python. Historically, suggestions to make Python tail recursive were rejected on the theory that the added stack frames helped debugging, and that Python was supposed to be an imperative language. But it has been dragged into supporting some FP idioms over the years and that has seemed like a good thing.
- raaid-rt 10mo agoCertain function calls that were unable to be run in Python can now be run. From that perspective, my thinking was that this was a net good since one can preserve the "clean syntax" from recursion while still being able to access the performance benefits of an iterative solution.
- jasonjmcghee 10mo agoIt's very llm-y. But, kudos to them for preserving the commit history / being honest about it.
- 10mo ago
- denys_potapov 10mo agoCool. Recursion in python is common bottleneck in competitive programming. Will give it a try. I created a similar tool for recursion [1]. But ended with rewriting AST and emulating stack. Pros - no need for accumulator, cons - almost unusable in real world. [1] https://dev.to/denyspotapov/callonce-python-macro-for-unlimited-recursion-depth-3l5k https://dev.to/denyspotapov/callonce-python-macro-for-unlimi...
- qsort 10mo agoDo you frequently use Python for competitive programming puzzles? I've done it a bit in the past, and everyone always used C++.
- denys_potapov 10mo agoAlways. Probably you can't get in top 100 with python[1]. But I like dicts with tuple keys, bigints, all the list things. [1] My best is around 1300 place in HackerCup.
- btilly 10mo agoI have to wonder how much better you'd do if they made pypy an option.
- simonw 10mo agoOK this is fun. I knocked up a quick browser-based playground to try it out: https://tools.simonwillison.net/tacopy-playground https://tools.simonwillison.net/tacopy-playground
- raaid-rt 10mo agothanks for making this! - curious to hear if you had any suggestions for what else you'd want supported by this tool
- hencq 10mo agoFrom the description, it doesn't really seem to be full Tail Call Optimization, but only optimizes tail recursion. At least all the examples are about tail recursion, where the function calls itself in tail position, which can indeed easily be changed to a loop. Tail Call Optimization would mean it optimizes any function call in tail position. Typically you'd implement that with a trampoline, but it doesn't seem like this does that. Edit: It's actually called out under limitations "No mutual recursion: Only direct self-recursion is optimized"
- nonameiguess 10mo agoIt's always amusing to see what Python has become thanks to NumPy and Django and how widely deployed it became when its intended purpose was always to be easy to understand, not performant. But just like JavaScript, people are going to hack it to death to make it fast anyway. Guido on tail calls 16 years ago: https://neopythonic.blogspot.com/2009/04/final-words-on-tail-calls.html https://neopythonic.blogspot.com/2009/04/final-words-on-tail...
- skylurk 10mo agoIt was not exactly the final word: https://blog.reverberate.org/2025/02/10/tail-call-updates.html https://blog.reverberate.org/2025/02/10/tail-call-updates.ht...
- spyc 10mo agoThe current implementation breaks semantics of functions with tail recursion from within loops: https://github.com/raaidrt/tacopy/issues/1 https://github.com/raaidrt/tacopy/issues/1