Python:typing
lastmod 2025-02-21created 2020-08-1150 edits
This module provides runtime support for type hints. The most fundamental support consists of the types Any, Union, Callable, TypeVar, and Generic. For a full specification, please see PEP 484. For a simplified introduction to type hints, see PEP 483.
Annotated
Add context-specific metadata to a type.
Example: Annotated[int, runtime_check.Unsigned] indicates to the hypothetical runtime_check module that this type is an unsigned int. Every other consumer of this type can ignore this metadata and treat this type as int.
The first argument to Annotated must be a valid type.
Details:
- It's an error to call
Annotatedwith less than two arguments. - Access the metadata via the
__metadata__attribute::
- Nested Annotated types are flattened::
- Instantiating an annotated type is equivalent to instantiating the underlying type::
- Annotated can be used as a generic type alias::
Optimized:
TypeAlias = Annotated[T, runtime.Optimize()]
assert Optimized[int] == Annotated[int, runtime.Optimize()]
OptimizedList:
TypeAlias = Annotated[list[T], runtime.Optimize()]
assert OptimizedList[int] == Annotated[list[int], runtime.Optimize()]
- Annotated cannot be used with an unpacked TypeVarTuple::
Variadic:
This would be equivalent to::
where T1, T2 etc. are TypeVars, which would be invalid, because only one type should be passed to Annotated.
At Runtime
def annotated_by(
annotated: object,
kind: type[T],
) -> Iterable[tuple[str, T, type]]:
for k, v in get_type_hints(annotated, include_extras=True).items():
all_args = get_args(v)
if not all_args:
continue
actual, *rest = all_args
for arg in rest:
if isinstance(arg, kind):
yield k, arg, actual
This gives us back the annotations we are looking for in a convenient, easy-to-use form. Here is a quick example of how to use it.
@dataclass
class AnAnnotation:
name: str
def a_function(
a: str,
b: Annotated[int, AnAnnotation("b")],
c: Annotated[float, AnAnnotation("c")],
) -> None:
...
print(list(annotated_by(a_function, AnAnnotation)))
# [('b', AnAnnotation(name='b'), <class 'int'>),
# ('c', AnAnnotation(name='c'), <class 'float'>)]
Callable
Frameworks expecting callback functions of specific signatures might be type hinted using:
For example:
from collections.abc import Callable
def feeder(get_next_item: Callable[[], str]) -> None:
# Body
def async_query(on_success: Callable[[int], None],
on_error: Callable[[int, Exception], None]) -> None:
# Body
It is possible to declare the return type of a callable without specifying the call signature by substituting a literal ellipsis for the list of arguments in the type hint:
Note that Callable[[...], ReturnType] is not allowed syntax.
ParamSpec
When a function takes callback functions of the #Callable type as arguments, the calling function sometimes depends on the argument types of those callbacks.
Decorators are the typical case. In such cases you can use ParamSpec. (This can also be seen as a kind of Generic type.)
from typing import TypeVar, Callable, ParamSpec
import logging
T = TypeVar('T')
P = ParamSpec('P')
def add_logging(f: Callable[P, T]) -> Callable[P, T]:
'''A type-safe decorator to add logging to a function.'''
def inner(*args: P.args, **kwargs: P.kwargs) -> T:
logging.info(f'{f.__name__} was called')
return f(*args, **kwargs)
return inner
@add_logging
def add_two(x: float, y: float) -> float:
'''Add two numbers together.'''
return x + y
@add_logging
def send_msg(msg: str) -> None:
print(f"I Sent {msg}")
ModuleType
It is in types, not typing.
Or add it yourself.
Awaitable Callable
Asynchronous functions can be used as follows.
from typing import Any, Union, Callable, Awaitable
Watcher = Callable[..., Union[Awaitable[Any], Any]]
# ...
from inspect import iscoroutinefunction
if iscoroutinefunction(watcher):
return await watcher(*args, **kwargs) # type: ignore[misc]
else:
return watcher(*args, **kwargs)
Generics
How to use generic classes such as List[X]:
from typing import TypeVar, Generic
T = TypeVar('T')
class Stack(Generic[T]):
def __init__(self) -> None:
# Create an empty list with items of type T
self.items: List[T] = []
def push(self, item: T) -> None:
self.items.append(item)
def pop(self) -> T:
return self.items.pop()
def empty(self) -> bool:
return not self.items
It can be used as follows:
# Construct an empty Stack[int] instance
stack = Stack[int]()
stack.push(2)
stack.pop()
stack.push('x') # Type error
Type inference works for user-defined generic types as well:
def process(stack: Stack[int]) -> None: ...
process(Stack()) # Argument has inferred type Stack[int]
Construction of instances of generic types is also type checked.
class Box(Generic[T]):
def __init__(self, content: T) -> None:
self.content = content
Box(1) # OK, inferred type is Box[int]
Box[int](1) # Also OK
s = 'some string'
Box[int](s) # Type error
init_subclass
class_getitem
Parameterizes a generic class.
At least, parameterizing a generic class is the *main* thing this method
does. For example, for some generic class `Foo`, this is called when we
do `Foo[int]` - there, with `cls=Foo` and `params=int`.
However, note that this method is also called when defining generic
classes in the first place with `class Foo(Generic[T]): ...`.
Used to support type indexing such as MyClass[int] in generic classes.
TypeVar
Allow only one of specific types (must match exactly):
With bound=..., the type or its subtypes are allowed:
Covariance (Covariant)
This is about type variance.
In Python typing, you can specify how a generic class's type parameters behave with respect to subtype relationships. The three kinds of type variance are:
- Covariant (+T): Also accepts subtypes of the given type. Use the
covariant=Trueflag. - Contravariant (-T): Also accepts supertypes of the given type. Use the
contravariant=Trueflag. - Invariant: Accepts neither subtypes nor supertypes; only exactly the same type is allowed.
Example:
from typing import TypeVar, Generic
# T is covariant
T = TypeVar('T', covariant=True)
# Base class
class Animal:
pass
# Subclass
class Dog(Animal):
pass
# A generic class with a covariant type parameter
class Box(Generic[T]):
def __init__(self, content: T) -> None:
self.content = content
# Because T is covariant, Box[Dog] can be assigned to Box[Animal]
dog_box: Box[Dog] = Box(Dog())
animal_box: Box[Animal] = dog_box
print(isinstance(animal_box.content, Animal)) # 출력: True
In the example above, because T is declared with covariant=True, Box[Dog] can be assigned to Box[Animal].
Thanks to the covariant type variable, the child class (Dog) is correctly treated as the parent class (Animal).
Type aliases
A type alias is defined by assigning the type to the alias. In this example, Vector and List[float] will be treated as interchangeable synonyms:
from typing import List
Vector = List[float]
def scale(scalar: float, vector: Vector) -> Vector:
return [scalar * num for num in vector]
# typechecks; a list of floats qualifies as a Vector.
new_vector = scale(2.0, [1.0, -4.2, 5.4])
Type aliases are useful for simplifying complex type signatures. For example:
from typing import Dict, Tuple, Sequence
ConnectionOptions = Dict[str, str]
Address = Tuple[str, int]
Server = Tuple[Address, ConnectionOptions]
def broadcast_message(message: str, servers: Sequence[Server]) -> None:
...
# The static type checker will treat the previous type signature as
# being exactly equivalent to this one.
def broadcast_message(
message: str,
servers: Sequence[Tuple[Tuple[str, int], Dict[str, str]]]) -> None:
...
Note that None as a type hint is a special case and is replaced by type(None).
NewType
In Rust, it is common to define data types that do not add new behavior to a general data type (e.g. an integer) but give it a new name suited to its purpose in the domain. This pattern is called “newtype”, and it can be used in Python too.
Let's look at the following situation:
class Database:
def get_car_id(self, brand: str) -> int:
def get_driver_id(self, name: str) -> int:
def get_ride_info(self, car_id: int, driver_id: int) -> RideInfo:
db = Database()
car_id = db.get_car_id("Mazda")
driver_id = db.get_driver_id("Stig")
info = db.get_ride_info(driver_id, car_id)
The arguments to get_ride_info were passed incorrectly. It should be </code>db.get_rider_info(car_id, driver_id)</code>. However, since the car ID and driver ID are both just integers, the types themselves are correct even though the function call is semantically wrong, so there is no type error.
This problem can be solved by using "NewType" to define separate types for the different kinds of IDs:
from typing import NewType
# Define a new type called "CarId", which is internally an `int`
CarId = NewType("CarId", int)
# Ditto for "DriverId"
DriverId = NewType("DriverId", int)
class Database:
def get_car_id(self, brand: str) -> CarId:
def get_driver_id(self, name: str) -> DriverId:
def get_ride_info(self, car_id: CarId, driver_id: DriverId) -> RideInfo:
db = Database()
car_id = db.get_car_id("Mazda")
driver_id = db.get_driver_id("Stig")
# Type error here -> DriverId used instead of CarId and vice-versa
info = db.get_ride_info(driver_id, car_id) ## 경고 포착!
ForwardRef
A class used for internal typing representation of string forward references. For example, List["SomeClass"] is implicitly transformed into List[ForwardRef("SomeClass")]. This class should not be instantiated by a user, but may be used by introspection tools.
Protocol
See PEP 544.
TypeGuard
- typing — Support for type hints — Python 3.12.3 documentation
- PEP 647 – User-Defined Type Guards | peps.python.org
Needed for Type Narrowing (narrowing types).
TypeGuard is a useful tool for Python's static type checker (mypy) that checks for a specific type based on a condition and, when that condition holds, helps infer the variable as that type. It is a concept defined in PEP 647, introduced in Python 3.10.
Type inference is generally useful when a function's return value or a variable has a fixed type, but when the type is narrowed through conditionals and the like, it can be hard to infer the type precisely. TypeGuard provides additional type hints in such situations, enabling better type inference.
def is_str_list(val: list[object]) -> TypeGuard[list[str]]:
'''Determines whether all objects in the list are strings'''
return all(isinstance(x, str) for x in val)
def func1(val: list[object]):
if is_str_list(val):
# Type of ``val`` is narrowed to ``list[str]``.
print(" ".join(val))
else:
# Type of ``val`` remains as ``list[object]``.
print("Not a list of strings!")
- Defining a TypeGuard: if you specify TypeGuard as a function's return type, the argument's type can be narrowed to the specified type when the function returns True.
- Conditional type narrowing: in the example above, the is_str_list function returns True when all elements of the list are strings, and as a result the type checker infers the list as list[str].
Unpack
| INFORMATION |
| Added in version 3.11 |
from typing import TypedDict, Unpack
class Movie(TypedDict):
name: str
year: int
# This function expects two keyword arguments - `name` of type `str`
# and `year` of type `int`.
def foo(**kwargs: Unpack[Movie]):
kwargs["name"] = ... # 여기에서 "name" 을 칠 때 AutoComplete 나타난다. @dataclass 클래스는 안되더라
See PEP 692 for more details on using Unpack for **kwargs typing.
overload
Helps the IDE's type inference, for example when the return value differs depending on the arguments. A use case is as follows:
from os import environ
from typing import Dict, Optional, TypeVar, Union, overload
from your_api.types.string.to_boolean import string_to_boolean
DefaultT = TypeVar("DefaultT", str, bool, int, float)
@overload
def get_typed_environ_value(key: str) -> Optional[str]: ...
@overload
def get_typed_environ_value(key: str, default: str) -> str: ...
@overload
def get_typed_environ_value(key: str, default: bool) -> bool: ...
@overload
def get_typed_environ_value(key: str, default: int) -> int: ...
@overload
def get_typed_environ_value(key: str, default: float) -> float: ...
def get_typed_environ_value(
key: str,
default: Optional[DefaultT] = None,
) -> Optional[Union[str, bool, int, float]]:
if default is None:
return environ.get(key)
value = environ.get(key, str(default))
if isinstance(default, str):
return value
elif isinstance(default, bool):
return string_to_boolean(value)
elif isinstance(default, int):
return int(value)
elif isinstance(default, float):
return float(value)
else:
raise TypeError(f"Unsupported default type: {type(default).__name__}")
get_args
Retrieves the arguments inside a type. (Personally) I use it to get the list of literal arguments.
TimedRotatingWhenLiteral = Literal[
"S", "M", "H", "D", "W0", "W1", "W2", "W3", "W4", "W5", "W6", "midnight"
] # W0=Monday
TIMED_ROTATING_WHEN: Final[Sequence[str]] = get_args(TimedRotatingWhenLiteral)
def my_func(when: str):
assert when in TIMED_ROTATING_WHEN
You can also check the list of classes in a Union.
from enum import IntEnum
from typing import Union, get_args
class Test1(IntEnum):
A = 1
B = 2
class Test2(IntEnum):
C = 3
D = 4
AnyTest = Union[Test1, Test2)
print(get_args(AnyTest)) # -> (<enum 'Test1'>, <enum 'Test2'>)
ABC vs Prototype
In short, if the inheritance structure is already in place and hard to control, use Prototype; if you can design from scratch, use ABC
Checking inherited class types
class User: ...
class BasicUser(User): ...
class ProUser(User): ...
class TeamUser(User): ...
# Accepts User, BasicUser, ProUser, TeamUser, ...
def make_new_user(user_class: Type[User]) -> User:
# ...
return user_class()
Checking a function's parameter types
- Stackoverflow - Python: Get type of
typing.List - Stackoverflow - How to get class variables and type hints?
import typing
def f(x: typing.List[MyType]):
...
print(f.__annotations__["x"].__args__[0]) # prints <class '__main__.MyType'>
print(f.__annotations__["x"].__base__) # prints typing.List[float]
print(f.__annotations__["x"].__orig_bases__[0]) # prints <class 'list'>
Using get_type_hints is better.
from typing import get_type_hints
class Person:
name: str
age: int
get_type_hints(Person)
# returns {'name': <class 'str'>, 'age': <class 'int'>}
| Latest update |
| Using the signature function of the inspect module is recommended. |
Troubleshooting
Invariant type variable
cvp/patterns/proxies/callable_proxy.py:10:1: error: Invariant type variable
"ValueT" used in protocol where covariant one is expected [misc]
class ReadableCallableProtocol(Protocol[ValueT]):
Literally translated:
This occurs because an invariant type variable was used where a covariant one is required. See #Covariance (Covariant) for details.
Simply use TypeVar(..., covariant=True).
error: Type variable "..." is unbound [valid-type]
mypy prints the following:
cvp/wsdl/schema.py: note: In class "XsdSchema":
cvp/wsdl/schema.py:21:19: error: Type variable "cvp.wsdl.schema._UrlKey" is unbound [valid-type]
_loaded: Dict[_UrlKey, _EtreeElement]
^
cvp/wsdl/schema.py:21:19: note: (Hint: Use "Generic[_UrlKey]" or "Protocol[_UrlKey]" base class to bind "_UrlKey" inside a class)
cvp/wsdl/schema.py:21:19: note: (Hint: Use "_UrlKey" in function signature to bind "_UrlKey" inside a function)
Looking at _UrlKey, where the error occurred, it is as follows:
Fix it as follows:
See also
Favorite site
- typing — Support for type hints — Python 3.8.5 documentation
- (Python) Type annotations with the typing module
- phi.log - (Clean code) type hint - 1