Yes, this is a bit of a rant, and my opinion may be a bit controversial, but I think Python type annotations are overrated when used in Blender add-on development, and perhaps even in general. Let me explain.
Annotation
The idea of annotation in Python is to add extra information to anything, be it a variable, function parameter or return type, or whatever.
One of its uses is to add type information 1 so that static type checkers can verify that variables are assigned values of a suitable type and IDEs can show additional information about a parameter for example 2.
Because Python is a dynamically types language this is not enforced at runtime, but exactly because Python is dynamically typed, adding extra information on what kind of types to expect can be incredible useful to let IDEs catch potential problems while writing your code.
For simple functions/methods with parameters of simple types, this is quite clear:
def multiply(a:float, b:float) -> float:
return a * b
If we would accidentally pass a string to this function somewhere in our code, the IDE would complain. It is also quite readable, but that starts to change once we are dealing with more complex types:
from typing import Any, Iterable
def convert(a: list[Iterable[Any]]) -> tuple[tuple[Any, ...], ...]:
"""
Convert a list of iterables to a tuple-of-tuples.
"""
return tuple([tuple(it)for it in a])
Not only do we have to import some types explicitly, it already starts to become difficult to parse for human brains, which goes a bit against the spirit of the Zen of Python: "Readability counts.".
Properties
In Blender we have another issue that goes beyond readability and that is to use of annotations to describe properties. Take a look at this code snippet:
VScode is not happy. Its typechecker, Pylance, reports "Call expression not allowed in type expression". And that is true for the typing specs, but Blender uses annotations for something completely different here: amount is not simply a float but a property that can hold a float value and at the same time knows how to display itself to the user, what its default is, and a lot more.
So the annotation here is used not for type checking, but for defining both the base type of the property and its behavior, something that couldn´t be described with a simple type annotation like amount: float
There is a solution of sorts for this, and that is to inform the type checker to disregard the annotation as a type hint:
Yes, this does get rid of the red squiggly line, but as you can see from the example() method, it also blocks type checking: We can now happily assign a float to amount which would cause havoc at runtime.
More type checking
Now type checking does have its benefits and if you don´t have to look at the type annotations yourself but let your IDE do the hard work, it arguably is beneficial to have libraries with comprehensive type annotations. The bpy module does not, and as far as I know there are no plans to add this, but some people have stepped in to fill the gap.
The fake-bpy-module provides a stub implementation that you can install inside a virtual environment to develop your Blender code outside of Blender while having the full benefits of comprehensively type annotated code.
It shouldn´t be confused with Blender-as-a-module which lets run Blender from Python directly for use in studio pipelines etc; fake-bpy-module doesn´t contain implementation code, it just provides type annotations. So when yo have it installed in your environment and have import bpy, your IDE knows all about the types, but you couldn´t run your code outside Blender. But would your run your code inside Blender, this import would get Blender's bundled bpy module and run just fine.
Now this is great, but be aware that this isn´t all teddy bears and unicorns: You still have the property annotation issue and sometimes the choices made in the fake-bpy-module aren't very convenient:
When I typed in def execute( in vscode, it auto expands the definition based on the type annotation of the execute() method it finds in the Operator class. At first glance this is fine, however, the return type annotation is now a copy of the type annotation of the execute() method in the superclass (i.e. Operator). So if that annotation would change, perhaps because a new possible status string would be added, we (and our IDE) wouldn´t know we could use that when we change our own code.
This apparently happens because the return type is not annotated with a class or type alias, but with a set, and this set definition is what the IDE copies:
def execute(
self, context: Context
) -> set[bpy.stub_internal.rna_enums.OperatorReturnItems]
...
I am no expert and I have to think about how this could be fixed (if it can be fixed at all) before submitting a PR perhaps, but Blender's Python API is large and complex, so I don´t blame the module maintainer at all, on the contrary, their efforts for the community are greatly appreciated.
Conclusion
Should we use type annotation in Blender addons? Probably, but not everywhere.
I would argue that type annotations are a benefit if you can factor out functions, which is a good idea for (unit)testing anyway. For top level stuff, like defining an Operator class and such, you do not gain much. I simple keep an execute() function as short as possible and define the actual functionality in separate modules/functions. You can then even put # type: ignore at the top of the file with the operator definition and registration code for minimal annoyance. With this setup you even avoid the property issue: they don´t get flagged when passed to a function defined elsewhere.
1: Some people argue that this is its only use, but they are clearly misguided: From the Python glossary "...used by convention as a type hint" (emphasis is mine). And this is important for Blender add-ons, see the section on properties.
2: Among other things: Python compilers, like pypy and others use it to generate optimized code for example.



No comments:
Post a Comment