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
+135-7Lines changed: 135 additions & 7 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -176,6 +176,9 @@ def f() -> AliasType:
176
176
177
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
178
179
+
In the below example, we define a type variable `U` that is bound to the `User` parent class.
180
+
This allows us to create a function that can return an instance of any subclass of `User`, while still providing type safety. See the [fastapi-demo for concrete example.](https://github.com/copdips/fastapi-demo/blob/d9922c99404f5d6406e2f10b02822d19a6bc3b91/app/services/base.py#L13-L33)
[From MyPy](https://mypy.readthedocs.io/en/stable/protocols.html#callback-protocols): We can use [Protocols](https://typing.python.org/en/latest/spec/protocol.html) to define [callable](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable) types with a special [**call**](https://docs.python.org/3/reference/datamodel.html#object.__call__) member:
354
+
355
+
Callback `protocols` and `Callable` types can be used mostly interchangeably, but protocols are more flexible and can be used to define more complex callable types.
batch_proc([], bad_cb) # Error! Argument 2 has incompatible type because of
375
+
# different name and kind in the callback
376
+
```
377
+
378
+
!!! warning "Protocol doesn't like isinstance()"
379
+
Although the `@runtime_checkable` decorator allows using `isinstance()` to check if an object conforms to a Protocol, this approach [has limitations and performance issues](https://mypy.readthedocs.io/en/stable/protocols.html#using-isinstance-with-protocols). Therefore, it's recommended to use `Protocol` exclusively for static type checking and avoid runtime `isinstance()` checks, at least until Python 3.13.
380
+
381
+
```python
382
+
from typing import Protocol, runtime_checkable
383
+
384
+
@runtime_checkable
385
+
class Drawable(Protocol):
386
+
def draw(self) -> None: ...
387
+
388
+
class Circle:
389
+
def draw(self) -> None:
390
+
print("Drawing a circle")
391
+
392
+
# This works but is not recommended
393
+
circle = Circle()
394
+
if isinstance(circle, Drawable): # Avoid this pattern
395
+
circle.draw()
396
+
397
+
# Preferred approach: rely on duck typing
398
+
def render(obj: Drawable) -> None:
399
+
obj.draw() # Type checker ensures obj has draw() method
400
+
401
+
render(circle) # Type-safe without runtime checks
402
+
```
403
+
404
+
## Type narrowing for parameters in multi-type
405
+
406
+
We know how to define parameters with union types `a: int | str`, but how can we help static type checkers understand which specific type a parameter has within if-else control flow?
407
+
408
+
Previously, we can simply use `isinstance()` function, Python 3.13 introduced `typing.TypeIs` ([PEP 742](https://peps.python.org/pep-0742/))for this purpose (use [typing_extensions.TypeIs](https://typing-extensions.readthedocs.io/en/latest/index.html#typing_extensions.TypeIs) for Python versions prior to 3.13).
409
+
410
+
```python title="use isinstance() for type narrowing"
!!! note "Don't use TypeGuard, it works only in if branch, not else branch. TypeIs works for both if and else branch."
455
+
456
+
### When to use TypeIs over isinstance()
457
+
458
+
[PEP 724 says](https://peps.python.org/pep-0742/#when-to-use-typeis): Python code often uses functions like `isinstance()` to distinguish between different possible types of a value. Type checkers understand `isinstance()` and various other checks and use them to narrow the type of a variable. However, sometimes you want to reuse a more complicated check in multiple places, or you use a check that the type checker doesn't understand. In these cases, you can define a `TypeIs` function to perform the check and allow type checkers to use it to narrow the type of a variable.
459
+
460
+
A TypeIs function takes a single argument and is annotated as returning `TypeIs[T]`, where `T` is the type that you want to narrow to. The function must return `True` if the argument is of type `T`, and `False` otherwise. The function can then be used in if checks, just like you would use `isinstance()`. For example:
@@ -362,24 +490,24 @@ Ref. Pyright in [this post](../2021/2021-01-04-python-lint-and-format.md#pyright
362
490
### RightTyper
363
491
364
492
During an internal tech demo at my working, I heard about [RightTyper](https://github.com/RightTyper/RightTyper), a Python tool that generates type annotations for function arguments and return values.
365
-
It’s important to note that RightTyper doesn’t statically parse your Python files to add types; instead, it needs to run your code to detect types on the fly. So, one of the best ways to use RightTyper is with python `-m pytest`, assuming you have good test coverage.
493
+
It's important to note that **RightTyper** doesn't statically parse your Python files to add types; instead, it needs to run your code to detect types on the fly. So, one of the best ways to use **RightTyper** is with python `-m pytest`, assuming you have good test coverage.
366
494
367
495
### ty
368
496
369
-
[ty](https://github.com/astral-sh/ty) represents the next generation of Python type checking tools. Developed by the team behind the popular [ruff](https://docs.astral.sh/ruff/) linter, ty is implemented in Rust for exceptional performance.
497
+
[ty](https://github.com/astral-sh/ty) represents the next generation of Python type checking tools. Developed by the team behind the popular [ruff](https://docs.astral.sh/ruff/) linter, **ty** is implemented in Rust for exceptional performance.
370
498
It functions both as a type checker and language server, offering seamless integration through its dedicated [VSCode extension ty-vscode](https://github.com/astral-sh/ty-vscode).
371
499
372
-
While Ruff excels at various aspects of Python linting, type checking remains outside its scope.
500
+
While **Ruff** excels at various aspects of Python linting, type checking remains outside its scope.
373
501
ty aims to fill this gap, though it's currently in preview and still evolving toward production readiness.
374
-
The combination of Ruff and ty promises to provide a comprehensive Python code quality toolkit.
502
+
The combination of **Ruff** and **ty** promises to provide a comprehensive Python code quality toolkit.
375
503
376
504
### pyrefly
377
505
378
506
[pyrefly](https://pyrefly.org/) emerges as another promising entrant in the Python type checking landscape.
379
-
Developed by Meta and also written in Rust, pyrefly offers both type checking capabilities and language server functionality.
507
+
Developed by Meta and also written in Rust, **pyrefly** offers both type checking capabilities and language server functionality.
380
508
While still in preview, it demonstrates the growing trend of high-performance Python tooling implemented in Rust.
381
509
382
510
The tool integrates smoothly with modern development environments through its [VSCode extension refly-vscode](https://marketplace.visualstudio.com/items?itemName=meta.pyrefly), making it accessible to a wide range of developers.
383
511
Its backing by Meta suggests potential for robust development and long-term support.
384
512
385
-
Just a quick test, pyrefly seems to generate more typing errors than ty.
513
+
Just a quick test, **pyrefly** seems to generate more typing errors than **ty**.
0 commit comments