A data migration that imports a model at module level operates on the model as it exists today, not as it existed at that point in migration history. Replaying migrations on a fresh database then fails with an OperationalError about a column that does not exist yet, or worse, quietly writes to a field that has since changed meaning.
The correct form takes the historical registry that Django passes in: def forwards(apps, schema_editor): Book = apps.get_model("catalog", "Book"). That model has exactly the fields declared as of this migration.
Two limits follow from how historical models are built. They do not have custom managers unless the manager sets use_in_migrations = True, and they do not have custom methods or save() overrides at all, so any logic in those must be duplicated inside the migration. Always pass a reverse function, or migrations.RunPython.noop, so the migration can be unapplied. Test by running migrate against an empty database in CI rather than only against a database that is already current.