# Lazy Loading

Lazy loading allows you to register commands using import path strings instead of direct function references. This defers importing command modules until they are actually executed, which could significantly improve CLI startup time for large applications that have expensive per-command imports.

## Basic Usage

Instead of importing and registering a function directly:

    from cyclopts import App
    from myapp.commands.users import create, delete, list_users  # Imported immediately

    app = App()
    user_app = App(name="user")

    user_app.command(create)
    user_app.command(delete)
    user_app.command(list_users, name="list")

    app.command(user_app)

Use an import path string:

    from cyclopts import App

    app = App()
    user_app = App(name="user")

    # No imports! Modules loaded only when commands execute
    user_app.command("myapp.commands.users:create")
    user_app.command("myapp.commands.users:delete")
    user_app.command("myapp.commands.users:list_users", name="list")

    app.command(user_app)

The import path format is `"module.path:function_name"`, similar to setuptools entry points.

Lazy commands are resolved/imported in these situations:

- Command Execution - When the user runs that specific command

- Subcommand Help - When displaying `--help` for the specific lazy command (e.g., `myapp lazy-cmd --help`)

- Direct Access - When accessing via `app["command_name"]`

Parent `--help` (e.g., `myapp --help`) displays lazy commands using metadata provided at registration time (`help=`, `group=`, `show=`, `sort_key=`) without resolving them.

In order to benefit from lazy loading, you have to make sure that the files are not imported by other means when your CLI starts up.

## Import Path Format

The import path string has two parts separated by a colon (`:`):

Module Path (before the `:`)

  The Python module to import, using dot notation (e.g., `myapp.commands.users`).

Attribute Name (after the `:`)

  The function or App to get from the module using `getattr()`.

Examples:

    # Simple function in a module
    app.command("myapp.commands:create_user")

    # Nested module path
    app.command("myapp.admin.database.operations:migrate")

    # Import an App instance, exposed to the CLI as "admin"
    app.command("myapp.admin:admin_app", name="admin")

Note

The attribute name (after `:`) is the actual Python name, not the CLI command name. Use the `name` parameter to specify the CLI command name.

## Name vs Function Name

The `name` parameter specifies how the command appears in the CLI, while the import path specifies what code to execute. They can be completely different:

    from cyclopts import App

    user_app = App(name="user")

    # Function name: "list_users"
    # CLI command name: "list"
    user_app.command("myapp.commands.users:list_users", name="list")

    # Function name: "delete"
    # CLI command name: "remove"
    user_app.command("myapp.commands.users:delete", name="remove")

    $ myapp user list --limit 10
    # Imports and runs myapp.commands.users:list_users

    $ myapp user remove --username alice
    # Imports and runs myapp.commands.users:delete

If `name` is not specified, Cyclopts derives it from the function name with `App.name_transform` applied (typically converting underscores to hyphens).

## Error Handling

If an import path/configuration is invalid, the error occurs when the command is executed, not when it's registered:

    from cyclopts import App

    app = App()

    # This won't error immediately - registration succeeds
    app.command("nonexistent.module:func")

    app()

    $ myapp func
    # Now the error occurs:
    ImportError: Cannot import module 'nonexistent.module'

To catch import errors early, you can access the command during testing:

    import pytest
    from cyclopts import App

    def test_lazy_commands_are_importable():
        app = App()
        app.command("myapp.commands:create")

        # This will trigger the import and fail if path is wrong
        resolved = app["create"]
        assert resolved is not None

## Groups and Lazy Loading

Tip

TL;DR: Pass `group=` at registration time, and define `Group` objects in your main CLI module, NOT in lazy-loaded modules.

Lazy commands support `group=` at registration time, so they appear in the correct group in `--help` output without being resolved. However, `Group` objects defined only inside unresolved lazy modules won't be available until those modules are imported. To avoid this, define `Group` objects in non-lazy modules.

    # myapp/cli.py (always imported)
    from cyclopts import App, Group

    # Define Group objects here
    admin_group = Group("Admin Commands", validator=require_admin_role)
    db_group = Group("Database", default_parameter=Parameter(envvar_prefix="DB_"))

    app = App()

    # Lazy commands can reference the Group objects
    app.command("myapp.admin:create_user", group=admin_group)
    app.command("myapp.admin:delete_user", group=admin_group)
    app.command("myapp.db:migrate", group=db_group)

What to avoid: Defining `Group` objects inside lazy-loaded modules:

    # myapp/admin.py (lazy-loaded)
    from cyclopts import App, Group

    # BAD: This Group won't be available to other commands until this module is imported
    admin_group = Group("Admin Commands", validator=require_admin_role)

    def create_user():
        ...

If you reference a group by string (e.g., `group="Admin Commands"`) and the `Group` object with that name is only defined in an unresolved lazy module, the group won't be available until that lazy module is imported. This means that:

- Validators defined on the lazy-loaded `Group` won't be applied to commands in other modules.

- `Group.default_parameter` and other settings won't be inherited by commands referencing the group by string.

Once the lazy module is imported (e.g., by executing one of its commands), the `Group` object becomes available and subsequent operations will use it correctly.