Module Lifecycle Hooks
Odoo provides hooks that run at specific points during module installation, upgrade, and uninstallation. These hooks let you run custom code that cannot be expressed in XML data files or model definitions.
| Hook | When It Runs | Use Case |
|---|---|---|
| pre_init_hook | Before module install/upgrade | Schema migration, prerequisite checks |
| post_init_hook | After module install, before commit | Data initialization, configuration setup |
| post_load | When module is loaded (every start) | Monkey-patching, early initialization |
| uninstall_hook | Before module uninstallation | Cleanup, data preservation |
Declaring Hooks in __manifest__.py
{
'name': 'My Module',
'version': '19.0.1.0.0',
'pre_init_hook': '_pre_init_hook',
'post_init_hook': '_post_init_hook',
'uninstall_hook': '_uninstall_hook',
'post_load': '_post_load_hook',
}The hook functions are defined in __init__.py of the module root:
# __init__.py
from . import models
from . import controllers
def _pre_init_hook(env):
"""Runs before module tables are created/updated."""
pass
def _post_init_hook(env):
"""Runs after module is fully installed."""
pass
def _uninstall_hook(env):
"""Runs before module is uninstalled."""
pass
def _post_load_hook():
"""Runs every time the module is loaded (server start)."""
passpre_init_hook
Runs before the module's models are registered and tables are created. Receives a cursor (cr) in older Odoo versions or env in Odoo 17+. Use it for:
- Database schema preparations
- Checking prerequisites
- Creating columns before ORM takes over
def _pre_init_hook(env):
"""Add column before ORM to avoid NOT NULL errors during migration."""
env.cr.execute("""
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'sale_order'
AND column_name = 'x_custom_field'
) THEN
ALTER TABLE sale_order
ADD COLUMN x_custom_field VARCHAR DEFAULT 'pending';
END IF;
END $$;
""")post_init_hook
Runs after the module is fully installed — models are registered, views are loaded, data files are processed. This is the most commonly used hook:
def _post_init_hook(env):
"""Initialize data after module installation."""
# Create default configuration
env['ir.config_parameter'].sudo().set_param(
'my_module.default_setting', 'enabled'
)
# Populate computed fields on existing records
orders = env['sale.order'].search([
('x_custom_field', '=', False)
])
for order in orders:
order.write({'x_custom_field': 'migrated'})
# Create default records
if not env['my.config'].search([]):
env['my.config'].create({
'name': 'Default Configuration',
'active': True,
})Common post_init_hook Patterns
Backfill Data for New Fields
def _post_init_hook(env):
"""Set default values for new field on existing records."""
env.cr.execute("""
UPDATE sale_order
SET priority_level = 'normal'
WHERE priority_level IS NULL
""")Create Unique Indexes
def _post_init_hook(env):
"""Create partial unique index for soft-delete pattern."""
env.cr.execute("""
CREATE UNIQUE INDEX IF NOT EXISTS
dm_instance_unique_name_active
ON dm_instance (name, server_id)
WHERE (cleanup_state IS NULL OR
cleanup_state IN ('pending', 'cleaning'))
""")Assign Security Groups
def _post_init_hook(env):
"""Add existing customers to the new security group."""
group = env.ref('my_module.group_customer')
customers = env['res.users'].search([
('share', '=', True),
('active', '=', True),
])
group.write({'users': [(4, u.id) for u in customers]})post_load
Runs every time the server starts and the module is loaded. No database access is available — this is for Python-level initialization:
def _post_load_hook():
"""Monkey-patch or register global handlers."""
import odoo.addons.base.models.res_partner as rp
# Extend or modify behavior at load time
original_method = rp.Partner.name_get
def custom_name_get(self):
# Custom logic
return original_method(self)
rp.Partner.name_get = custom_name_getUse post_load sparingly — it runs on every server restart and does not have database access.
uninstall_hook
Runs before the module is uninstalled. Use it for cleanup that the ORM cannot handle automatically:
def _uninstall_hook(env):
"""Clean up before module removal."""
# Remove scheduled actions
crons = env.ref('my_module.cron_cleanup', raise_if_not_found=False)
if crons:
crons.unlink()
# Clean up config parameters
env['ir.config_parameter'].sudo().search([
('key', 'like', 'my_module.%')
]).unlink()
# Remove custom indexes
env.cr.execute("""
DROP INDEX IF EXISTS dm_instance_unique_name_active
""")Hook Execution Order During Install
- Module dependencies are resolved
pre_init_hookruns- Models are registered, tables created/updated
- Data files (XML, CSV) are loaded
- Demo data is loaded (if applicable)
post_init_hookruns- Module marked as installed
- Transaction commits
Hook Execution Order During Upgrade (-u)
pre_init_hookruns (only if defined in new version)- Models updated, new fields added
- Data files reloaded
post_init_hookruns- Module version updated
Error Handling in Hooks
def _post_init_hook(env):
try:
# Migration logic
env.cr.execute("UPDATE sale_order SET x_field = 'default'")
except Exception:
_logger.exception('post_init_hook failed')
# Do NOT re-raise unless you want to abort the installation
# If you raise, the entire module installation rolls backIf a hook raises an exception, the entire module installation is rolled back. Only raise if the error is truly fatal.
Best Practices
- Use
post_init_hookfor most initialization — it is the safest and most common hook - Use raw SQL in
pre_init_hookwhen you need to prepare the schema before ORM - Always use
IF NOT EXISTS/IF EXISTSfor idempotent hooks — they may run multiple times - Log what your hooks do — silent hooks are impossible to debug
- Do not raise exceptions unless the error should abort installation
- Keep hooks fast — they run synchronously during install/upgrade