Justin Thurman
Today I Learned

Today I Learned

About Postponed Evaluation of Annotations

Justin Thurman's photo
Justin Thurman
·Aug 5, 2021·

Let's say you're working on a Django project, and you have a model with a custom create method on the manager:

# managers.py
class MyManager:
    def create(self):
        # your custom logic here
        return super().create()

# models.py
from .managers import MyManager

class MyModel:
    objects = MyManager()

So far, so good. But now what if you need access to MyModel inside managers.py? You can't import it directly due to circular imports. You could just inline the import, but what if you don't like that? You want all your imports at the top of your file, in global scope. self.model to the rescue!

# managers.py
class MyManager:
    def create(self):
        # self.model here is equivalent to MyModel
        self.model.do_a_thing() 
        return super().create()

But now what if you want to include type annotations on your manager method? create returns the created model instance, so...

# managers.py
class MyManager:
    def create(self) -> self.model: # THIS DOES NOT WORK
        self.model.do_a_thing()
        return super().create()

Can't do that! So now what?

PEP 563 to the rescue! Turns out the typing module has a fancy constant: TYPE_CHECKING:

# managers.py
from __future__ import annotations # if pre-3.10

import typing

if typing.TYPE_CHECKING:
    from .models import MyModel

class MyManager:
    def create(self) -> MyModel: # hooray!
        self.model.do_a_thing()
        return super().create()

TYPE_CHECKING is true only when type checking. It works with your IDE (probably), it works with mypy -- but it's false at runtime. No circular imports, no inline imports, and all the benefits of type hints!

 
Share this