Reference:
https://realpython.com/python-type-checking/
https://docs.python.org/3/library/typing.html
Type Systems
All programming languages include some kind of type system that formalizes which categories of objects it can work with and how those categories are treated.
Dynamic Typing
Python is a dynamically typed language. This means that the Python interpreter does type checking only as code runs, and that the type of a variable is allowed to change over its lifetime. The following dummy examples demonstrate that Python has dynamic typing:
>>> if False:
... 1 + "two" # This line never runs, so no TypeError is raised
... else:
... 1 + 2
...
3
>>> 1 + "two" # Now this is type checked, and a TypeError is raised
TypeError: unsupported operand type(s) for +: 'int' and 'str'
In the example above, the branch 1 + "two"
never runs so it’s never type checked. The second example shows that when 1 + "two"
is evaluated it raises a TypeError
since you can’t add an integer and a string in Python.
Static Typing
The opposite of dynamic typing is static typing. Static type checks are performed without running the program. In most statically typed languages, for instance C and Java, this is done as your program is compiled.
With static typing, variables generally are not allowed to change types, although mechanisms for casting a variable to a different type may exist.
String thing;
thing = "Hello";
The first line declares that the variable name thing
is bound to the String
type at compile time. The name can never be rebound to another type. In the second line, thing
is assigned a value. It can never be assigned a value that is not a String
object. For instance, if you were to later say thing = 28.1f
the compiler would raise an error because of incompatible types.
Python will always remain a dynamically typed language. However, PEP (Python Enhancement Proposal) 484 introduced type hints, which make it possible to also do static type checking of Python code.
Unlike how types work in most other statically typed languages, type hints by themselves don’t cause Python to enforce types. As the name says, type hints just suggest types. There are other tools, which you’ll see later, that perform static type checking using type hints.
Hello Types
In this section you’ll see how to add type hints to a function. The following function turns a text string into a headline by adding proper capitalization and a decorative line:
def headline(text, align=True):
if align:
return f"{text.title()}\n{'-' * len(text)}"
else:
return f" {text.title()} ".center(50, "o")
By default the function returns the headline left aligned with an underline. By setting the align
flag to False
you can alternatively have the headline be centered with a surrounding line of o
:
>>> print(headline("python type checking"))
Python Type Checking
--------------------
>>> print(headline("python type checking", align=False))
oooooooooooooo Python Type Checking oooooooooooooo
It’s time for our first type hints! To add information about types to the function, you simply annotate its arguments and return value as follows:
def headline(text: str, align: bool = True) -> str:
...
The text: str
syntax says that the text
argument should be of type str
. Similarly, the optional align
argument should have type bool
with the default value True
. Finally, the -> str
notation specifies that headline()
will return a string.
In terms of style, PEP 8 recommends the following:
- Use normal rules for colons, that is, no space before and one space after a colon:
text: str
. - Use spaces around the
=
sign when combining an argument annotation with a default value:align: bool = True
. - Use spaces around the
->
arrow:def headline(...) -> str
.
Adding type hints like this has no runtime effect: they are only hints and are not enforced on their own. For instance, if we use a wrong type for the (admittedly badly named) align
argument, the code still runs without any problems or warnings:
>>> print(headline("python type checking", align="left"))
Python Type Checking
--------------------
Note: The reason this seemingly works is that the string "left"
compares as truthy. Using align="center"
would not have the desired effect as "center"
is also truthy.
To catch this kind of error you can use a static type checker. That is, a tool that checks the types of your code without actually running it in the traditional sense.
Example: A Deck of Cards
The following example shows an implementation of a regular (French) deck of cards:
1# game.py
2
3import random
4
5SUITS = "♠ ♡ ♢ ♣".split()
6RANKS = "2 3 4 5 6 7 8 9 10 J Q K A".split()
7
8def create_deck(shuffle=False):
9 """Create a new deck of 52 cards"""
10 deck = [(s, r) for r in RANKS for s in SUITS]
11 if shuffle:
12 random.shuffle(deck)
13 return deck
14
15def deal_hands(deck):
16 """Deal the cards in the deck into four hands"""
17 return (deck[0::4], deck[1::4], deck[2::4], deck[3::4])
18
19def play():
20 """Play a 4-player card game"""
21 deck = create_deck(shuffle=True)
22 names = "P1 P2 P3 P4".split()
23 hands = {n: h for n, h in zip(names, deal_hands(deck))}
24
25 for name, cards in hands.items():
26 card_str = " ".join(f"{s}{r}" for (s, r) in cards)
27 print(f"{name}: {card_str}")
28
29if __name__ == "__main__":
30 play()
Each card is represented as a tuple of strings denoting the suit and rank. The deck is represented as a list of cards. create_deck()
creates a regular deck of 52 playing cards, and optionally shuffles the cards. deal_hands()
deals the deck of cards to four players.
Finally, play()
plays the game. As of now, it only prepares for a card game by constructing a shuffled deck and dealing cards to each player. The following is a typical output:
$ python game.py
P4: ♣9 ♢9 ♡2 ♢7 ♡7 ♣A ♠6 ♡K ♡5 ♢6 ♢3 ♣3 ♣Q
P1: ♡A ♠2 ♠10 ♢J ♣10 ♣4 ♠5 ♡Q ♢5 ♣6 ♠A ♣5 ♢4
P2: ♢2 ♠7 ♡8 ♢K ♠3 ♡3 ♣K ♠J ♢A ♣7 ♡6 ♡10 ♠K
P3: ♣2 ♣8 ♠8 ♣J ♢Q ♡9 ♡J ♠4 ♢8 ♢10 ♠9 ♡4 ♠Q
You will see how to extend this example into a more interesting game as we move along.
Sequences and Mappings
Let’s add type hints to our card game. In other words, let’s annotate the functions create_deck()
, deal_hands()
, and play()
. The first challenge is that you need to annotate composite types like the list used to represent the deck of cards and the tuples used to represent the cards themselves.
With simple types like str
, float
, and bool
, adding type hints is as easy as using the type itself:
>>> name: str = "Guido"
>>> pi: float = 3.142
>>> centered: bool = False
With composite types, you are allowed to do the same:
>>> names: list = ["Guido", "Jukka", "Ivan"]
>>> version: tuple = (3, 7, 1)
>>> options: dict = {"centered": False, "capitalize": True}
However, this does not really tell the full story. What will be the types of names[2]
, version[0]
, and options["centered"]
? In this concrete case you can see that they are str
, int
, and bool
, respectively. However, the type hints themselves give no information about this.
Instead, you should use the special types defined in the typing
module. These types add syntax for specifying the types of elements of composite types. You can write the following:
>>> from typing import Dict, List, Tuple
>>> names: List[str] = ["Guido", "Jukka", "Ivan"]
>>> version: Tuple[int, int, int] = (3, 7, 1)
>>> options: Dict[str, bool] = {"centered": False, "capitalize": True}
Note that each of these types start with a capital letter and that they all use square brackets to define item types:
names
is a list of stringsversion
is a 3-tuple consisting of three integersoptions
is a dictionary mapping strings to Boolean values
The typing
module contains many more composite types, including Counter
, Deque
, FrozenSet
, NamedTuple
, and Set
. In addition, the module includes other kinds of types that you’ll see in later sections.
Let’s return to the card game. A card is represented by a tuple of two strings. You can write this as Tuple[str, str]
, so the type of the deck of cards becomes List[Tuple[str, str]]
. Therefore you can annotate create_deck()
as follows:
8def create_deck(shuffle: bool = False) -> List[Tuple[str, str]]:
9 """Create a new deck of 52 cards"""
10 deck = [(s, r) for r in RANKS for s in SUITS]
11 if shuffle:
12 random.shuffle(deck)
13 return deck
In addition to the return value, you’ve also added the bool
type to the optional shuffle
argument.
Note: Tuples and lists are annotated differently.
A tuple is an immutable sequence, and typically consists of a fixed number of possibly differently typed elements. For example, we represent a card as a tuple of suit and rank. In general, you write Tuple[t_1, t_2, ..., t_n]
for an n-tuple.
A list is a mutable sequence and usually consists of an unknown number of elements of the same type, for instance a list of cards. No matter how many elements are in the list there is only one type in the annotation: List[t]
.
In many cases your functions will expect some kind of sequence, and not really care whether it is a list or a tuple. In these cases you should use typing.Sequence
when annotating the function argument:
from typing import List, Sequence
def square(elems: Sequence[float]) -> List[float]:
return [x**2 for x in elems]
Using Sequence
is an example of using duck typing. A Sequence
is anything that supports len()
and .__getitem__()
, independent of its actual type.
Special forms (from typing import List, Optional, Union )
These can be used as types in annotations using []
, each having a unique syntax.
typing.
Tuple
Tuple type;
Tuple[X, Y]
is the type of a tuple of two items with the first item of type X and the second of type Y. The type of the empty tuple can be written asTuple[()]
.Example:
Tuple[T1, T2]
is a tuple of two elements corresponding to type variables T1 and T2.Tuple[int, float, str]
is a tuple of an int, a float and a string.To specify a variable-length tuple of homogeneous type, use literal ellipsis, e.g.
Tuple[int, ...]
. A plainTuple
is equivalent toTuple[Any, ...]
, and in turn totuple
.
typing.
Union
Union type;
Union[X, Y]
means either X or Y.To define a union, use e.g.
Union[int, str]
. Details:The arguments must be types and there must be at least one.
Unions of unions are flattened, e.g.:
Unions of a single argument vanish, e.g.:
Redundant arguments are skipped, e.g.:
When comparing unions, the argument order is ignored, e.g.:
You cannot subclass or instantiate a union.
You cannot write
Union[X][Y]
.You can use
Optional[X]
as a shorthand forUnion[X, None]
.
typing.
Optional
Optional type.
Optional[X]
is equivalent toUnion[X, None]
.Note that this is not the same concept as an optional argument, which is one that has a default. An optional argument with a default does not require the
Optional
qualifier on its type annotation just because it is optional. For example:On the other hand, if an explicit value of
None
is allowed, the use ofOptional
is appropriate, whether the argument is optional or not. For example:
No comments:
Post a Comment