# _inherit vs _inherits in Odoo, explained with examples

Canonical: https://erpfly.com/blog/odoo-inherit-vs-inherits/
Last updated: September 15, 2026

Published August 11, 2026 in Odoo. _inherit extends or copies a model, _inherits links to a parent record by delegation. Examples for all three patterns, view xpath inheritance and gotchas.

In Odoo, `_inherit` without a new `_name` adds fields and methods to an existing model, in the same database table. `_inherit` with a new `_name` creates a new model that copies the parent's fields and methods into its own table. `_inherits` is different again: the new model gets its own table, but it links to a parent record through a required Many2one and reads and writes the parent's fields through that link. Most customizations need the first. The other two are for specific situations, and picking them by accident is a common source of odd bugs.

Here's each one with code that runs on Odoo 17, 18 and 19, followed by view inheritance and the mistakes we see in code reviews.

### The three patterns at a glance

| Pattern | Declared with | New table? | Parent fields | Parent methods |
|---|---|---|---|---|
| Extension | `_inherit = "res.partner"` (no `_name`) | No, same table | Added to the model | Overridable with `super()` |
| Prototype (copy) | `_name = "x.y"` plus `_inherit` | Yes, full copy | Copied into new table | Copied |
| Delegation | `_inherits = {"res.partner": "partner_id"}` | Yes, plus parent row | Stored on parent record | Not available |

### Extension: `_inherit` on an existing model

This is what you want nine times out of ten. You're adding a field to contacts, a check to sale orders, a button to invoices. The model keeps its name, its table and every place in Odoo that already uses it now sees your change.

```python
from odoo import api, fields, models
from odoo.exceptions import ValidationError


class ResPartner(models.Model):
    _inherit = "res.partner"

    loyalty_tier = fields.Selection(
        [("bronze", "Bronze"), ("silver", "Silver"), ("gold", "Gold")],
        default="bronze",
    )
    credit_hold = fields.Boolean()

    @api.constrains("credit_hold", "loyalty_tier")
    def _check_gold_not_on_hold(self):
        for partner in self:
            if partner.credit_hold and partner.loyalty_tier == "gold":
                raise ValidationError(self.env._("Gold customers can't be put on credit hold without downgrading first."))

    @api.model_create_multi
    def create(self, vals_list):
        for vals in vals_list:
            if vals.get("is_company") and not vals.get("loyalty_tier"):
                vals["loyalty_tier"] = "silver"
        return super().create(vals_list)
```

The Python class name doesn't matter to Odoo. What matters is `_inherit`, and that your module lists the module that defines the model in its manifest's `depends`. For `res.partner` that's `base`, which is implicit, but for `sale.order` it's `sale`.

Always call `super()` when overriding. Skip it on `create` or `write` and you silently break every other module that extends the same method, including Odoo's own.

#### Who runs first when several modules override the same method

Each extension becomes a class layered on top of the previous ones, in module load order. The module loaded last sits on top, so its override runs first and its `super()` call walks down to the modules loaded before it, ending in the original definition. Load order follows `depends`. If your module depends on `sale`, you're guaranteed to sit above it.

What you can't rely on is the order between two modules that don't depend on each other. If your override of `action_confirm` on `sale.order` must run after a third-party module's, add that module to your `depends`. Don't count on the order you happened to see on your machine, because a different install order on staging can flip it.

### Prototype inheritance: `_inherit` with a new `_name`

When `_name` is set to something new and `_inherit` points to an existing model, Odoo builds a brand-new model. It gets its own table, and the parent's fields and methods are copied into it. The original model isn't touched.

You use this all the time without thinking about it, through mixins:

```python
class EquipmentRental(models.Model):
    _name = "equipment.rental"
    _description = "Equipment Rental"
    _inherit = ["mail.thread", "mail.activity.mixin"]

    name = fields.Char(required=True)
```

`mail.thread` and `mail.activity.mixin` are abstract models, so there's no parent table. Your rental model simply gains the chatter fields and methods.

Copying a **concrete** model is rarer, and we'd push back on most uses of it:

```python
class ArchivedPartner(models.Model):
    _name = "archived.partner"
    _description = "Archived Partner Snapshot"
    _inherit = "res.partner"
```

This creates a second table with every `res.partner` field, including fields added by modules loaded before yours. It doesn't get the partner views, so you're writing those from scratch. Many2many fields that declare an explicit relation table get copied with the same table name, which can collide with the original. And every future module that extends `res.partner` won't extend your copy, so the two drift. If you want a small related record, create a lean model with a Many2one instead.

### Delegation inheritance: `_inherits`

`_inherits` is for "this thing **is a** partner, plus extra data." Each record of your model is linked to exactly one parent record. Parent fields appear on your model and are stored on the parent. Odoo's own examples: `res.users` delegates to `res.partner`, and `product.product` delegates to `product.template`.

```python
class LibraryMember(models.Model):
    _name = "library.member"
    _description = "Library Member"
    _inherits = {"res.partner": "partner_id"}

    partner_id = fields.Many2one(
        "res.partner",
        required=True,
        ondelete="cascade",
    )
    card_number = fields.Char(required=True, copy=False)
    membership_end = fields.Date()
```

Now you can do this:

```python
member = env["library.member"].create({
    "name": "Asha Menon",          # stored on res.partner
    "email": "asha@example.com",   # stored on res.partner
    "card_number": "LIB-00042",    # stored on library_member
})
member.partner_id.name   # "Asha Menon"
member.email             # read through the link
```

Creating the member created the partner too. That partner shows up in Contacts, can receive invoices and emails, and can be selected anywhere Odoo asks for a partner. This is the main reason to use delegation: your records join the rest of Odoo without you rebuilding contact handling.

The same thing can be written on the field with `delegate=True`, which some people find easier to read:

```python
partner_id = fields.Many2one("res.partner", required=True, ondelete="cascade", delegate=True)
```

#### What delegation doesn't give you

**Methods.** Only fields are delegated. `member.message_post()` won't work just because partners have a chatter. Call it on `member.partner_id`, or add `mail.thread` to your model with `_inherit`.

**Clean deletes in both directions.** `ondelete="cascade"` removes the member when the partner is deleted. Deleting the member leaves the partner behind. Decide whether that's what you want, and handle it in `unlink` if not.

**Separate values.** If two members point at the same partner, they share its name and email. Changing one changes both.

**Cheap searches.** Searching or sorting on a delegated field joins the parent table. On a big table, index what you filter on.

**Your own field with the same name.** If `library.member` defines its own `email` field, that one wins and the partner's `email` is no longer reachable directly on the member. It's legal, and it's confusing for everyone who reads the code later. Pick a different name.

### View inheritance with xpath

Models and views are inherited separately. Adding `loyalty_tier` to `res.partner` doesn't put it on any form. For that you inherit the view:

```xml
<odoo>
    <record id="view_partner_form_loyalty" model="ir.ui.view">
        <field name="name">res.partner.form.loyalty</field>
        <field name="model">res.partner</field>
        <field name="inherit_id" ref="base.view_partner_form"/>
        <field name="arch" type="xml">
            <xpath expr="//field[@name='vat']" position="after">
                <field name="loyalty_tier" invisible="not is_company"/>
                <field name="credit_hold"/>
            </xpath>
            <xpath expr="//field[@name='website']" position="attributes">
                <attribute name="required">is_company</attribute>
            </xpath>
        </field>
    </record>
</odoo>
```

The `position` values you'll use: `after`, `before`, `inside`, `replace` and `attributes`. For simple cases there's a shorthand that skips the xpath:

```xml
<field name="vat" position="after">
    <field name="loyalty_tier"/>
</field>
```

Remember that since Odoo 17, `attrs` is gone. Conditions are plain expressions in `invisible`, `readonly` and `required`, as above.

To build a separate view based on an existing one, rather than modifying it, set the mode to primary:

```xml
<record id="view_partner_form_member" model="ir.ui.view">
    <field name="name">res.partner.form.member</field>
    <field name="model">res.partner</field>
    <field name="inherit_id" ref="base.view_partner_form"/>
    <field name="mode">primary</field>
    <field name="arch" type="xml">
        <xpath expr="//notebook" position="inside">
            <page string="Membership" name="membership">
                <field name="loyalty_tier"/>
            </page>
        </xpath>
    </field>
</record>
```

Point a window action at this view and the standard partner form stays untouched for everyone else.

### Gotchas we keep finding in reviews

**Using `position="replace"` on standard views.** It works until another module's view targets the node you removed, then that module fails to load. Hide with `position="attributes"` and `invisible="1"` instead of replacing.

**Fragile xpaths.** `//group[2]/field[3]` breaks on the next Odoo version. Anchor on `@name` wherever possible, and when there's no name, `hasclass()` is better than a position index.

**An xpath that matches more than one node.** Only the first match is used. If a field appears twice in the view, your change lands in one place and you'll wonder why the other didn't change.

**Missing `depends`.** Your extension loads before the module that defines the model or view. You get "model not found" or "Element cannot be located in parent view" on a clean install, but not on your laptop.

**Using `_inherits` when you meant `_inherit`.** Someone wants extra fields on partners, writes `_inherits`, and ends up with a new model and a second menu nobody asked for.

**Overriding `display_name` the old way.** `name_get` was deprecated in Odoo 17 (it still runs, with a warning) and removed in 18. Override `_compute_display_name` instead, and add `@api.depends` for the fields it uses. Be careful doing this on `res.partner`, whose display name already depends on context; extend it with `super()` rather than replacing it.

```python
@api.depends("name", "card_number")
def _compute_display_name(self):
    for member in self:
        member.display_name = f"{member.name} ({member.card_number})" if member.card_number else member.name
```

### Picking the right one

Adding to something Odoo already has: `_inherit` alone. Building a new document that needs chatter or activities: new `_name` with the mixins in `_inherit`. Building something that is a contact, product or user with extra data: `_inherits`. Copying a concrete model wholesale: almost never.

If you're still deciding whether any code is needed, our piece on [Odoo Studio vs a custom module](https://erpfly.com/blog/odoo-studio-vs-custom-module/) covers when clicks are enough, and the [step-by-step guide to creating an Odoo 19 module](https://erpfly.com/blog/create-custom-module-odoo/) shows the files around these models.

erpfly picks between these patterns for you when it generates an addon, and explains the choice in the pull request so a reviewer can disagree. See [Odoo module development](https://erpfly.com/odoo-module-development/) for new models, or [Odoo customization](https://erpfly.com/odoo-customization/) when you're extending what's there.

### Sources

- [ORM API, Odoo 19 developer documentation](https://www.odoo.com/documentation/19.0/developer/reference/backend/orm.html)
- [View records, Odoo 19 developer documentation](https://www.odoo.com/documentation/19.0/developer/reference/user_interface/view_records.html)
- [ORM Changelog, Odoo 19 developer documentation](https://www.odoo.com/documentation/19.0/developer/reference/backend/orm/changelog.html)
- [ir_ui_view.py (17.0), Odoo source code](https://github.com/odoo/odoo/blob/17.0/odoo/addons/base/models/ir_ui_view.py)