5 ms·
As someone who owns multiple label makers and organizes my utensils by type when I put them into the dishwasher I really like Mypy. Cool writeup, good points. I
by codekansas 4y ago
As someone who owns multiple label makers and organizes my utensils by type when I put them into the dishwasher I really like Mypy. Cool writeup, good points. I think a lot of people (ML scientist people especially) who haven't been forced to use a type checker don't realize the productivity benefits of having a type checker running on your whole codebase - they only see the annoying slow-down of having to do things a particular way and having the type checker bug you for inconspicuous errors.
Example: One pattern (which I don't think a lot of people are familiar with?) that I started adopting recently is the use of `Literal` for type-checking strings. For example, instead of something like
(on closer reading I realized this was in the blog post as well, but I suspect maybe some ML people will have seen this specific case before)
class ActivationType(enum.Enum):
sigmoid = "sigmoid"
tanh = "tanh"
def get_activation(key: str | ActivationType) -> nn.Module:
key = ActivationType[key]
if key == ActivationType.sigmoid:
return nn.Sigmoid()
if key == ActivationType.tanh:
return nn.Tanh()
raise KeyError(key)
you can do something like this instead:
from typing import Literal
ActivationType = Literal["sigmoid", "tanh"]
def get_activation(key: ActivationType) -> nn.Module:
if key == "sigmoid":
return nn.Sigmoid()
if key == "tanh":
return nn.Tanh()
raise KeyError(key)
The advantage is that you can do something like
act = get_activation("tahn")
and Mypy will show an error for your typo (instead of having to run your code and eventually hit the `KeyError`). So if you're just trying to quickly implement an idea, you don't have to kill brain cells searching for typos.
Of course, doesn't make a difference if your coworkers all use Vim and Emacs with no extensions...
- stavros 4y agoI love your username. The militants turn, startled.
- throwaway343244 4y ago> raise KeyError(key) This would be flagged as unreachable I believe. mypy/pyright also supports exhaustive checking with unions def get_activation(key: Literal['sigmoid', 'tanh']) -> nn.Module: match key: case 'sigmoid': return nn.Sigmoid() case 'tanh': return nn.tanh() There are limits to mypy/pyright's exhaustive checker.. it will fail if the union is in CNF, you will need to convert it to DNF. [1]: https://en.wikipedia.org/wiki/Disjunctive_normal_form https://en.wikipedia.org/wiki/Disjunctive_normal_form
- LtWorf 4y agoWith Literal you can also do this: @dataclass class Message: event: Literal['message'] msg: str @dataclass class File: event: Literal['file'] url: str typedload.load(data, File | Message) Where data is something like `{'event': 'message', 'msg': 'bla'}`. After the load, you can trust that your objects are well formed and let mypy do its thing. So types can be useful at runtime as well. Otherwise the alternative would be to use raw dictionary, which mypy can't check. pydantic does a similar thing but it uses its own different typing, so it needs a mypy plugin or it flags everything as wrong.
- evilsnoopi3 4y agoYou can also use TypedDict if you would normally use a raw dictionary but want the type checking.
- LtWorf 4y agoYes, but you still need a module like typedload to do the runtime checking. TypedDict performs no checking by itself at runtime. class A(TypedDict): a: int A(d=32) # Returns {'d': 32} typedload.load({'d': 32}, A) # TypedloadValueError: Value does not contain fields: {'a'} which are necessary for type A
- ghostwriter 4y agounlike fixed records, dictionaries access is impossible to optimise via JIT like PyPy
- saila 4y agoFor those who aren't familiar, typedload is a third party library: https://pypi.org/project/typedload/ https://pypi.org/project/typedload/
- saila 4y agoIt seems to me that the problem with the first version is that it's stringly typed with `key: str | ActivationType`. There would be no issue if `key` was required to be an `ActivationType` with `key: ActivationType`, as you've done in the second version. Code like the first version is usually someone is trying to make the API more convenient, but perhaps a better way, that's more in line with static type checking, would be to have a separate `activation_from_str(...)` function. For a similar situation, I was thinking about doing something along these lines (somewhat Rust inspired), although the duplication of variant names in the from_str method isn't ideal: import enum import typing # stubs so this example is self-contained & runnable class Sigmoid: pass class Tanh: pass class ActivationType(enum.Enum): sigmoid = Sigmoid tanh = Tanh class Activation: @classmethod def new(cls, variant: ActivationType): return variant.value() @classmethod def from_str(cls, variant: typing.Literal["sigmoid", "tanh"]): return cls.new(ActivationType[variant]) activation1 = Activation.new(ActivationType.sigmoid) activation2 = Activation.from_str("tanh") Now, adding a new activation type just means adding it as a variant to ActivationType and adding a string to the literal list in from_str. Duplicating the name in from_str isn't great, but IMO it's better than having to add a new branch for each variant and the external API is nice too.
- BerislavLopac 4y agoJust ran into an interesting issue with Literal >>> l = Literal["foo", "bar", "baz"] >>> "foo" in l TypeError: typing.Literal['foo', 'bar', 'baz'] is not a generic class >>> >>> isinstance("foo", l) TypeError: Subscripted generics cannot be used with class and instance checks So Literal is a Schroedinger's generic.
- geekraver 4y agoType classes in Python are meant as annotations; they are not for run-time. They essentially get erased (not exactly, as they are available for introspection, but they aren't your typical code).
- BerislavLopac 4y agoNo, I understand that (although things like @runtime_checkable and NamedTuple muddle things a bit). My point is that, based on the error messages, Literal both is and isn't a generic type.