Skip to content

Odoo Install Hooks and post_init_hook: Module Lifecycle Guide

DeployMonkey Team · March 23, 2026 11 min read

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.

HookWhen It RunsUse Case
pre_init_hookBefore module install/upgradeSchema migration, prerequisite checks
post_init_hookAfter module install, before commitData initialization, configuration setup
post_loadWhen module is loaded (every start)Monkey-patching, early initialization
uninstall_hookBefore module uninstallationCleanup, 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)."""
    pass

pre_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_get

Use 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

  1. Module dependencies are resolved
  2. pre_init_hook runs
  3. Models are registered, tables created/updated
  4. Data files (XML, CSV) are loaded
  5. Demo data is loaded (if applicable)
  6. post_init_hook runs
  7. Module marked as installed
  8. Transaction commits

Hook Execution Order During Upgrade (-u)

  1. pre_init_hook runs (only if defined in new version)
  2. Models updated, new fields added
  3. Data files reloaded
  4. post_init_hook runs
  5. 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 back

If 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_hook for most initialization — it is the safest and most common hook
  • Use raw SQL in pre_init_hook when you need to prepare the schema before ORM
  • Always use IF NOT EXISTS / IF EXISTS for 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