Skip to content

Fix RecursionError in Odoo Computed Fields: Maximum Recursion Depth Exceeded

DeployMonkey Team · March 23, 2026 9 min read

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 0

Field 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 0

Both 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 0

Fix 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.tax

This 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=debug to see field recomputation order
  • Keep compute methods pure — no write() calls on the same record