Skip to content

Accessing __defaults__ attribute

Detects accessing the __defaults__ attribute of a function, which can bypass type checking and lead to runtime type errors.

The __defaults__ attribute stores the default values for a function's parameters. When you modify this attribute directly, type checkers cannot verify that the new defaults match the expected parameter types, potentially causing type errors at runtime.

What gets flagged

Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
def foo(x: int = 1) -> int:
    return x

foo.__defaults__ = ("string",)

foo.__defaults__ = None

def bar(x: int = 1, y: str = "string", z: bool = True) -> int:
    return x

bar.__defaults__ = (1, "string", "string")

bar.__defaults__ = None

bar.__defaults__ = (1, "string")
Unsoundness Checker Output
Text Only
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
error[invalid-function-defaults]: Setting `__defaults__` to an object of type `tuple[Literal["string"]]` on a function may lead to runtime type errors.
 --> main.py:4:1
  |
2 |     return x
3 |
4 | foo.__defaults__ = ("string",)
  | ^^^^^^^^^^^^^^^^
5 |
6 | foo.__defaults__ = None
  |
info: rule `invalid-function-defaults` was selected in the configuration file

error[invalid-function-defaults]: Setting `__defaults__` to an object of type `None` on a function may lead to runtime type errors.
 --> main.py:6:1
  |
4 | foo.__defaults__ = ("string",)
5 |
6 | foo.__defaults__ = None
  | ^^^^^^^^^^^^^^^^
7 |
8 | def bar(x: int = 1, y: str = "string", z: bool = True) -> int:
  |
info: rule `invalid-function-defaults` was selected in the configuration file

error[invalid-function-defaults]: Setting `__defaults__` to an object of type `tuple[Literal[1], Literal["string"], Literal["string"]]` on a function may lead to runtime type errors.
  --> main.py:11:1
   |
 9 |     return x
10 |
11 | bar.__defaults__ = (1, "string", "string")
   | ^^^^^^^^^^^^^^^^
12 |
13 | bar.__defaults__ = None
   |
info: rule `invalid-function-defaults` was selected in the configuration file

error[invalid-function-defaults]: Setting `__defaults__` to an object of type `None` on a function may lead to runtime type errors.
  --> main.py:13:1
   |
11 | bar.__defaults__ = (1, "string", "string")
12 |
13 | bar.__defaults__ = None
   | ^^^^^^^^^^^^^^^^
14 |
15 | bar.__defaults__ = (1, "string")
   |
info: rule `invalid-function-defaults` was selected in the configuration file

error[invalid-function-defaults]: Setting `__defaults__` to an object of type `tuple[Literal[1], Literal["string"]]` on a function may lead to runtime type errors.
  --> main.py:15:1
   |
13 | bar.__defaults__ = None
14 |
15 | bar.__defaults__ = (1, "string")
   | ^^^^^^^^^^^^^^^^
   |
info: rule `invalid-function-defaults` was selected in the configuration file

Mypy: No Diagnostic Emitted

Pyright: No Diagnostic Emitted

Ty: No Diagnostic Emitted

What is okay

We do not emit errors if the types of the new defaults match the expected parameter types.

Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
def foo(x: str = "string") -> str:
    return x

foo.__defaults__ = ("another_string",)

foo.__defaults__ = ("another_string", "another_string")

def bar(x: int): ...

bar.__defaults__ = (1,)

def baz(x: int): ...

baz.__defaults__ = None

Unsoundness Checker: No Diagnostic Emitted

Mypy: No Diagnostic Emitted

Pyright: No Diagnostic Emitted

Ty: No Diagnostic Emitted