Stanley Ulili I'm a freelance web developer and researcher from Malawi. I love learning new things, and writing helps me understand and solidify concepts. I hope by sharing my experience, others can learn something from them.

Understanding type annotation in Python

19 min read 5429

Understanding type annotation in Python

Python is highly recognized for being a dynamically typed language, which implies that the datatype of a variable is determined at runtime. In other words, as a Python developer, you are not mandated to declare the data type of the value that a variable accepts because Python realizes the data type of this variable based on the current value it holds.

The flexibility of this feature, however, comes with some disadvantages that you typically would not experience when using a statically typed language like Java or C++:

  • More errors will be detected at runtime that could have been avoided at the development time
  • Absence of compilation could lead to poor performing codes
  • Verbose variables make codes harder to read
  • Incorrect assumptions about the behavior of specific functions
  • Errors due to type mismatch

Python 3.5 introduced type hints, which you can add to your code using the type annotations introduced in Python 3.0. With type hints, you can annotate variables and functions with datatypes. Tools like mypy, pyright, pytypes, or pyre perform the functions of static type-checking and provide hints or warnings when these types are used inconsistently.

This tutorial will explore type hints and how you can add them to your Python code. It will focus on the mypy static type-checking tool and its operations in your code. You’ll learn how to annotate variables, functions, lists, dictionaries, and tuples. You’ll also learn how to work with the Protocol class, function overloading, and annotating constants.

 

Before you begin

To get the most out of this tutorial, you should have:

  • Python ≥3.10 installed
  • Knowledge of how to write functions, f-strings, and running Python code
  • Knowledge of how to use the command-line

We recommend Python ≥3.10, as those versions have new and better type-hinting features. If you’re using Python ≤3.9, Python provides an alternatives type-hint syntax that I’ll demonstrate in the tutorial.

What is static type checking?

When declaring a variable in statically-typed languages like C and Java, you are mandated to declare the data type of the variable. As a result, you cannot assign a value that does not conform to the data type you specified for the variable. For example, if you declare a variable to be an integer, you can’t assign a string value to it at any point in time.

int x = 4;
x = "hello";  // this would trigger a type error

In statically-typed languages, a compiler monitors the code as it is written and strictly ensures that the developer abides by the rules of the language. If no issues are found, the program can be run.

Using static type-checkers has numerous advantages; some of which include:

  • Detecting type errors
  • Preventing bugs
  • Documenting your code — anyone who wants to use an annotated function will know the type of parameters it accepts and the return value type at a glance
  • Additionally, IDEs understand your code much better and offer good autocompletion suggestions

Static typing in Python is optional and can be introduced gradually (this is known as gradual typing). With gradual typing, you can choose to specify the portion of your code that should be dynamically or statically typed. The static type-checkers will ignore the dynamically-typed portions of your code and will not give out warnings on code that does not have type hints nor prevents inconsistent types from compiling during runtime.

What is mypy?

Since Python is by default, a dynamically-typed language, tools like mypy were created to give you the benefits of a statically-typed environment. mypy is a optional static type checker created by Jukka Lehtosalo. It checks for annotated code in Python and emits warnings if annotated types are used inconsistently.

mypy also checks the code syntax and issues syntax errors when it encounters invalid syntax. Additionally, supports gradual typing, allowing you to add type hints in your code slowly at your own pace.

Adding type hints to variables

In Python, you can define a variable with a type hint using the following syntax:

variable_name: type = value

Let’s look at the following variable:

name = "rocket”

You assign a string value "rocket" to the name variable.

To annotate the variable, you need to append a colon (:) after the variable name, and declare a type str:

name: str = "rocket"

In Python, you can read the type hints defined on variables using the __annotations__ dictionary:

>>> name: str = "rocket"
>>> __annotations__
{'name': <class 'str'>}

The __annotations__ dictionary will show you the type hints on all global variables.



As mentioned earlier, the Python interpreter does not enforce types, so defining a variable with a wrong type won’t trigger an error:

>>> name: int = "rocket"
>>>

On the other hand, a static type checker like mypy will flag this as an error:

error: Incompatible types in assignment (expression has type "str", variable has type "int")

Declaring type hints for other data types follows the same syntax. The following are some of the simple types you can use to annotate variables:

  • float: float values, such as 3.10
  • int: integers, such as 3, 7
  • str: strings, such as 'hello'
  • bool: boolean value, which can be True or False
  • bytes: represents byte values, such as b'hello'

Annotating variables with simple types like int, or str may not be necessary because mypy can infer the type. However, when working with complex datatypes like lists, dictionary or tuples, it is important that you declare type hints to the corresponding variables because mypy may struggle to infer types on those variables.

Adding types hints to functions

To annotate a function, declare the annotation after each parameter and the return value:

def function_name(param1: param1_type, param2: param2_type) -> return_type:

Let’s annotate the following function that returns a message:

def announcement(language, version):
    return f"{language} {version} has been released"

announcement("Python", 3.10)

The function accepts a string as the first parameter, a float as the second parameter, and returns a string. To annotate the function parameters, we will append a colon(:) after each parameter and follow it with the parameter type:

  • language: str
  • version: float

To annotate return value type, add -> immediately after closing the parameter parentheses, just before the function definition colon(:):

def announcement(language: str, version: float) -> str:
    ...

The function now has type hints showing that it receives str and float arguments, and returns str.

When you invoke the function, the output should be similar to what is obtained as follows:

result = announcement("Python", 4.11)
print(result) # Python 4.11 has been released

Although our code has type hints, the Python interpreter won’t provide warnings if you invoke the function with wrong arguments:

result = announcement(True, "Python")
print(result) # True Python has been released

The function executes successfully, even when you passed a Boolean True as the first argument , and a string "Python" as the second argument. To receive warnings about these mistakes, we need to use a static type-checker like mypy.

Static type-checking with mypy

We will now begin our tutorial on static type-checking with mypy to get warnings about type errors in our code.

Create a directory called type_hints and move it into the directory:

mkdir type_hints && cd type_hints

Create and activate the virtual environment:

python3.10 -m venv venv
source venv/bin/activate

Install the latest version of mypy with pip:

pip install mypy

With mypy installed, create a file called announcement.py and enter the following code:

def announcement(language, version):
    return f"{language} {version} has been released"

announcement("Python", 3.10)

Save the file and exit. We’re going to reuse the same function from the previous section.

Next, run the file with mypy:

mypy announcement.py
Success: no issues found in 1 source file

As you can see, mypy does not emit any warnings. Static typing in Python is optional, and with gradual typing, you should not receive any warnings unless you opt in by adding type hints to functions. This allows you to annotate your code slowly.

Let’s now understand why mypy doesn’t show us any warnings.


More great articles from LogRocket:


The Any type

As we noted, mypy ignores code with no type hints. This is because it assumes the Any type on code without hints.

The following is how mypy sees the function:

def announcement(language: Any, version: Any) -> Any:
    return f"{language} {version} has been released"

announcement("Python", 3.10)

The Any type is a dynamic type that’s compatible with, well, any type. So mypy will not complain whether the function argument types are bool, int, bytes, etc.

Now that we know why mypy doesn’t always issue warnings, let’s configure it to do that.

Configuring mypy for type checking

mypy can be configured to suit your workflow and code practices. You can run mypy in strict mode, using the --strict option to flag any code without type hints:

mypy --strict announcement.py

announcement.py:1: error: Function is missing a type annotation
announcement.py:4: error: Call to untyped function "print_release" in typed context
Found 2 errors in 1 file (checked 1 source file)

The --strict option is the most restrictive option and doesn’t support gradual typing. Most of the time, you won’t need to be this strict. Instead, adopt gradual typing to add the type hints in phases.

mypy also provides a --disallow-incomplete-defs option. This option flags functions that don’t have all of their parameters and return values annotated. This option is so handy when you forget to annotate a return value or a newly added parameter, causing mypy to warn you. You can think of this as your compiler that reminds you to abide by the rules of static typing in your code development.

To understand this, add the type hints to the parameters only and omit the return value types (pretending you forgot):

def announcement(language: str, version: float):
    return f"{language} {version} has been released"

announcement("Python", 3.10)

Run the file with mypy without any command-line option:

mypy announcement.py
Success: no issues found in 1 source file

As you can see, mypy does not warn us that we forgot to annotate the return type. It assumes the Any type on the return value. If the function was large, it would be difficult to figure out the type of value it returns. To know the type, we would have to inspect the return value, which is time-consuming.

To protect ourselves from these issues, pass the --disallow-incomplete-defs option to mypy:

mypy --disallow-incomplete-defs announcement.py

announcement.py:1: error: Function is missing a return type annotation
Found 1 error in 1 file (checked 1 source file

Now run the file again with the --disallow-incomplete-defs option enabled:

def announcement(language: str, version: float) -> str:
    ...
mypy --disallow-incomplete-defs announcement.py
Success: no issues found in 1 source file

Not only does the --disallow-incomplete-defs option warn you about missing type hint, it also flags any datatype-value mismatch. Consider the example below where bool and str values are passed as arguments to a function that accepts str and float respectively:

def announcement(language: str, version: float) -> str:
    return f"{language} {version} has been released"

announcement(True, "Python")  # bad arguments

Let’s see if mypy will warn us about this now:

mypy --disallow-incomplete-defs announcement.py
announcement.py:4: error: Argument 1 to "print_release" has incompatible type "bool"; expected "str"
announcement.py:4: error: Argument 2 to "print_release" has incompatible type "str"; expected "float"
Found 2 errors in 1 file (checked 1 source file)

Great! mypy warns us that we passed the wrong arguments to the function.

Now, let’s eliminate the need to type mypy with the --disallow-incomplete-defs option.

mypy allows you save the options in a mypy.ini file. When running mypy, it will check the file and run with the options saved in the file.

You don’t necessarily need to add the --disallow-incomplete-defs option each time you run the file using mypy. Mypy gives you an alternative of adding this configuration in a mypy.ini file where you can add some mypy configurations.

Create the mypy.ini file in your project root directory and enter the following code:

[mypy]
python_version = 3.10
disallow_incomplete_defs = True

In the mypy.ini file, we tell mypy that we are using Python 3.10 and that we want to disallow incomplete function definitions.

Save the file in your project, and next time you can run mypy without any command-line options:

mypy  announcement.py
Success: no issues found in 1 source file

mypy has many options you can add in the mypy file. I recommend referring to the mypy command line documentation to learn more.

Adding type hints to functions without return statements

Not all functions have a return statement. When you create a function with no return statement, it still returns a None value:

def announcement(language: str, version: float):
    print(f"{language} {version} has been released")


result = announcement("Python", 4.11)
print(result)  # None

The None value isn’t totally useful as you may not be able to perform an operation with it. It only shows that the function was executed successfully. You can hint that a function has no return type by annotating the return value with None:

def announcement(language: str, version: float) -> None:
    ...

Adding union type hints in function parameters

When a function accepts a parameter of more than one type, you can use the union character (|) to separate the types.

For example, the following function accepts a parameter that can be either str or int:

def show_type(num):
    if(isinstance(num, str)):
        print("You entered a string")
    elif (isinstance(num, int)):
        print("You entered an integer")

show_type('hello') # You entered a string
show_type(3)       # You entered an integer

You can invoke the function show_type with a string or an integer, and the output depends on the data type of the argument it receives.

To annotate the parameter, we will use the union character |, which was introduced in Python 3.10, to separate the types as follows:

def show_type(num: str | int) -> None:
...

show_type('hello')
show_type(3)

The union | now shows that the parameter num is either str or int.

If you’re using Python ≤3.9, you need to import Union from the typing module. The parameter can be annotated as follows:

from typing import Union

def show_type(num: Union[str, int]) -> None:
    ...

Adding type hints to optional function parameters

Not all parameters in a function are required; some are optional. Here’s an example of a function that takes an optional parameter:

def format_name(name: str, title = None) -> str:
    if title:
        return f"Name: {title}. {name.title()}"
    else:
        return f"Name: {name.title()}"

format_name("john doe", "Mr")

The second parameter title is an optional parameter that has a default value of None if it receives no argument at the point of invoking the function. The typing module provides the Optional[<datatype>] annotation to annotate this optional parameter with a type hint:

parameter_name: Optional[<datatype>] = <default_datatype>

Below is an example of how you can perform this annotation:

from typing import Optional

def format_name(name: str, title: Optional[str] = None) -> str:
    ...

format_name("john doe", "Mr")

Adding type hints to lists

Python lists are annotated based on the types of the elements they have or expect to have. Starting with Python ≥3.9, to annotate a list, you use the list type, followed by []. [] contains the element’s type data type.

For example, a list of strings can be annotated as follows:

names: list[str] = ["john", "stanley", "zoe"]

If you’re using Python ≤3.8, you need to import List from the typing module:

from typing import List

names: List[str] = ["john", "stanley", "zoe"]

In function definitions, the Python documentation recommends that the list type should be used to annotate the return types:

def print_names(names: str) -> list[int]:
...

However, for function parameters, the documentation recommends using these abstract collection types:

  • Iterable
  • Sequence

When to use the Iterable type to annotate function parameters

The Iterable type should be used when the function takes an iterable and iterates over it.

An iterable is an object that can return one item at a time. Examples range from lists, tuples, and strings to anything that implements the __iter__ method.

You can annotate an Iterable as follows, in Python ≥3.9:

from collections.abc import Iterable

def double_elements(items: Iterable[int]) -> list[int]:
    return [item * 2 for item in items]

print(double_elements([2, 4, 6])) # list
print(double_elements((2, 4)))     # tuple

In the function, we define the items parameter and assign it an Iterable[int] type hint, which specifies that the Iterable contains int elements.

The Iterable type hint accepts anything that has the __iter__ method implemented. Lists and tuples have the method implemented, so you can invoke the double_elements function with a list or a tuple, and the function will iterate over them.

To use Iterable in Python ≤3.8, you have to import it from the typing module:

from typing import Iterable
...

Using Iterable in parameters is more flexible than if we had a list type hint or any other objects that implements the __iter__ method. This is because you wouldn’t need to convert a tuple for example, or any other iterable to a list before passing it into the function.

When to use the Sequence type

A sequence is a collection of elements that allows you to access an item or compute its length.

A Sequence type hint can accept a list, string, or tuple. This is because they have special methods: __getitem__ and __len__. When you access an item from a sequence using items[index], the __getitem__ method is used. When getting the length of the sequence len(items), the __len__ method is used.

In the following example, we use the Sequence[int] type to accept a sequence that has integer items:

from collections.abc import Sequence

def get_last_element(data: Sequence[int]) -> int:
    return data[-1]

first_item = get_last_element((3, 4, 5))    # 5
second_item = get_last_element([3, 8]    # 8

This function accepts a sequence and access the last element from it with data[-1]. This uses the __getitem__ method on the sequence to access the last element.

As you can see, we can call the function with a tuple or list and the function works properly. We don’t have to limit parameters to list if all the function does is get an item.

For Python ≤3.8, you need to import Sequence from the typing module:

from typing import Sequence
...

Adding type hints to dictionaries

To add type hints to dictionaries, you use the dict type followed by [key_type, value_type]:

For example, the following dictionary has both the key and the value as a string:

person = { "first_name": "John", "last_name": "Doe"}

You can annotate it as follows:

person: dict[str, str] = { "first_name": "John", "last_name": "Doe"}

The dict type specifies that the person dictionary keys are of type str and values are of type str.

If you’re using Python ≤3.8, you need to import Dict from the typing module.

from typing import Dict

person: Dict[str, str] = { "first_name": "John", "last_name": "Doe"}

In function definitions, the documentation recommends using dict as a return type:

def make_student(name: str) -> dict[str, int]:
    ...

For function parameters, it recommends using these abstract base classes:

  • Mapping
  • MutableMapping

When to use the Mapping class

In function parameters, when you use the dict type hints, you limit the arguments the function can take to only dict, defaultDict, or OrderedDict. But, there are many dictionary subtypes, such as UserDict and ChainMap, that can be used similarly.

You can access an element and iterate or compute their length like you can with a dictionary. This is because they implement:

  • __getitem__: for accessing an element
  • __iter__: for iterating
  • __len__: computing the length

So instead of limiting the structures the parameter accepts, you can use a more generic type Mapping since it accepts:

  • dict
  • UserDict
  • defaultdict
  • OrderedDict
  • ChainMap

Another benefit of the Mapping type is that it specifies that you are only reading the dictionary and not mutating it.

The following example is a function that access items values from a dictionary:

from collections.abc import Mapping

def get_full_name(student: Mapping[str, str]) -> str:
    return f'{student.get("first_name")} {student.get("last_name")}'

john = {
  "first_name": "John",
  "last_name": "Doe",
}

get_full_name(john)

The Mapping type hint in the above function has the [str, str] depiction that specifies that the student data structure has keys and values both of type str.

If you’re using Python ≤3.8, import Mapping from the typing module:

from typing import Mapping

Using the MutableMapping class as a type hint

Use MutableMapping as a type hint in a parameter when the function needs to mutate the dictionary or its subtypes. Examples of mutation are deleting items or changing item values.

The MutableMapping class accepts any instance that implements the following special methods:

  • __getitem__
  • __setitem__
  • __delitem__
  • __iter__
  • __len__

The __delitem__ and __setitem__ methods are used for mutation, and these are methods that separate Mapping type from the MutableMapping type.

In the following example, the function accepts a dictionary and mutates it:

from collections.abc import MutableMapping

def update_first_name(student: MutableMapping[str, str], first_name: str) -> None:
    student["first_name"] = first_name

john = {
    "first_name": "John",
    "last_name": "Doe",
}

update_first_name(john, "james")

In the function body, the value in the first_name variable is assigned to the dictionary and replaces the value paired to the first_name key. Changing a dictionary key value invokes the __setitem__ method.

If you are on Python ≤3.8, import MutableMapping from the typing module.

from typing import MutableMapping
...

Using the TypedDict class as a type hint

So far, we have looked at how to annotate dictionaries with dict, Mapping, and MutableMapping, but most of the dictionaries have only one type: str. However, dictionaries can contain a combination of other data types.

Here is an example of a dictionary whose keys are of different types:

student = {
  "first_name": "John",
  "last_name": "Doe",
  "age": 18,
  "hobbies": ["singing", "dancing"],
}

The dictionary values range from str, int, and list. To annotate the dictionary, we will use a TypedDict that was introduced in Python 3.8. It allows us to annotate the value types for each property with a class-like syntax:

from typing import TypedDict

class StudentDict(TypedDict):
    first_name: str
    last_name: str
    age: int
    hobbies: list[str]

We define a class StudentDict that inherits from TypedDict. Inside the class, we define each field and its expected type.

With the TypedDict defined, you can use it to annotate a dictionary variable as follows:

from typing import TypedDict

class StudentDict(TypedDict):
    ...

student1: StudentDict = {
    "first_name": "John",
    "last_name": "Doe",
    "age": 18,
    "hobbies": ["singing", "dancing"],
}

You can also use it to annotate a function parameter that expects a dictionary as follows:

def get_full_name(student: StudentDict) -> str:
    return f'{student.get("first_name")} {student.get("last_name")}'

If the dictionary argument doesn’t match StudentDict, mypy will show a warning.

Adding type hints to tuples

A tuple stores a fixed number of elements. To add type hints to it, you use the tuple type, followed by [], which takes the types for each elements.

The following is an example of how to annotate a tuple with two elements:

student: tuple[str, int] = ("John Doe", 18)

Regardless of the number of elements the tuple contains, you’re required to declare the type for each one of them.

The tuple type can be used as a type hint for a parameter or return type value:

def student_info(student: tuple[str, int]) -> None:
    ...

If your tuple is expected to have an unknown amount of elements of a similar type, you can use tuple[type, ...] to annotate them:

letters: tuple[str, ...] = ('a', 'h', 'j', 'n', 'm', 'n', 'z')

To annotate a named tuple, you need to define a class that inherits from NamedTuple. The class fields define the elements and their types:

from typing import NamedTuple

class StudentTuple(NamedTuple):
    name: str
    age: int

john = StudentTuple("John Doe", 33)

If you have a function that takes a named tuple as a parameter, you can annotate the parameter with the named tuple:

def student_info(student: StudentTuple) -> None:
    name, age = student
    print(f"Name: {name}\nAge: {age}")

student_info(john)

Creating and using protocols

There are times when you don’t care about the argument a function takes. You only care if it has the method you want.

To implement this behavior, you’d use a protocol. A protocol is a class that inherits from the Protocol class in the typing module. In the protocol class, you define one or more methods that the static type checker should look for anywhere the protocol type is used.

Any object that implements the methods on the protocol class will be accepted. You can think of a protocol as an interface found in programming languages such as Java, or TypeScript. Python provides predefined protocols, a good example of this is the Sequence type. It doesn’t matter what kind of object it is, as long as it implements the __getitem__ and __len__ methods, it accepts them.

Let’s consider the following code snippets. Here is an example of a function that calculates age by subtracting the birth year from the current year:

def calc_age(current_year: int, data) -> int:
    return current_year - data.get_birthyear()

The function takes two parameters: current_year, an integer, and data, an object. Within the function body, we find the difference between the current_year and the value returned from get_birthyear() method.

Here is an example of a class that implements the get_birthyear method:

class Person:
    def __init__(self, name, birthyear):
        self.name = name
        self.birthyear = birthyear

    def get_birthyear(self) -> int:
        return self.birthyear

# create an instance
john = Person("john doe", 1996)

This is one example of such a class, but there could be other classes such as Dog or Cat that implements the get_birthyear method. Annotating all the possible types would be cumbersome.

Since we only care about the get_birthyear() method. To implement this behavior, let’s create our protocol:

from typing import Protocol

class HasBirthYear(Protocol):
    def get_birthyear(self) -> int: ...

The class HasBirthYear inherits from Protocol, which is part of the typing module. To make the Protocol aware about the get_birthyear method, we will redefine the method exactly as it is done in the Person class example we saw earlier. The only exception would be the function body, where we have to replace the body with an ellipsis (...).

With the Protocol defined, we can use it on the calc_age function to add a type hint to the data parameter:

from typing import Protocol

class HasBirthYear(Protocol):
    def get_birthyear(self) -> int: ...

def calc_age(current_year: int, data: HasBirthYear) -> int:
    return current_year - data.get_birthyear()

Now the data parameter has been annotated with the HasBirthYear Protocol. The function can now accept any object as long it has the get_birthyear method.

Here is the full implementation of the code using Protocol:

from typing import Protocol

class HasBirthYear(Protocol):
    def get_birthyear(self) -> int: ...

class Person:
    def __init__(self, name, birthyear):
        self.name = name
        self.birthyear = birthyear

    def get_birthyear(self) -> int:
        return self.birthyear

def calc_age(current_year: int, data: HasBirthYear) -> int:
    return current_year - data.get_birthyear()

john = Person("john doe", 1996)
print(calc_age(2021, john))

Running the code with mypy will give you no issues.

Annotating overloaded functions

Some functions produce different outputs based on the inputs you give them. For example, let’s look at the following function:

def add_number(value, num):
    if isinstance(value, int):
        return value + num
    elif isinstance(value, list):
        return [i + num for i in value]

print(add_number(3, 4))              # 7
print(add_number([1, 2, 5], 4))    # [5, 6, 9]

When you call the function with an integer as the first argument, it returns an integer. If you invoke the function with a list as the first argument, it returns a list with each element added with the second argument value.

Now, how can we annotate this function? Based on what we know so far, our first instinct would be to use the union syntax:

def add_number(value: int | list, num: int) -> int | list:
 ...

However, this could be misleading due to its ambiguity. The above code describes a function that accepts an integer as the first argument, and the function returns either a list or an int. Similarly, when you pass a list as the first argument, the function will return either a list or an int.

You can implement function overloading to properly annotate this function. With function overloading, you get to define multiple definitions of the same function without the body, add type hints to them, and place them before the main function implementations.

To do this, annotate the function with the overload decorator from the typing module. Let’s define two overloads before the add_number function implementation:

from typing import overload

@overload
def add_number(value: int, num: int) -> int: ...

@overload
def add_number(value: list, num: int) -> list: ...

def add_number(value, num):
    if isinstance(value, int):
        return value + num
    elif isinstance(value, list):
        return [i + num for i in value]

print(add_number(3, 4))
print(add_number([1, 2, 5], 4)

We define two overloads before the main function add_number. The overloads parameters are annotated with the appropriate types and their return value types. Their function bodies contains an ellipsis (...).

The first overload shows that if you pass int as the first argument, the function will return int.

@overload
def add_number(value: int, num: int) -> int: ...

The second overload shows that if you pass a list as the first argument, the function will return a list.

@overload
def add_number(value: list, num: int) -> list: ...

Finally, the main add_number implementation does not have any type hints.

As you can now see, the overloads annotate the function behavior much better than using unions.

Annotating constants with Final

At the time of writing, Python does not have an inbuilt way of defining constants. Starting with Python 3.10, you can use the Final type from the typing module. This will mean mypy will emit warnings if there are attempts to change the variable value.

from typing import Final
MIN: Final = 10
MIN = MIN + 3

Running the code with mypy with issue a warning:

final.py:5: error: Cannot assign to final name "MIN"
Found 1 error in 1 file (checked 1 source file)

This is because we are trying to modify the MIN variable value to MIN = MIN + 3.

Note that, without mypy or any static file-checker, Python won’t enforce this and the code will run without any issues:

>>> from typing import Final
>>> MIN: Final = 10
>>> MIN = MIN + 3
>>> MIN
>>> 13

As you can see, during runtime you can change the variable value MIN any time. To enforce a constant variable in your codebase, you have to depend on mypy.

Dealing with type-checking in third-party packages

While you may be able to add annotations to your code, the third-party modules you use may not have any type hints. As a result, mypy will warn you.

If you receive those warnings, you can use a type comment that will ignore the third-party module code:

import third_party # type ignore

You also have the option of adding type hints with stubs. To learn how to use stubs, see Stub files in the mypy documentation.

Conclusion

This tutorial explored the differences between statically typed and dynamically typed codes. You learned the different approaches you can use to add type hints to your functions and classes. You also learned about static type-checking with mypy and how to add type hints to variables, functions, lists, dictionaries, and tuples as well as working with Protocols, function overloading, and how to annotate constants.

To continue building your knowledge, visit typing — Support for type hints. To learn more about mypy, visit the mypy documentation.

Get setup with LogRocket's modern error tracking in minutes:

  1. Visit https://logrocket.com/signup/ to get an app ID.
  2. Install LogRocket via NPM or script tag. LogRocket.init() must be called client-side, not server-side.
  3. $ npm i --save logrocket 

    // Code:

    import LogRocket from 'logrocket';
    LogRocket.init('app/id');
    Add to your HTML:

    <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script>
    <script>window.LogRocket && window.LogRocket.init('app/id');</script>
  4. (Optional) Install plugins for deeper integrations with your stack:
    • Redux middleware
    • ngrx middleware
    • Vuex plugin
Get started now
Stanley Ulili I'm a freelance web developer and researcher from Malawi. I love learning new things, and writing helps me understand and solidify concepts. I hope by sharing my experience, others can learn something from them.

Leave a Reply