The RecursionError Problem
One of the most frustrating errors in Odoo development is hitting RecursionError: maximum recursion depth exceeded in computed fields. The traceback is usually massive — hundreds of repeated lines — and the root cause is not always obvious. This guide walks through why it happens and how to fix it.
What the Error Looks Like
RecursionError: maximum recursion depth exceeded while calling a Python object
File "/odoo/odoo/fields.py", line 1095, in __get__
self.compute_value(recs)
File "/odoo/odoo/fields.py", line 1280, in compute_value
records.env.computed[self][record] = ...
File "/odoo/addons/my_module/models/sale_order.py", line 45, in _compute_total
for rec in self:
rec.total = rec.subtotal + rec.tax
File "/odoo/odoo/fields.py", line 1095, in __get__
self.compute_value(recs)
... (repeated hundreds of times)The key indicator is the same __get__ and compute_value lines repeating in the traceback. Python hits its default recursion limit of 1000 frames and crashes.
Why It Happens
1. Circular Compute Dependencies
The most common cause is two computed fields that depend on each other, creating an infinite loop:
# BAD: circular dependency
class SaleOrder(models.Model):
_inherit = 'sale.order'
margin_amount = fields.Float(compute='_compute_margin', store=True)
margin_percent = fields.Float(compute='_compute_margin_percent', store=True)
@api.depends('margin_percent', 'amount_total')
def _compute_margin(self):
for rec in self:
rec.margin_amount = rec.amount_total * rec.margin_percent / 100
@api.depends('margin_amount', 'amount_total')
def _compute_margin_percent(self):
for rec in self:
rec.margin_percent = (rec.margin_amount / rec.amount_total * 100) if rec.amount_total else 0Field A depends on B, and B depends on A. When either is accessed, it triggers the other, creating infinite recursion.
2. Implicit Dependencies via Related Fields
Sometimes the cycle is hidden behind related fields or inherited models. Field A depends on a related field that internally triggers computation of field B, which depends back on A.
3. Overriding _compute Methods Without super()
If you override a compute method in an inherited model and the dependency chain differs from the parent, you can accidentally introduce a cycle.
4. Write Methods Triggering Recomputation
A compute method that calls write() on the same record can trigger field recomputation, which calls the compute method again.
How to Identify the Cycle
Step 1: Read the Traceback Carefully
Look for the repeating pattern. Identify which compute methods appear in the cycle. Usually two or three methods alternate repeatedly.
Step 2: Map the Dependency Graph
For each computed field in the cycle, list its @api.depends() arguments. Draw arrows between fields. If there is a loop, you have found the problem.
Step 3: Check Related Fields
If the depends includes a related field like order_id.total, check what total depends on. The cycle might span multiple models.
Step 4: Use Odoo Shell
# In odoo shell, inspect field dependencies
model = env['sale.order']
for field_name, field in model._fields.items():
if hasattr(field, 'depends') and field.depends:
print(f"{field_name}: depends on {field.depends}")Fixes
Fix 1: Break the Circular Dependency
Redesign so one field is the source of truth and the other is derived from independent data:
# GOOD: no circular dependency
@api.depends('order_line.price_subtotal', 'order_line.product_cost')
def _compute_margin(self):
for rec in self:
revenue = sum(rec.order_line.mapped('price_subtotal'))
cost = sum(rec.order_line.mapped('product_cost'))
rec.margin_amount = revenue - cost
rec.margin_percent = (rec.margin_amount / revenue * 100) if revenue else 0Both fields are computed in the same method from the same source data — no cycle.
Fix 2: Use a Single Compute Method
When two fields are interdependent, compute them together in one method:
margin_amount = fields.Float(compute='_compute_margins', store=True)
margin_percent = fields.Float(compute='_compute_margins', store=True)
@api.depends('amount_total', 'cost_total')
def _compute_margins(self):
for rec in self:
rec.margin_amount = rec.amount_total - rec.cost_total
rec.margin_percent = (rec.margin_amount / rec.amount_total * 100) if rec.amount_total else 0Fix 3: Remove store=True When Not Needed
Non-stored computed fields are computed on-the-fly and do not trigger cascading recomputation. If you do not need to search or sort by the field, remove store=True.
Fix 4: Use api.depends_context Instead
If the dependency is on context values rather than fields, use @api.depends_context('key') to avoid field-level dependency chains.
Fix 5: Guard Against Re-entry
As a last resort, use a context flag to prevent re-entry:
def _compute_total(self):
if self.env.context.get('_computing_total'):
return
self = self.with_context(_computing_total=True)
for rec in self:
rec.total = rec.subtotal + rec.taxThis is a workaround, not a proper fix. It masks the design problem.
Prevention
- Always draw out your dependency graph before adding computed fields
- Never have two stored computed fields that depend on each other
- Use a single compute method for related fields
- Test with
--log-level=debugto see field recomputation order - Keep compute methods pure — no
write()calls on the same record