16.01.2025 PyWaw #117
<style scoped> .to-right { text-align: right; } </style>Shoutout to:
- Carl Meyer, for helping me understand the type theory
- a user named
decorator-factory
, for helping me understand the true purpose of this presentation - Jelle Zijlstra, for participating in the discussion about my talk
- everyone else involved who encouraged me in this endeavor
Everyone! And especially for you, if you:
- are interested in using typing and have no prior practice (
)
- occasionally use typing, but not a lot (
)
- already use typing extensively and maybe like it (
)
It's about describing what sets of runtime values can reside in particular variables.
Type hinting is as simple as turning
<style scoped> .flex { display: flex; gap: 1rem; width: 40rem; justify-content: space-between; align-items: center; } .flex pre { width: 30rem; } </style>def cube_area(e):
return f"Cube area: {6 * e ** 2}."
into
def cube_area(e: float) -> str:
return f"Cube area: {6 * e ** 2}."
Let's learn about two main kinds of types really quickly.
- Python type system specification
typing
standard library docs- relevant PEPs
- typeshed
- docs of particular type checkers (mypy, pyright, et alia)
- Python docs
- YouTube videos from Anthony Sottile, James Murphy, me et alia
- You want to stick with the most popular option
- You want to compile your code with
mypyc
to C extensions (~2.5x speedup)
Docs: https://mypy.readthedocs.io/en/stable/
- You use Pylance in VS Code
- You like pyright's approach, design decisions and behaviors that differ from mypy's
Docs: https://microsoft.github.io/pyright/, comparison with mypy
- You want to check out the type checker used for linting Instagram
- You've heard about Pysa and want to test it too
Docs: https://github.com/facebook/pyre-check Some background: https://news.ycombinator.com/item?id=17048682
- You prefer lenient type checking
- You want to rely more on type inference than on explicit annotations (no gradual typing)
Docs: https://google.github.io/pytype/, comparison with mypy
Coming soon: red-knot
check out those: basedmypy, basedpyright, pyanalyze
<style scoped> img { width: 25rem; padding-top: 1rem; } </style>
*on my machine
For every typing feature, do the following:
- Learn about it
- Gradual introduction (remember about chunking and aliasing)
- Troubleshooting (optionally, trouble-shouting) / Getting it right
- Staying up to date (but not up late)
Example: From a linter
- Work through the errors reported by your type checker
- Don't be afraid to google things
- Suggest improvements to type checkers / File bug reports
- Ask questions in Python Discord's
#type-hinting
- Follow the changelogs (or videos about them)
- Subscribe to Codezarre (https://codezarre.com—try to print this website)
- Read the new typing PEPs
- Participate in typing discussions!
- Contribute to typing-related projects or create them!
Typing and runtime have their own, sometimes discrepant rules. In the end, it's the runtime that matters.
<style scoped> .examples { display: flex; padding-top: 1rem; justify-content: space-evenly; gap: 1rem; } pre { width: 100%; } </style># passes type checking
x: complex = True
# fails at runtime
assert isinstance(x, complex)
# passes type checking
message: str = NotImplemented
# fails at runtime
assert isinstance(message, str)
from typing import TYPE_CHECKING
if TYPE_CHECKING:
# analyzed by type checkers
# never executed at runtime
from circular import something
from costly import just_for_types
else:
# always executed at runtime
# ignored by type checkers
something very hacky!
In Python, classes are object factories defined by the
class
statement, and returned by thetype(obj)
built-in function. Class is a dynamic, runtime concept.
Classes are commonly used to create:
- Nominal types (e.g.
str
) - Structural types (
TypedDict
constructs,Protocol
s)
Besides that, there are...
- Special forms (e.g.
Never
,Literal
,Generic
,TypedDict
) - Weird types (e.g.
None
,NotImplemented
,NewType
)
The Liskov substitution principle.
We have a function compute_salary(e: Employee)
.
It accepts an argument of type Employee
.
Does it also accept an argument of type Manager
,
given that Manager
is a subtype of Employee
?
Don't make others narrow down your types. Have an async function? Create a separate coroutine function. Have a sync function? Create a separate function.
Not both at the same time ;)
Opinionated section.
- Easier learning of available functionality
- Thinking about developer experience => Faster development in the long run
import this
def foo() -> int:
return 5
writing the -> int
ensures you always return int
,
and not a supertype or other incompatible type.
- You are encouraged to rely on single-purpose and statically known constructs
- The code structure can be fairly simpler to be understood by the type checker
...otherwise don't.
Good reasons:
- the impossibility of expressing a type in the current type system
- lack of ergonomicity to specifying the correct type
- DX—see Werkzeug proxies! (
flask.g
,flask.request
)
Some great projects do lie, so just be sure to have reasons if you need to.
That's your homework assignment! https://github.com/bswck (pinned repo)
- Use
typing.Self
(3.11+) ortyping_extensions.Self
(3.9+) for methods returningcls
/self
. - Leverage PEP 696 (
Generator[int, None, None]
->Generator[int]
) to reduce redundancy. - Type hint using generic built-in types (3.9+, PEP 585). E.g. use
list[str]
, nottyping.List[str]
. - Use
X | Y
instead oftyping.Union[X, Y]
in 3.10+ codebases (PEP 604). This applies totyping.Optional
, too! - Don't use
dict[str, Any]
for annotating fixed-structure data (useTypedDict
, dataclasses, or other models instead).
- Review your
TypeGuard
s that could beTypeIs
s. See (TypeIs vs TypeGuard and PEP 742). - Don't bother using
TYPE_CHECKING
in a module where any of your types are evaluated at runtime (e.g. in Pydantic models). - Be pragmatic about
TYPE_CHECKING
. Use it to optimize import times, avoid circular imports and import symbols from stubs.
- Don't confuse
Any
withobject
(check this). - Don't use
Any
as an easy way to type a hard-to-annotate interface. Read how to move away fromAny
. - Don't use the deprecated aliases from
typing
. - Try not to have to use overloads, but use them to logically associate call conventions, especially when unions are involved.
- Use stub files for annotating extension modules.
- DON'T use bare
# type: ignore
(this applies also to# noqa
). - DON'T skip annotating return values (here's a writeup).
- DO allow yourself to use PEP 563 despite future deprecation.
- DO prefer
typing.Never
totyping.NoReturn
(no difference). - Avoid
T | Awaitable[T]
(T | Coroutine[T, None, None]
). Single responsibility and interface segregation.
- DO type-check at tail (minimum supported version) or all supported versions (to cover
if sys.version_info
branches). - DO use
__all__
to control re-exports. - DO minimize runtime overhead if using inlined types. E.g. this PR
https://github.com/bswck (pinned repo)
<style scoped> img { width: 40%; } </style>