Joshua's Docs - Python Types - Resources, Notes, and Tips
Light

📄 Parent resource: My general Python notes

Resources

Different Python Type-Checkers

https://github.com/ethanhs/python-typecheckers

Typing Functions

Typing Decorators

Good resources:

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
  • 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] vs Optional[T]?
    • NotRequired[T] can be used to mark a property as being completely optional
    • Optional[T] means the property is still required, but can be None (type is T | None / Union[T, None])
  • Null or undefined types?
    • Python uses None instead of Null or Undefined.
    • Typing a possibly unset variable?
      • Although you can use possible_string: Union[None, other_type] to represent the type of None | other_type, a shorter way is with Optional[other_type]
    • Checking for None?
      • if my_var is None or even just if 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, or if {MY_VAR}
  • Equivalent to TS ReturnType<T>?
  • Equivalent to TS typeof {MY_VAR} (reusing a variable value or inferred type as a new type annotation)/?
  • 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
  • Equivalent to TS assertions (like myVar as myType)?
  • Why don't if type() checks narrow the type?
    • They can, depending on checker. Try using isinstance() to narrow instead though.
  • Using isinstance() is not working for narrowing
    • Try different syntax approaches.
      • if not isinstance(my_obj, MyClass): instead of if isinstance(my_obj, MyClass) is not True:
  • Equivalent to TS Parameters<T>?
  • Equivalent to instance type?
    • Type[ClassType] (typing.Type) or type[] (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
  • 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 of my_dict["my_key"]
  • 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:

Markdown Source Last Updated:
Sun May 28 2023 02:18:36 GMT+0000 (Coordinated Universal Time)
Markdown Source Created:
Sat Aug 06 2022 12:32:20 GMT+0000 (Coordinated Universal Time)
© 2023 Joshua Tzucker, Built with Gatsby
Feedback