Playing With Python Types
In this lesson, you’ll practice playing with Python types and learn about adding annotations and type hints for sequence objects.
Up until now, you’ve only used basic types like str
, float
, and bool
in your type hints. The Python type system is quite powerful and supports many kinds of more complex types. This is necessary as it needs to be able to reasonably model Python’s dynamic duck typing nature.
In this lesson, you’ll learn more about this type system by making a card game. You’ll see how to specify:
- The type of sequences and mappings like tuples, lists, and dictionaries
- Type aliases that make code easier to read
The following example shows an implementation of a regular (French) deck of cards:
# game.py
import random
SUITS = "♠ ♡ ♢ ♣".split()
RANKS = "2 3 4 5 6 7 8 9 10 J Q K A".split()
def create_deck(shuffle=False):
"""Create a new deck of 52 cards"""
deck = [(s, r) for r in RANKS for s in SUITS]
if shuffle:
random.shuffle(deck)
return deck
def deal_hands(deck):
"""Deal the cards in the deck into four hands"""
return (deck[0::4], deck[1::4], deck[2::4], deck[3::4])
def play():
"""Play a 4-player card game"""
deck = create_deck(shuffle=True)
names = "P1 P2 P3 P4".split()
hands = {n: h for n, h in zip(names, deal_hands(deck))}
for name, cards in hands.items():
card_str = " ".join(f"{s}{r}" for (s, r) in cards)
print(f"{name}: {card_str}")
if __name__ == "__main__":
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:
$ python3 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
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 basic types like str
, float
, and bool
, adding type hints is as straightforward 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", "Thomas", "Bobby"]
>>> version: tuple = (3, 7, 1)
>>> options: dict = {"centered": False, "capitalize": True}
>>> type(names[2])
<class 'str'>
>>> __annotations__
{'name': <class 'str'>, 'pi': <class 'float'>, 'centered': <class 'bool'>, 'names': <class 'list'>, 'version': <class 'tuple'>, 'options': <class 'dict'>}
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", "Thomas", "Bobby"]
>>> version: Tuple[int, int, int] = (3, 7, 1)
>>> options: Dict[str, bool] = {"centered": False, "capitalize": True}
>>> __annotations__
{'name': <class 'str'>, 'pi': <class 'float'>, 'centered': <class 'bool'>, 'names': typing.List[str], 'version': typing.Tuple[int, int, int], 'options': typing.Dict[str, bool]}
Note that each of these types starts with a capital letter and that they all use square brackets to define item types:
names
is a list of strings.version
is a 3-tuple consisting of three integers.options
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:
def create_deck(shuffle: bool = False) -> List[Tuple[str, str]]:
"""Create a new deck of 52 cards"""
deck = [(s, r) for r in RANKS for s in SUITS]
if shuffle:
random.shuffle(deck)
return deck
The type hints might become quite oblique when you’re working with nested types, like the deck of cards. You may need to stare at List[Tuple[str, str]]
a bit before figuring out that it matches our representation of a deck of cards.
Now consider how you would annotate deal_hands()
:
def deal_hands(
deck: List[Tuple[str, str]]
) -> Tuple[
List[Tuple[str, str]],
List[Tuple[str, str]],
List[Tuple[str, str]],
List[Tuple[str, str]],
]:
"""Deal the cards in the deck into four hands"""
return (deck[0::4], deck[1::4], deck[2::4], deck[3::4])
That’s just terrible!
Remember that type annotations are regular Python expressions. That means that you can define your own type aliases by assigning them to new variables. You can, for instance, create Card
and Deck
type aliases:
from typing import List, Tuple
Card = Tuple[str, str]
Deck = List[Card]
Card
can now be used in type hints or in the definition of new type aliases, like Deck
in the example above. When you use these aliases, the annotations of deal_hands()
become much more readable:
def deal_hands(deck: Deck) -> Tuple[Deck, Deck, Deck, Deck]:
"""Deal the cards in the deck into four hands"""
return (deck[0::4], deck[1::4], deck[2::4], deck[3::4])
Type aliases are great for making your code and its intent clearer.
00:00 In this video, I’m going to take you a little bit further into playing with Python types. To do that, I’m going to use an example of a card game, and the card is going to include a few more advanced types.
00:13 You get to practice typing for sequences and mappings. That includes tuples, lists, and dictionaries. I’ll also show you type aliases that can make the code a little easier to read, especially with repetitive items. So, let me have you dive into the code.
00:29
You can copy the code in from the text below this video. What you need to do first is create a file named game.py
and then paste the code into it. So, what’s happening here?
00:42
We’re creating a simple card game that deals out to four different players from a deck of 52 cards. First off, you import random
. And then you’re creating a list by taking this text string of these four symbols and using the .split()
method on it to create the suits.
01:00
And then you’re creating a list of ranks by taking this long string of the numbers and face cards, and again, using the .split()
there. If you’re not familiar with how that works, you can just copy it into a REPL to see what happens. Here, I could run bpython
.
01:18
So, what did that do? Basically, it created a list with each one separately. That’s what the .split()
method does. With the RANKS
we’ll do the same.
01:29 Using that, you’re creating a deck here with this function. These are some of the things that we’re going to go ahead and add type hints to. It’s going to create the new deck here.
01:38
It takes the suits and the ranks, and for r
in RANKS
and for s
in SUITS
, it’s going to go ahead and return a deck
.
01:46
There is an optional .shuffle()
statement in the middle there, that takes that Boolean argument. Here where it deals out to the hands, you’re using list indexing, starting with the first card, and then every fourth card, and the same thing here.
02:02
So, play()
creates a deck and then you can choose this optional thing to shuffle. There’s a set of players. In this case, there’s four. Again, using that same .split()
method. And then it’s creating hands for each player, the names
—player 1, player 2. And then for each one of those, it’s going to create a hand for each player.
02:19
And here, the zip()
basically is combining them together, taking the iterable of the names
and dealing the hands to each of them.
02:26
You can run any of this separately to get familiar with how the code works, and that would be good practice for you also. The last thing that happens is that creating a card_str
(card string), which is going to join together from this tuple of suit and rank in the cards
, and it’s going to basically make an f-string to tie them together and then print out the character’s name, and then all those card strings. To run this, python3 game.py
.
02:48 And here you can see, it dealed out player 1, 2, 3, and 4, dealing out their sets of cards. So, not much of a game, but there’s much more you can do with this and I’ll have links to a continuation of this tutorial, where you can dive much deeper into the gameplay and so forth, creating a hearts game. For the purposes of this video, you haven’t ever created type hints for tuples or lists or other types of sequence items, so it’s time to discuss that.
03:19
And then I’ll get back into my REPL. So, each card is represented by a tuple of strings denoting the suit and the rank. For sequences and mapping, you’re going to add hints to the card game. You need to annotate create_deck()
, deal_hands()
, and play()
. Up to now, you’ve used simple types like str
, float
, and bool
. Like name
,
03:40
the example you’ve used a few times of pi
, or even the Boolean with the default value of False
. For composite types, you can use, say, that it’s a list
.
04:03
And for something like a version number, you could have a tuple
,
04:12
or even a dict
(dictionary).
04:24
Now, sure, names
is a list
and version
is a tuple
, but what about the sequence of objects inside those? What are the types for them?
04:38
Here in the __annotations__
, you can see here that 'version'
is a <class 'tuple'>
, but it doesn’t say anything about the items inside of it.
04:44
And same thing with 'options'
. It just says that it’s a dictionary. And we know that, okay, it’s a string with a bool, and here, these are all ints, and then the list is of strings.
04:54
There’s an additional module that you can use. And to use it, you need to import from typing
. This is from the built-in library. I’m going to import those items of a Dict
(dictionary), a List
, and of a Tuple
. Now to use them, it’s going to look pretty similar, but here you’re going to use capital List
and then say it’s a List
of strings.
05:20
In the version number example, capital Tuple
, and explicitly say the types of each. In this case, a 3-tuple.
05:36
And for the Dict
, you can say your key and your value types.
05:49
Great. And here you can see a little bit further of what was created, that you have 'names'
, it’s of the typing
class List
and has strings inside of it, 'version'
is of typing.Tuple
and has ints, 'options'
is of typing.Dict
with a str
and a bool
.
06:04
And make sure that you note that each of these starts with a capital letter instead of the lowercase version you saw earlier. That’s coming from, again, the typing
module.
06:13
There’s additional items in the typing
module that you can go into further, such as a Counter
, a Deque
, a FrozenSet
, a NamedTuple
, and other items like a Set
. Again, we’re going to dive in and just play a little bit here with some of these types.
06:28
Go back to the game. So here, when you’re creating the deck, shuffle
—add your first type hint—is a bool
. What’s it return? Well, it returns a List
. Oh, so what do we need to do? Back up top here after you import random
…
06:47
Okay. So now let’s say that it returns a List
that has a Tuple
with two strings in it, one string being the suit and the other string being the rank.
07:01
To do deal_hands()
, to annotate that,
07:09
you’re going to start with the deck
, which is a List
with a Tuple
, and a string and a string. So that’s what’s coming in, right? What you’re creating here, with creating it. Okay.
07:22 What’s returned? Well, that’s a bit more complex. You’re returning four hands, right? This one, this one, this one, this one. Okay. So, that itself is a tuple, and each one is one of these.
07:40 There’s your first one, there’s your second one, third one, and your fourth. Okay. And then wrap it up with a colon there. So, that’s the return statement. Well, that’s pretty ugly. There must be an easier way. Well, actually there is. What you can do is define what a card is and what a deck is, and create what is known as an alias.
08:00
So go to the top here and after importing List
and Tuple
, define those two things. What is a card? A Card
is of a type Tuple
and has two strings: the suit and the rank.
08:12
Great. And then you could say a Deck
—well, that’s a List
, but instead of having to repeat this text again inside of it, it’s a list of what? Well, it’s a list of cards, so we use Card
. Nice.
08:25
So now you can go back and change here your return statement. It’s going to return a Deck
. And here when you deal the hands, deck
is going to return what? Well, a Deck
.
08:36
Maybe you can fit it all on one line. deck
coming back here, and returns what? Well, it returns a Tuple
08:49
with four kind of smaller decks, if you will. Again, a Deck
is a list of these cards. Each of the players is getting a quarter of the deck.
09:04
Okay. We should run this through Mypy. Let me save, exit from the REPL. So, mypy game.py
.
09:16
21
[…] invalid syntax
. Oh! Yeah, that would be invalid syntax. I missed adding the parentheses on line 21. Try it again. And I need to save.
09:26 Save. All right, no errors. Great! Does it play the same? Yep! It looks good. So here we’re coming out, again, as each of those card types. And again, each one of these is a whole deck. Again, a quarter of the deck, but that’s okay. As far as defining it, it makes it a lot easier than having to go much deeper in it.
09:46
So there, you’ve practiced a little bit with aliases and sequences using this more advanced tool of the typing
module. With that, you’re ready for the conclusion and course review—up next.
Become a Member to join the conversation.