Tail-Call Optimization for Python
Tacopy is a Python library that provides a decorator to optimize tail-recursive functions by transforming them into iterative loops. This eliminates the risk of stack overflow errors for deep recursion.
- Automatic Tail-Call Optimization: Transforms tail-recursive functions into efficient loops
- Stack Overflow Prevention: Handle arbitrarily deep recursion without hitting Python's recursion limit
- Significant Performance Gains: 1.41x-2.88x faster than regular recursion (see benchmarks)
- Validation: Ensures functions are properly tail-recursive before transformation
- No Runtime Overhead: Optimization happens once at decoration time
- Preservation of Function Metadata: Keeps docstrings, type hints, and other metadata intact
# Using uv (recommended for development)
uv add tacopy-optimization
# Using pip
pip install tacopy-optimizationfrom tacopy import tacopy
@tacopy
def factorial_mod_k(acc: int, n: int, k: int) -> int:
"""Calculate (acc * n!) mod k using tail recursion."""
if n == 0:
return acc % k
return factorial_mod_k(acc * n % k, n - 1, k)
# This would normally cause a stack overflow, but works with @tacopy
result = factorial_mod_k(1, 1_000_000, 79)
print(result) # Output: 0Tacopy uses AST (Abstract Syntax Tree) transformation to convert tail-recursive functions into iterative loops. For example:
Original function:
@tacopy
def factorial(n: int, acc: int = 1) -> int:
if n == 0:
return acc
return factorial(n - 1, acc * n)Transformed to (conceptually):
def factorial(n: int, acc: int = 1) -> int:
_n = n
_acc = acc
while True:
if _n == 0:
return _acc
_n, _acc = _n - 1, _acc * _nThe transformation:
- Hoists parameters to uniquely-named local variables (using UUIDs to avoid collisions)
- Wraps the function body in a
while Trueloop - Replaces tail-recursive calls with variable assignments and
continuestatements
@tacopy
def fibonacci(n: int, a: int = 0, b: int = 1) -> int:
"""Calculate the nth Fibonacci number."""
if n == 0:
return a
if n == 1:
return b
return fibonacci(n - 1, b, a + b)
# Calculate very large Fibonacci numbers
fib_5000 = fibonacci(5000)@tacopy
def gcd(a: int, b: int) -> int:
"""Calculate GCD using Euclidean algorithm."""
if b == 0:
return a
return gcd(b, a % b)
print(gcd(1071, 462)) # Output: 21@tacopy
def sum_to_n(n: int, acc: int = 0) -> int:
"""Calculate sum from 1 to n."""
if n == 0:
return acc
return sum_to_n(n - 1, acc + n)
print(sum_to_n(100)) # Output: 5050For a function to be optimizable with @tacopy, it must be properly tail-recursive:
- Module-level function - The function must be defined at module level, not nested inside another function
- All recursive calls must be in tail position - the return value of the recursive call must be immediately returned, with no further operations
- No async functions - Async functions are not supported due to potential issues with shared state
@tacopy
def valid(n, acc):
if n == 0:
return acc
return valid(n - 1, acc + n) # � Tail call - immediately returned@tacopy
def invalid(n):
if n == 0:
return 1
return n * invalid(n - 1) # � NOT tail recursive - multiplication after calldef outer():
@tacopy # ❌ Error: nested functions cannot use @tacopy
def helper(n, acc):
if n == 0:
return acc
return helper(n - 1, acc + n)
return helper(10, 0)
# ✅ Correct: Extract to module level
@tacopy
def helper(n, acc):
if n == 0:
return acc
return helper(n - 1, acc + n)
def outer():
return helper(10, 0)The decorator will raise a TailRecursionError if the function is not properly tail-recursive or if it is nested inside another function.
You can view the transformed code without executing it:
from tacopy import show_transformed_code
def factorial(n: int, acc: int = 1) -> int:
if n == 0:
return acc
return factorial(n - 1, acc * n)
print(show_transformed_code(factorial))Tacopy provides significant performance improvements over regular recursion. Below are benchmark results comparing tail-recursive functions with and without the @tacopy decorator (100 runs each, recursion depth of 1000):
| Function | Without tacopy | With tacopy | Speedup | Performance Change |
|---|---|---|---|---|
factorial(1000) |
0.000230 ± 0.000117s | 0.000163 ± 0.000019s | 1.41x faster | 29.2% faster |
fibonacci(1000) |
0.000083 ± 0.000008s | 0.000045 ± 0.000013s | 1.86x faster | 46.4% faster |
sum_to_n(1000) |
0.000074 ± 0.000013s | 0.000026 ± 0.000002s | 2.88x faster | 65.2% faster |
power(2, 1000) |
0.000087 ± 0.000008s | 0.000044 ± 0.000008s | 1.97x faster | 49.3% faster |
- 1.41x-2.88x speedup for typical tail-recursive functions
- Eliminates stack overflow: Regular Python recursion is limited to 1000 calls, while tacopy can handle millions
- Lower variance: Tacopy-optimized functions show more consistent performance (lower standard deviation)
You can run the benchmarking suite yourself:
uv run python benchmarking/benchmark.pyThe benchmarks use a recursion depth of 1000 for non-tacopy functions and pure integer arithmetic in the sample implementations. See benchmarking/README.md for more details.
# Clone the repository
git clone https://github.com/yourusername/tacopy.git
cd tacopy
# Install dependencies using uv
uv sync
# Activate virtual environment
source .venv/bin/activate # On Unix/macOS
# or
.venv\Scripts\activate # On Windows# Run all tests
pytest tests/ -v
# Run with coverage
pytest tests/ --cov=tacopy --cov-report=html
# Update snapshots (if you've changed transformation logic)
pytest tests/ --snapshot-updatetacopy/
├── src/
│ └── tacopy/
│ ├── __init__.py # Main decorator and public API
│ ├── validator.py # Tail recursion validation
│ ├── transformer.py # AST transformation logic
│ └── unparser.py # AST to code conversion
├── tests/
│ ├── test_validator.py # Validator unit tests
│ ├── test_transformer.py # Transformer unit tests
│ └── test_integration.py # End-to-end integration tests
├── main.py # Example usage
├── DESIGN.md # Design document
└── README.md # This file
- Module-level functions only: The decorator can only be applied to functions defined at module level, not nested inside other functions. If you need to optimize a helper function, extract it to module level.
- Async functions not supported: The decorator will raise an error if applied to async functions
- Source code required: The function's source code must be accessible via
inspect.getsource() - No mutual recursion: Only direct self-recursion is optimized
- Python 3.10+: Requires Python 3.10 or higher
Contributions are welcome! Please feel free to submit a Pull Request.
This project is licensed under the GNU General Public License v3.0 - see the LICENSE file for details.