📄 Parent resource: My general Python notes
Resources
- Cheatsheets
- Adam Johnson: mypy blog posts
- Pyright - Type Concepts
- Official typing main page
- Official PEPs
- PlainEnglish Python: Python for TypeScript Developers
- My docs: Using Python Type-Checking Features in VSCode
- Benita: Exhaustiveness Checking with Mypy
- awesome-python-typing
Different Python Type-Checkers
https://github.com/ethanhs/python-typecheckers
To Capitalize or Not
list[str]
vsList[str]
vsList[Str]
In general, most Python "built-ins", such as list
and str
, support being used as type annotations and the from typing import List
usage is considered deprecated. You can find this documented in the official Python docs.
Typing Functions
To express / represent a function as a type (without actually declaring it), you can use the Callable
type:
from typing import Callable
AddTwoIntsFunction = Callable[[int, int], int]
Typing Decorators
Good resources:
- rednafi.github.io/reflections/static-typing-python-decorators.html
- blog.whtsky.me/tech/2021/decorator-type-gymnastics-in-python/
Typing While Unpacking / Destructuring
Python supports unpacking, which is a more concise syntax for creating variables out of elements of a list or tuple:
alpha, beta = ['alpha', 'beta']
Unfortunately, if you need to type annotate variables used in unpacking, you have to do it separately above the assignment, not inline:
alpha: Literal['alpha']
beta: Literal['beta']
alpha, beta = ['alpha', 'beta']
Strongly-Typed Dictionaries and Objects
Typed-Dicts
A TypedDict is really just a type-annotation layer over a standard dictionary.
Syntax:
from typing import TypedDict
# Class declaration
class Snack(TypedDict, total=True):
name: str
calories: int
apple: Snack = {
'name': 'Medium Apple',
'calories': 95
}
# Alternate syntax
Snack = TypedDict('Snack', {'name': str, 'calories': int}, total=True)
apple: Snack = {
'name': 'Medium Apple',
'calories': 95
}
Currently, inline / anonymous TypedDicts are not supported, although there is discussion around supporting them
There is are known issue (#10759 and #7981) with mypy and
TypedDict.values()
returning the wrong types
TypedDict with Generics
Starting with Python 3.11 (but also back-ported in typing_extensions.TypedDict
) (PR 27633), you can mix generics with TypedDict, which unlocks some powerful ways to compose interfaces with a minimal amount of code.
PizzaUnits: TypeAlias = Literal['slices', 'pizzas']
SoupUnits: TypeAlias = Literal['bowls', 'cups']
I = TypeVar('I', PizzaUnits, SoupUnits)
class Lunch(TypedDict, Generic[I]):
unit: I
price: float
my_lunch: Lunch[PizzaUnits] = {
'unit': 'bowls',
# ^ ERROR: Expression of type "Lunch[SoupUnits]"
# cannot be assigned to declared type "Lunch[PizzaUnits]"
'price': 0.44
}
Nested TypedDict Objects
Currently, the only way to declare a strongly-typed TypedDict object, with strongly-typed nesting, is to declare multiple flat TypeDicts and then combine them into a larger declaration:
class User(TypedDict):
user_id: int
name: str
class Page(TypedDict):
id: int
author: User
# ^ Notice how we can't just declare `User` inline
If all the nested objects share the same type, but only the keys differ, you could use a nested regular Dict
+ Literal
for a shorter declaration:
# Dict[Literal["a","b"], MyType]
Finally, if you think generic keys are OK on your nested objects, you can also just use a Dict
, which makes the declaration a little easier, but much less type-safe:
class Page(TypedDict):
id: int
author: Dict[str, int | str]
Type Narrowing
General Type-Narrowing
Here are some general strategies for type-narrowing:
- To exclude
None
from being inferred, add branching or early-return code that checks for it- E.g.
assert my_var is not None
- E.g.
- To narrow a class instance to a certain class type, you can use
isinstance(my_var, MyClass)
- To narrow a variable to a certain built-in type (in most languages, you might call these primitives), you can also use
isinstance(my_var, type)
, since the built-in types are all classes:int
,str
,bool
,float
,list
, etc.
Type-Narrowing Lists
Type-narrowing a list generally works better with list comprehension than it does with filter()
, at least with the current type-checkers.
Type-Narrowing via TypeGuards
https://adamj.eu/tech/2021/06/09/python-type-hints-how-to-narrow-types-with-typeguard/
Miscellaneous Questions and TypeScript Equivalents
NotRequired[T]
vsOptional[T]
?NotRequired[T]
can be used to mark a property as being completely optionalOptional[T]
means the property is still required, but can beNone
(type isT | None
/Union[T, None]
)
- Null or undefined types?
- Python uses
None
instead ofNull
orUndefined
. - Typing a possibly unset variable?
- Although you can use
possible_string: Union[None, other_type]
to represent the type ofNone | other_type
, a shorter way is withOptional[other_type]
- Although you can use
- Checking for
None
?if my_var is None
or even justif not my_var
works for checking if variable ==None
, but not for checking if a variable exists period (this will throw an exception is variable is not defined)
- How to assert something is not
None
/ narrow to non-None type?assert {MY_VAR} is not None
,if {MY_VAR} is not None:
- If variable is a type that coerces easily to a boolean, then simply
assert {MY_VAR}
should work, orif {MY_VAR}
- Python uses
- Declare a string variable as being constant, with shorthand like TS's
as const
?- Doesn't seem to exist. You can use
typing.cast
to cast, but would still require double-typing
- Doesn't seem to exist. You can use
- Equivalent to TS
ReturnType<T>
?- Does not exist - see issue #769
- Equivalent to TS
typeof {MY_VAR}
(reusing a variable value or inferred type as a new type annotation)/?- Nope, same as above
- Equivalent to
@ts-ignore
?# type: ignore
- If you get an Invalid "type: ignore" comment error, a possible cause is adding your own text after it. E.g.,
# type: ignore blah blah
- If you get an Invalid "type: ignore" comment error, a possible cause is adding your own text after it. E.g.,
- Equivalent to TS assertions (like
myVar as myType
)?- Most efficient:
myVar as myType
->typing.cast(my_type, my_var)
- Less efficient (but perhaps safer): Using
assert
- https://stackoverflow.com/a/57931741/11447682
- Most efficient:
- Why don't
if type()
checks narrow the type?- They can, depending on checker. Try using
isinstance()
to narrow instead though.
- They can, depending on checker. Try using
- Using
isinstance()
is not working for narrowing- Try different syntax approaches.
if not isinstance(my_obj, MyClass):
instead ofif isinstance(my_obj, MyClass) is not True:
- Try different syntax approaches.
- Equivalent to TS
Parameters<T>
? - Equivalent to instance type?
Type[ClassType]
(typing.Type
) ortype[]
(Python >= 3.9)
Troubleshooting
Looking for troubleshooting or setup specifically for Visual Studio Code?
A word of warning; stubs can (and often are) shipped separately from the actual library; for example, VSCode's pylance extension provides out-of-the-box type stubs for Django, but these might not match the actual version of Django you happen to have installed in your project!
Various issues:
- Error: Unsupported target for indexed assignment
- You will often get this when assigning a value to a nested dict property. E.g.,
my_dict["level_a"]["level_b"] = 2
. The root cause here is that mypy et.al. are not perfect at inferring the full type of a dictionary without an explicit annotation- Quick solution: Annotate dict as
dict
- e.g.my_dict: dict = {}
- Robust solution: Implement a
TypedDict
and annotate the instance as such
- Quick solution: Annotate dict as
- You will often get this when assigning a value to a nested dict property. E.g.,
- TypeError: 'type' object is not subscriptable
- This usually happens when accidentally indexing into a type or trying to somehow call it:
- Example: Trying to pass in a type to a generic slot, to a function that does not have a generic slot, like
some_fn[Dict[str, Any]](args)
- Example: Indexing into a type instead of your variable, like
dict["my_key"]
instead ofmy_dict["my_key"]
- Example: Trying to pass in a type to a generic slot, to a function that does not have a generic slot, like
- This usually happens when accidentally indexing into a type or trying to somehow call it:
- Intellisense isn't kicking in when adding elements in an array, dict, etc.
- Try adding a comma / separator before actually writing out the element. For some reason in VSCode, it won't work unless you do this when adding elements to a list.
Python Types Wishlist / Broken Things
It feels bad to highlight things that are broken in a system that is actively being improved, but there are enough non-obvious "gotchas" and things that hold back my flow while writing strongly-typed Python that it feels worth writing out as a consolidated list so I can more easily keep tabs.
Here are some of the things that are broken, or not yet implemented in Python types (or specific checkers), which I think are important:
- TypedDict (tracking issue - #11753)
- Unpacking, particularly via
**
(Issue #9408) - Support for declaring TypeDicts with nesting (without requiring multiple un-nested declarations) - not likely to happen at this current time
- Anonymous / inline TypedDict declarations
TypedDict.values()
returning the wrong types (mypy issue #10759, will likely be solved by @final support)
- Unpacking, particularly via
- dataclasses
.asdict
returning a strongly-typed TypedDict, instead ofDict[str, Any]
(there is an open, yet somewhat stale PR for this)
- Allowing for type-reuse via an existing variable (like TypeScript's
typeof
orReturnType
)- Closest issues seems to be #769
- Easy intersection composing (like TypeScript's
TypeA & TypeB
)