4

I created an empty migration, which now looks like this:

def forwards(apps, schema_editor):
    Foo = apps.get_model('app', 'Foo')
    FixedResponse.objects.create(name='Bar')


def backwards(apps, schema_editor):
    Foo = apps.get_model('app', 'Foo')
    Foo.objects.filter(name='Bar').delete()


class Migration(migrations.Migration):

    dependencies = [
        ('app', '0035_fixedresponse'),
    ]

    operations = [
        migrations.RunPython(forwards, backwards)
    ]

Since there is no Django documentation anywhere on this topic (migrations specific) or best practice,

I want to know how do I type hint the code above?

Antoine Pinsard
  • 33,148
  • 8
  • 67
  • 87
elad silver
  • 9,222
  • 4
  • 43
  • 67
  • 1
    Does this answer your question? [Django Migrations Do I need type hints and how?](https://stackoverflow.com/questions/74107907/django-migrations-do-i-need-type-hints-and-how) – jonrsharpe Oct 18 '22 at 11:12
  • can u explain why do you want to ? – ilyasbbu Oct 18 '22 at 11:12
  • @jonrsharpe there is no answer in there, also the mentioned question is asked by elad silver himself – ilyasbbu Oct 18 '22 at 11:13
  • @ilyasbbu I want to do this because it's now a requirement in our MR's no MR goes without type hinting even if it's machine generated – elad silver Oct 18 '22 at 11:16
  • @jonrsharpe That question was opened with too much details (as I've been told) so no specific question. because it is now closed no one can answer that. – elad silver Oct 18 '22 at 11:17
  • personally speaking I don't see specific reasons to type hints migrations (unless p.e. when done manually), but if it's required, you could found a workaround withTypeVar https://docs.python.org/3/library/typing.html#typing.TypeVar and https://stackoverflow.com/questions/67948879/type-hinting-for-django-model-subclass – Carlo Oct 18 '22 at 11:18
  • @ilyasbbu I'm aware of that, thank you. OP: so you _edit_ it and then it gets reviewed for reopening, you don't just repost. – jonrsharpe Oct 18 '22 at 11:19
  • "No documentation"? https://docs.djangoproject.com/en/4.1/ref/migration-operations/#django.db.migrations.operations.RunPython – deceze Oct 18 '22 at 11:19
  • @Carlo, This can be an answer for you but my boss (and I assume many others) need to know why? do you have Django docs saying so? Regarding the TypeVar I know it exists but maybe there is some thing better than a workaround? maybe there isn't. This is the need for answers from community experts. especially since this is one of the most common cases in Django. – elad silver Oct 18 '22 at 11:22
  • @deceze - There are no mentions to typehinting in that doc link you posted – elad silver Oct 18 '22 at 11:24
  • 1
    @eladsilver completely understanding your point. Gonna search, hopefully be back later with some – Carlo Oct 18 '22 at 11:25
  • 2
    AFAIK type-hinting isn't mentioned *anywhere* in the docs, because it's not necessary. If you know how type annotations work, then all you need to know is the expected types for certain operations; the docs give you that piece of information. – deceze Oct 18 '22 at 11:36
  • @deceze - That is a big part of the question! "AFAIK" is not an answer but good enough for me to start going to a specific direction (another reason why I have no idea the reason for downvotes) if you use a CI tool to check that all modified files have typehints it will fail. At my company we want to do things the right way, what is the right way in this case? – elad silver Oct 18 '22 at 11:57
  • 1
    "AFAIK" means *I have not read the entire documentation top to bottom, but I have read a lot of it and do not remember any mention of type hints, and I also did a quick search which didn't bring up anything either.* That's about as scientific as you can get here. And I'm still not sure what you want to hear. Type annotations are optional in Python, and obviously Django also doesn't care about them. – deceze Oct 18 '22 at 12:02
  • 1
    If you want to annotate *your* code… go ahead, it's just Python code which you can annotate. Again, the only thing you need to know for that is the types of certain parameters, and the docs provide that in most cases in some form or another. Not in the form of annotation syntax, but in a descriptive manner. If you want to know anything even more specifically: tell us exactly what you need to know, because this is about as exhaustive as one can get without a more specific question. – deceze Oct 18 '22 at 12:02
  • 1
    @eladsilver i spoke on twitter to one of the member of django technical board. he said that hints are needed only to bypass MyPy (and similar) limitations. Sort of: with strict enough Mypy settings, you need hints on initial migrations’ Migration.dependencies like: dependencies: list[tuple[str, str]] = [] But Mypy should be able to infer most types in migrations. I have no further clue about docs, but usually assume "not mentioned = not needed" – Carlo Oct 18 '22 at 12:29

2 Answers2

6

There is no universal answer to that question, and the only reputable source you will find about it is Django's reference and documentation, which don't make any mention about annotations for migrations.

The "correct way to handle type hinting in Django" strictly depends on your company's policy. Prior to asking how, you must ask why. Once you know why, figuring out how will likely be straightforward. However, if instead of asking "why?", you (or your boss) ask "why not?", that's taking the issue upside down. You need a reason to do something, but you don't need a reason not to do something. (1)

If the reason is "to pass CI", this is no valid reason. CI is meant to work for you, not the opposite.

If the reason is "to document the code", this is no valid reason either. Type hints are documentation, the purpose of writing documentation is not "to document the code". Why is any documentation needed? Who is going to read this documentation? What information do they need that is not already clear in the code?

I see 3 valid reasons to use type hints:

  1. Provide help for developers using the functions. In the case of migrations, forwards and backwards functions are only used internally by Django. Developers reading your code are expected to have knowledge about migrations. If they don't know what apps and schema_editor are, it seems fair to expect them to refer to migrations.RunPython documentation, rather than re-explaining it for every single backwards and forwards callbacks of every single RunPython operation of every single migration.
  2. QA type checking. But type checking is only useful for your own code. You'll never import the functions from migrations to use them in your code, so there is nothing to check.
  3. Provide meta information for some smart tools (internal or third party) using introspection to do magic stuff. An excellent example of this use case is FastAPI, their use of annotations is highly advanced and a powerful part of their API.

Unless your case fits in one of these three, or you have any other reason to use type hints, don't try to fit to "policies" blindly. Don't write annotations just because you can. Keep calm and (re)read the Zen of Python. :) (2)

TLDR: Ask your boss to tell you why he wants type hints for migrations. If the answer is a well-defined purpose, write hints to serve that purpose. If the answer is anyhow stupid, write stupid type hints. ¯\(ツ)(3)


Footnotes

(1) The double negation might be confusing. What I mean here is that if someone can't explain the purpose of doing something, you don't need to find the purpose of not doing it. Or at least, there is a very straightforward purpose: saving the time and resources required to do it and maintain it.

(2) Don't try to find any principle specific to this situation in the Zen of Python. I only meant this reference as a general reminder of Python's philosophy: keep things simple, clear explicit code is worth more than poor paraphrasing documentation, that kind of stuff.

(3) If the purpose is "to pass CI" or such, I'd personally recommend something silly like def forwards(apps: ..., schema_editor: ...) -> ... (I mean, literally annotate with the Ellipsis object). Either nobody notice and it was indeed pointless, either your boss or your colleagues throw it back at you and hopefully come with a better defined purpose.

Antoine Pinsard
  • 33,148
  • 8
  • 67
  • 87
  • 3
    Excellent answer! But please never suggest annotating something with ellipsis, it is weird pylance extension, [not supported by mypy](https://mypy-play.net/?mypy=latest&python=3.10&gist=45ff54abebec4c64594c28309b1ecdf1), and pylance treats it as `typing.Any` synonym. So use `typing.Any` instead. – STerliakov Oct 22 '22 at 01:04
  • @SUTerliakov Thanks for your feedback. That was intended as a joke to emphasis my statement about non-sense rules imply non-sense code. – Antoine Pinsard Oct 23 '22 at 11:44
3

The documentation:

code (and reverse_code if supplied) should be callable objects that accept two arguments; the first is an instance of django.apps.registry.Apps containing historical models that match the operation’s place in the project history, and the second is an instance of SchemaEditor.

https://docs.djangoproject.com/en/stable/ref/migration-operations/#django.db.migrations.operations.RunPython

So, if you want to annotate your data migrations with types, these are the appropriate annotations:

from django.apps.registry import Apps
from django.db.backends.base.schema import BaseDatabaseSchemaEditor

def forwards(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
    ...
Antoine Pinsard
  • 33,148
  • 8
  • 67
  • 87
deceze
  • 510,633
  • 85
  • 743
  • 889
  • Thanks, do you think it is worth importing BaseDatabaseSchemaEditor just so I can typehint it? – elad silver Oct 18 '22 at 11:59
  • If you want to type-hint it, well, then you'll need to import it. Unless your type checker can work with string annotations instead, in which case you can maybe skip the import statement. Depends on *your specific type checker and/or requirements.* — I might do this to get some IDE auto-completion help, but besides that there's little use in this here. – deceze Oct 18 '22 at 12:04
  • 1
    @eladsilver If you don't want to import things for type-hinting (which is understandable), you can use string annotations or `annotations` future-import + `if TYPE_CHECKING:` guard. Then nothing is imported on runtime, but typechecker is still aware about required classes. Keep in mind that you'll need string annotations in some places even with `annotations` future. – STerliakov Oct 22 '22 at 01:00