Every Odoo module has exactly one required file that isn’t a Python class or an XML view: __manifest__.py, a plain Python dict literal sitting at the module’s root. It’s easy to copy one from another module, change the obvious fields, and never actually learn what the rest of the keys do — until one of them silently changes how your module installs.
The short version: __manifest__.py is a dict literal with one required key (name) and roughly twenty optional ones, grouped below by what they actually control: identity, dependencies, data loading, install behavior, and lifecycle hooks. The one that causes the most confusing bugs is data — files load in the exact order listed, and referencing something not loaded yet breaks the install with an error that doesn't obviously point back to manifest ordering.
The minimum manifest
{
'name': 'My Module',
}
That’s a technically valid, installable Odoo module — everything else has a sensible default. In practice almost every real module needs at least depends and data too:
{
'name': 'Sales Order Extensions',
'version': '18.0.1.0.0',
'summary': 'Adds a customer PO reference field to sale orders.',
'author': 'Your Company',
'website': 'https://example.com',
'license': 'LGPL-3',
'category': 'Sales',
'depends': ['sale'],
'data': [
'security/ir.model.access.csv',
'views/sale_order_views.xml',
],
'installable': True,
'application': False,
}
Identity and documentation keys
| Key | Type | Default | What it controls |
|---|---|---|---|
name | str | — (required) | The human-readable title shown everywhere in the Apps list and module UI. |
version | str | 1.0 | Free-form, but the Odoo/OCA convention prefixes the Odoo series: 18.0.1.0.0. |
summary | str | empty | A one-line subtitle shown right under the name in the Apps list. |
description | str | empty | Longer body text (reStructuredText), the module’s own “about” page. |
author | str | empty | Name or organization credited as the author. |
maintainer | str | falls back to author | Who’s actually responsible for upkeep, if different from the original author. |
website | str | empty | A URL shown alongside the author. |
license | str | LGPL-3 | One of Odoo’s recognized license identifiers (GPL-2, GPL-3, AGPL-3, LGPL-3, OEEL-1, OPL-1, or a proprietary variant). |
category | str | Uncategorized | Groups the module in Apps; nest with /, e.g. 'Accounting/Invoicing'. |
None of these affect how the module actually behaves at runtime — they’re purely how it presents itself in the Apps list and module UI. Getting them right matters if you ever intend to publish the module (on the Odoo Apps Store, on odoo.sh, or just for a client to browse), not for correctness.
Dependencies: depends and external_dependencies
depends is the one key that genuinely controls behavior, not just presentation: it’s the list of other Odoo modules that must be installed — and fully loaded — before this one. Odoo computes the full install order from every module’s depends graph; list base explicitly even though it’s always present, since an explicit dependency documents why your module needs it and keeps the manifest self-describing.
external_dependencies is a separate, smaller thing: a dict declaring non-Odoo requirements the module needs to actually import, keyed by kind —
'external_dependencies': {
'python': ['xlrd', 'requests'],
'bin': ['wkhtmltopdf'],
},
Odoo checks these exist before allowing the module to install, and reports a clear error instead of an obscure ImportError deep in your own code if a Python package is missing.
Watch out: a module referenced in an XML file's ref="other_module.some_id" but not listed in depends can still work by accident during development — if that other module happens to already be installed in your database — and then fail on a clean install elsewhere. depends is the only thing that guarantees load order; a working dev database is not proof the dependency list is correct.
Data loading: data, demo, and file order
data is a list of relative file paths (XML or CSV) always loaded on install and on every upgrade. demo is the same shape, but only loaded when demo data is enabled — sample records meant for evaluating the module, never for production.
The part that trips people up: files in data load in the exact order listed, top to bottom, and later files can reference records defined in earlier ones — never the other way around. A near-universal convention, and a good default to follow:
'data': [
'security/ir.model.access.csv',
'security/ir_rule.xml',
'views/sale_order_views.xml',
'reports/report_templates.xml',
'data/ir_cron_data.xml',
],
Security first (so views can reference groups that already exist), then views, then anything that references those views (reports, menus tied to actions). A view’s XML referencing a group defined later in a file listed further down data fails on install with an error about a missing external ID — one that doesn’t obviously say “check your manifest’s file order,” which is exactly why this is worth internalizing rather than debugging from scratch each time. Every external ID referenced this way needs to actually exist under the name you typed — the External ID / XML ID Helper is built for confirming a reference resolves to what you think it does before the install ever runs.
Install behavior
| Key | Type | Default | What it controls |
|---|---|---|---|
installable | bool | True | Whether the module can be installed at all — set False to hide a work-in-progress module from the Apps list without deleting it. |
application | bool | False | True marks it as a full application (gets its own top-level entry and category in Apps); False marks it as a technical add-on to an existing app. |
auto_install | bool or list | False | True installs it automatically the moment all of depends is satisfied. A list of module names installs it automatically once that subset is satisfied — the mechanism behind Odoo’s own “glue” modules that activate automatically when two specific apps are both installed. |
Lifecycle hooks
Three keys name a Python function (defined in the module’s own code, commonly __init__.py or a dedicated file) that Odoo calls at a specific point in the module’s lifecycle, each receiving the current environment:
pre_init_hook— runs before the module’s own models/data are set up. Useful for one-time preparation that has to happen first.post_init_hook— runs immediately after installation finishes. The common use: computing/backfilling a new field’s value across existing records right after the column is created.uninstall_hook— runs when the module is uninstalled, for cleanup that Odoo’s own automatic uninstall (which removes the module’s own records and fields) doesn’t handle — external files, third-party API cleanup, and similar.
The module icon
By convention, a PNG at static/description/icon.png inside the module becomes its icon in the Apps list — no manifest key required for the icon itself. The images key is for something adjacent: additional screenshots/banner images shown on the module’s own description page, e.g. 'images': ['static/description/banner.png'].
Quick reference: the keys most modules actually need
| Situation | Keys to set |
|---|---|
| Any real module | name, version, depends, data |
| Publishing publicly (Apps Store, a client review) | + summary, description, author, website, license, category, an icon |
| Needs a non-Odoo Python package or binary | external_dependencies |
| Should activate itself once its dependencies are met | auto_install |
| Needs to backfill data right after install | post_init_hook |
| A full standalone app, not an add-on to one | application: True |
Frequently asked questions
Does the order of keys within the manifest dict matter?
No — it's a plain Python dict, so key order is cosmetic. The order that genuinely matters is inside the data list, which loads top to bottom.
What happens if I forget a module in depends but reference it in an XML file?
Nothing, if that module happens to already be installed in your current database — Odoo doesn't cross-check every ref= against depends at install time. It surfaces as a broken install only on a fresh database where load order is actually enforced, which is exactly why it's easy to miss during development.
Is version actually enforced or just informational?
Odoo doesn't hard-block an install over the version string's format, but the Odoo/OCA convention of prefixing the target Odoo series (18.0.1.0.0) is what several tools (module upgrade tooling, some marketplaces) rely on to determine which Odoo version a module targets — worth following even though nothing enforces it directly.
Can data and demo reference each other?
A demo file can reference records from data (demo data is loaded after regular data), but not the reverse — regular data files must never depend on demo-only records, since demo data is absent in a production database with demo mode disabled.
What's the difference between installable: False and simply not listing the module?
installable: False keeps the module's code loadable (Odoo still imports it, so your Python code is syntax-checked and any shared utility code in it stays usable by other modules) while hiding it from the Apps list as something a user could install — useful for a module that's mid-development or deliberately abstract/base-only.
Further reading
- Module Manifests — official Odoo 18.0 developer documentation, the authoritative field-by-field reference this article is grounded in.
- Command-line interface (CLI) — official Odoo 18.0 documentation, for how installed modules interact with server startup flags.
And on this site: the External ID / XML ID Helper for confirming a ref= in a data file actually resolves, and Why Can’t This User See a Record They Should? for what belongs in security/ before your views do.