avatarYanick Andrade

Summary

The provided content discusses the new features and enhancements in Python 3.12's typing system, including generic types, the TypeVar mechanism, and the introduction of the type statement, while also emphasizing the benefits of typing in Python.

Abstract

The article "What You Need to Know About Python 3.12 Typing – Part I" offers an in-depth look at the advancements in Python's typing library with the release of Python 3.12. It begins by acknowledging Python's dynamic typing nature and the voluntary aspect of type hints, which are designed to improve code readability and maintainability without enforcing strict typing. The author highlights the influence of the static type checker mypy on Python's typing and the ability to gradually introduce type checking. The article delves into the concept of generic types, explaining how Python now allows for the creation of generic type representations similar to Java's generics, without the need for explicit TypeVar declarations in the module namespace. The new type statement is introduced as a cleaner and more straightforward way to define type aliases, replacing the now-deprecated TypeAlias. The author concludes by sharing personal experiences with Python 3.12 and invites readers to explore the new typing features to enhance their Python code.

Opinions

  • The author expresses amazement at the new typing features in Python 3.12 and believes readers will share this sentiment after understanding these updates.
  • Typing in Python is seen as beneficial for making code more readable, maintainable, and easier to integrate with other systems.
  • The author advocates for a balanced approach between strict and flexible typing, suggesting that strict typing should not be mandatory but can be introduced gradually.
  • mypy is praised for its role in allowing developers to add type hints progressively and for its strict type-checking capabilities.
  • The author is enthusiastic about the new generic type system in Python 3.12, which brings more flexibility and freedom in handling generic types, akin to Java's generics.
  • The introduction of the type statement is welcomed as it simplifies the creation of new type aliases, making the code cleaner and the intention clearer.
  • The author shares a personal anecdote about encountering issues with their computer while installing Python 3.12, indicating that early adoption of new software versions can come with challenges.
  • The article concludes with an invitation for readers to join the author on a journey of discovery and improvement in Python programming, suggesting that the new typing features can significantly benefit their coding practices.

What You Need to Know About Python 3.12 Typing – Part I

Prepare to be amazed

A programmer amazed by Python 3.12 Typing — AI-Generated by the Author

I am truly amazed by the new Python 3.12 and after reading this article you will be as well.

I promise.

In the previous article, we discussed three things you should know about Python 3.12; in this one, we focus 100% on the new typing library.

So this is what we are going to cover in this article:

  • Python typing library
  • Generic types in Python’s typing — TypeVar
  • The type statement

Let’s start from the basics, as I always like to do. I’ll start with a brief overview of the typing library to ensure we are all on the same page.

So, without further ado, let's get coding.

Python typing library

Unlike Java and many other programming languages, Python is a dynamic programming language.

This means that you can decide whether to type your variable or not:

name: str = "Yanick" # explicitly typing my variable

# defining the parameter type and the return type of our function
def say_hello(name: str) -> None:
    print(f"Hello, {name}")

In our previous example, we decided that it’s a good idea to keep our code type to make it more readable and with that, easy to maintain.

But, that doesn’t necessarily mean that if we send other things rather than string as an argument to our function say_hello it will not work.

Type hints will be used by type checkers, linters, and your IDE to warn and prevent you from passing or assigning inconsistent types to functions and variables.

“It should also be emphasized that Python will remain a dynamically typed language, and the authors have no desire to ever make type hints mandatory, even by convention.” — PEP 484

Typing hints in Python is more like an informational thing you add to your code.

It makes your code easier to read, easier to use and integrate with other systems, easier to document, and easier to maintain.

I use it every chance I get with a healthy balance between strict and flexible typing to take the best of both worlds.

“Type hints are the biggest change in the history of Python since the unification of types and classes in Python 2.2, released in 2001.” — Fluent Python (Ramalho. L, 2022)

— What happens if you don’t explicitly type your code?

If you don’t explicitly type your code, Python will do it for you at runtime through the __annotations__ attribute.

— What if you want to make type-checking more strict?

Python typing was strongly inspired by mypy according to PEP 484.

“The proposal is strongly inspired by mypy.”

Mypy is a static type checker that allows you to add type hints gradually as you go.

Unlike regular typing in Python, using mypy will raise an exception or error when the type is not right.

# defining the parameter type and the return type of our function
def say_hello(name: str) -> None:
    print(f"Hello, {name}")

If you call our function say_hello with the wrong type with mypy, we will get a clear error saying that we sent the wrong type argument:

say_hello(1) # call our function with int instead of str
# error: Argument 1 to "say_hello" has incompatible type "int"; expected "str"

To start using mypy you simply install it using pip:

pip install mypy # install mypy to your virtualenv
mypy <my-script>.py # run your python code using mypy

Before you install it make sure you’re in a virtual environment to avoid breaking your Python on your machine in case of any issue.

As I said previously, mypy allows you to gradually type your code as you go. You don’t have to type every function if you don’t want to.

But in case you want to make sure that every single function in your code is typed, mypy can also make this even strictly.

We have a set of commands we can use to make mypy more strict regarding type-checking. One of them is — disallow-untyped-defs and — disallow-incomplete-defs.

The command — disallow-untyped-defs reports an error when it finds a function that has no annotation or is partially annotated:

def funct_a(name: str, age): # report error. function partially annotated ❌
    ...

def funct_b(name, age): # report error. function not annotated ❌
    ...

On the other hand, — disallow-incomplete-defs will report an error only when a function is partially annotated:

def funct_a(name: str, age): # report error. function partially annotated ❌
    ...

def funct_b(name, age): # no error. ✅
    ...

Perfect. Now that we have covered some of the basics of Python typing, I think we can delve into the new things from Python 3.12 as promised.

Generic types in Python’s typing

In Computer Science we define data types as generic when we don’t know beforehand which type we’re dealing with.

The type will be inferred later.

from dataclasses import dataclass


@dataclass
class Person:
    name: str
    age: int


people: list[Person] = [] # a generic type (list) that infer that types are Person

In our example above, we create a variable people that is a list. A list is a generic type since it can contain any data type we want.

In this case, we explicitly specified that our generic type, list, will be a list of Person. So when we iterate over our list, the type of each item will be inferred as Person:

# people: list[Person] = [] from example above


# person variable type will be Person
for person in people:
    print(f"Name - {person.name}, Age - {person.age}")

— What if we don’t know which data type to use?

Sometimes we don’t know the data type or it can be of any type.

people = [] # a empty list with that can be of any type

Another way of doing this is by using the builtin TypeVar from the typing library:

from typing import TypeVar

T = TypeVar('T') # this meas T can be of any type

people: list[T] = [] # a empty list with that can be of any type

TypeVar allows us to

— Why TypeVar?

If you’re familiar with other programming languages like Java, you’ve probably seen something like this:

/**
 * Generic version of the Dictionary class.
 * @param <K> the key of the value for our dictionary
 * @param <T> the type of the our dictionary
 */
public class Dictionary<K, T> { /* ... */ }

In Java, as you can see we can create a representation of a generic type — T and K — without the need to explicitly define them.

The same was not possible in Python. Not since Python3.12.

Before Python3.12, if we wanted to define a generic type we had to explicitly create the type so it could be recognized inside our namespace.

from typing import TypeVar

T = TypeVar('T')
K = TypeVar('K')

class Dictionary:
    def __init__(self, key: K, value: T):
        pass

A generic type can also be bound or constrained to a certain data type if we wish:

from typing import TypeVar

T = TypeVar('T', bound=str) # any str or subtype of str object
K = TypeVar('K', str, int) # have to be a str or int

With Python3.12, now we don’t have to explicitly declare the TypeVar in our namespace — module —, we can take the same approach as we saw in the Java example:

class Dictionary[T, K]: # the new way of creating generic
    def __init__(self, key: K, value: T):
        pass

We declare our generics after the class name adding a list with our expected parameters — generics.

This introduction brings lots of flexibility and freedom in how we create and handle generic types.

The same thing we did in a class, can be done with a function:

def get_value[K, T](key: K) -> T:
    pass

Here we define a function that receives a key that can be of any type and returns a generic type as response — T.

If we want to create a bound or define a type, we can easily do that as well:

def get_value[K: (str, int), T: str](key: K) -> T:
    pass

With this, we are saying that our key — K, has to be of type str or int and our generic T can be of any kind of str or subtype of str object.

Now let’s jump into my favorite part — The type statement.

The type statement

If you want to define a new type in Python, let’s say a Dictionary like in our previous example, one of the ways to do it is by using a TypeAlias.

A TypeAlias is a type used to create aliases that serve as a ‘nickname’ to a fixed type that you create.

from typing import TypeAlias

Dictionary: TypeAlias = dict[str | int, str] # new type alias

Without the TypeAlias Python would consider our line as a variable assignment.

With TypeAlias, we have a new type that we can use to define our variables:

items = []

def new_item(item: Dictionary) -> list[Dictionary]:
    return items.append(item)

So what does this have to do with the type statement you might ask?

Well, TypeAlias is deprecated in Python3.12. It’s not removed, but it's not encouraged either.

Introduced in Python3.12, we now have the type statement.

The type statement makes it cleaner and easier to spot a new type when is created.

The goal with this is to make it similar to how we create a new class using the class keyword or a function with the def keyword.

type Dictionary = dict[str | int, str] # new type alias

Much better, if you ask me.

We can use a combination of generic types when creating our type alias and do all sorts of things. We’re only limited by our imagination and needs, of course.

type Dictionary = dict[K, T] # new type alias

By the way, Python automatically understands that T and K are generic types. You can use any letter you want.

Let’s make a final test with everything we saw so far and see if nothing breaks:

type Dictionary = dict[K, T] # new type alias with generic types


items = []

def new_item(item: Dictionary) -> list[Dictionary]:
    global items # access the global variable and append item to it
    items.append(item)

new_item({'name': 'Yanick'})
new_item({1: 1})

print(items)
# the output should be - [{'name': 'Yanick'}, {1: 1}]

Final thoughts

As I continue to delve into Python3.12 I continue to find new things that amaze me and make me want to explore more and share it with you guys here.

As I told you in my previous article, I had some issues with my computer and last week was hard since the problem was deeper than expected, some issues with Nvidia graphics I guess.

I couldn’t do much and the work accumulated so this article took longer than expected but we are finally here.

I hope this article brings value to you and helps you boost your Python code like it’s doing with mine.

See you soon enough.

Hi! Thank you for your time reading my article. If you enjoyed this article and would like to receive similar content directly to your inbox

In Plain English 🚀

Thank you for being a part of the In Plain English community! Before you go:

Python
Python Programming
Python3
Data Science
Programming
Recommended from ReadMedium