Skip to content

Commit fca83ff

Browse files
committed
update python type hints
1 parent b6f9aaf commit fca83ff

1 file changed

Lines changed: 189 additions & 2 deletions

File tree

docs/posts/2025/2025-02-01-python-type-hints.md

Lines changed: 189 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,13 @@ date:
1414

1515
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.
1616

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.
1818

1919
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.
2020

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+
2124
<!-- more -->
2225

2326
## typing module vs collections module
@@ -123,7 +126,191 @@ Some types like: `typing.Any`, `typing.Generic`, `typing.TypeVar`, etc. are stil
123126
| indexing (e.g., `seq[0]`) | Yes | No |
124127
| Membership Checks (`x in data`) | Yes | Yes |
125128

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+
def f() -> 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+
def new_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+
class C1:
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+
class C2:
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+
class User(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:
217+
218+
```python title="File foo.py" hl_lines="3-6"
219+
from typing import TYPE_CHECKING
220+
221+
if TYPE_CHECKING:
222+
import bar
223+
224+
def listify(arg: 'bar.BarClass') -> 'list[bar.BarClass]':
225+
return [arg]
226+
```
227+
228+
```python title="File bar.py" hl_lines="1"
229+
from foo import listify
230+
231+
class BarClass:
232+
def listifyme(self) -> 'list[BarClass]':
233+
return listify(self)
234+
```
235+
236+
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:
237+
238+
```python title="File models/parent.py" linenums="1" hl_lines="1 8 19-20"
239+
from __future__ import annotations # (1)!
240+
from typing import TYPE_CHECKING, List
241+
from sqlalchemy import String, Integer
242+
from sqlalchemy.orm import Mapped, mapped_column, relationship
243+
from database import Base
244+
245+
if TYPE_CHECKING:
246+
from models.child import Child # (2)!
247+
248+
class Parent(Base):
249+
__tablename__ = "parent"
250+
251+
# SQLAlchemy v2 syntax
252+
id: Mapped[int] = mapped_column(Integer, primary_key=True)
253+
name: Mapped[str] = mapped_column(String(50), nullable=False)
254+
email: Mapped[str] = mapped_column(String(100), unique=True, nullable=False)
255+
256+
# One-to-Many (Parent -> Children)
257+
# children: Mapped[List[Child]] = relationship( # (3)!
258+
children: Mapped[List["Child"]] = relationship( # (4)!
259+
"Child", # (5)!
260+
back_populates="parent",
261+
cascade="all, delete-orphan",
262+
lazy="selectin" # one of sqlalchemy 2 lazy loading strategies
263+
)
264+
265+
def __repr__(self) -> str:
266+
return f"<Parent(id={self.id}, name='{self.name}')>"
267+
```
268+
269+
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.
278+
279+
```python title="File models/child.py" linenums="1" hl_lines="1 8 24-25"[](https://github.com/jlowin/fastmcp/issues/905)
280+
from __future__ import annotations
281+
from typing import TYPE_CHECKING, Optional
282+
from sqlalchemy import String, Integer, ForeignKey
283+
from sqlalchemy.orm import Mapped, mapped_column, relationship
284+
from database import Base
285+
286+
if TYPE_CHECKING:
287+
from models.parent import Parent
288+
289+
class Child(Base):
290+
__tablename__ = "child"
291+
292+
id: Mapped[int] = mapped_column(Integer, primary_key=True)
293+
name: Mapped[str] = mapped_column(String(50), nullable=False)
294+
age: Mapped[int] = mapped_column(Integer, nullable=False)
295+
296+
parent_id: Mapped[int] = mapped_column(
297+
Integer,
298+
ForeignKey("parents.id", ondelete="CASCADE"),
299+
nullable=False
300+
)
301+
302+
# Many-to-One (Child -> Parent)
303+
parent: Mapped[Parent] = relationship(
304+
"Parent",
305+
back_populates="children",
306+
lazy="selectin"
307+
)
308+
309+
def __repr__(self) -> str:
310+
return f"<Child(id={self.id}, name='{self.name}', parent_id={self.parent_id})>"
311+
```
312+
313+
## Type hints
127314

128315
## Typing tools
129316

0 commit comments

Comments
 (0)