Basics Intermediate Advanced
aialgorithmsapibest-practicescareercommunitydatabasesdata-sciencedata-structuresdata-vizdevopsdjangodockereditorsflaskfront-endgamedevguimachine-learningnewsnumpyprojectspythonstdlibtestingtoolsweb-devweb-scraping
Recommended Course

Python Type Checking
1h 7m · 11 lessons

Python Type Checking (Guide)
Table of Contents
Recommended Course
Python Type Checking(1h 7m)
In this guide, you will get a look into Python type checking. Traditionally, types have been handled by the Python interpreter in a flexible but implicit way. Recent versions of Python allow you to specify explicit type hints that can be used by different tools to help you develop your code more efficiently.
In this tutorial, you’ll learn about the following:
- Type annotations and type hints
- Adding static types to code, both your code and the code of others
- Running a static type checker
- Enforcing types at runtime
This is a comprehensive guide that will cover a lot of ground. If you want to just get a quick glimpse of how type hints work in Python, and see whether type checking is something you would include in your code, you don’t need to read all of it. The two sectionsHello Types andPros and Cons will give you a taste of how type checking works and recommendations about when it’ll be useful.
Free Bonus:5 Thoughts On Python Mastery, a free course for Python developers that shows you the roadmap and the mindset you’ll need to take your Python skills to the next level.
Take the Quiz: Test your knowledge with our interactive “Python Type Checking” quiz. You’ll receive a score upon completion to help you track your learning progress:
Interactive Quiz
Python Type CheckingIn this quiz, you'll test your understanding of Python type checking. You'll revisit concepts such as type annotations, type hints, adding static types to code, running a static type checker, and enforcing types at runtime. This knowledge will help you develop your code more efficiently.
Type Systems
All programming languages include some kind oftype system that formalizes which categories of objects it can work with and how those categories are treated. For instance, a type system can define a numerical type, with42 as one example of an object of numerical type.
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:
>>>ifFalse:...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 raisedTypeError: unsupported operand type(s) for +: 'int' and 'str'In the first example, the branch1 + "two" never runs so it’s never type checked. The second example shows that when1 + "two" is evaluated it raises aTypeError since you can’t add an integer and a string in Python.
Next, let’s see if variables can change type:
>>>thing="Hello">>>type(thing)<class 'str'>>>>thing=28.1>>>type(thing)<class 'float'>type() returns the type of an object. These examples confirm that the type ofthing is allowed to change, and Python correctly infers the type as it changes.
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 instanceC andJava, 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.
Let’s look at a quick example from a statically typed language. Consider the following Java snippet:
Stringthing;thing="Hello";The first line declares that the variable namething is bound to theString 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 aString object. For instance, if you were to later saything = 28.1f the compiler would raise an error because of incompatible types.
Python will alwaysremain a dynamically typed language. However,PEP 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, whichyou’ll see later, that perform static type checking using type hints.
Duck Typing
Another term that is often used when talking about Python isduck typing. This moniker comes from the phrase “if it walks like a duck and it quacks like a duck, then it must be a duck” (orany of its variations).
Duck typing is a concept related to dynamic typing, where the type or the class of an object is less important than the methods it defines. Using duck typing you do not check types at all. Instead you check for the presence of a given method or attribute.
As an example, you can calllen() on any Python object that defines a.__len__() method:
>>>classTheHobbit:...def__len__(self):...return95022...>>>the_hobbit=TheHobbit()>>>len(the_hobbit)95022Note that the call tolen() gives the return value of the.__len__() method. In fact, the implementation oflen() is essentially equivalent to the following:
deflen(obj):returnobj.__len__()In order to calllen(obj), the only real constraint onobj is that it must define a.__len__() method. Otherwise, the object can be of types as different asstr,list,dict, orTheHobbit.
Duck typing is somewhat supported when doing static type checking of Python code, usingstructural subtyping. You’ll learnmore about duck typing later.
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:
defheadline(text,align=True):ifalign:returnf"{text.title()}\n{'-'*len(text)}"else:returnf"{text.title()} ".center(50,"o")By default the function returns the headline left aligned with an underline. By setting thealign flag toFalse you can alternatively have the headline be centered with a surrounding line ofo:
>>>print(headline("python type checking"))Python Type Checking-------------------->>>print(headline("python type checking",align=False))oooooooooooooo Python Type Checking ooooooooooooooIt’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:
defheadline(text:str,align:bool=True)->str:...Thetext: str syntax says that thetext argument should be of typestr. Similarly, the optionalalign argument should have typebool with the default valueTrue. Finally, the-> str notation specifies thatheadline() will return a string.
Interms 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. Usingalign="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.
You might already have such a type checker built into your editor. For instancePyCharm immediately gives you a warning:

The most common tool for doing type checking ismypy though. You’ll get a short introduction to mypy in a moment, while you can learn much more about how it workslater.
If you don’t already have mypy on your system, you can install it usingpip:
$pipinstallmypyPut the following code in a file calledheadlines.py:
1# headlines.py 2 3defheadline(text:str,align:bool=True)->str: 4ifalign: 5returnf"{text.title()}\n{'-'*len(text)}" 6else: 7returnf"{text.title()} ".center(50,"o") 8 9print(headline("python type checking"))10print(headline("use mypy",align="center"))This is essentially the same code you saw earlier: the definition ofheadline() and two examples that are using it.
Now run mypy on this code:
$mypyheadlines.pyheadlines.py:10: error: Argument "align" to "headline" has incompatible type "str"; expected "bool"Based on the type hints, mypy is able to tell us that we are using the wrong type on line 10.
To fix the issue in the code you should change the value of thealign argument you are passing in. You might also rename thealign flag to something less confusing:
1# headlines.py 2 3defheadline(text:str,centered:bool=False)->str: 4ifnotcentered: 5returnf"{text.title()}\n{'-'*len(text)}" 6else: 7returnf"{text.title()} ".center(50,"o") 8 9print(headline("python type checking"))10print(headline("use mypy",centered=True))Here you’ve changedalign tocentered, and correctly used aBoolean value forcentered when callingheadline(). The code now passes mypy:
$mypyheadlines.pySuccess: no issues found in 1 source fileThe success message confirms there were no type errors detected. Older versions of mypy used to indicate this by showing no output at all. Furthermore, when you run the code you see the expected output:
$pythonheadlines.pyPython Type Checking--------------------oooooooooooooooooooo Use Mypy ooooooooooooooooooooThe first headline is aligned to the left, while the second one is centered.
Pros and Cons
The previous section gave you a little taste of what type checking in Python looks like. You also saw an example of one of the advantages of adding types to your code: type hints helpcatch certain errors. Other advantages include:
Type hints helpdocument your code. Traditionally, you would usedocstrings if you wanted to document the expected types of a function’s arguments. This works, but as there is no standard fordocstrings despitePEP 257 they can’t be easily used for automatic checks.)
Type hintsimprove IDEs and linters. They make it much easier to statically reason about your code. This in turn allows IDEs to offer better code completion and similar features. With the type annotation, PyCharm knows that
textis a string, and can give specific suggestions based on this:Type hints help youbuild and maintain a cleaner architecture. The act of writing type hints forces you to think about the types in your program. While the dynamic nature of Python is one of its great assets, being conscious about relying on duck typing, overloaded methods, ormultiple return types is a good thing.
Of course, static type checking is not all peaches and cream. There are also some downsides you should consider:
Type hintstake developer time and effort to add. Even though it probably pays off in spending less timedebugging, you will spend more time entering code.
Type hintswork best in modern Pythons. Annotations were introduced in Python 3.0, and it’s possible to usetype comments in Python 2.7. Still, improvements likevariable annotations andpostponed evaluation of type hints mean that you’ll have a better experience doing type checks using Python 3.6 or evenPython 3.7.
Type hintsintroduce a slight penalty in startup time. If you need to use the
typingmodule theimport time may be significant, especially in short scripts.
You’lllater learn about thetyping module, and how it’s necessary in most cases when you add type hints. Importing modules necessarily take some time, but how much?
To get some idea about this, create two files:empty_file.py should be an empty file, whileimport_typing.py should contain the following line:
importtypingOn Linux it’s quite easy to check how much time thetypingimport takes using theperf utility, whichPython 3.12 supports:
$perfstat-r1000python3.6import_typing.py Performance counter stats for 'python3.6 import_typing.py' (1000 runs): [ ... extra information hidden for brevity ... ] 0.045161650 seconds time elapsed ( +- 0.77% )So running theimport typing.py script takes about 45 milliseconds. Of course this is not all time spent on importingtyping. Some of this is overhead in starting the Python interpreter, so let’s compare to running Python on an empty file:
$perfstat-r1000python3.6empty_file.py Performance counter stats for 'python3.6 empty_file.py' (1000 runs): [ ... extra information hidden for brevity ... ] 0.028077845 seconds time elapsed ( +- 0.49% )Based on this test, the import of thetyping module takes around 17 milliseconds on Python 3.6.
One of the advertised improvements in Python 3.7 isfaster startup. Let’s see if the results are different:
$perfstat-r1000python3.7import_typing.py [...] 0.025979806 seconds time elapsed ( +- 0.31% )$perfstat-r1000python3.7empty_file.py [...] 0.020002505 seconds time elapsed ( +- 0.30% )Indeed, the general startup time is reduced by about 8 milliseconds, and the time to importtyping is down from 17 to around 6 milliseconds—almost 3 times faster.
Usingtimeit
There are similar tools on other platforms. Python itself comes with thetimeit module in the standard library. Typically, we would directly usetimeit for the timings above. However,timeit struggles to time imports reliably because Python is clever about importing modules only once. Consider the following example:
$python3.6-mtimeit"import typing"10000000 loops, best of 3: 0.134 usec per loopWhile you get a result, you should be suspicious about the result: 0.1 microsecond is more than 100000 times faster than whatperf measured! Whattimeit has actually done is to run theimport typing statement 30 million times, with Python actually only importingtyping once.
To get reasonable results you can telltimeit to only run once:
$python3.6-mtimeit-n1-r1"import typing"1 loops, best of 1: 9.77 msec per loop$python3.7-mtimeit-n1-r1"import typing"1 loop, best of 1: 1.97 msec per loopThese results are on the same scale as the results fromperf above. However, since these are based on only one execution of the code, they are not as reliable as those based on multiple runs.
The conclusion in both these cases is that importingtyping takes a few milliseconds. For the majority of programs and scripts you write this will probably not be an issue.
The Newimporttime Option
In Python 3.7 there is also a new command line option that can be used to figure out how much time imports take. Using-X importtime you’ll get a report about all imports that are made:
$python3.7-Ximporttimeimport_typing.pyimport time: self [us] | cumulative | imported package[ ... some information hidden for brevity ... ]import time: 358 | 358 | zipimportimport time: 2107 | 14610 | siteimport time: 272 | 272 | collections.abcimport time: 664 | 3058 | reimport time: 3044 | 6373 | typingThis shows a similar result. Importingtyping takes about 6 milliseconds. If you’ll read the report closely you can notice that around half of this time is spent on importing thecollections.abc andre modules whichtyping depends on.
So, should you use static type checking in your own code? Well, it’s not an all-or-nothing question. Luckily, Python supports the concept ofgradual typing. This means that you can gradually introduce types into your code. Code without type hints will be ignored by the static type checker. Therefore, you can start adding types to critical components, and continue as long as it adds value to you.
Looking at the lists above of pros and cons you’ll notice that adding types will have no effect on your running program or the users of your program. Type checking is meant to make your life as a developer better and more convenient.
A few rules of thumb on whether to add types to your project are:
If you are just beginning to learn Python, you can safely wait with type hints until you have more experience.
Type hints add little value inshort throw-away scripts.
In libraries that will be used by others, especially onespublished on PyPI, type hints add a lot of value. Other code using your libraries need these type hints to be properly type checked itself. For examples of projects using type hints see
cursive_re,black, our ownReal Python Reader, andMypy itself.In bigger projects, type hints help you understand how types flow through your code, and are highly recommended. Even more so in projects where you cooperate with others.
In his excellent articleThe State of Type Hints in Python Bernát Gábor recommends that “type hints should be used whenever unit tests are worth writing.” Indeed, type hints play a similar role astests in your code: they help you as a developer write better code.
Hopefully you now have an idea about how type checking works in Python and whether it’s something you would like to employ in your own projects.
In the rest of this guide, we’ll go into more detail about the Python type system, including how you run static type checkers (with particular focus on mypy), how you type check code that uses libraries without type hints, and how you use annotations at runtime.
Annotations
Annotations wereintroduced in Python 3.0, originally without any specific purpose. They were simply a way to associate arbitrary expressions to function arguments and return values.
Years later,PEP 484 defined how to add type hints to your Python code, based off work that Jukka Lehtosalo had done on his Ph.D. project—mypy. The main way to add type hints is using annotations. As type checking is becoming more and more common, this also means that annotations should mainly be reserved for type hints.
The next sections explain how annotations work in the context of type hints.
Function Annotations
For functions, you can annotate arguments and the return value. This is done as follows:
deffunc(arg:arg_type,optarg:arg_type=default)->return_type:...For arguments the syntax isargument: annotation, while the return type is annotated using-> annotation. Note that the annotation must be a valid Python expression.
The following simple example adds annotations to a function that calculates the circumference of a circle:
importmathdefcircumference(radius:float)->float:return2*math.pi*radiusWhen running the code, you can also inspect the annotations. They are stored in a special.__annotations__ attribute on the function:
>>>circumference(1.23)7.728317927830891>>>circumference.__annotations__{'radius': <class 'float'>, 'return': <class 'float'>}Sometimes you might be confused by how mypy is interpreting your type hints. For those cases there are special mypy expressions:reveal_type() andreveal_locals(). You can add these to your code before running mypy, and mypy will dutifully report which types it has inferred. As an example, save the following code toreveal.py:
1# reveal.py 2 3importmath 4reveal_type(math.pi) 5 6radius=1 7circumference=2*math.pi*radius 8reveal_locals()Next, run this code through mypy:
$mypyreveal.pyreveal.py:4: error: Revealed type is 'builtins.float'reveal.py:8: error: Revealed local types are:reveal.py:8: error: circumference: builtins.floatreveal.py:8: error: radius: builtins.intEven without any annotations mypy has correctly inferred the types of the built-inmath.pi, as well as our local variablesradius andcircumference.
Note: The reveal expressions are only meant as a tool helping you add types and debug your type hints. If you try to run thereveal.py file as a Python script it will crash with aNameError sincereveal_type() is not a function known to the Python interpreter.
If mypy says that “Name ‘reveal_locals’ is not defined” you might need to update your mypy installation. Thereveal_locals() expression is available inmypy version 0.610 and later.
Variable Annotations
In the definition ofcircumference() in the previous section, you only annotated the arguments and the return value. You did not add any annotations inside the function body. More often than not, this is enough.
However, sometimes the type checker needs help in figuring out the types of variables as well. Variable annotations were defined inPEP 526 and introduced in Python 3.6. The syntax is the same as for function argument annotations:
pi:float=3.142defcircumference(radius:float)->float:return2*pi*radiusThe variablepi has been annotated with thefloat type hint.
Note: Static type checkers are more than able to figure out that3.142 is a float, so in this example the annotation ofpi is not necessary. As you learn more about the Python type system, you’ll see more relevant examples of variable annotations.
Annotations of variables are stored in the module level__annotations__ dictionary:
>>>circumference(1)6.284>>>__annotations__{'pi': <class 'float'>}You’re allowed to annotate a variable without giving it a value. This adds the annotation to the__annotations__ dictionary, while the variable remains undefined:
>>>nothing:str>>>nothingNameError: name 'nothing' is not defined>>>__annotations__{'nothing': <class 'str'>}Since no value was assigned tonothing, the namenothing is not yet defined.
Type Comments
As mentioned, annotations were introduced in Python 3, and they’ve not been backported to Python 2. This means that if you’re writing code that needs to supportlegacy Python, you can’t use annotations.
Instead, you can use type comments. These are specially formatted comments that can be used to add type hints compatible with older code. To add type comments to a function you do something like this:
importmathdefcircumference(radius):# type: (float) -> floatreturn2*math.pi*radiusThe type comments are just comments, so they can be used in any version of Python.
Type comments are handled directly by the type checker, so these types are not available in the__annotations__ dictionary:
>>>circumference.__annotations__{}A type comment must start with thetype: literal, and be on the same or the following line as the function definition. If you want to annotate a function with several arguments, you write each type separated by comma:
defheadline(text,width=80,fill_char="-"):# type: (str, int, str) -> strreturnf"{text.title()} ".center(width,fill_char)print(headline("type comments work",width=40))You are also allowed to write each argument on a separate line with its own annotation:
1# headlines.py 2 3defheadline( 4text,# type: str 5width=80,# type: int 6fill_char="-",# type: str 7):# type: (...) -> str 8returnf"{text.title()} ".center(width,fill_char) 910print(headline("type comments work",width=40))Run the example through Python and mypy:
$pythonheadlines.py---------- Type Comments Work ----------$mypyheadlines.pySuccess: no issues found in 1 source fileIf you have errors, for instance if you happened to callheadline() withwidth="full" on line 10, mypy will tell you:
$mypyheadline.pyheadline.py:10: error: Argument "width" to "headline" has incompatible type "str"; expected "int"You can also add type comments to variables. This is done similarly to how you add type comments to arguments:
pi=3.142# type: floatIn this example,pi will be type checked as a float variable.
So, Type Annotations or Type Comments?
Should you use annotations or type comments when adding type hints to your own code? In short:Use annotations if you can, use type comments if you must.
Annotations provide a cleaner syntax keeping type information closer to your code. They are also theofficially recommended way of writing type hints, and will be further developed and properly maintained in the future.
Type comments are more verbose and might conflict with other kinds of comments in your code likelinter directives. However, they can be used in code bases that don’t support annotations.
There is also hidden option number three:stub files. You will learn about these later, when we discussadding types to third party libraries.
Stub files will work in any version of Python, at the expense of having to maintain a second set of files. In general, you only want to use stub files if you can’t change the original source code.
Playing With Python Types, Part 1
Up until now you’ve only used basic types likestr,float, andbool 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 section you will learn more about this type system, while implementing a simple card game. You will see how to specify:
- The type ofsequences and mappings like tuples, lists and dictionaries
- Type aliases that make code easier to read
- That functions and methodsdo not return anything
- Objects that may be ofany type
After a short detour into sometype theory you will then seeeven more ways to specify types in Python. You can find the code examples from this sectionhere.
Example: A Deck of Cards
The following example shows an implementation of aregular (French) deck of cards:
1# game.py 2 3importrandom 4 5SUITS="♠ ♡ ♢ ♣".split() 6RANKS="2 3 4 5 6 7 8 9 10 J Q K A".split() 7 8defcreate_deck(shuffle=False): 9"""Create a new deck of 52 cards"""10deck=[(s,r)forrinRANKSforsinSUITS]11ifshuffle:12random.shuffle(deck)13returndeck1415defdeal_hands(deck):16"""Deal the cards in the deck into four hands"""17return(deck[0::4],deck[1::4],deck[2::4],deck[3::4])1819defplay():20"""Play a 4-player card game"""21deck=create_deck(shuffle=True)22names="P1 P2 P3 P4".split()23hands={n:hforn,hinzip(names,deal_hands(deck))}2425forname,cardsinhands.items():26card_str=" ".join(f"{s}{r}"for(s,r)incards)27print(f"{name}:{card_str}")2829if__name__=="__main__":30play()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:
$pythongame.pyP4: ♣9 ♢9 ♡2 ♢7 ♡7 ♣A ♠6 ♡K ♡5 ♢6 ♢3 ♣3 ♣QP1: ♡A ♠2 ♠10 ♢J ♣10 ♣4 ♠5 ♡Q ♢5 ♣6 ♠A ♣5 ♢4P2: ♢2 ♠7 ♡8 ♢K ♠3 ♡3 ♣K ♠J ♢A ♣7 ♡6 ♡10 ♠KP3: ♣2 ♣8 ♠8 ♣J ♢Q ♡9 ♡J ♠4 ♢8 ♢10 ♠9 ♡4 ♠QYou 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 functionscreate_deck(),deal_hands(), andplay(). 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 likestr,float, andbool, adding type hints is as easy as using the type itself:
>>>name:str="Guido">>>pi:float=3.142>>>centered:bool=FalseWith 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 ofnames[2],version[0], andoptions["centered"]? In this concrete case you can see that they arestr,int, andbool, respectively. However, the type hints themselves give no information about this.
Instead, you should use the special types defined in thetyping module. These types add syntax for specifying the types of elements of composite types. You can write the following:
>>>fromtypingimportDict,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:
namesis a list of stringsversionis a 3-tuple consisting of three integersoptionsis a dictionary mapping strings to Boolean values
Thetyping module contains many more composite types, includingCounter,Deque,FrozenSet,NamedTuple, andSet. 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 asTuple[str, str], so the type of the deck of cards becomesList[Tuple[str, str]]. Therefore you can annotatecreate_deck() as follows:
8defcreate_deck(shuffle:bool=False)->List[Tuple[str,str]]: 9"""Create a new deck of 52 cards"""10deck=[(s,r)forrinRANKSforsinSUITS]11ifshuffle:12random.shuffle(deck)13returndeckIn addition to the return value, you’ve also added thebool type to the optionalshuffle 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 writeTuple[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 ofsequence, and not really care whether it is a list or a tuple. In these cases you should usetyping.Sequence when annotating the function argument:
fromtypingimportList,Sequencedefsquare(elems:Sequence[float])->List[float]:return[x**2forxinelems]UsingSequence is an example of using duck typing. ASequence is anything that supportslen() and.__getitem__(), independent of its actual type.
Type Aliases
The type hints might become quite oblique when working with nested types like the deck of cards. You may need to stare atList[Tuple[str, str]] a bit before figuring out that it matches our representation of a deck of cards.
Now consider how you would annotatedeal_hands():
15defdeal_hands(16deck:List[Tuple[str,str]]17)->Tuple[18List[Tuple[str,str]],19List[Tuple[str,str]],20List[Tuple[str,str]],21List[Tuple[str,str]],22]:23"""Deal the cards in the deck into four hands"""24return(deck[0::4],deck[1::4],deck[2::4],deck[3::4])That’s just terrible!
Recall 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 createCard andDeck type aliases:
fromtypingimportList,TupleCard=Tuple[str,str]Deck=List[Card]Card can now be used in type hints or in the definition of new type aliases, likeDeck in the example above.
Using these aliases, the annotations ofdeal_hands() become much more readable:
15defdeal_hands(deck:Deck)->Tuple[Deck,Deck,Deck,Deck]:16"""Deal the cards in the deck into four hands"""17return(deck[0::4],deck[1::4],deck[2::4],deck[3::4])Type aliases are great for making your code and its intent clearer. At the same time, these aliases can be inspected to see what they represent:
>>>fromtypingimportList,Tuple>>>Card=Tuple[str,str]>>>Deck=List[Card]>>>Decktyping.List[typing.Tuple[str, str]]Note that when printingDeck, it shows that it’s an alias for a list of 2-tuples of strings.
Functions Without Return Values
You may know that functions without an explicit return still returnNone:
>>>defplay(player_name):...print(f"{player_name} plays")...>>>ret_val=play("Jacob")Jacob plays>>>print(ret_val)NoneWhile such functions technically return something, that return value is not useful. You should add type hints saying as much by usingNone also as the return type:
1# play.py 2 3defplay(player_name:str)->None: 4print(f"{player_name} plays") 5 6ret_val=play("Filip")The annotations help catch the kinds of subtle bugs where you are trying to use a meaningless return value. Mypy will give you a helpful warning:
$mypyplay.pyplay.py:6: error: "play" does not return a valueNote that being explicit about a function not returning anything is different from not adding a type hint about the return value:
# play.pydefplay(player_name:str):print(f"{player_name} plays")ret_val=play("Henrik")In this latter case, mypy has no information about the return value so it will not generate any warning:
$mypyplay.pySuccess: no issues found in 1 source fileAs a more exotic case, note that you can also annotate functions that are never expected to return normally. This is done usingNoReturn:
fromtypingimportNoReturndefblack_hole()->NoReturn:raiseException("There is no going back ...")Sinceblack_hole() always raises an exception, it will never return properly.
Example: Play Some Cards
Let’s return to ourcard game example. In this second version of the game, we deal a hand of cards to each player as before. Then a start player is chosen and the players take turns playing their cards. There are not really any rules in the game though, so the players will just play random cards:
1# game.py 2 3importrandom 4fromtypingimportList,Tuple 5 6SUITS="♠ ♡ ♢ ♣".split() 7RANKS="2 3 4 5 6 7 8 9 10 J Q K A".split() 8 9Card=Tuple[str,str]10Deck=List[Card]1112defcreate_deck(shuffle:bool=False)->Deck:13"""Create a new deck of 52 cards"""14deck=[(s,r)forrinRANKSforsinSUITS]15ifshuffle:16random.shuffle(deck)17returndeck1819defdeal_hands(deck:Deck)->Tuple[Deck,Deck,Deck,Deck]:20"""Deal the cards in the deck into four hands"""21return(deck[0::4],deck[1::4],deck[2::4],deck[3::4])2223defchoose(items):24"""Choose and return a random item"""25returnrandom.choice(items)2627defplayer_order(names,start=None):28"""Rotate player order so that start goes first"""29ifstartisNone:30start=choose(names)31start_idx=names.index(start)32returnnames[start_idx:]+names[:start_idx]3334defplay()->None:35"""Play a 4-player card game"""36deck=create_deck(shuffle=True)37names="P1 P2 P3 P4".split()38hands={n:hforn,hinzip(names,deal_hands(deck))}39start_player=choose(names)40turn_order=player_order(names,start=start_player)4142# Randomly play cards from each player's hand until empty43whilehands[start_player]:44fornameinturn_order:45card=choose(hands[name])46hands[name].remove(card)47print(f"{name}:{card[0]+card[1]:<3} ",end="")48print()4950if__name__=="__main__":51play()Note that in addition to changingplay(), we have added two new functions that need type hints:choose() andplayer_order(). Before discussing how we’ll add type hints to them, here is an example output from running the game:
$pythongame.pyP3: ♢10 P4: ♣4 P1: ♡8 P2: ♡QP3: ♣8 P4: ♠6 P1: ♠5 P2: ♡KP3: ♢9 P4: ♡J P1: ♣A P2: ♡AP3: ♠Q P4: ♠3 P1: ♠7 P2: ♠AP3: ♡4 P4: ♡6 P1: ♣2 P2: ♠KP3: ♣K P4: ♣7 P1: ♡7 P2: ♠2P3: ♣10 P4: ♠4 P1: ♢5 P2: ♡3P3: ♣Q P4: ♢K P1: ♣J P2: ♡9P3: ♢2 P4: ♢4 P1: ♠9 P2: ♠10P3: ♢A P4: ♡5 P1: ♠J P2: ♢QP3: ♠8 P4: ♢7 P1: ♢3 P2: ♢JP3: ♣3 P4: ♡10 P1: ♣9 P2: ♡2P3: ♢6 P4: ♣6 P1: ♣5 P2: ♢8In this example, playerP3 was randomly chosen as the starting player. In turn, each player plays a card: firstP3, thenP4, thenP1, and finallyP2. The players keep playing cards as long as they have any left in their hand.
TheAny Type
choose() works for both lists of names and lists of cards (and any other sequence for that matter). One way to add type hints for this would be the following:
importrandomfromtypingimportAny,Sequencedefchoose(items:Sequence[Any])->Any:returnrandom.choice(items)This means more or less what it says:items is a sequence that can contain items of any type andchoose() will return one such item of any type. Unfortunately, this is not that useful. Consider the following example:
1# choose.py 2 3importrandom 4fromtypingimportAny,Sequence 5 6defchoose(items:Sequence[Any])->Any: 7returnrandom.choice(items) 8 9names=["Guido","Jukka","Ivan"]10reveal_type(names)1112name=choose(names)13reveal_type(name)While mypy will correctly infer thatnames is a list of strings, that information is lost after the call tochoose() because of the use of theAny type:
$mypychoose.pychoose.py:10: error: Revealed type is 'builtins.list[builtins.str*]'choose.py:13: error: Revealed type is 'Any'You’ll see a better way shortly. First though, let’s have a more theoretical look at the Python type system, and the special roleAny plays.
Type Theory
This tutorial is mainly a practical guide and we will only scratch the surface of the theory underpinning Python type hints. For more detailsPEP 483 is a good starting point. If you want to get back to the practical examples, feel free toskip to the next section.
Subtypes
One important concept is that ofsubtypes. Formally, we say that a typeT is a subtype ofU if the following two conditions hold:
- Every value from
Tis also in the set of values ofUtype. - Every function from
Utype is also in the set of functions ofTtype.
These two conditions guarantees that even if typeT is different fromU, variables of typeT can always pretend to beU.
For a concrete example, considerT = bool andU = int. Thebool type takes only two values. Usually these are denotedTrue andFalse, but these names are just aliases for the integer values1 and0, respectively:
>>>int(False)0>>>int(True)1>>>True+True2>>>issubclass(bool,int)TrueSince 0 and 1 are both integers, the first condition holds. Above you can see that Booleans can be added together, but they can also do anything else integers can. This is the second condition above. In other words,bool is a subtype ofint.
The importance of subtypes is that a subtype can always pretend to be its supertype. For instance, the following code type checks as correct:
defdouble(number:int)->int:returnnumber*2print(double(True))# Passing in bool instead of intSubtypes are somewhat related to subclasses. In fact all subclasses corresponds to subtypes, andbool is a subtype ofint becausebool is a subclass ofint. However, there are also subtypes that do not correspond to subclasses. For instanceint is a subtype offloat, butint is not a subclass offloat.
Covariant, Contravariant, and Invariant
What happens when you use subtypes inside composite types? For instance, isTuple[bool] a subtype ofTuple[int]? The answer depends on the composite type, and whether that type iscovariant, contravariant, or invariant. This gets technical fast, so let’s just give a few examples:
Tupleis covariant. This means that it preserves the type hierarchy of its item types:Tuple[bool]is a subtype ofTuple[int]becauseboolis a subtype ofint.Listis invariant. Invariant types give no guarantee about subtypes. While all values ofList[bool]are values ofList[int], you can append aninttoList[int]and not toList[bool]. In other words, the second condition for subtypes does not hold, andList[bool]is not a subtype ofList[int].Callableis contravariant in its arguments. This means that it reverses the type hierarchy. You will see howCallableworkslater, but for now think ofCallable[[T], ...]as a function with its only argument being of typeT. An example of aCallable[[int], ...]is thedouble()function defined above. Being contravariant means that if a function operating on aboolis expected, then a function operating on anintwould be acceptable.
In general, you don’t need to keep these expressions straight. However, you should be aware that subtypes and composite types may not be simple and intuitive.
Gradual Typing and Consistent Types
Earlier we mentioned that Python supportsgradual typing, where you can gradually add type hints to your Python code. Gradual typing is essentially made possible by theAny type.
SomehowAny sits both at the top and at the bottom of the type hierarchy of subtypes. Any type behaves as if it is a subtype ofAny, andAny behaves as if it is a subtype of any other type. Looking at the definition of subtypes above this is not really possible. Instead we talk aboutconsistent types.
The typeT is consistent with the typeU ifT is a subtype ofU or eitherT orU isAny.
The type checker only complains about inconsistent types. The takeaway is therefore that you will never see type errors arising from theAny type.
This means that you can useAny to explicitly fall back to dynamic typing, describe types that are too complex to describe in the Python type system, or describe items in composite types. For instance, a dictionary with string keys that can take any type as its values can be annotatedDict[str, Any].
Do remember, though, if you useAny the static type checker will effectively not do any type checking.
Playing With Python Types, Part 2
Let’s return to our practical examples. Recall that you were trying to annotate the generalchoose() function:
importrandomfromtypingimportAny,Sequencedefchoose(items:Sequence[Any])->Any:returnrandom.choice(items)The problem with usingAny is that you are needlessly losing type information. You know that if you pass a list of strings tochoose(), it will return a string. Below you’ll see how to express this using type variables, as well as how to work with:
- Duck types and protocols
- Arguments with
Noneas default value - Class methods
- The type of your own classes
- Variable number of arguments
Type Variables
A type variable is a special variable that can take on any type, depending on the situation.
Let’s create a type variable that will effectively encapsulate the behavior ofchoose():
1# choose.py 2 3importrandom 4fromtypingimportSequence,TypeVar 5 6Choosable=TypeVar("Choosable") 7 8defchoose(items:Sequence[Choosable])->Choosable: 9returnrandom.choice(items)1011names=["Guido","Jukka","Ivan"]12reveal_type(names)1314name=choose(names)15reveal_type(name)A type variable must be defined usingTypeVar from thetyping module. When used, a type variable ranges over all possible types and takes the most specific type possible. In the example,name is now astr:
$mypychoose.pychoose.py:12: error: Revealed type is 'builtins.list[builtins.str*]'choose.py:15: error: Revealed type is 'builtins.str*'Consider a few other examples:
1# choose_examples.py 2 3fromchooseimportchoose 4 5reveal_type(choose(["Guido","Jukka","Ivan"])) 6reveal_type(choose([1,2,3])) 7reveal_type(choose([True,42,3.14])) 8reveal_type(choose(["Python",3,7]))The first two examples should have typestr andint, but what about the last two? The individual list items have different types, and in that case theChoosable type variable does its best to accommodate:
$mypychoose_examples.pychoose_examples.py:5: error: Revealed type is 'builtins.str*'choose_examples.py:6: error: Revealed type is 'builtins.int*'choose_examples.py:7: error: Revealed type is 'builtins.float*'choose_examples.py:8: error: Revealed type is 'builtins.object*'As you’ve already seenbool is a subtype ofint, which again is a subtype offloat. So in the third example the return value ofchoose() is guaranteed to be something that can be thought of as afloat. In the last example, there is no subtype relationship betweenstr andint, so the best that can be said about the return value is that it is an object.
Note that none of these examples raised a type error. Is there a way to tell the type checker thatchoose() should accept both strings and numbers, but not both at the same time?
You can constrain type variables by listing the acceptable types:
1# choose.py 2 3importrandom 4fromtypingimportSequence,TypeVar 5 6Choosable=TypeVar("Choosable",str,float) 7 8defchoose(items:Sequence[Choosable])->Choosable: 9returnrandom.choice(items)1011reveal_type(choose(["Guido","Jukka","Ivan"]))12reveal_type(choose([1,2,3]))13reveal_type(choose([True,42,3.14]))14reveal_type(choose(["Python",3,7]))NowChoosable can only be eitherstr orfloat, and mypy will note that the last example is an error:
$mypychoose.pychoose.py:11: error: Revealed type is 'builtins.str*'choose.py:12: error: Revealed type is 'builtins.float*'choose.py:13: error: Revealed type is 'builtins.float*'choose.py:14: error: Revealed type is 'builtins.object*'choose.py:14: error: Value of type variable "Choosable" of "choose" cannot be "object"Also note that in the second example the type is consideredfloat even though the input list only containsint objects. This is becauseChoosable was restricted to strings and floats andint is a subtype offloat.
In our card game we want to restrictchoose() to be used forstr andCard:
Choosable=TypeVar("Choosable",str,Card)defchoose(items:Sequence[Choosable])->Choosable:...We briefly mentioned thatSequence represents both lists and tuples. As we noted, aSequence can be thought of as a duck type, since it can be any object with.__len__() and.__getitem__() implemented.
Duck Types and Protocols
Recall the following example fromthe introduction:
deflen(obj):returnobj.__len__()len() can return the length of any object that has implemented the.__len__() method. How can we add type hints tolen(), and in particular theobj argument?
The answer hides behind the academic sounding termstructural subtyping. One way to categorize type systems is by whether they arenominal orstructural:
In anominal system, comparisons between types are based on names and declarations. The Python type system is mostly nominal, where an
intcan be used in place of afloatbecause of their subtype relationship.In astructural system, comparisons between types are based on structure. You could define a structural type
Sizedthat includes all instances that define.__len__(), irrespective of their nominal type.
There is ongoing work to bring a full-fledged structural type system to Python throughPEP 544 which aims at adding a concept called protocols. Most of PEP 544 is alreadyimplemented in mypy though.
A protocol specifies one or more methods that must be implemented. For example, all classes defining.__len__() fulfill thetyping.Sized protocol. We can therefore annotatelen() as follows:
fromtypingimportSizeddeflen(obj:Sized)->int:returnobj.__len__()Otherexamples of protocols defined in thetyping module includeContainer,Iterable,Awaitable, andContextManager.
You can also define your own protocols. This is done by inheriting fromProtocol and defining the function signatures (with empty function bodies) that the protocol expects. The following example shows howlen() andSized could have been implemented:
fromtyping_extensionsimportProtocolclassSized(Protocol):def__len__(self)->int:...deflen(obj:Sized)->int:returnobj.__len__()At the time of writing the support for self-defined protocols is still experimental and only available through thetyping_extensions module. This module must be explicitly installed fromPyPI by doingpip install typing-extensions.
TheOptional Type
A common pattern in Python is to useNone as a default value for an argument. This is usually done either to avoid problems withmutable default values or to have a sentinel value flagging special behavior.
In the card example, theplayer_order() function usesNone as a sentinel value forstart saying that if no start player is given it should be chosen randomly:
27defplayer_order(names,start=None):28"""Rotate player order so that start goes first"""29ifstartisNone:30start=choose(names)31start_idx=names.index(start)32returnnames[start_idx:]+names[:start_idx]The challenge this creates for type hinting is that in generalstart should be a string. However, it may also take the special non-string valueNone.
In order to annotate such arguments you can use theOptional type:
fromtypingimportSequence,Optionaldefplayer_order(names:Sequence[str],start:Optional[str]=None)->Sequence[str]:...TheOptional type simply says that a variable either has the type specified or isNone. An equivalent way of specifying the same would be using theUnion type:Union[None, str]
Note that when using eitherOptional orUnion you must take care that the variable has the correct type when you operate on it. This is done in the example by testing whetherstart is None. Not doing so would cause both static type errors as well as possible runtime errors:
1# player_order.py 2 3fromtypingimportSequence,Optional 4 5defplayer_order( 6names:Sequence[str],start:Optional[str]=None 7)->Sequence[str]: 8start_idx=names.index(start) 9returnnames[start_idx:]+names[:start_idx]Mypy tells you that you have not taken care of the case wherestart isNone:
$mypyplayer_order.pyplayer_order.py:8: error: Argument 1 to "index" of "list" has incompatible type "Optional[str]"; expected "str"Note: The use ofNone for optional arguments is so common that mypy handles it automatically. Mypy assumes that a default argument ofNone indicates an optional argument even if the type hint does not explicitly say so. You could have used the following:
defplayer_order(names:Sequence[str],start:str=None)->Sequence[str]:...If you don’t want mypy to make this assumption you can turn it off with the--no-implicit-optional command line option.
Example: The Object(ive) of the Game
Let’s rewrite the card game to be moreobject-oriented. This will allow us to discuss how to properly annotate classes and methods.
A more or less direct translation of our card game into code that uses classes forCard,Deck,Player, andGame looks something like the following:
1# game.py 2 3importrandom 4importsys 5 6classCard: 7SUITS="♠ ♡ ♢ ♣".split() 8RANKS="2 3 4 5 6 7 8 9 10 J Q K A".split() 910def__init__(self,suit,rank):11self.suit=suit12self.rank=rank1314def__repr__(self):15returnf"{self.suit}{self.rank}"1617classDeck:18def__init__(self,cards):19self.cards=cards2021@classmethod22defcreate(cls,shuffle=False):23"""Create a new deck of 52 cards"""24cards=[Card(s,r)forrinCard.RANKSforsinCard.SUITS]25ifshuffle:26random.shuffle(cards)27returncls(cards)2829defdeal(self,num_hands):30"""Deal the cards in the deck into a number of hands"""31cls=self.__class__32returntuple(cls(self.cards[i::num_hands])foriinrange(num_hands))3334classPlayer:35def__init__(self,name,hand):36self.name=name37self.hand=hand3839defplay_card(self):40"""Play a card from the player's hand"""41card=random.choice(self.hand.cards)42self.hand.cards.remove(card)43print(f"{self.name}:{card!r:<3} ",end="")44returncard4546classGame:47def__init__(self,*names):48"""Set up the deck and deal cards to 4 players"""49deck=Deck.create(shuffle=True)50self.names=(list(names)+"P1 P2 P3 P4".split())[:4]51self.hands={52n:Player(n,h)forn,hinzip(self.names,deck.deal(4))53}5455defplay(self):56"""Play a card game"""57start_player=random.choice(self.names)58turn_order=self.player_order(start=start_player)5960# Play cards from each player's hand until empty61whileself.hands[start_player].hand.cards:62fornameinturn_order:63self.hands[name].play_card()64print()6566defplayer_order(self,start=None):67"""Rotate player order so that start goes first"""68ifstartisNone:69start=random.choice(self.names)70start_idx=self.names.index(start)71returnself.names[start_idx:]+self.names[:start_idx]7273if__name__=="__main__":74# Read player names from command line75player_names=sys.argv[1:]76game=Game(*player_names)77game.play()Now let’s add types to this code.
Type Hints for Methods
First of all type hints for methods work much the same as type hints for functions. The only difference is that theself argument need not be annotated, as it always will be a class instance. The types of theCard class are easy to add:
6classCard: 7SUITS="♠ ♡ ♢ ♣".split() 8RANKS="2 3 4 5 6 7 8 9 10 J Q K A".split() 910def__init__(self,suit:str,rank:str)->None:11self.suit=suit12self.rank=rank1314def__repr__(self)->str:15returnf"{self.suit}{self.rank}"Note that the.__init__() method always should haveNone as its return type.
Classes as Types
There is a correspondence between classes and types. For example, all instances of theCard class together form theCard type. To use classes as types you simply use the name of the class.
For example, aDeck essentially consists of a list ofCard objects. You can annotate this as follows:
17classDeck:18def__init__(self,cards:List[Card])->None:19self.cards=cardsMypy is able to connect your use ofCard in the annotation with the definition of theCard class.
This doesn’t work as cleanly though when you need to refer to the class currently being defined. For example, theDeck.create()class method returns an object with typeDeck. However, you can’t simply add-> Deck as theDeck class is not yet fully defined.
Instead, you are allowed to use string literals in annotations. These strings will only be evaluated by the type checker later, and can therefore contain self and forward references. The.create() method should use such string literals for its types:
20classDeck:21@classmethod22defcreate(cls,shuffle:bool=False)->"Deck":23"""Create a new deck of 52 cards"""24cards=[Card(s,r)forrinCard.RANKSforsinCard.SUITS]25ifshuffle:26random.shuffle(cards)27returncls(cards)Note that thePlayer class also will reference theDeck class. This is however no problem, sinceDeck is defined beforePlayer:
34classPlayer:35def__init__(self,name:str,hand:Deck)->None:36self.name=name37self.hand=handUsually annotations are not used at runtime. This has given wings to the idea ofpostponing the evaluation of annotations. Instead of evaluating annotations as Python expressions and storing their value, the proposal is to store the string representation of the annotation and only evaluate it when needed.
Such functionality is planned to become standard in the still mythicalPython 4.0. However, inPython 3.7 and later, forward references are available through a__future__ import:
from__future__importannotationsclassDeck:@classmethoddefcreate(cls,shuffle:bool=False)->Deck:...With the__future__ import you can useDeck instead of"Deck" even beforeDeck is defined.
Returningself orcls
As noted, you should typically not annotate theself orcls arguments. Partly, this is not necessary asself points to an instance of the class, so it will have the type of the class. In theCard example,self has the implicit typeCard. Also, adding this type explicitly would be cumbersome since the class is not defined yet. You would have to use the string literal syntax,self: "Card".
There is one case where you might want to annotateself orcls, though. Consider what happens if you have a superclass that other classes inherit from, and which has methods that returnself orcls:
1# dogs.py 2 3fromdatetimeimportdate 4 5classAnimal: 6def__init__(self,name:str,birthday:date)->None: 7self.name=name 8self.birthday=birthday 910@classmethod11defnewborn(cls,name:str)->"Animal":12returncls(name,date.today())1314deftwin(self,name:str)->"Animal":15cls=self.__class__16returncls(name,self.birthday)1718classDog(Animal):19defbark(self)->None:20print(f"{self.name} says woof!")2122fido=Dog.newborn("Fido")23pluto=fido.twin("Pluto")24fido.bark()25pluto.bark()While the code runs without problems, mypy will flag a problem:
$mypydogs.pydogs.py:24: error: "Animal" has no attribute "bark"dogs.py:25: error: "Animal" has no attribute "bark"The issue is that even though the inheritedDog.newborn() andDog.twin() methods will return aDog the annotation says that they return anAnimal.
In cases like this you want to be more careful to make sure the annotation is correct. The return type should match the type ofself or the instance type ofcls. This can be done using type variables that keep track of what is actually passed toself andcls:
# dogs.pyfromdatetimeimportdatefromtypingimportType,TypeVarTAnimal=TypeVar("TAnimal",bound="Animal")classAnimal:def__init__(self,name:str,birthday:date)->None:self.name=nameself.birthday=birthday@classmethoddefnewborn(cls:Type[TAnimal],name:str)->TAnimal:returncls(name,date.today())deftwin(self:TAnimal,name:str)->TAnimal:cls=self.__class__returncls(name,self.birthday)classDog(Animal):defbark(self)->None:print(f"{self.name} says woof!")fido=Dog.newborn("Fido")pluto=fido.twin("Pluto")fido.bark()pluto.bark()There are a few things to note in this example:
The type variable
TAnimalis used to denote that return values might be instances of subclasses ofAnimal.We specify that
Animalis an upper bound forTAnimal. Specifyingboundmeans thatTAnimalwill only beAnimalor one of its subclasses. This is needed to properly restrict the types that are allowed.The
typing.Type[]construct is the typing equivalent oftype(). You need it to note that the class method expects a class and returns an instance of that class.
Annotating*args and**kwargs
In theobject oriented version of the game, we added the option to name the players on the command line. This is done by listing player names after the name of the program:
$pythongame.pyGeirArneDanJoannaDan: ♢A Joanna: ♡9 P1: ♣A GeirArne: ♣2Dan: ♡A Joanna: ♡6 P1: ♠4 GeirArne: ♢8Dan: ♢K Joanna: ♢Q P1: ♣K GeirArne: ♠5Dan: ♡2 Joanna: ♡J P1: ♠7 GeirArne: ♡KDan: ♢10 Joanna: ♣3 P1: ♢4 GeirArne: ♠8Dan: ♣6 Joanna: ♡Q P1: ♣Q GeirArne: ♢JDan: ♢2 Joanna: ♡4 P1: ♣8 GeirArne: ♡7Dan: ♡10 Joanna: ♢3 P1: ♡3 GeirArne: ♠2Dan: ♠K Joanna: ♣5 P1: ♣7 GeirArne: ♠JDan: ♠6 Joanna: ♢9 P1: ♣J GeirArne: ♣10Dan: ♠3 Joanna: ♡5 P1: ♣9 GeirArne: ♠QDan: ♠A Joanna: ♠9 P1: ♠10 GeirArne: ♡8Dan: ♢6 Joanna: ♢5 P1: ♢7 GeirArne: ♣4This is implemented by unpacking and passing insys.argv toGame() when it’s instantiated. The.__init__() method uses*names to pack the given names into a tuple.
Regarding type annotations: even thoughnames will be a tuple of strings, you should only annotate the type of each name. In other words, you should usestr and notTuple[str]:
46classGame:47def__init__(self,*names:str)->None:48"""Set up the deck and deal cards to 4 players"""49deck=Deck.create(shuffle=True)50self.names=(list(names)+"P1 P2 P3 P4".split())[:4]51self.hands={52n:Player(n,h)forn,hinzip(self.names,deck.deal(4))53}Similarly, if you have a function or method accepting**kwargs, then you should only annotate the type of each possible keyword argument.
Callables
Functions arefirst-class objects in Python. This means that you can use functions as arguments to other functions. That also means that you need to be able to add type hints representing functions.
Functions, as well as lambdas, methods and classes, arerepresented bytyping.Callable. The types of the arguments and the return value are usually also represented. For instance,Callable[[A1, A2, A3], Rt] represents a function with three arguments with typesA1,A2, andA3, respectively. The return type of the function isRt.
In the following example, the functiondo_twice() calls a given function twice and prints the return values:
1# do_twice.py 2 3fromtypingimportCallable 4 5defdo_twice(func:Callable[[str],str],argument:str)->None: 6print(func(argument)) 7print(func(argument)) 8 9defcreate_greeting(name:str)->str:10returnf"Hello{name}"1112do_twice(create_greeting,"Jekyll")Note the annotation of thefunc argument todo_twice() on line 5. It says thatfunc should be a callable with one string argument, that also returns a string. One example of such a callable iscreate_greeting() defined on line 9.
Most callable types can be annotated in a similar manner. However, if you need more flexibility, check outcallback protocols andextended callable types.
Example: Hearts
Let’s end with a full example of the game ofHearts. You might already know this game from other computer simulations. Here is a quick recap of the rules:
Four players play with a hand of 13 cards each.
The player holding the ♣2 starts the first round, and must play ♣2.
Players take turns playing cards, following the leading suit if possible.
The player playing the highest card in the leading suit wins the trick, and becomes start player in the next turn.
A player can not lead with a ♡ until a ♡ has already been played in an earlier trick.
After all cards are played, players get points if they take certain cards:
- 13 points for the ♠Q
- 1 point for each ♡
A game lasts several rounds, until one player has 100 points or more. The player with the least points wins.
More details can befound online.
There are not many new typing concepts in this example that you have not already seen. We’ll therefore not go through this code in detail, but leave it as an example of annotated code.
You can download this code and other examples fromGitHub:
# hearts.pyfromcollectionsimportCounterimportrandomimportsysfromtypingimportAny,Dict,List,Optional,Sequence,Tuple,UnionfromtypingimportoverloadclassCard:SUITS="♠ ♡ ♢ ♣".split()RANKS="2 3 4 5 6 7 8 9 10 J Q K A".split()def__init__(self,suit:str,rank:str)->None:self.suit=suitself.rank=rank@propertydefvalue(self)->int:"""The value of a card is rank as a number"""returnself.RANKS.index(self.rank)@propertydefpoints(self)->int:"""Points this card is worth"""ifself.suit=="♠"andself.rank=="Q":return13ifself.suit=="♡":return1return0def__eq__(self,other:Any)->Any:returnself.suit==other.suitandself.rank==other.rankdef__lt__(self,other:Any)->Any:returnself.value<other.valuedef__repr__(self)->str:returnf"{self.suit}{self.rank}"classDeck(Sequence[Card]):def__init__(self,cards:List[Card])->None:self.cards=cards@classmethoddefcreate(cls,shuffle:bool=False)->"Deck":"""Create a new deck of 52 cards"""cards=[Card(s,r)forrinCard.RANKSforsinCard.SUITS]ifshuffle:random.shuffle(cards)returncls(cards)defplay(self,card:Card)->None:"""Play one card by removing it from the deck"""self.cards.remove(card)defdeal(self,num_hands:int)->Tuple["Deck",...]:"""Deal the cards in the deck into a number of hands"""returntuple(self[i::num_hands]foriinrange(num_hands))defadd_cards(self,cards:List[Card])->None:"""Add a list of cards to the deck"""self.cards+=cardsdef__len__(self)->int:returnlen(self.cards)@overloaddef__getitem__(self,key:int)->Card:...@overloaddef__getitem__(self,key:slice)->"Deck":...def__getitem__(self,key:Union[int,slice])->Union[Card,"Deck"]:ifisinstance(key,int):returnself.cards[key]elifisinstance(key,slice):cls=self.__class__returncls(self.cards[key])else:raiseTypeError("Indices must be integers or slices")def__repr__(self)->str:return" ".join(repr(c)forcinself.cards)classPlayer:def__init__(self,name:str,hand:Optional[Deck]=None)->None:self.name=nameself.hand=Deck([])ifhandisNoneelsehanddefplayable_cards(self,played:List[Card],hearts_broken:bool)->Deck:"""List which cards in hand are playable this round"""ifCard("♣","2")inself.hand:returnDeck([Card("♣","2")])lead=played[0].suitifplayedelseNoneplayable=Deck([cforcinself.handifc.suit==lead])orself.handifleadisNoneandnothearts_broken:playable=Deck([cforcinplayableifc.suit!="♡"])returnplayableorDeck(self.hand.cards)defnon_winning_cards(self,played:List[Card],playable:Deck)->Deck:"""List playable cards that are guaranteed to not win the trick"""ifnotplayed:returnDeck([])lead=played[0].suitbest_card=max(cforcinplayedifc.suit==lead)returnDeck([cforcinplayableifc<best_cardorc.suit!=lead])defplay_card(self,played:List[Card],hearts_broken:bool)->Card:"""Play a card from a cpu player's hand"""playable=self.playable_cards(played,hearts_broken)non_winning=self.non_winning_cards(played,playable)# Strategyifnon_winning:# Highest card not winning the trick, prefer pointscard=max(non_winning,key=lambdac:(c.points,c.value))eliflen(played)<3:# Lowest card maybe winning, avoid pointscard=min(playable,key=lambdac:(c.points,c.value))else:# Highest card guaranteed winning, avoid pointscard=max(playable,key=lambdac:(-c.points,c.value))self.hand.cards.remove(card)print(f"{self.name} ->{card}")returncarddefhas_card(self,card:Card)->bool:returncardinself.handdef__repr__(self)->str:returnf"{self.__class__.__name__}({self.name!r},{self.hand})"classHumanPlayer(Player):defplay_card(self,played:List[Card],hearts_broken:bool)->Card:"""Play a card from a human player's hand"""playable=sorted(self.playable_cards(played,hearts_broken))p_str=" ".join(f"{n}:{c}"forn,cinenumerate(playable))np_str=" ".join(repr(c)forcinself.handifcnotinplayable)print(f"{p_str} (Rest:{np_str})")whileTrue:try:card_num=int(input(f"{self.name}, choose card: "))card=playable[card_num]except(ValueError,IndexError):passelse:breakself.hand.play(card)print(f"{self.name} =>{card}")returncardclassHeartsGame:def__init__(self,*names:str)->None:self.names=(list(names)+"P1 P2 P3 P4".split())[:4]self.players=[Player(n)forninself.names[1:]]self.players.append(HumanPlayer(self.names[0]))defplay(self)->None:"""Play a game of Hearts until one player go bust"""score=Counter({n:0forninself.names})whileall(s<100forsinscore.values()):print("\nStarting new round:")round_score=self.play_round()score.update(Counter(round_score))print("Scores:")forname,total_scoreinscore.most_common(4):print(f"{name:<15}{round_score[name]:>3}{total_score:>3}")winners=[nforninself.namesifscore[n]==min(score.values())]print(f"\n{' and '.join(winners)} won the game")defplay_round(self)->Dict[str,int]:"""Play a round of the Hearts card game"""deck=Deck.create(shuffle=True)forplayer,handinzip(self.players,deck.deal(4)):player.hand.add_cards(hand.cards)start_player=next(pforpinself.playersifp.has_card(Card("♣","2")))tricks={p.name:Deck([])forpinself.players}hearts=False# Play cards from each player's hand until emptywhilestart_player.hand:played:List[Card]=[]turn_order=self.player_order(start=start_player)forplayerinturn_order:card=player.play_card(played,hearts_broken=hearts)played.append(card)start_player=self.trick_winner(played,turn_order)tricks[start_player.name].add_cards(played)print(f"{start_player.name} wins the trick\n")hearts=heartsorany(c.suit=="♡"forcinplayed)returnself.count_points(tricks)defplayer_order(self,start:Optional[Player]=None)->List[Player]:"""Rotate player order so that start goes first"""ifstartisNone:start=random.choice(self.players)start_idx=self.players.index(start)returnself.players[start_idx:]+self.players[:start_idx]@staticmethoddeftrick_winner(trick:List[Card],players:List[Player])->Player:lead=trick[0].suitvalid=[(c.value,p)forc,pinzip(trick,players)ifc.suit==lead]returnmax(valid)[1]@staticmethoddefcount_points(tricks:Dict[str,Deck])->Dict[str,int]:return{n:sum(c.pointsforcincards)forn,cardsintricks.items()}if__name__=="__main__":# Read player names from the command lineplayer_names=sys.argv[1:]game=HeartsGame(*player_names)game.play()Here are a few points to note in the code:
For type relationships that are hard to express using
Unionor type variables, you can use the@overloaddecorator. SeeDeck.__getitem__()for an example andthe documentation for more information.Subclasses correspond to subtypes, so that a
HumanPlayercan be used wherever aPlayeris expected.When a subclass reimplements a method from a superclass, the type annotations must match. See
HumanPlayer.play_card()for an example.
When starting the game, you control the first player. Enter numbers to choose which cards to play. The following is an example of game play, with the highlighted lines showing where the player made a choice:
$pythonhearts.pyGeirArneAldrenJoannaBradStarting new round:Brad -> ♣2 0: ♣5 1: ♣Q 2: ♣K (Rest: ♢6 ♡10 ♡6 ♠J ♡3 ♡9 ♢10 ♠7 ♠K ♠4) GeirArne, choose card: 2GeirArne => ♣KAldren -> ♣10Joanna -> ♣9GeirArne wins the trick 0: ♠4 1: ♣5 2: ♢6 3: ♠7 4: ♢10 5: ♠J 6: ♣Q 7: ♠K (Rest: ♡10 ♡6 ♡3 ♡9) GeirArne, choose card: 0GeirArne => ♠4Aldren -> ♠5Joanna -> ♠3Brad -> ♠2Aldren wins the trick...Joanna -> ♡JBrad -> ♡2 0: ♡6 1: ♡9 (Rest: ) GeirArne, choose card: 1GeirArne => ♡9Aldren -> ♡AAldren wins the trickAldren -> ♣AJoanna -> ♡QBrad -> ♣J 0: ♡6 (Rest: ) GeirArne, choose card: 0GeirArne => ♡6Aldren wins the trickScores:Brad 14 14Aldren 10 10GeirArne 1 1Joanna 1 1Static Type Checking
So far you have seen how to add type hints to your code. In this section you’ll learn more about how to actually perform static type checking of Python code.
The Mypy Project
Mypy was started by Jukka Lehtosalo during his Ph.D. studies at Cambridge around 2012. Mypy was originally envisioned as a Python variant with seamless dynamic and static typing. SeeJukka’s slides from PyCon Finland 2012 for examples of the original vision of mypy.
Most of those original ideas still play a big part in the mypy project. In fact, the slogan “Seamless dynamic and static typing” is stillprominently visible on mypy’s home page and describes the motivation for using type hints in Python well.
The biggest change since 2012 is that mypy is no longer avariant of Python. In its first versions mypy was a stand-alone language that was compatible with Python except for its type declarations. Following asuggestion by Guido van Rossum, Mypy was rewritten to use annotations instead. Today, mypy is a static type checker forregular Python code.
Running Mypy
Before running mypy for the first time, you must install the program. This is most easily done usingpip:
$pipinstallmypyWith mypy installed, you can run it as a regular command line program:
$mypymy_program.pyRunning Mypy on yourmy_program.py Python file will check it for type errors without actually executing the code.
There are many available options when type checking your code. As Mypy is still under very active development, command line options are liable to change between versions. You should refer to Mypy’s help to see which settings are default on your version:
$mypy--helpusage: mypy [-h] [-v] [-V] [more options; see below] [-m MODULE] [-p PACKAGE] [-c PROGRAM_TEXT] [files ...]Mypy is a program that will type check your Python code.[... The rest of the help hidden for brevity ...]Additionally, themypy command line documentation online has a lot of information.
Let’s look at some of the most common options. First of all, if you are using third-party packages without type hints, you may want to silence Mypy’s warnings about these. This can be done with the--ignore-missing-imports option.
The following example usesNumpy to calculate and print the cosine of several numbers:
1# cosine.py 2 3importnumpyasnp 4 5defprint_cosine(x:np.ndarray)->None: 6withnp.printoptions(precision=3,suppress=True): 7print(np.cos(x)) 8 9x=np.linspace(0,2*np.pi,9)10print_cosine(x)Note thatnp.printoptions() is only available in version 1.15 and later of NumPy. Running this example prints some numbers to the console:
$pythoncosine.py[ 1. 0.707 0. -0.707 -1. -0.707 -0. 0.707 1. ]The actual output of this example is not important. However, you should note that the argumentx is annotated withnp.ndarray on line 5, as we want to print the cosine of a full array of numbers.
You can run mypy on this file as usual:
$mypycosine.pycosine.py:3: error: No library stub file for module 'numpy'cosine.py:3: note: (Stub files are from https://github.com/python/typeshed)These warnings may not immediately make much sense to you, but you’ll learn aboutstubs andtypeshed soon. You can essentially read the warnings as mypy saying that the NumPy package does not contain type hints.
In most cases, missing type hints in third-party packages is not something you want to be bothered with so you can silence these messages:
$mypy--ignore-missing-importscosine.pySuccess: no issues found in 1 source fileIf you use the--ignore-missing-import command line option, Mypy willnot try to follow or warn about any missing imports. This might be a bit heavy-handed though, as it also ignores actual mistakes, like misspelling the name of a package.
Two less intrusive ways of handling third-party packages are using type comments or configuration files.
In a simple example as the one above, you can silence thenumpy warning by adding a type comment to the line containing the import:
3importnumpyasnp# type: ignoreThe literal# type: ignore tells Mypy to ignore the import of Numpy.
If you have several files, it might be easier to keep track of which imports to ignore in a configuration file. Mypy reads a file calledmypy.ini in the current directory if it is present. This configuration file must contain a section called[mypy] and may contain module specific sections of the form[mypy-module].
The following configuration file will ignore that Numpy is missing type hints:
# mypy.ini[mypy][mypy-numpy]ignore_missing_imports=TrueThere are many options that can be specified in the configuration file. It is also possible to specify a global configuration file. See thedocumentation for more information.
Adding Stubs
Type hints are available for all the packages in the Python standard library. However, if you are using third-party packages you’ve already seen that the situation can be different.
The following example uses theParse package to do simple text parsing. To follow along you should first install Parse:
$pipinstallparseParse can be used to recognize simple patterns. Here is a small program that tries its best to figure out your name:
1# parse_name.py 2 3importparse 4 5defparse_name(text:str)->str: 6patterns=( 7"my name is{name}", 8"i'm{name}", 9"i am{name}",10"call me{name}",11"{name}",12)13forpatterninpatterns:14result=parse.parse(pattern,text)15ifresult:16returnresult["name"]17return""1819answer=input("What is your name? ")20name=parse_name(answer)21print(f"Hi{name}, nice to meet you!")The main flow is defined in the last three lines: ask for your name, parse the answer, and print a greeting. Theparse package is called on line 14 in order to try to find a name based on one of the patterns listed on lines 7-11.
The program can be used as follows:
$pythonparse_name.pyWhat is your name? I am Geir ArneHi Geir Arne, nice to meet you!Note that even though I answerI am Geir Arne, the program figures out thatI am is not part of my name.
Let’s add a small bug to the program, and see if Mypy is able to help us detect it. Change line 16 fromreturn result["name"] toreturn result. This will return aparse.Result object instead of the string containing the name.
Next run Mypy on the program:
$mypyparse_name.pyparse_name.py:3: error: Cannot find module named 'parse'parse_name.py:3: note: (Perhaps setting MYPYPATH or using the "--ignore-missing-imports" flag would help)Mypy prints a similar error to the one you saw in the previous section: It doesn’t know about theparse package. You could try to ignore the import:
$mypyparse_name.py--ignore-missing-importsSuccess: no issues found in 1 source fileUnfortunately, ignoring the import means that Mypy has no way of discovering the bug in our program. A better solution would be to add type hints to the Parse package itself. AsParse is open source you can actually add types to the source code and send a pull request.
Alternatively, you can add the types in astub file. A stub file is a text file that contains the signatures of methods and functions, but not their implementations. Their main function is to add type hints to code that you for some reason can’t change. To show how this works, we will add some stubs for the Parse package.
First of all, you should put all your stub files inside one common directory, and set theMYPYPATH environment variable to point to this directory. On Mac and Linux you can setMYPYPATH as follows:
$exportMYPYPATH=/home/gahjelle/python/stubsYou can set the variable permanently by adding the line to your.bashrc file. On Windows you can click the start menu and search forenvironment variables to setMYPYPATH.
Next, create a file inside your stubs directory that you callparse.pyi. It must be named for the package that you are adding type hints for, with a.pyi suffix. Leave this file empty for now. Then run Mypy again:
$mypyparse_name.pyparse_name.py:14: error: Module has no attribute "parse"If you have set everything up correctly, you should see this new error message. Mypy uses the newparse.pyi file to figure out which functions are available in theparse package. Since the stub file is empty, mypy assumes thatparse.parse() does not exist, and then gives the error you see above.
The following example does not add types for the wholeparse package. Instead it shows the type hints you need to add in order for mypy to type check your use ofparse.parse():
# parse.pyifromtypingimportAny,Mapping,Optional,Sequence,Tuple,UnionclassResult:def__init__(self,fixed:Sequence[str],named:Mapping[str,str],spans:Mapping[int,Tuple[int,int]],)->None:...def__getitem__(self,item:Union[int,str])->str:...def__repr__(self)->str:...defparse(format:str,string:str,evaluate_result:bool=...,case_sensitive:bool=...,)->Optional[Result]:...Theellipsis... are part of the file, and should be written exactly as above. The stub file should only contain type hints for variables, attributes, functions, and methods, so the implementations should be left out and replaced by... markers.
Finally, mypy is able to spot the bug we introduced:
$mypyparse_name.pyparse_name.py:16: error: Incompatible return value type (got "Result", expected "str")This points straight to line 16 and the fact that we return aResult object and not the name string. Changereturn result back toreturn result["name"], and run mypy again to see that it’s happy.
Typeshed
You’ve seen how to use stubs to add type hints without changing the source code itself. In the previous section we added some type hints to the third-party Parse package. Now, it wouldn’t be very effective if everybody needs to create their own stubs files for all third-party packages they are using.
Typeshed is a Github repository that contains type hints for the Python standard library, as well as many third-party packages. Typeshed comes included with mypy so if you are using a package that already has type hints defined in Typeshed, the type checking will just work.
You can alsocontribute type hints to Typeshed. Make sure to get the permission of the owner of the package first though, especially because they might be working on adding type hints into the source code itself—which is thepreferred approach.
Other Static Type Checkers
In this tutorial, we have mainly focused on type checking using mypy. However, there are other static type checkers in the Python ecosystem.
ThePyCharm editor comes with its own type checker included. If you are using PyCharm to write your Python code, it will be automatically type checked.
Facebook has developedPyre. One of its stated goals is to be fast and performant. While there are some differences, Pyre functions mostly similar to mypy. See thedocumentation if you’re interested in trying out Pyre.
Furthermore, Google has createdpytype. This type checker also works mostly the same as mypy. In addition to checking annotated code, Pytype has some support for running type checks on unannotated code and even adding annotations to code automatically. See thequickstart document for more information.
Using Types at Runtime
As a final note, it’s possible to use type hints also at runtime during execution of your Python program. Runtime type checking will probably never be natively supported in Python.
However, the type hints are available at runtime in the__annotations__ dictionary, and you can use those to do type checks if you desire. Before you run off and write your own package for enforcing types, you should know that there are already several packages doing this for you. Have a look atEnforce,Pydantic, orPytypes for some examples.
Another use of type hints is for translating your Python code to C and compiling it for optimization. The popularCython project uses a hybrid C/Python language to write statically typed Python code. However, since version 0.27 Cython has also supported type annotations. Recently, theMypyc project has become available. While not yet ready for general use, it can compile some type annotated Python code to C extensions.
Conclusion
Type hinting in Python is a very useful feature that you can happily live without. Type hints don’t make you capable of writing any code you can’t write without using type hints. Instead, using type hints makes it easier for you to reason about code, find subtle bugs, and maintain a clean architecture.
In this tutorial you have learned how type hinting works in Python, and how gradual typing makes type checks in Python more flexible than in many other languages. You’ve seen some of the pros and cons of using type hints, and how they can be added to code using annotations or type comments. Finally you saw many of the different types that Python supports, as well as how to perform static type checking.
There are many resources to learn more about static type checking in Python.PEP 483 andPEP 484 give a lot of background about how type checking is implemented in Python. Themypy documentation has a greatreference section detailing all the different types available.
Take the Quiz: Test your knowledge with our interactive “Python Type Checking” quiz. You’ll receive a score upon completion to help you track your learning progress:
Interactive Quiz
Python Type CheckingIn this quiz, you'll test your understanding of Python type checking. You'll revisit concepts such as type annotations, type hints, adding static types to code, running a static type checker, and enforcing types at runtime. This knowledge will help you develop your code more efficiently.
Recommended Course
Python Type Checking(1h 7m)
🐍 Python Tricks 💌
Get a short & sweetPython Trick delivered to your inbox every couple of days. No spam ever. Unsubscribe any time. Curated by the Real Python team.

AboutGeir Arne Hjelle
Geir Arne is an avid Pythonista and a member of the Real Python tutorial team.
» More about Geir ArneMasterReal-World Python Skills With Unlimited Access to Real Python
Join us and get access to thousands of tutorials, hands-on video courses, and a community of expert Pythonistas:
MasterReal-World Python Skills
With Unlimited Access to Real Python
Join us and get access to thousands of tutorials, hands-on video courses, and a community of expert Pythonistas:
What Do You Think?
What’s your #1 takeaway or favorite thing you learned? How are you going to put your newfound skills to use? Leave a comment below and let us know.
Commenting Tips: The most useful comments are those written with the goal of learning from or helping out other students.Get tips for asking good questions andget answers to common questions in our support portal.
Looking for a real-time conversation? Visit theReal Python Community Chat or join the next“Office Hours” Live Q&A Session. Happy Pythoning!
Keep Learning
Keep reading Real Python by creating a free account or signing in:
Already have an account?Sign-In





