Thanks to Continuous Integration, I have found a typing problem in our genetic engine program synthesis framework. It boiled down to me not defining a scope for a type variable.
I started with some code that looked like the following:
with the following error:
main.py:18: error: Argument 1 has incompatible type "P@consume"; expected "P@__init__" [arg-type]
Found 1 error in 1 file (checked 1 source file)
You can load this example on the MyPy playground if you want to play around with it.
In this case, MyPy is inferring the type of data as dict[str, Callable[[P
__init__], bool]@, where the key is the __init__ part of the type variable that ends up being different than the use o P inside the consume function. This behavior is because type vars are, by default, bound to the function/method, and not the class. The first step is to actually introduce the explicit annotation for data with the
dict[str, Callable[[P],bool]]@ type, inside Subclass. Now we get a different error:
main.py:17: error: Dict entry 0 has incompatible type "str": "Callable[[P
__init__], bool]”; expected “str”: “Callable[[P], bool]” [dict-item]@
Now the P type variable in the field annotation is different than the ones inside the method. To actually bind the type variable to the whole class, we need to extend Generic[P]:
Now, we have no typing errors, and we do not even need the explicit type declaration for data.
Most of this issue was due to me not clearly understanding the default behaviors of type variables1. Luckily, if you are able to only support Python 3.12 and upwards, you can use the new, saner syntax. And maybe someday I’ll finish the draft post where I explain why Python’s approach to typing is the best (for prototyping type systems and meta-programming techniques, like we do in GeneticEngine) and the worst (for real-world use).
1 Who the hell creates a type variable through the definition of a variable??