<jupyter_start><jupyter_text>Typing in pythonIn recent years, type annotation syntax and semantics have been gradually introduced into the Python language. Typing in Python is still a fairly new and often misunderstood subject. In this course, we will present its basics, while some more advanced features will be covered in the rest of this text.All programming languages include some kind of type system that formalizes which categories of objects it can work with and how those categories are treated. For instance, a type system can define a numerical type, with 42 as one example of an object of numerical type. Dynamic TypingA language is dynamically typed if the type is associated with run-time values, and not named variables/fields/etc. This means that you as a programmer can write a little quicker because you do not have to specify types every time (unless using a statically-typed language with type inference).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 Consider the following code. In Python, this is totally acceptable.<jupyter_code>age = 21
print(age)  # 21
age = "Twenty One"
print(age)  # Twenty One<jupyter_output><empty_output><jupyter_text>In the code above, the value of ```age``` is first an ```int```, but then we change it to a ```str``` later on. Every variable can represent any value at any point in the program. That is the power of dynamic typing! Let's try to do the same thing in a ```statically typed``` language, like Java.```javaint age = 21;System.out.print(age);age = "Twenty One";System.out.print(age);```We actually end up with the following error.```Error: incompatible types: String cannot be converted to int```That's because we are trying to assign ```"Twenty One"``` (a ```String```) to the variable ```age``` that was declared as an ```int```.  Duck TypingAnother term that is often used when talking about Python is duck 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” (or any 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 call ```len()``` on any Python object that defines a ```.__len__()``` method:<jupyter_code>class TheHobbit:
    def __len__(self):
        return 95022

the_hobbit = TheHobbit()
len(the_hobbit)<jupyter_output><empty_output><jupyter_text>Note that the call to ```len()``` gives the return value of the ```.__len__()``` method. In fact, the implementation of ```len()``` is essentially equivalent to the following:<jupyter_code>def len(obj):
    return obj.__len__()<jupyter_output><empty_output><jupyter_text>In order to call ```len(obj)```, the only real constraint on ___obj___ is that it must define a ```.__len__()``` method. Otherwise, the object can be of types as different as ___str___, ___list___, ___dict___, or ___TheHobbit___.Duck typing is somewhat supported when doing static type checking of Python code, using ___structural subtyping___.  What Are Type Annotations?Type Annotations are a new feature added in PEP 484 that allow for adding **_type hints_** to variables. They are used to inform someone reading the code what the type of a variable **_should be_**. This brings a sense of statically typed control to the dynamically typed Python. This is accomplished by adding ```: ``` after initializing/declaring a variable. An example is shown below, which adds the ```: int``` when we declare the variable to show that ```age``` should be of type ```int```.<jupyter_code>age: int = 5
print(age)<jupyter_output><empty_output><jupyter_text>It is important to note that type annotations do not affect the runtime of the program in any way. These hints are ignored by the interpreter and are used solely to increase the readability for other programmers and yourself. They are not enforced are runtime.  Function Annotations Consider the following code below:<jupyter_code>def mystery_combine(a, b, times):
    return (a + b) * times<jupyter_output><empty_output><jupyter_text>We can see what that function is doing, but do we know what ```a```, ```b```, or ```times``` are supposed to be? Look at the following code, especially at the two lines where we call the ```mystery_combine``` with different types of arguments. Observe each version's output, which is shown in the comments below each block.<jupyter_code>print(mystery_combine(2, 3, 4))
print(mystery_combine('Hello ', 'World! ', 4))<jupyter_output>Hello World! Hello World! Hello World! Hello World!<jupyter_text>Hmm, based on what we pass the function, we get two totally different results. With integers we get some nice math, but when we pass strings to the function, we can see that the first two arguments are concatenated, and that resulting string is multiplied ```times``` times. It turns out that the developer who wrote the function actually anticipated the second version to be the use case of ```mystery_combine```! Using type annotations, we can clear up this confusion.<jupyter_code>def mystery_combine(a: str, b: str, times: int) -> str:
    return (a + b) * times<jupyter_output><empty_output><jupyter_text>We have added ```: str```, ```: str```, and ```: int``` to the function's parameters to show what types they should be. This will hopefully make the code clearer to read, and reveal it's purpose a little more.We also added the ```-> str``` to show that this function will return a ___str___. Using ```-> ```, we can more easily show the return value types of any function or method, to avoid confusion by future developers!  Complex TypesThe previous section handles many of the basic use cases of type annotations, but nothing is ever just basic, so let's break down some more complex cases.For anything more than a primitive in Python, use the ```typing``` class. It describes types to type annotate any variable of any type. It comes preloaded with type annotations such as ```Dict```, ```Tuple```, ```List```, ```Set```, and more! Then you can expand your type hints into use cases like the example below.<jupyter_code>from typing import List

def print_names(names: List[str]) -> None:
    for student in names:
        print(student)<jupyter_output><empty_output><jupyter_text>This will tell the reader that names should be a list of strings. Dictionaries work in a similar fashion.<jupyter_code>from typing import Dict

def print_name_and_grade(grades: Dict[str, float]) -> None:
    for student, grade in grades.items():
        print(student, grade)<jupyter_output><empty_output><jupyter_text>The ```Dict[str, float]``` type hint tells us that ```grades``` should be a dictionary where the keys are strings and the values are floats. Use ```Optional``` when the value will be be either of a certain ___type___ or ```None```, exclusively.<jupyter_code>from typing import Optional

def try_to_print(some_num: Optional[int]):
    if some_num:
        print(some_num)
    else:
        print('Value was None!')<jupyter_output><empty_output><jupyter_text>Use ```Union``` when the value can take on more specific ___types___.<jupyter_code>from typing import Union

def print_grade(grade: Union[int, str]):
    if isinstance(grade, str):
        print(grade + ' percent')
    else:
        print(str(grade) + '%')<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.08637787056367432, 'fraction_numerical': 0.0027400835073068896, 'mean_word_length': 5.102707006369426, 'pattern_counts': {'":': 0, '<': 44, '<?xml version=': 0, '>': 49, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '11', 'dir': 'jupyter-structured-clean-dedup', 'id': '345786', 'license': 'MIT', 'n_tokens_mistral': 2065, 'n_tokens_neox': 2004, 'n_words': 1052, 'path': 'Content/1.python/2.python_advanced/08.Typing/00.Typing.ipynb', 'repo_name': 'becodeorg/ai_fundamentals_1.0', 'size': '7664'}