https://sciware.flatironinstitute.org/24_Types
https://github.com/flatironinstitute/learn-sciware-dev/tree/main/24_Types
Activities where participants all actively work to foster an environment which encourages participation across experience levels, coding language fluency, technology choices*, and scientific disciplines.
*though sometimes we try to expand your options
- Avoid discussions between a few people on a narrow topic
- Provide time for people who haven't spoken to speak/ask questions
- Provide time for experts to share wisdom and discuss
- Work together to make discussions accessible to novices
- Dedicated Zoom moderator to field questions.
- Please stay muted if not speaking. (Host may mute you.)
- We are recording. Link will be posted to https://sciware.flatironinstitute.org/
- Planning for the next few months:
- Modern C++ (Oct 13?)
- File formats, data management, hdf5 (Nov)
- Suggest topics or contribute to content in #sciware Slack
- Numeric types, performance
- Thinking about types: concepts, syntax
- Types in python: mypy
When do you need to worry about types of numeric data in your code?
Lehman Garrison (SCC, formerly CCA)
- In scientific computing, we often deal with numeric data: floats and ints
- Don't always need to think about how these are represented by the computer
- Often, the computer just does the Right Thing
- But sometimes it doesn't...
- Broadly two categories of when you might worry about types of numeric data
- Performance
- Correctness
- Consider an array of 32-bit floats vs 64-bit floats:
a32 = np.ones(10**9, dtype=np.float32)
a64 = np.ones(10**9, dtype=np.float64)
- Each 32-bit float takes up 4 bytes, and 64-bit 8 bytes
- 32-bit has
${\sim}10^{-7}$ fractional precision; 64-bit has${\sim}10^{-16}$ - Total sizes are 4 GB and 8 GB:
>>> print(a32.nbytes/1e9)
4.0
>>> print(a64.nbytes/1e9)
8.0
- How long to multiply each array by a scalar?
>>> %timeit 2*a32
1.1 s ± 2.09 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
>>> %timeit 2*a64
2.2 s ± 2.84 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
- For simple arithmetic operations, memory bandwidth usually limits the performance
- As opposed to, e.g., CPU speed
- Using a narrower dtype can be an easy 2x performance gain
- What about a more complex operation?
>>> %timeit np.sin(a32)
1.5 s ± 4.77 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
>>> %timeit np.sin(a64)
9.12 s ± 19.1 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
- Using 32-bit was 6x faster in this case
- Factor 2x is often a good first estimate of potential speedup by halving the type width
- Speedup might be more, might be less
- Generally the relationship between type and performance is complex (see next slide)
- Processors come with (short) vector units: AVX2, AVX512, ...
- A single processor operation is multiple math operations!
- CPU Performance: single (fp32) vs double (fp64) precision
- GROMACS: hybrid (fp32+fp64) vs fp64: +40%
- GPUs specs show how type selection affects performance
- LAMMPS+GPU: on single node, fp32 vs fp64: +28%
- Why not use 32-bit floats all the time?
- When you need the accuracy of 64-bit
>>> np.float32(10**8 + 1) - np.float32(10**8)
0.0 # where did the 1 go?
>>> np.sin(np.float32(np.pi))
-8.742278e-08 # not bad
>>> np.sin(np.float32(np.pi * 100000))
-0.015358375 # way off!
>>> np.sin(np.float64(np.pi * 100000))
-3.3960653996302193e-11 # much better (*)
- * 64-bit works better here, but it's still good practice to ensure the arguments to trig functions are small
- Use
arr.dtype
to check the types of Numpy arrays
>>> print(a32.dtype)
float32
>>> print((np.float64(2.)*a32).dtype)
float32
>>> print((a64*a32).dtype)
float64
- Use
arr.astype(dtype)
to forcibly cast an array to the type you want - See Numpy doc on automatic type conversion rules
- Use Python's builtin
type()
to check the result type of operations with nativefloat
andint
- Using smaller types for numeric data can speed up your code
- Most common conversion is to use
float32
instead offloat64
- Most common conversion is to use
- But smaller types can result in less accurate calculations
- Important to have tests in place before modifying your code's types!
- Formal numerical stability analysis may help you understand how your application will behave too (look for a session at FWAM)
- IEEE 754: the standard bit representation of floats
- Roofline model for understanding performance
- Intel Intrinsics Guide for looking up the CPU cycle cost of instructions
- Numerical stability article on Wikipedia
- Numpy doc on automatic type conversion rules
- Kahan summation for reducing numerical error in sums of many floats
- Not type theory (a branch of mathematics involving propositional logic and category theory)
- Abstract tools to approach coding a bit differently
- "algebraic data types"
- Fun with math, set operations, combinatorics
- In calculations, units can often be used to find mistakes: \(\frac{\texttt{mass}}{\texttt{time}^2} \ne \texttt{force} \)
- Distinguishing different types of data (e.g., input, output) can help automatically detect coding mistakes
def process(x0):
x1 = step(x0)
return x0
- So far we've been talking about storage and performance, bits:
float
,double
,int32
,string
complex
,struct
,class
,list
(of what?)
- Types are really about the values these bits represent
- Useful for thinking abstractly about your data (not the algorithm or implementation)
So what is a type?
Think of a type as representing a set of possible values:
By saying \( x \) has type \( T \) we mean $$ x \in T $$
- \( \left|T\right| \) is the number of possible values in \( T \) (the cardinality)
- Practically, cardinality is always finite (computers have finite memory)
- We may define types with infinite cardinality, but always countably infinite!
No need to limit yourself to established types!
- Many languages represent "finite" data types with labeled values as enumerations
A couple simple types may seem silly but are quite useful:
Unit
is the singleton type with only one possible value (None
in python,Nothing
in Julia)Void
is the empty type with no possible values (impossible, a value that can never exist, the return value of a function that never returns)Never
,void
in C?
Documentation, optimization, error checking, logic!
def compute(order ∈ {1,2,3}):
if order == 1: ...
elif order == 2: ...
else: ... # order == 3
#if order == 0 (ERROR?)
- Can be helpful for describing and thinking about code even if the types are not perfectly represented in the programming language
- Once a variable has a type, its value must be in that type
Different languages use a variety of syntax to represent types
\( x \in T \) | languages |
---|---|
x: T , x: int |
Python, TypeScript |
x :: T , x::Int |
Haskell, Julia |
T x , int x |
C, C++, Fortran 77 |
T :: x , integer :: x |
Fortran 90 |
Sometimes we want to allow different types of values, so we make a new type by combining other types with a union:
- Set unions are not particularly useful, as they can usually be represented by a different type
- C
union
- C
- Instead...
Just like a union, but keeps all values (not just distinct)
- Sometimes called a "tagged" union because values are tagged by which type they're from
- "discriminated": each value is either from
T
orS
(and you can tell)
- Types can have parameters (arguments) of other types
- \(+\) is an operator (function) that builds existing types into a new one: \( T+S = \texttt{Union}(T,S) \)
- Syntax for type parameters (and unions):
\( T + S \) | language |
---|---|
Union[T,S] |
Python |
T|S (Union<T,S> ) |
TypeScript |
Union{T,S} |
Julia |
variant<T, S> |
C++ |
Either T S |
Haskell |
- Adding \( \texttt{Unit} \) to a type is often useful
- Provides a "missing" option (
NULL
,None
,nothing
,NA
) - Often has a special name:
Optional[T] = Union[T,None]
(Python)optional<T>
(C++)Maybe T
(Haskell)
- Unions can only have one value, one type OR the other
- Products allow one value from each type (AND)
- Represents every possible combination of two types (cross product, outer product)
- Often represented by pairs or tuples:
(T,S)
,T*S
,Tuple[T,S]
,pair<T,S>
(1.2, 3.1, 5) : Tuple[Float,Float,Int]
- Larger tuples with labeled fields can be "structs" or "records"
Union{}
(Julia)
- \( T \)
- \( \texttt{Void} \)
- \( \texttt{Unit} \)
- none of the above
- \( T \)
- \( \texttt{Void} \)
- \( \texttt{Unit} \)
- none of the above
- \( T \)
- \( \texttt{Void} \)
- \( \texttt{Unit} \)
- none of the above
- \( T \) \( = T \cup T \)
- \( \texttt{Void} \)
- \( \texttt{Unit} \)
- none of the above
- Fixed-length arrays are equivalent to tuples: $$ \texttt{Array}_n(T) = \prod_{i=1}^n T = T^n \qquad \left|T^n\right| = \left|T\right|^n \\ (x_1, x_2, \ldots, x_n) \in T^n \qquad x_i \in T \\ T^0 \cong \texttt{Unit} $$
- (Reminder: focus on possible values, not representation)
- Variable-length arrays can be thought of in a couple (equivalent) ways: $$ \begin{align} \texttt{Array}(T) &= \sum_{n=0}^\infty T^n = \texttt{Unit} + T + T^2 + \cdots \\ \texttt{List}(T) &= \texttt{Unit} + (T \times \texttt{List}(T)) \\ \texttt{Array}(\texttt{Bool}) &= \{(), (\mathsf{F}), (\mathsf{T}), (\mathsf{F},\mathsf{F}), (\mathsf{T},\mathsf{F}), \dots \} \end{align} $$
- Infinite type
- By restricting \( \sum_{n=a}^b T^n \) we can represent arrays of certain lengths (non-empty, at most 10, etc.)
\( \texttt{Array}_{[n]}(T) \) | language |
---|---|
List[T] |
Python |
Array{T} |
Julia |
Array<T> , T[] |
TypeScript |
[T] |
Haskell |
list<T> , vector<T> |
C++ |
T v[n] |
C |
v(n) , DIMENSION |
Fortran |
- \( \mathbb{N} \)
- \( \texttt{Void} \)
- \( \texttt{Unit} \)
- none of the above
- \( \mathbb{N} \)
- \( \texttt{Void} \)
- \( \texttt{Unit} \)
- none of the above
Exercise: Strings?
- Some languages have an "any" type representing union of all possible types, containing all possible values
Any
,void *
Math has the concept of functions mapping $$ f(x) = x^2 \\ f : \mathbb{R} \to \mathbb{R} $$ Same idea here: $$ f \in T \to S \\ x \in T \implies f(x) \in S \\ g(x) = \text{if } x \text{ then } 1 \text{ else } 2 \\ g \in \texttt{Bool} \to \texttt{Int} $$
\( T \to S \) | \( \texttt{Any} \to \texttt{Any} \) | language |
---|---|---|
Callable[[T], S] |
Python | |
(x: T) => S |
Function |
TypeScript |
T -> S |
Haskell | |
S (*)(T) |
C (function pointer) | |
function<S(T)> |
Callable |
C++ |
Function |
Julia |
\( T, S \to R \) | language |
---|---|
Callable[[T, S], R] |
Python |
(x: T, y: S) => R |
TypeScript |
T -> S -> R |
Haskell |
R (*)(T, S) |
C (function pointer) |
function<R(T, S)> |
C++ |
def f(x: T, y: S) -> R:
z: R = # ...python...
return z
function f(x: T, y: S): R {
let z: R = /* ...typescript... */;
return z;
}
function f(x::T, y::S)::R
z::R = # ...julia...
z
end
R f(T x, S y) {
R z = /* ...c/c++... */;
return z;
}
f :: T -> S -> R
f x y = {- ...haskell... -}
- How would you represent the position and mass of a particle in the 3D unit box?
- What about an arbitrary number of particles?
- What's the type of a function that calculates the center of mass for these particles?
- Name your types (type "aliases"), even simple ones
- Use them to construct larger types
Float01 = float # float bounded [0,1]
Position = Tuple[Float01,Float01,Float01] # x,y,z
Mass = float # positive
Particle = Tuple[Position,Mass]
Particles = List[Particle]
def centerOfMass(particles: Particles) -> Position:
- Much of the advantage of types comes from checking them to make sure they hold
- This can be done in one of two ways:
- "Statically": before the program runs, by the compiler or static analysis tool
- Lets you catch errors (typos, bugs) before they happen
- "Dynamically": while the program runs, as values are created or used
- Extra checks can slow down your code
- "Statically": before the program runs, by the compiler or static analysis tool
- Most languages end up using a mix of both
- In OO languages you can use classes to help constrain types
- Additional constraints on the values beyond can be verified in the constructor
- Dynamic checking: optional, only in "debug" mode
class Order(int):
def __init__(self, val: int):
assert val in (1,2,3)
def compute(order: Order) -> float:
- Try to think about your problem starting with data: how do you represent your input, state, etc.
- Write down and name your types
- Build functions that transform between representations to process data
- Consider replacing error checking at the beginning of functions with constrained types
https://bit.ly/sciware-typing-2022
pip install mypy
-
The interpreter doesn't care!
-
You want to annotate types because they:
- make your intentions clearer
- make your expectations more consistent
- help you think about code structure
- reduce the load on your human memory
- enable tooling that can help you even more
def turn_a_list_into_a_square_matrix(values):
root = math.sqrt(len(values))
if root < 1 or root % 1 != 0:
return None
array = np.asarray(values)
array = array.reshape((root, root))
return array
if __name__ == '__main__':
input_int = 54
untyped = turn_a_list_into_a_square_matrix(input_int)
def turn_a_list_into_a_square_matrix(values):
root = math.sqrt(len(values))
if root < 1 or root % 1 != 0:
return None
array = np.asarray(values)
array = array.reshape((root, root))
return array
if __name__ == '__main__':
input_int = 54
untyped = turn_a_list_into_a_square_matrix(input_int)
## oops! 54 isn't a list--so len(54) causes an error
## ...but did you catch the other error too?
- Auto-completing members of known classes
- Reminding you what a variable should contain
- Forces you to think about design
- But gives you a framework for thinking
- As design evolves, type checker can make sure you've caught everything
- For further research, see the docs:
- Note these docs privilege exhaustiveness over readability...
- Basics
- Installation
- Syntax
- Allowed types
- Dealing with parameterized types
- Using the Type System
- Type Inference
- Maybe some live coding?
- Static analysis (SA)--determining possible behavior of code just from its structure, without running it
- Type Checker/Type Analyzer--SA tool that reasons about variable and function types (e.g.
mypy
) - Linter--SA tool that checks style, code reachability, & more
- Duck typing--"if it quacks like a duck, it's a duck"
- If it has matching properties, it counts as a ___
$ pip install mypy
$ mypy list_to_square.py
- VSCode:
- Install Python and/or Pylance extensions
- File -> Preferences -> Settings (ctrl-,)
- For pylance extension:
Extensions -> Pylance -> Type Checking Mode - For python extension:
Extensions -> Python -> Linting: Mypy Enabled, Mypy Path
- For pylance extension:
- Pycharm
- Install mypy executable, then mypy plugin from JetBrains plugin site
- Postfix type hints:
- Form is
<VARIABLE_NAME>: <TYPE_NAME>
int_valued_variable: int = 5
- Applies anywhere a variable is first used, including function parameters
- Form is
- Function return values use an arrow:
def f(x: int) -> float:
- This says
f
takes anint
and returns afloat
- Typing for a function is called a type signature
-
Primitive types: Basic data
(Types that actually exist in the interpreter)int
float
bool
str
-
Non-primitives
None
-- technically not a primitiveAny
-- Matches anything; turns off type checkingobject
-- Matches anything; doesn't turn off checking- Any classes in your namespace
- (e.g.
ddt.Client
if you did
import dask.distributed as ddt
)
- (e.g.
-
Parameterized types
- Built-in container types:
List
Set
Tuple
Dict
Callable
Literal
- ...
- Built-in container types:
-
We'll talk about these in detail in a minute
-
Any class defined in your namespace can be used as a type
- Built-in classes
- Classes from imports
- Classes from your code
-
Child classes count as the parent class
-
Built-in classes work (but you may need to import the types explicitly):
from io import TextIOWrapper
def write_to_file(msg: str, handle: TextIOWrapper) -> None:
handle.write(msg)
- Child classes count as members of the parent class:
class Parent(): pass
class Child(Parent): pass
def example() -> None:
a: Parent = Parent()
b: Parent = Child() # no problem: Child counts as Parent
c: Child = Parent() # error: Parent does not count as Child
d: Child = b # Works! We know b *must* be a Child here
- Linters differ on using a class during its definition
class MyClass():
def __init__(self) -> None:
pass
def compare(self, other: MyClass) -> int:
return 5
-
mypy
thinks this is fine -
VSCode complains about using
MyClass
incompare
- To fix: put it in quotes (
other: 'MyClass'
) - Recognizes
MyClass
fine outside its definition
- To fix: put it in quotes (
-
self
never needs type annotation: it's inferred -
__init__
is inferred to returnNone
- BUT
mypy
skips functions without type annotations - Fix: if you have no non-
self
parameters, explicitly returnNone
- Or use
--strict
- BUT
- Container types need parameters to be useful
- e.g.
List[int]
, not justList
- (What's in the list? Defaults to
Any
)
- e.g.
- pre-3.10:
- Capital letters (
List
,Set
etc) - imported from
typing
package
- Capital letters (
- 3.10+:
- Built-ins (
list
/set
/dict
/tuple
) can be lower-case - And don't need to be imported
- More complex types now come from
collections.abc
- Built-ins (
- Examples below use the older syntax
- Parameters go in square brackets:
List[str]
orSet[int]
from typing import List
def sum(items: List[int]) -> int:
pass
Or in 3.10+:
def sum(items: list[int]) -> int:
pass
- A
Tuple
has a fixed* number of ordered elements- Used whenever you pack values together
e.g. returning(x, y)
as one unit
- Used whenever you pack values together
price_and_count: Tuple[float, int] = (1.5, 3)
- (An empty tuple is typed as
Tuple[()]
)
- Stores unique keys that point to values
- Must provide types for both
(Dict[KEYTYPE, VALUETYPE]
)
heights_per_person: Dict[str, float] = {}
- Along with variable naming, this documents the data structure
- Sometimes a variable can be one of a few different types:
cash_on_hand: Union[int, float, None] = ...
- This allows flexibility while still providing guidance.
- In 3.10, can just use an
or
pipe:
- In 3.10, can just use an
cash: int | float | None
cash_per_person: Dict[str, Union[int, float, None]] = {...}
But it gets long...
points: Dict[str, Dict[int, List[float]]] = {...}
# for each student, for each test number, store how many points they got on each question
points["Kushim"][1]
is a list of how many points Kushim got on each question on the first Accounting exam at Uruk University
- Instead we can define type aliases
from typing import TypeAlias
Gradebook: TypeAlias = Dict[str, Dict[int, List[float]]]
my_gradebook: Gradebook = get_gradebook_for_semester()
grades_for_student = gradebook['Kushim']
-
Lets you use a shorter but more meaningful name for something complex
- Note: explicitly adding
TypeAlias
is a 3.10 thing--previously it was inferred, which led to some ambiguity.
- Note: explicitly adding
-
Or go even further:
from typing import TypeAlias
StudentName: TypeAlias = str
TestId: TypeAlias = int
PointsPerQuestion: TypeAlias = List[float]
Gradebook: TypeAlias = Dict[StudentName, Dict[TestId, PointsPerQuestion]]
my_gradebook: Gradebook = get_gradebook_for_semester()
kushim_points_test_one = gradebook['Kushim'][1]
- Pros:
- Meaningful names document intention
- Shorter: easier to read and type
- Cons:
- Sometimes mouseover hints don't drill down enough
- (might just get
Gradebook
, not actual structure)
- (might just get
- Might be better off with an actual class or a dataclass
- Sometimes mouseover hints don't drill down enough
- Describes the type of a function stored in a variable.
- Takes list of parameter types, then return type
from typing import Callable
sum_then_square: Callable[[float, float], float]
sum_then_square = lambda x, y: (x + y) ** 2
- Useful for higher-order programming & function composition
Literal
means a specific valueLiteral[15]
is15
, not any other integer
way: Union[Literal['North'], Literal['South'], Literal['East'], Literal['West']]
way = 'west'
- Did you catch the error there?
- You probably want an Enum
Literal
is mostly the result of type inference
- You pretty much don't!
- Errors from your linter are not enforced at runtime
- Interpreter has its own type & object system
- Type hinting is entirely for static code analysis
- You could use
TypeGuard
s (see bonus content)
- You could use
- Highlighting of type errors
- mouseover information for variables, functions
- auto-completion of member reference
- (
my_ClassA_var.
pops up methods fromClassA
)
- (
- auto-populates docstring skeleton
-
Generates a report of detected type errors
-
By default:
- ONLY looks at functions having explicit type hinting
- Stops tracking variables which are annotated
Any
-
--strict
flag makes it more aggressive -
Useful as a continuous integration step
-
Customize
mypy
behavior with command-line arguments- Full command line documentation
- In particular:
--strict
and--disallow-any-expr
--strict
turns on a lot of checks that are skipped by default--disallow-any-expr
reports every use ofAny
-
mypy
also prints out informative messages if you ask it toreveal_type(x)
prints the typex
has when the statement is encountered duringmypy
analysis- For more advice, see the Type hints cheat sheet
- Type annotations are most useful for documenting functions, key variables
- The analyzer can often figure out a type through static analysis, without hints:
def random_integer() -> int:
return 15 # TODO: Pick more-random number
def do_math() -> int:
a = random_integer() # linter knows a is an int!
b = random_integer()
c = a + b
return c
Union
types can be understood more precisely when context eliminates some possibilities- Type checker understands a few built-in functions
- Type is narrowed only within a conditional branch
- Linter understands how
assert
,return
, & exceptions affect this
def narrowing_example(my_var: Union[str, int]) -> int:
if isinstance(my_var, str):
# In this branch, we know my_var is a str
return len(my_var)
# All strs exit the function at the "return" statement
# So by process of elimination, my_var must be an int
# And mouseover will show 'int' here
return my_var + 1
-
isinstance()
actually works at runtime, somypy
knows this is safe -
Also supported:
issubclass(cls, MyClass)
if (type(a) is int)
if (variable is None)
if (callable(fn))
(though this doesn't distinguishCallable
s with different signatures)- User-defined type guards
-
Optional[TYPE]
is short forUnion[TYPE, None]
and benefits from narrowing:
def narrowing_example_2(a: Optional[int]) -> None:
print(a) # here 'a' could be int or None
if (a is None):
raise Exception('Exception ends this branch!')
a # here a must be int
- This is useful for handling user inputs
- For when inference fails!
- If you know better than the analyzer, you can overrule it
- Use sparingly: there is usually a better way
- Even if it's just type-hinting the receiving variable
import xarray as xr
from typing import cast
def load_xarray(filename: str) -> xr.Dataset:
unknown_results = xr.open_dataset(filename)
# xarray doesn't publish types (yet), so open_dataset is not annotated.
# But we know it returns a Dataset object.
# So use cast(TYPE, VALUE) to tell the checker that VALUE is of type TYPE
results = cast(xr.Dataset, unknown_results)
return results
- If a variable is explicitly marked
Any
, it gets ignored from then on - Even casting it won't turn type checks back on for it
- Sometimes you can say something about a type without knowing everything about it.
- Suppose we have a function that adds a new item to the front of a typed list:
def prepend_to_list(value: int, values: List[int]) -> List[int]:
return [value] + values
-
Great, now do one for every other possible type of
list
...??? -
Instead, use a variable type parameter
TypeVar
:
from typing import TypeVar
T = TypeVar('T')
def prepend_to_list(value: T, values: List[T]) -> List[T]:
return [value] + values
- This now works for any type!
- But some operations don't make sense for any arbitrary type?
- There are ways to add restrictions to the type parameter
- Runtime uses more precise types (
dtypes
)- Based on actual bit representation
- Closer to types in C
- Since 2021,
numpy
hasnumpy.typing
(official doc)- Annotating array shapes is not yet supported
- 3rd-party extensions (
nptyping
) do allow typing array dimensions
- Here are some topics which are beyond today's main scope.
- Means you shouldn't change the variable's value
- Like
const
in JavaScript/TypeScript, C/C++/C#, etc.
- Like
x: Final[int] = 15
x = 22 # will generate a warning
- This is great for supporting functional programming
and reducing mistakes
- But it is not enforced at runtime!
- There are other not-strictly-type annotations available; see docs
TypeGuard
: a function that returns whether a variable is of a given type- Connected to narrowing
- Code that both actually works at runtime...
- ...and is also understood by the type analyzer
def is_str_list(val: List[object]) -> TypeGuard[List[str]]:
return all(isinstance(x, str) for x in val)
- This returns true if & only if every element of
val
is astr
- Other code can now call
is_str_list(some_list)
and branch on the result - in a branch where
is_str_list(x)
returnedTrue
, the type checker also knowsx
is aList[str]
- Other code can now call
- You can do a lot with these
-
TypeVar()
lets you add subtype constraints to limit what generic types you support- e.g.
TypeVar('T', int, float)
meansT
must be anint
orfloat
. - How is that different from
Union[int, float]
?List[T]
: all elements areT
(eitherint
orfloat
)List[Union[int, float]]
could contain someint
s and somefloat
s
- e.g.
-
You can also restrict to type families:
TypeVar('S', bound=str)
meansS
has to extendstr
to qualify.- This supports the possibility of more specific types while guaranteeing that certain functionality will be available
-
One
TypeVar
can only use either binding or subtype constraints- it doesn't make sense to use both
-
Suppose I have an
Animal
class- both
Cat
andDog
inherit fromAnimal
- both
-
Is
List[Cat]
a subtype ofList[Animal]
?- What about
Callable[]
s? - Generally: how does class inheritance play with container types?
- What about
-
Covariant: Means that if
A
is a subtype ofB
, thenGeneric[A]
is always a subtype ofGeneric[B]
-
Contravariant: Means that if
A
is a subtype ofB
, thenGeneric[B]
is always a subtype ofGeneric[A]
- (Note it's reversed!)
-
Invariant: the relationship is inconsistent.
-
Functions are "contravariant in parameters but covariant in returns":
f() -> Cat
is a subtype off() -> Animal
- Any caller expecting an
Animal
can take aCat
. - This is covariance
- Any caller expecting an
- BUT
f(x: Animal) -> None
is a subtype off(x: Cat) -> None
!- Of all the functions that operate on
Cat
s, only some work on allAnimal
s. - This is contravariance
- Of all the functions that operate on
-
This mostly comes up with
Callable
s, function-valued variables, and callbacks -
The most general callback takes the most specific class of inputs and returns the most general class of outputs
-
Lists are invariant:
- All items from a
List[Dog]
are legitimate items of aList[Animal]
- So
List[Dog]
is a subtype when acting as a source
- So
List[Animal]
can receive all items that could be added toList[Dog]
, but not vice versa- So
List[Animal]
is a subtype when acting as a sink
- So
- Relationship is not fixed -- thus invariant
- All items from a