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
+225-2Lines changed: 225 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
@@ -26,6 +29,38 @@ Since Python 3.9, most of types in `typing` module is [deprecated](https://docs.
26
29
27
30
Some types like: `typing.Any`, `typing.Generic`, `typing.TypeVar`, etc. are still not deprecated.
28
31
32
+
!!! note "Thanks to subscription support in many collections since Python3.9"
33
+
The `collections` module is now the preferred way to import many types (not all yet), as [they support subscription at runtime](https://mypy.readthedocs.io/en/stable/runtime_troubles.html#using-generic-builtins). [Subscription](https://docs.python.org/3/reference/expressions.html#subscriptions) refers to using square brackets `[]` to indicate the type of elements in a collection. **Subscription at runtime** means we can use `list[int]`, `dict[str, int]`, etc. directly without importing from `typing.List`, `typing.Dict`, etc.
TypeError: descriptor '__getitem__' for 'list' objects doesn't apply to a 'type' object
62
+
```
63
+
29
64
### Aliases to Built-in Types
30
65
31
66
| Deprecated Alias | Replacement |
@@ -123,7 +158,195 @@ Some types like: `typing.Any`, `typing.Generic`, `typing.TypeVar`, etc. are stil
123
158
| indexing (e.g., `seq[0]`) | Yes | No |
124
159
| Membership Checks (`x in data`) | Yes | Yes |
125
160
126
-
!!! note "(Python 3.9+) Both `typing.Sequence` and `typing.Collection` are [deprecated aliases](#typing-module-vs-collections-module)."
161
+
## Type aliases
162
+
163
+
[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.
164
+
The definition may contain forward references without having to use string literal escaping, **since it is evaluated lazily**, which improves also the loading performance.
165
+
166
+
```python
167
+
type AliasType = list[dict[tuple[int, str], set[int]]] | tuple[str, list[str]]
168
+
169
+
# Now we can use AliasType in place of the full name:
170
+
171
+
def f() -> AliasType:
172
+
...
173
+
```
174
+
175
+
## Type variable
176
+
177
+
[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)).
178
+
179
+
```python title="Python 3.12 syntax"
180
+
def new_user[U: User](user_class: type[U]) -> U:
181
+
# Same implementation as before
182
+
```
183
+
184
+
Here is the example using the legacy syntax (**Python 3.11 and earlier**):
185
+
186
+
```python title="Python 3.11 and earlier syntax"
187
+
U = TypeVar('U', bound=User)
188
+
189
+
def new_user(user_class: type[U]) -> U:
190
+
# Same implementation as before
191
+
```
192
+
193
+
Now mypy will infer the correct type of the result when we call new_user() with a specific subclass of User:
194
+
195
+
```python
196
+
beginner = new_user(BasicUser) # Inferred type is BasicUser
197
+
beginner.upgrade() # OK
198
+
199
+
```
200
+
201
+
## Annotating \_\_init\_\_ methods
202
+
203
+
[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.
204
+
205
+
```python
206
+
class C1:
207
+
# __init__ has no argument is annotated,
208
+
# so we should add return type declaration
209
+
def __init__(self) -> None:
210
+
self.var = 42
211
+
212
+
class C2:
213
+
# __init__ has at least one argument is annotated,
214
+
# so it's allowed to omit the return type declaration
215
+
# so in most cases, we don't need to add return type.
216
+
def __init__(self, arg: int):
217
+
self.var = arg
218
+
219
+
```
220
+
221
+
## Postponed Evaluation of Annotations
222
+
223
+
[PEP 563 (Postponed Evaluation of Annotations)](https://peps.python.org/pep-0563/) (also known as Future annotations import) allows you to use `from __future__ import annotations` to defer evaluation of type annotations until they're actually needed. Generally speaking, it turns every annotation into a string. This helps with:
`from __future__ import annotations` **must be the first executable line** in the file. You can only have shebang and comment lines before it.
230
+
231
+
```python hl_lines="1 7"
232
+
from __future__ import annotations
233
+
from pydantic import BaseModel
234
+
235
+
class User(BaseModel):
236
+
name: str
237
+
age: int
238
+
friends: list[User] = [] # Forward reference works
239
+
240
+
# This works in Pydantic v2
241
+
user = User(name="Alice", age=30, friends=[])
242
+
```
243
+
244
+
!!! warning "from \_\_future\_\_ import annotation is not fully compatible with Pydantic"
245
+
See this [warning](https://mypy.readthedocs.io/en/stable/runtime_troubles.html#future-annotations-import-pep-563), and see this [github issue](https://github.com/jlowin/fastmcp/issues/905), and [this issue](https://github.com/pydantic/pydantic/issues/2678) for the compatibility issues with Pydantic and postponed evaluation of annotations.
246
+
Future annotations import [doesn't support Python3.10 new syntax for union type](https://mypy.readthedocs.io/en/stable/runtime_troubles.html#using-x-y-syntax-for-unions) (e.g., `int | str`), and it also doesn't support the new syntax for type variables with upper bounds (e.g., `type[C]`), neither for some dynamic evaluation of annotations.
247
+
So it's preferable **NOT TO USE** `from __future__ import annotation` as much as possible, just use `string literal annotations` for forward references and circular imports.
248
+
249
+
## Import cycles
250
+
251
+
[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.
306
+
307
+
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).
308
+
309
+
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.
310
+
311
+
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.
312
+
313
+
5. SQLAlchemy uses string literals (e.g., `"Child"`) to reference models, allowing for lazy loading and avoiding circular dependencies.
0 commit comments