El tipado hace referencia a la anotación de los valores y estructuras en un lenguaje de programación con el objetivo de determinar las operaciones que pueden efectuarse sobre un valor. En el tipado estático, los tipos los asigna el programador y las operaciones se chequean "en tiempo de compilación". En un lenguaje con tipado dinámico el tipo se asigna "en tiempo de ejecución" por el programa y las operaciones se chequean durante la ejecución del programa.
Como habrás podido comprobar, Python es un lenguaje dinámicamente tipado.
Existen diferentes ventajas asociadas a cada estilo de tipado y también existe una acalorada discusión acerca de qué aproximación es mejor. Numerosas fuentes en Internet ponen empeño en defender una u otra postura:
- Nación Lumpen sobre tipado dinámico frente a tipado estático.
- Static vs Dynamic Typing by Salva de la Puente.
- Type wars by Robert C. Martin.
- Intrinsic and extrinsic views of typing.
- Church vs Curry Types.
En este curso utilizaremos el tipado estático como una herramienta de diseño opcional. Algo que es posible gracias al "tipado progresivo".
El tipado progresivo es una forma de tipado híbrida en la que algunos valores son anotados y estáticamente tipados, antes de la ejecución, mientras que otros son anotados dinámicamente y comprobados en tiempo de ejecución.
El tipado progresivo nos permite evolucionar la rigidez de nuestras interfaces, progresivamente, a lo largo del proceso de diseño e implementación del software.
- What is Gradual Typing by Jeremy Siek.
- Gradual Typing for Functional Languages paper.
Python 3.0 implementa el PEP-3107 que permite anotar el valor de retorno de una función y sus argumentos con una expresión arbitraria:
def add(a: int, b: int) -> int:
return a + b
Las anotaciones de la función están disponibles a través de la propiedad
__annotations__
de la función:
add.__annotations__
El PEP-484 añade el concepto de "pista de tipado" o type hint e introduce los "comentarios de tipo" (type commentaries) para anotar variables:
class Point2D:
def __init__(self, x: int, y: int):
self.x = x
self.y = y
origin = Point2D(0, 0) # type: Point2D
En Python 3.6 se añadió la capacidad de anotar variables de una clase, función o módulo:
class Point2D:
version: str = 'v1.0'
def __init__(self, x: int, y: int):
self.x = x
self.y = y
origin: Point2D = Point2D(0, 0)
Sin embargo, los comentarios de tipo siguen siendo la única opción en algunas situaciónes:
name, age = 'Salva', 33 # type: str, int
Las anotaciones de una clase están disponibles a través de:
Point2D.__annotations__
Las anotaciones del módulo están disponibles a través de:
__annotations__
Se pueden obtener las anotaciones de un módulo, clase, método o función con
la función typing.get_type_hints
.
from typing import get_type_hints
get_type_hints(Point2D)
get_type_hints(Point2D.__init__)
Mypy no viene con Python, no es parte de la biblioteca estándar y no corre
automáticamente cuando lanzamos un proyecto. Mypy es un software aparte, que
debes instalar con pip
. Asegúrate de que lo instalas en el entorno virtual
de tu proyecto:
$ pip install mypy
También puedes instalar las extensiones a tipos para acceder a las últimas características de mypy:
$ pip install typing-extensions
Ahora puedes correr mypy sobre un fichero con:
$ mypy modulo.py
La ejecución de un módulo con Python nunca ejecutará mypy automáticamente:
$ python modulo.py
El comando anterior ejecutará modulo.py
, sea su contendio erróneo o no para
mypy.
PyCharm ya incluye un razonador de tipos que escanea tu código conforme escribes. Sin embargo, nosotros vamos a usar mypy así que, para esta lección, instala el plugin "mypy" de PyCharm y desactiva el comprobador de tipos (type checker) que viene de serie.
Con este plugin, los errores se señalarán en el editor. Como esta vez no estarás usando una consola interactiva, no podrás redefinir las mismas cosas dos veces porque mypy se quejará de que lo estás haciendo.
Por ello, los ejemplos de este tema son autocontenidos, y contienen todos
los import
necesarios, de forma que puedas reemplazar completamente
el contenido de tu fichero con cada uno de ellos.
- Lee la documentación acerca de cómo ejecutar mypy para más opciones.
Todos los razonadores tienen como fin conservar la coherencia de los programas observando que las operaciones se usan sobre los tipos correctos. Un tipo correcto es el tipo declarado en la anotación o cualquiera compatible. La regla de compatibilidad es la herencia basada en el comportamiento que has estudiado. La misma de la que hablaba Liskov y que se recoge en los principios SOLID.
Fíjate que mypy asume que tus jerarquías de herencia siguen esta definición.
Python no se pensó para utilizarse con un razonador de tipos y multitud de
software de terceros no está anotado. Afortunadamente, mypy viene con
una copia de typeshed
, una colección
de anotaciones para el software más popular.
La colección typeshed
no contiene una copia anotada de todo el software
sino que provee de
ficheros "esqueleto" o stubs
que proporcionan, únicamente, las anotaciones faltantes.
Mypy considera que una función sin anotaciones puede aceptar cualquier tipo de parámetros y devolver cualquier tipo de retorno.
-
Una función sin anotaciones está tipada dinámicamente para mypy:
def id(obj): return obj
-
El tipo
typing.Any
es el equivalente explícito:from typing import Any def wrap(obj: Any) -> Any: return [obj]
-
No te impongas el deber de anotar completamente una función. Utiliza
Any
para los parámetros o tipos de retorno que no tengas muy claros.class Shape: ... def draw_shape(shape: Shape, config: Any = None) -> Any: ...
La gracia de usar tipado progresivo es la de añadir anotaciones conforme tengas necesidad.
Una forma de expresar que "no se devuelve nada" es utilizando el tipo None
:
-
Esta función no devuelve nada:
def draw_shape(shape: Shape, config: Any = None) -> None: ...
-
Como no devuelve nada, no podemos usarla en una asignación:
shape = Shape() result = draw_shape(shape) # not OK
Ojo, este código ejecuta sin problema en Python. Lo único que ocurre es que mypy se queja.
Comencemos con una exploración de cómo se anotan los tipos más sencillos de Python:
-
Los números se anotan utilizando sus tipos incluidos por defecto en Python:
x: int = 1 c: complex = 1j f: float = 3.5
-
Sorprendentemente, esto funciona:
def add(x: float, y: float) -> float: return x + y add(1, 1)
Pese a que:
assert not isinstance(1, float)
El PEP-484 tiene una excepción para la torre numérica y no require que los tipos numéricos estén enlazados en una jerarquía.
-
Las cadenas de texto y de bytes se anotan con
str
ybytes
:some_chars: str = 'I am Ziltoid' some_bytes: bytes = b'I am Ziltoid'
Existe el tipo
Text
, que es un alias destr
en Python 3 y deunicode
en Python 2, para hacer más claro que la variable contendrá texto Unicode:from typing import Text some_text: Text = b'I am Ziltoid' # not OK
-
mypy permite, incluso, utilizar algunos valores como tipos, con
typing_extensions.Literal
. Por ejemplo:from typing_extensions import Literal def get_rgb_value(colorname: Literal['red', 'green', 'blue']): ... get_rgb_value('pink') # not OK
-
Listas, tuplas, diccionarios y conjuntos pueden anotarse con
list
,tuple
,dict
yset
pero se prefiere el uso de los tipos en el módulotyping
:from typing import List, Tuple, Dict, Set l: List = [1, 2, 3, 4] t: Tuple = ('red', 13) d: Dict = {'red': 13} s: Set = {1j}
-
En este caso,
List
,Dict
ySet
representan implementaciones de una secuencia y un mapa mutables. Sus versiones abstractas serían:from typing import MutableSequence, MutableMapping, MutableSet mseq: MutableSequence = [1, 2, 3, 4] mmap: MutableMapping = {'red': 13} mset: MutableSet = {1j}
-
Al tipar contenedores, conviene considerar la mutabilidad del contenedor. Las versiones inmutables de estos tipos son:
from typing import Sequence, Mapping, Set seq: Sequence = [1, 2, 3, 4] map_: Mapping = {'red': 13} set_: Set = {1j}
¿Es una lista una secuencia inmutable? ¿Por qué?
¿Por qué el siguiente código no es coherente para mypy?
map_['blue'] = 9 # not OK seq.append(4) # not OK
-
Los iterable se tipan con
Iterable
:from typing import Iterable iterable: Iterable = 'ABCDEFG' for item in iterable: print(item)
Se dice que los contenedores y las funciones son "genéricos" porque pueden actuar sobre otros tipos arbitrarios. Podemos realizar anotaciones más precisas mediante el uso de la notación índice:
-
Las tuplas pueden declarar el tipo de sus elementos:
from typing import Tuple name_and_age: Tuple[str, int] = ('Salva', 33)
-
Por cierto, si quieres usar una tupla con nombre (
namedtuple
), puedes usar el contructor de tipos que viene contyping
:from typing import NamedTuple Profile = NamedTuple('Profile', (('name', str), ('age', int))) salva_info = Profile('Salva', age=33)
O, a partir de Python 3.6, su vertiente como clase base:
from typing import NamedTuple class Profile(NamedTuple): name: str age: int salva_info = Profile('Salva', age=33)
-
Para indicar una tupla con un número indifinido de valores (incluyendo ningún valor):
from typing import Tuple def do_max(l: Tuple[int, ...]) -> int: return max(l) do_max((1, 2)) do_max((1, 2, 3)) do_max(tuple()) # OK but raise ValueError at runtime
-
El resto de contenedores tiene un comportamiento similar:
from typing import List, Mapping, Set numbers: List[int] = [1, 2, 3] ratings: Mapping[str, float] = {'Good Omens': 8.5, 'Game of Thrones': 8.0} complex_set: Set[complex] = {1, 1j}
Fíjate que los tipos para contenedores importados del módulo
typing
se parece mucho a aquellos encollections.abc
. Sin embargo, los últimos no admiten la notación índice:from typing import Set from collections import abc Set[str] # OK abc.Set[str] # raises TypeError
Acuérdate de usar los del módulo
typing
para anotar y los del módulocollections.abc
para implementar la funcionalidad. -
Es común utilizar diccionarios como estructuras de datos ligeras así que el módulo
mypy_extensions
contiene la claseTypedDict
que, de manera similar aNamedTuple
permite dotar de cierta estructura a los diccionarios:from mypy_extensions import TypedDict Identification = TypedDict('Identification', { 'name': str, 'id': int }) ziltoid_id: Identification = {'name': 'Ziltoid'} # not OK
Para hacer los campos no obligatorios, tendrías hacer:
from mypy_extensions import TypedDict Identification = TypedDict('Identification', { 'name': str, 'id': int }, total=False) ziltoid_id: Identification = {'name': 'Ziltoid'} # OK
Usando la notación de clase, a partir de Python 3.6:
from mypy_extensions import TypedDict class Identification(TypedDict, total=False): name: str id: int marvin_id = Identification(id=123456) # OK
¿Cuál es el tipo, en tiempo de ejecución, de
marvin_id
?- Con la notación de clase, puedes utilizar la herencia para que parte de los campos sean obligatorios y otros no.
-
Las funciones tienen tipo
Callable
:from typing import Callable def apply_binary( op: Callable[[int, int], int], a: int, b: int) -> int: return op(a, b) def add(a: int, b: int) -> int: return a + b def sub(a: int, b: int) -> int: return a - b def neg(a: int) -> int: return -a apply_binary(add, 5, 10) # OK apply_binary(sub, 5, 10) # OK apply_binary(neg, 5, 10) # not OK
¿Por qué falla la última expresión?
-
Un generador, como viste, devuelve un iterable así que su tipo de retorno es
Iterable
:from typing import Iterable def perfect_squares(start: int = 0) -> Iterable[int]: current = start while True: yield current ** 2 current += 1
-
Recuerda la signatura de
apply_binary
de hace un par de ejemplos:from typing import Callable def apply_binary( op: Callable[[int, int], int], a: int, b: int) -> int: return op(a, b)
-
Realmente, no hace falta restringir las operaciones binarias sobre
int
. Lo único que necesitas es que el tipo de los parámetrosa
yb
sean los que aceptaop
; y que el tipo de retorno sea el que devuelveop
. Para ello podemos declarar variables de tipo:from typing import TypeVar, Callable, Tuple A = TypeVar('A') B = TypeVar('B') C = TypeVar('C') def apply_binary( op: Callable[[A, B], C], a: A, b: B) -> C: return op(a, b) def make_point(a: complex, b: complex) -> Tuple[complex, complex]: return a, b apply_binary(make_point, 1, 1j) # OK
Cuando usemos
TypeVar
es obligatorio que el primer parámetro sea una cadena cuyo valor sea el nombre de la variable donde estemos asignando. -
Las variables de tipo, en principio, pueden adoptar cualquier valor aunque podemos restringirlo. Por ejemplo, salvo algunas excepciones, las operaciones sobre cadenas de texto funcionan sobre cadenas de bytes. Es normal querer decir algo como:
from typing import TypeVar A = TypeVar('A') def concat(a: A, b: A) -> A: return a + b
Donde
A
es ostr
obytes
. Podemos restringirA
pasando los tipos entre los que puede elegir:from typing import TypeVar StrOrBytes = TypeVar('StrOrBytes', str, bytes) def concat(a: StrOrBytes, b: StrOrBytes) -> StrOrBytes: return a + b
Esta construción sobre cadenas es tan común, que el módulo
typing
la proporciona con el nombreAnyStr
.from typing import AnyStr def concat(a: AnyStr, b: AnyStr) -> AnyStr: return a + b
-
Por último, puedes definir tipos genéricos mediante
typing.Generic
:from typing import Generic, TypeVar P = TypeVar('P') class Point2D(Generic[P]): def __init__(self, x: P, y: P): self.x = x self.y = y complex_point: Point2D[complex] = Point2D(0j, 0j) # OK integer_point: Point2D[int] = Point2D(0j, 0j) # not OK
- Documentación sobre genéricos*
-
Considera este tipo para matrices:
from typing import Sequence matrix: Sequence[Sequence[complex]] = [[1, 0], [0, 1]]
-
Resulta algo engorroso de escribir. Por ejemplo en la función:
from typing import Sequence def add( ma: Sequence[Sequence[complex]], mb: Sequence[Sequence[complex]], target: Sequence[Sequence[complex]] ) -> Sequence[Sequence[complex]]: """Add ma and mb and leaves the result in target.""" return ma + mb
-
En estos casos es mejor definir un alias o sobrenombre:
from typing import Sequence Matrix = Sequence[Sequence[complex]] def add(ma: Matrix, mb: Matrix, target: Matrix) -> Matrix: ... return target
Es relativamente normal que queramos expresar que algo puede ser de uno u otro tipo.
-
Por ejemplo para expresar una lista heterogénea de elementos de tipo cadena o entero.
array = [1, 'a', 2, 'b', 3, 'c']
Utilizamos
Union
para expresar la unión de varios tipos:from typing import List, Union array: List[Union[str, int]] = [1, 'a', 2, 'b', 3, 'c']
-
La unión también se utiliza para poder asignar
None
. A veces queremos poder expresar "la ausencia de un valor".from typing import TypeVar, Callable, Iterable, Union T = TypeVar('T') def find_if(condition: Callable[[T], bool], haystack: Iterable[T]) -> Union[T, None]: """Return the first item that satisfies the condition or `None`.""" for item in haystack: if condition(item): return item return None def is_perfect_square(n: float) -> bool: int_root = int(n ** 0.5) return int_root ** 2 == n assert find_if(is_perfect_square, [6, 7, 8]) is None
-
Esta construcción
Union[T, None]
es tan común, que mypy nos daOptional[T]
para expresar lo mismo:from typing import Optional, Callable, Iterable, TypeVar T = TypeVar('T') def find_if(condition: Callable[[T], bool], haystack: Iterable[T]) -> Optional[T]: """Return the first item that satisfies the condition or `None`.""" for item in haystack: if condition(item): return item return None def is_perfect_square(n: float) -> bool: int_root = int(n ** 0.5) return int_root ** 2 == n nullable_int: Optional[int] = None # now OK if ...: nullable_int = find_if(is_perfect_square, [6, 7, 8, 9]) # also OK
-
Las clases definidas por el usuario son tipos y por tanto se pueden usar para anotar funciones y variables. Considera la siguiente clase:
class Point2D: ...
-
Puedes utilizar
Point2D
como cualquier otro tipo:class Point2D: ... def magnitude(p: Point2D) -> float: ...
-
Cuando se trabaja con herencia, las clases derivadas pueden reescribir los métodos de las clases base:
class MyMap: def get(item: Any) -> None: ... class FastMap(MyMap): def get(item: Any, cache: Any) -> None: ...
mypy decidirá que esto es un error porque las signaturas de
get
en las clases base y derivada no son complatibles. -
Una clase puede especificar varias signaturas para un método, gracias al decorador
overload
:from typing import Optional, overload class Point2D: @overload def __init__(self): ... @overload def __init__(self, x: complex): ... def __init__(self, x: Optional[complex] = None, y: Optional[complex] = None): self.x: complex self.y: complex if x is None and y is None: self.x, self.y = 0j, 0j if x is not None and y is None: self.x, self.y = x, x if x is not None and y is not None: self.x, self.y = x, y
Observa los múltiples
if
del ejemplo anterior. El razonador de mypy no ejecuta código pero es capaz de extraer información de las condiciones y entender qué variables pueden y no pueden serNone
en cada condicional. También funciona conisinstance
. -
Con mypy podemos anotar los miembros de la instancia, tanto en el cuerpo de la clase como en el método
__init__
. -
Podemos marcar una propiedad para que pertenezca a la clase sólamente con
typing.ClassVar
:from typing import ClassVar, Optional, overload class Point2D: version: ClassVar[str] = '1.0.0' @overload def __init__(self): ... @overload def __init__(self, x: complex): ... def __init__(self, x: Optional[complex] = None, y: Optional[complex] = None): self.x: complex self.y: complex if x is None and y is None: self.x, self.y = 0j, 0j if x is not None and y is None: self.x, self.y = x, x if x is not None and y is not None: self.x, self.y = x, y p = Point2D() p.version = '' # not OK
-
Si además quieres prohibir que se sobre-escriba, anota la propiedad con
typing_extensions.Final
:from typing import ClassVar, Optional, overload from typing_extensions import Final class Point2D: version: Final[ClassVar[str]] = '1.0.0' @overload def __init__(self): ... @overload def __init__(self, x: complex): ... def __init__(self, x: Optional[complex] = None, y: Optional[complex] = None): self.x: complex self.y: complex if x is None and y is None: self.x, self.y = 0j, 0j if x is not None and y is None: self.x, self.y = x, x if x is not None and y is not None: self.x, self.y = x, y p = Point2D() Point2D.version = '5.0.0' # not OK
Puedes anotar métodos con el decorador
typing_extensions.final
para evitar que se sobreescriban o anotar la clase entera para evitar que se puedan crear clases derivadas de ella.- Más, acerca de
Final
en la documentación.
- Más, acerca de
-
mypy también puede representar el tipo de una clase con
typing.Type
:from typing import TypeVar, Type class Profile: ... class RichProfile(Profile): ... TProfile = TypeVar('TProfile', bound=Profile) def new_profile(profile_cls: Type[TProfile]) -> TProfile: return profile_cls() new_profile(Profile) # OK new_profile(RichProfile) # OK new_profile(object) # not OK
El parámetro
bound
deTypeVar
permite establecer un límite superior en la jerarquía de herencia.- El tipo de las clases, en la documentación.
Hasta ahora has estudiado lo que se llama especialización nominal (nominal subtyping). Una técnica que consiste en declarar qué está heredando de qué, explícitamente, la declaración explícita de las clases base. El [PEP-544](Simulación de tipos, y protocolos) formaliza el concepto de protocolo, que viste en la lección Simulación de tipos, y protocolos, y el concepto de duck-typing, nombrado a lo largo del curso en numerosas ocasiones. A este tipo de herencia, se la denomina especialización estructural (structural subtyping).
-
Observa cómo mypy no se queja con el siguiente código:
from typing import Iterable def print_all(collection: Iterable): for item in collection: print(item) class Rgb: def __iter__(self): yield 'red' yield 'green' yield 'blue' print_all(Rgb())
Esto es así porque
Iterable
es un protocolo y, por tanto, mypy busca la existencia de los métodos declarados en el protocolo y nada más. -
Para declarar nuestros propios protocolos:
from typing import Any from typing_extensions import Protocol class SupportsMod(Protocol): def __mod__(self, other: Any) -> Any: ... class AlwaysOdd: def __mod__(self, other: int) -> int: return 1 def is_even(something: SupportsMod) -> Any: return something % 2 == 0 is_even('%s') # OK! is_even(2) # OK! is_even(AlwaysOdd()) # OK! is_even({}) # not OK!
Si no marcamos con
Protocol
la clase que define el protocolo, mypy no comprobará la especialización estructural y esperaría especialización nominal, con clases heredando explícitamente deSupportsMod
.
- Para un mayor control sobre la signatura de las funciones, echa un vistazo a los protocolos de llamadas.
La herencia de comportamiento establece ciertas restricciones cuando se
componen tipos. Por ejemplo, considera la clase Point2D
genérica:
from typing import Generic, TypeVar
P = TypeVar('P')
class Point2D(Generic[P]):
def __init__(self, x: P, y: P):
self.x = x
self.y = y
Sabiendo que int
es un subtipo de complex
, ¿podrías decir que
Point2D[int]
es subtipo de Point2D[complex]
?
Si es así, diríamos que Point2D
es covariante con su tipo genérico
(porque varía igual que él). La realidad es que también podría ser
contravariante si la relación de herencia se diera al revés o
invariante si no se pudiera afirmar que una hereda de otra.
mypy no es capaz de calcular la varianza, sino que esta se debe indicar
explícitamente. En particular, para este ejemplo, Point2D
es, en efecto,
covariante, como demuestra:
from typing import Generic, TypeVar
P = TypeVar('P')
class Point2D(Generic[P]):
def __init__(self, x: P, y: P):
self.x = x
self.y = y
def conjugated_point(point: Point2D[complex]) -> Point2D[complex]:
return Point2D(point.x.conjugate(), point.y.conjugate())
conjugated_point(Point2D(0, 0))
-
En general, para cualquier razonador de tipos moderno, las colecciones inmutables son covariantes:
from typing import Sequence class Employee: def do_work(self): print('Working...') class Manager(Employee): def do_management(self): print('Managing stuff...') def team_size(self): return 10 def get_last_employee(collection: Sequence[Employee]) -> Employee: return collection[-1] get_last_employee((Manager(), Manager()))
-
Por otro lado, las colecciones mutables son invariantes:
from typing import MutableSequence, List class Employee: def do_work(self): print('Working...') class Manager(Employee): def do_management(self): print('Managing stuff...') def team_size(self): return 10 def add_employee(collection: MutableSequence[Employee]): collection.append(Employee()) managers: List[Manager] = [Manager(), Manager()] add_employee(managers) # Unsafe! managers[-1].do_management()
Si mypy permitiera esta operación, la función
add_employee
cambiaría el tipo declarado demanagers
deList[Manager]
aList[Union[Manager, Employee]]
. -
Por último, los invocables son covariantes respecto a aquello que devuelven:
from typing import MutableSequence, Callable class Employee: def do_work(self): print('Working...') class Manager(Employee): def do_management(self): print('Managing stuff...') def team_size(self): return 10 def add_employee(collection: MutableSequence[Employee]): collection.append(Employee()) def hire_employee(): print('Hiring Alice') def hire_manager(): print('Hiring manager Mary') def hire(procedure: Callable[[], Employee]) -> Employee: return procedure() hire(hire_employee) # OK hire(hire_manager) # also OK
Al fin y al cabo, la llamada a una función puede reemplazarse por su valor de retorno por lo que podría decirse que el tipo de una función puede reemplazarse por el tipo del valor de retorno donde operan las reglas de herencia simples, sea nominal o estructural.
-
Sin embargo, los invocables son contravariantes con sus argumentos:
from typing import Callable class Employee: def do_work(self): print('Working...') class Manager(Employee): def do_management(self): print('Managing stuff...') def team_size(self): return 10 def employee_salary(person: Employee) -> float: return 2000 def manager_salary(person: Manager) -> float: return 2000 + 100 * person.team_size() def pay(payment_calculation: Callable[[Employee], float], person: Employee): print(f'Paying {payment_calculation(person)}') pay(employee_salary, Employee()) # OK pay(manager_salary, Employee()) # unsafe!
Si mypy permitiera este comportamiento, la función
manager_salary
, que espera unManager
podría utilizar algún método que solo estuviera enManager
sobre unEmployee
.Sin embargo, si hubiéramos definido
pay
como:from typing import Callable class Employee: def do_work(self): print('Working...') class Manager(Employee): def do_management(self): print('Managing stuff...') def team_size(self): return 10 def employee_salary(person: Employee) -> float: return 2000 def manager_salary(person: Manager) -> float: return 2000 + 100 * person.team_size() def pay(payment_calculation: Callable[[Manager], float], person: Manager): print(f'Paying {payment_calculation(person)}') pay(manager_salary, Manager()) # OK pay(employee_salary, Manager()) # OK
Fíjate en que
Manager
es subclase deEmployee
pero esCallable[[Employee], float]
la que es subclase deCallable[Manager], float
.- Documentación acerca de la varianza.