Why is Mypy not giving a typing error when assigning attributes in __init__ that have been type hinted in the class body?

Viewed 967

Here's a sample python file I have

class Person:
    name: str
    age: int

    def __init__(self, name, age):
        self.name = name
        self.age = age


p = Person(5, 5)

But when I run mypy test.py I get the following output

$ mypy test.py                     
Success: no issues found in 1 source file

Shouldn't it be complaining that it's trying to assign 5 to the name variable and I've indicated that should be of type str

2 Answers

TLDR: Annotate the initialiser, not the fields:

class Person:
    def __init__(self, name: str, age: int):
        self.name = name
        self.age = age

p = Person(5, 5)  #  Argument 1 to "Person" has incompatible type "int"; expected "str"

See the mypy Class Basics for this style.


There are two issues at play here:

  • Entirely unannotated functions/methods are not checked by MyPy:

    MyPy: Function signatures and dynamic vs static typing

    A function without type annotations is considered to be dynamically typed by mypy:

    def greeting(name):
        return 'Hello ' + name
    

    By default, mypy will not type check dynamically typed functions. This means that with a few exceptions, mypy will not report any errors with regular unannotated Python.

    If you want to eliminate such issues slipping through, use the flags --disallow-untyped-defs or --check-untyped-defs.

  • Unannotated parameters default to Any:

    class Person:
        name: str
        age: int
    
        def __init__(self, name, age: int) -> None:
            self.name = name
            reveal_type(name)       # Revealed type is 'Any'
    
            reveal_type(self.name)  # Revealed type is 'builtins.str'
            self.age = age
    

    Even if your __init__ is checked, its arguments take and provide the always-compatible Any type.

To directly fix both issues, annotate the initialiser arguments instead of the class slots if the two coincide. Only annotate the class slots if __init__ is generated automatically, e.g. via dataclasses.dataclass or typing.NamedTuple, or annotate them in addition if inference is not sufficently precise.

Using reveal_type gives you a helpful hint why this happens:

class Person:
    name: str
    age: int

    def __init__(self, name, age):
        self.name = name
        self.age = age
        reveal_type(self.name)


p = Person(5, 5)

Output:

test_mypy1.py:8: note: Revealed type is 'Any'
test_mypy1.py:8: note: 'reveal_type' always outputs 'Any' in unchecked functions

So the reason is that Mypy doesn't check the __init__ method for type errors at all by default, since you haven't type annotated it.

Related