Ad – 728×90
🔧 Backend Development

Odoo Model Inheritance – Classical, Prototype, and Delegation

Inheritance is central to how Odoo modules extend and customise existing functionality without forking core code. Odoo has three distinct inheritance mechanisms, each solving a different problem: classical inheritance extends an existing model in place, prototype inheritance creates a copy as a new model, and delegation inheritance embeds one model inside another via a foreign key. Understanding which to use — and when — is a key skill for any Odoo backend developer.

⏱️ 25 min 🎯 Intermediate 📅 Updated 2026

📋 What You Will Learn

  • The three inheritance types and when to use each
  • Classical inheritance: extending models with _inherit
  • Prototype inheritance: forking a model with _inherit + _name
  • Delegation inheritance: embedding with _inherits
  • Extending methods safely with super()
  • Useful mixins: mail.thread and mail.activity.mixin

Three Types of Inheritance

TypeHow to declareCreates new table?Use case
Classical_inherit = 'existing.model'No — extends in placeAdd fields/methods to any model from another module
Prototype_inherit = 'existing.model' + _name = 'new.model'Yes — new table, new modelStart from an existing model's structure for a related but distinct concept
Delegation_inherits = {'existing.model': 'field_id'}Yes — own table + FKEmbed an existing model's fields transparently (e.g. embed res.partner)

Classical Inheritance (_inherit)

Classical inheritance is the most common pattern in Odoo. You write a new Python class with only _inherit (no _name), and Odoo merges it into the existing model at startup. The changes apply globally — every module that uses the model sees the new fields and methods.

Python
from odoo import models, fields, api

# Extending res.partner — adds library-specific fields to the contact model
class ResPartner(models.Model):
    _inherit = 'res.partner'   # no _name — extends in place

    # These fields are added to the existing res.partner table
    is_library_member = fields.Boolean(string='Library Member', default=False)
    library_card_number = fields.Char(string='Library Card Number')
    membership_expiry = fields.Date(string='Membership Expiry')
    active_loan_count = fields.Integer(
        string='Active Loans',
        compute='_compute_active_loan_count',
    )

    @api.depends()
    def _compute_active_loan_count(self):
        for partner in self:
            partner.active_loan_count = self.env['library.loan'].search_count([
                ('borrower_id', '=', partner.id),
                ('state', '=', 'active'),
            ])

There is no limit on how many modules can inherit the same model. All extensions are stacked — Odoo's registry merges them all into one final class at startup time using Python's MRO (method resolution order).

Prototype Inheritance (_inherit + _name)

Prototype inheritance copies the structure of an existing model to create a brand-new model with its own database table. You specify both _inherit (the source) and _name (the new model's technical name).

Python
from odoo import models, fields

# Start from mail.thread + mail.activity.mixin structure
# Create a new, independent model for eBook loans
class LibraryEbookLoan(models.Model):
    _name = 'library.ebook.loan'      # new model — new table
    _description = 'eBook Loan'
    _inherit = 'library.loan'         # copy fields and methods from library.loan

    # Add eBook-specific fields on top of inherited ones
    download_link = fields.Char(string='Download URL')
    device_id = fields.Char(string='Device ID')
    drm_token = fields.Char(string='DRM Token')
⚠️
Prototype creates a completely separate model

Changes made to library.loan after the prototype was created do NOT automatically propagate to library.ebook.loan. Use prototype inheritance sparingly — only when you genuinely need a separate model. Most of the time, classical inheritance with an extra field to distinguish the subtype is a better approach.

Ad – 336×280

Delegation Inheritance (_inherits)

Delegation inheritance is the most sophisticated type. Your model has a Many2one field pointing to another model, and _inherits tells Odoo to transparently expose all fields of the linked model as if they were your own fields. Two tables exist in the database, but the ORM presents them as one.

Python
from odoo import models, fields

class LibraryMember(models.Model):
    _name = 'library.member'
    _description = 'Library Member'

    # Mandatory: the field that points to the delegated model
    partner_id = fields.Many2one(
        'res.partner',
        string='Contact',
        required=True,
        ondelete='cascade',
        auto_join=True,
    )

    # Delegation: all res.partner fields (name, email, phone, address…)
    # are accessible directly on library.member as if they were native
    _inherits = {'res.partner': 'partner_id'}

    # Library-specific fields (stored in library_member table)
    card_number = fields.Char(string='Library Card Number')
    membership_tier = fields.Selection([
        ('standard', 'Standard'),
        ('premium', 'Premium'),
    ], string='Membership Tier', default='standard')

# Usage — name and email come from res.partner transparently:
# member = env['library.member'].create({
#     'name': 'Jane Smith',          # → writes to res.partner
#     'email': 'jane@example.com',   # → writes to res.partner
#     'card_number': 'LIB-00042',    # → writes to library.member
# })
When to use delegationWhen NOT to use delegation
You want to extend res.partner (or similar) with a distinct identityWhen classical inheritance is simpler and sufficient
You need the delegated record to exist independently (e.g. the partner survives member deletion)When the two tables becoming out-of-sync would cause bugs
You want to reuse all the partner fields in forms and reports without copying themWhen you don't need the parent model's features

Inheriting from Multiple Models

A model can inherit from multiple models simultaneously by providing a list to _inherit. This is primarily used with mixins — abstract models that provide reusable behaviour without creating a table.

Python
from odoo import models, fields

class LibraryBook(models.Model):
    _name = 'library.book'
    _description = 'Library Book'

    # Inherit from multiple models — all are mixins (abstract)
    _inherit = [
        'mail.thread',           # adds chatter (message log) to the form
        'mail.activity.mixin',   # adds activity scheduling (call, email, todo)
    ]

    # mail.thread adds: message_ids, follower_ids, message tracking
    # mail.activity.mixin adds: activity_ids, activity scheduling UI

    name = fields.Char(string='Title', required=True, tracking=True)
    state = fields.Selection([
        ('available', 'Available'),
        ('borrowed', 'Borrowed'),
        ('damaged', 'Damaged'),
    ], string='State', default='available', tracking=True)  # tracking=True logs changes to chatter

Extending Methods (super())

When you override a method in classical inheritance, you almost always want to call the parent implementation first. Use Python's super() for this:

Python
from odoo import models, fields, api
import logging

_logger = logging.getLogger(__name__)

class LibraryBook(models.Model):
    _inherit = 'library.book'

    # Override create — add custom logic before/after the standard create
    @api.model_create_multi
    def create(self, vals_list):
        for vals in vals_list:
            # Pre-processing: generate a reference number if not provided
            if not vals.get('ref'):
                vals['ref'] = self.env['ir.sequence'].next_by_code('library.book')

        # Call the parent implementation (does the actual INSERT)
        records = super().create(vals_list)

        # Post-processing: send a welcome message to followers
        for record in records:
            record.message_post(
                body=f"Book '{record.name}' has been added to the library.",
                message_type='notification',
            )
            _logger.info("Created library book: %s (id=%d)", record.name, record.id)

        return records

    def write(self, vals):
        # Log state transitions
        if 'state' in vals:
            for record in self:
                _logger.info(
                    "Book %s state: %s → %s",
                    record.name, record.state, vals['state']
                )
        return super().write(vals)

    def unlink(self):
        # Prevent deletion of books that are currently borrowed
        for record in self:
            if record.state == 'borrowed':
                from odoo.exceptions import UserError
                raise UserError(
                    f"Cannot delete '{record.name}' — it is currently borrowed."
                )
        return super().unlink()
💡
Always use @api.model_create_multi

In Odoo 17+, the preferred pattern for overriding create is @api.model_create_multi which receives a list of value dicts. The older @api.model + single dict pattern is deprecated. Using create_multi allows Odoo to batch-create records efficiently and is required for compatibility with bulk operations.

Summary

📋 Key Points

  • Classical (_inherit only): extends the existing model in place — most common pattern for customisation
  • Prototype (_inherit + _name): creates a new model starting from an existing one's structure — use sparingly
  • Delegation (_inherits): embeds another model via FK, exposing its fields transparently — ideal for res.partner extension
  • Always call super() when overriding create, write, or unlink unless you have a very specific reason not to
  • Use @api.model_create_multi (not @api.model) for create overrides in Odoo 17+
  • mail.thread and mail.activity.mixin are the standard way to add the chatter and activity features to any model

FAQ

Can I override a method added by another module's classical inheritance? +

Yes. Since all classical inheritance extensions are merged into one class by Odoo's registry, any module can override any method on the merged class. The override order is determined by the module dependency order — a module that depends on yours will override your overrides. Always call super() to ensure the full override chain executes.

What is the difference between _inherit and _inherits (plural)? +

_inherit (singular, string) is used for classical and prototype inheritance — it names the model to extend or copy from. _inherits (plural, dict) is used for delegation inheritance — it maps a model name to the Many2one field name on your model that links to it. They are completely different mechanisms despite the similar names.

How does mail.thread work — what does it add to my model? +

mail.thread adds: a message_ids One2many of messages (the chatter), follower tracking (message_follower_ids), automatic logging of field changes when tracking=True is set on a field, and the message_post() method for sending messages. It also adds the Chatter widget to any form view that includes <div class="oe_chatter">.

Should I use delegation inheritance or just add fields to res.partner directly? +

Both approaches are valid. Adding fields directly to res.partner (classical inheritance) is simpler and means all library-related partner data is in one place. Delegation inheritance (library.member with _inherits) makes more sense when a library member has a distinct lifecycle and many additional fields — it keeps the partner record clean and separates concerns. For most small modules, classical inheritance is sufficient.