Skip to content

Commit 4227fb6

Browse files
authored
Merge pull request #476 from yukinarit/new-strict-typecheck
pyserde is powered by beartype
2 parents 2471217 + 83d9308 commit 4227fb6

39 files changed

+566
-522
lines changed

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ repos:
1414
args:
1515
- .
1616
- repo: https://github.com/RobertCraigie/pyright-python
17-
rev: v1.1.309
17+
rev: v1.1.345
1818
hooks:
1919
- id: pyright
2020
additional_dependencies: ['pyyaml', 'msgpack', 'msgpack-types']

docs/en/SUMMARY.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,6 @@
88
- [Class Attributes](class-attributes.md)
99
- [Field Attributes](field-attributes.md)
1010
- [Union](union.md)
11-
- [Type Check](type-check.md)
11+
- [Type Checking](type-check.md)
1212
- [Extension](extension.md)
1313
- [FAQ](faq.md)

docs/en/decorators.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,11 +91,13 @@ class Wrapper(External):
9191
pyserde supports forward references. If you replace a nested class name with with string, pyserde looks up and evaluate the decorator after nested class is defined.
9292

9393
```python
94+
from __future__ import annotations # make sure to import annotations
95+
9496
@dataclass
9597
class Foo:
9698
i: int
9799
s: str
98-
bar: 'Bar' # Specify type annotation in string.
100+
bar: Bar # Bar can be specified although it's declared afterward.
99101

100102
@serde
101103
@dataclass

docs/en/type-check.md

Lines changed: 46 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,59 @@
11
# Type Checking
22

3-
This is one of the most awaited features. `pyserde` v0.9 adds the experimental type checkers. As this feature is still experimental, the type checking is not perfect. Also, [@tbsexton](https://github.com/tbsexton) is looking into [more beautiful solution](https://github.com/yukinarit/pyserde/issues/237#issuecomment-1191714102), the entire backend of type checker may be replaced by [beartype](https://github.com/beartype/beartype) in the future.
3+
pyserde offers runtime type checking since v0.9. It was completely reworked at v0.14 using [beartype](https://github.com/beartype/beartype) and it became more sophisticated and reliable. It is highly recommended to enable type checking always as it helps writing type-safe and robust programs.
44

5-
### `NoCheck`
5+
## `strict`
66

7-
This is the default behavior until pyserde v0.8.3 and v0.9.x. No type coercion or checks are run. Even if a user puts a wrong value, pyserde doesn't complain anything.
7+
Strict type checking is to check every field value against the declared type during (de)serialization and object construction. This is the default type check mode since v0.14. What will happen with this mode is if you declare a class with `@serde` decorator without any class attributes, `@serde(type_check=strict)` is assumed and strict type checking is enabled.
88

99
```python
1010
@serde
11-
@dataclass
1211
class Foo
1312
s: str
13+
```
1414

15+
If you call `Foo` with wrong type of object,
16+
```python
1517
foo = Foo(10)
16-
# pyserde doesn't complain anything. {"s": 10} will be printed.
18+
```
19+
20+
you get an error
21+
```python
22+
beartype.roar.BeartypeCallHintParamViolation: Method __main__.Foo.__init__() parameter s=10 violates type hint <class 'str'>, as int 10 not instance of str.
23+
```
24+
25+
> **NOTE:** beartype exception instead of SerdeError is raised from constructor because beartype does not provide post validation hook as of Feb. 2024.
26+
27+
similarly, if you call (de)serialize APIs with wrong type of object,
28+
29+
```python
1730
print(to_json(foo))
1831
```
1932

20-
### `Coerce`
33+
again you get an error
34+
35+
```python
36+
serde.compat.SerdeError: Method __main__.Foo.__init__() parameter s=10 violates type hint <class 'str'>, as int 10 not instance of str.
37+
```
38+
39+
> **NOTE:** There are several caveats regarding type checks by beartype.
40+
>
41+
> 1. beartype can not validate on mutated properties
42+
>
43+
> The following code mutates the property "s" at the bottom. beartype can not detect this case.
44+
> ```python
45+
> @serde
46+
> class Foo
47+
> s: str
48+
>
49+
> f = Foo("foo")
50+
> f.s = 100
51+
> ```
52+
>
53+
> 2. beartype can not validate every one of elements in containers. This is not a bug. This is desgin principle of beartype. See [Does beartype actually do anything?](https://beartype.readthedocs.io/en/latest/faq/#faq-o1].
54+
> ```
55+
56+
## `coerce`
2157
2258
Type coercing automatically converts a value into the declared type during (de)serialization. If the value is incompatible e.g. value is "foo" and type is int, pyserde raises an `SerdeError`.
2359
@@ -33,40 +69,17 @@ foo = Foo(10)
3369
print(to_json(foo))
3470
```
3571
36-
### `Strict`
72+
## `disabled`
3773
38-
Strict type checking is to check every value against the declared type during (de)serialization. We plan to make `Strict` a default type checker in the future release.
74+
This is the default behavior until pyserde v0.8.3 and v0.9.x. No type coercion or checks are run. Even if a user puts a wrong value, pyserde doesn't complain anything.
3975
4076
```python
41-
@serde(type_check=Strict)
77+
@serde
4278
@dataclass
4379
class Foo
4480
s: str
4581
4682
foo = Foo(10)
47-
# pyserde checks the value 10 is instance of `str`.
48-
# SerdeError will be raised in this case because of the type mismatch.
83+
# pyserde doesn't complain anything. {"s": 10} will be printed.
4984
print(to_json(foo))
5085
```
51-
52-
> **NOTE:** Since pyserde is a serialization framework, it provides type checks or coercing only during (de)serialization. For example, pyserde doesn't complain even if incompatible value is assigned in the object below.
53-
>
54-
> ```python
55-
> @serde(type_check=Strict)
56-
> @dataclass
57-
> class Foo
58-
> s: str
59-
>
60-
> f = Foo(100) # pyserde doesn't raise an error
61-
> ```
62-
>
63-
> If you want to detect runtime type errors, I recommend to use [beartype](https://github.com/beartype/beartype).
64-
> ```python
65-
> @beartype
66-
> @serde(type_check=Strict)
67-
> @dataclass
68-
> class Foo
69-
> s: str
70-
>
71-
> f = Foo(100) # beartype raises an error
72-
> ```

examples/enum34.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,7 @@ def main() -> None:
4343
print(f)
4444
s = to_json(f)
4545

46-
# You can also pass an enum-compabitle value (in this case True for E.B).
47-
# Caveat: Foo takes any value IE accepts. e.g., Foo(True) is also valid.
48-
s = to_json(Foo(3)) # type: ignore
46+
s = to_json(Foo(IE(3)))
4947
print(s)
5048

5149

examples/forward_reference.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from __future__ import annotations
12
from dataclasses import dataclass
23

34
from serde import serde
@@ -8,7 +9,7 @@
89
class Foo:
910
i: int
1011
s: str
11-
bar: "Bar" # Specify type annotation in string.
12+
bar: Bar
1213

1314

1415
@serde

examples/generics_nested.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,12 @@ def main() -> None:
4444
print(event_a)
4545
print(new_event_a)
4646

47-
payload = Payload(1, A("a_str"))
48-
payload_dict = to_dict(payload)
49-
new_payload = from_dict(Payload[A], payload_dict)
50-
print(payload)
51-
print(new_payload)
47+
# This has a bug, see https://github.com/yukinarit/pyserde/issues/464
48+
# payload = Payload(1, A("a_str"))
49+
# payload_dict = to_dict(payload)
50+
# new_payload = from_dict(Payload[A], payload_dict)
51+
# print(payload)
52+
# print(new_payload)
5253

5354

5455
if __name__ == "__main__":

examples/init_var.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ class Foo:
1212
b: Optional[int] = field(default=None, init=False)
1313
c: InitVar[Optional[int]] = 1000
1414

15-
def __post_init__(self, c: Optional[int]) -> None:
15+
def __post_init__(self, c: Optional[int]) -> None: # type: ignore
1616
self.b = self.a * 10
1717

1818

examples/runner.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
import skip
3737
import tomlfile
3838
import type_check_coerce
39-
import type_check_strict
39+
import type_check_disabled
4040
import type_datetime
4141
import type_decimal
4242
import union
@@ -82,8 +82,8 @@ def run_all() -> None:
8282
run(nested)
8383
run(lazy_type_evaluation)
8484
run(literal)
85-
run(type_check_strict)
8685
run(type_check_coerce)
86+
run(type_check_disabled)
8787
run(user_exception)
8888
run(pep681)
8989
run(variable_length_tuple)

examples/type_check_coerce.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
from dataclasses import dataclass
22
from typing import Dict, List, Optional
33

4-
from serde import Coerce, serde
4+
from serde import coerce, serde
55
from serde.json import from_json, to_json
66

77

8-
@serde(type_check=Coerce)
8+
@serde(type_check=coerce)
99
@dataclass
1010
class Bar:
1111
e: int
1212

1313

14-
@serde(type_check=Coerce)
14+
@serde(type_check=coerce)
1515
@dataclass
1616
class Foo:
1717
a: int

0 commit comments

Comments
 (0)