You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: docs/posts/2025/2025-02-01-python-type-hints.md
+189-2Lines changed: 189 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -14,10 +14,13 @@ date:
14
14
15
15
Python is a dynamically typed language, meaning variable types don't require explicit declaration. However, as projects grow in complexity, type annotations become increasingly valuable for code maintainability and clarity.
16
16
17
-
Type hints have been a major focus of recent Python releases, and I was particularly intrigued when I heard about [Guido van Rossum's work on MyPy at Dropbox](https://blog.dropbox.com/topics/company/thank-you--guido), where the team needed robust tooling to migrate their codebase from Python 2 to Python 3.
17
+
Type hints ([PEP 484](https://peps.python.org/pep-0484/)) have been a major focus of recent Python releases, and I was particularly intrigued when I heard about [Guido van Rossum's work on MyPy at Dropbox](https://blog.dropbox.com/topics/company/thank-you--guido), where the team needed robust tooling to migrate their codebase from Python 2 to Python 3.
18
18
19
19
Today, type hints are essential for modern Python development. They significantly enhance IDE capabilities and AI-powered development tools by providing better code completion, static analysis, and error detection. This mirrors the evolution we've seen with TypeScript's adoption over traditional JavaScript—explicit typing leads to more reliable and maintainable code.
20
20
21
+
!!! note "Typed Python vs data science projects"
22
+
We know that type hints are [not very popular among data science projects](https://engineering.fb.com/2024/12/09/developer-tools/typed-python-2024-survey-meta/) for [some reasons](https://typing.python.org/en/latest/guides/typing_anti_pitch.html), but we won't discuss them here.
23
+
21
24
<!-- more -->
22
25
23
26
## typing module vs collections module
@@ -123,7 +126,191 @@ Some types like: `typing.Any`, `typing.Generic`, `typing.TypeVar`, etc. are stil
123
126
| indexing (e.g., `seq[0]`) | Yes | No |
124
127
| Membership Checks (`x in data`) | Yes | Yes |
125
128
126
-
!!! note "(Python 3.9+) Both `typing.Sequence` and `typing.Collection` are [deprecated aliases](#typing-module-vs-collections-module)."
129
+
## Type aliases
130
+
131
+
[From Mypy](https://mypy.readthedocs.io/en/stable/kinds_of_types.html#type-aliases): Python 3.12 introduced the `type` statement for defining explicit type aliases. Explicit type aliases are unambiguous and can also improve readability by making the intent clear.
132
+
The definition may contain forward references without having to use string literal escaping, **since it is evaluated lazily**, which improves also the loading performance.
133
+
134
+
```python
135
+
type AliasType = list[dict[tuple[int, str], set[int]]] | tuple[str, list[str]]
136
+
137
+
# Now we can use AliasType in place of the full name:
138
+
139
+
deff() -> AliasType:
140
+
...
141
+
```
142
+
143
+
## Type variable
144
+
145
+
[From MyPy](https://mypy.readthedocs.io/en/stable/kinds_of_types.html#the-type-of-class-objects): Python 3.12 introduced new syntax to use the `type[C]` and a type variable with an upper bound (see [Type variables with upper bounds](https://mypy.readthedocs.io/en/stable/generics.html#type-variable-upper-bound)).
146
+
147
+
```python title="Python 3.12 syntax"
148
+
def new_user[U: User](user_class: type[U]) -> U:
149
+
# Same implementation as before
150
+
```
151
+
152
+
Here is the example using the legacy syntax (**Python 3.11 and earlier**):
153
+
154
+
```python title="Python 3.11 and earlier syntax"
155
+
U = TypeVar('U', bound=User)
156
+
157
+
defnew_user(user_class: type[U]) -> U:
158
+
# Same implementation as before
159
+
```
160
+
161
+
Now mypy will infer the correct type of the result when we call new_user() with a specific subclass of User:
162
+
163
+
```python
164
+
beginner = new_user(BasicUser) # Inferred type is BasicUser
165
+
beginner.upgrade() # OK
166
+
```
167
+
168
+
## Annotating \_\_init\_\_ methods
169
+
170
+
[From MyPy](https://mypy.readthedocs.io/en/stable/class_basics.html#annotating-init-methods): It is allowed to omit the return type declaration on \_\_init\_\_ methods if at least one argument is annotated.
171
+
172
+
```python
173
+
classC1:
174
+
# __init__ has no argument is annotated,
175
+
# so we should add return type declaration
176
+
def__init__(self) -> None:
177
+
self.var =42
178
+
179
+
classC2:
180
+
# __init__ has at least one argument is annotated,
181
+
# so it's allowed to omit the return type declaration
182
+
# so in most cases, we don't need to add return type.
183
+
def__init__(self, arg: int):
184
+
self.var = arg
185
+
```
186
+
187
+
## Postponed Evaluation of Annotations
188
+
189
+
[PEP 563 (Postponed Evaluation of Annotations)](https://peps.python.org/pep-0563/) allows you to use `from __future__ import annotations` to defer evaluation of type annotations until they're actually needed. Generally speaking, turns every annotation into a string. This helps with:
190
+
191
+
- Forward references
192
+
- Circular imports
193
+
- Performance improvements
194
+
195
+
`from __future__ import annotations`**must be the first executable line** in the file. You can only have shebang and comment lines before it.
196
+
197
+
```python hl_lines="1 7"
198
+
from__future__import annotations
199
+
from pydantic import BaseModel
200
+
201
+
classUser(BaseModel):
202
+
name: str
203
+
age: int
204
+
friends: list[User] = [] # Forward reference works
205
+
206
+
# This works in Pydantic v2
207
+
user = User(name="Alice", age=30, friends=[])
208
+
```
209
+
210
+
!!! warning "from \_\_future\_\_ import annotation is not fully compatible with Pydantic"
211
+
See this [github issue](https://github.com/jlowin/fastmcp/issues/905), and [this issue](https://github.com/pydantic/pydantic/issues/2678).
212
+
So prefer not use it as much as you can.
213
+
214
+
## Import cycles
215
+
216
+
[From MyPy](https://mypy.readthedocs.io/en/stable/runtime_troubles.html#import-cycles): If the cycle import is only needed for type annotations:
SqlAlchemy also uses [string literal](https://mypy.readthedocs.io/en/stable/runtime_troubles.html#string-literal-types-and-type-comments) for lazy evaluation and [typing.TYPE_CHECKING](https://mypy.readthedocs.io/en/stable/runtime_troubles.html#typing-type-checking) for typing:
1.`from __future__ import annotations` (PEP 563) turns every annotation into a string. Should be used with careful.
270
+
271
+
2. The `TYPE_CHECKING` import enables static type checking tools (MyPy, IDEs) to analyze types without affecting runtime behavior. For more details, see the [SQLModel documentation](https://sqlmodel.tiangolo.com/tutorial/code-structure/#import-only-while-editing-with-type_checking).
272
+
273
+
3. While `from __future__ import annotations` (PEP 563) allows direct usage of `children: Mapped[List[Child]]`, the preferred approach is `children: Mapped[List["Child"]]`. The latter avoids potential compatibility issues with libraries like Pydantic while maintaining clear type hints.
274
+
275
+
4. By using `if TYPE_CHECKING:`, we ensure the type checker recognizes `children` as a list of `Child` objects (even it's in string format `"Child"`) while preventing circular imports at runtime.
276
+
277
+
5. SQLAlchemy uses string literals (e.g., `"Child"`) to reference models, allowing for lazy loading and avoiding circular dependencies.
0 commit comments