2.11. New Features in Python#

This section shows some new features in Python 3.10 and above.

2.11.1. Simplify Conditional Execution with Match Statements#

It is common to use the if-else statements to execute multiple conditional statements.

def get_price(food: str):
    if food == "apple" or food == "peach":
        return 4
    elif food == "orange":
        return 3
    elif food == "grape":
        return 5
    else:
        return "Unknown"


get_price("peach")
4

In Python 3.10 and above, you can use the match statements to do the same thing.

Match statements can provide a more concise and readable syntax, especially when dealing with complex pattern matching scenarios.

def get_price(food: str):
    match food:
        case "apple" | "peach":
            return 4
        case "orange":
            return 3
        case "grape":
            return 5
        case _:
            return "Unknown"


get_price("peach")
4

2.11.2. Structural Pattern Matching in Python 3.10#

Extracting data from nested structures often leads to complex, error-prone code with multiple checks and conditionals. Consider this traditional approach:

def get_youngest_pet(pet_info):
    if isinstance(pet_info, list) and len(pet_info) == 2:
        if all("age" in pet for pet in pet_info):
            print("Age is extracted from a list")
            return min(pet_info[0]["age"], pet_info[1]["age"])
    elif isinstance(pet_info, dict) and "age" in pet_info:
        if isinstance(pet_info["age"], dict):
            print("Age is extracted from a dict")
            ages = pet_info["age"].values()
            return min(ages)

    # Handle other cases or raise an exception
    raise ValueError("Invalid input format")
pet_info1 = [
    {"name": "bim", "age": 1},
    {"name": "pepper", "age": 9},
]
get_youngest_pet(pet_info1)
Age is extracted from a list
1
pet_info2 = {'age': {"bim": 1, "pepper": 9}}
get_youngest_pet(pet_info2)
Age is extracted from a dict
1

Python 3.10’s pattern matching provides a more declarative and readable way to handle complex data structures, reducing the need for nested conditionals and type checks.

def get_youngest_pet(pet_info):
    match pet_info:
        case [{"age": age1}, {"age": age2}]:
            print("Age is extracted from a list")
            return min(age1, age2)

        case {'age': {}}:
            print("Age is extracted from a dict")
            ages = pet_info['age'].values()
            return min(ages)

        case _:
            raise ValueError("Invalid input format")
pet_info1 = [
    {"name": "bim", "age": 1},
    {"name": "pepper", "age": 9},
]
get_youngest_pet(pet_info1)
Age is extracted from a list
1
pet_info2 = {'age': {"bim": 1, "pepper": 9}}
get_youngest_pet(pet_info2)
Age is extracted from a dict
1

2.11.3. Enhance Code Readability with Python Dataclasses and Match Statements#

You can use Python dataclasses with Python match statements to create cleaner and more readable code. This approach can be particularly useful when setting conditions based on multiple attributes of a class, as it can simplify the code and make it easier to understand.

Using if-else:

from dataclasses import dataclass

@dataclass
class SubscriptionPlan:
    name: str
    price: float
    unit: str

def get_plan_details(plan):
    if plan.name == "basic" and plan.unit == "month":
        return f"${plan.price} per month for one month."
    elif plan.name == "premium" and plan.unit == "year":
        return f"${plan.price} per year for one year."
    elif plan.name == "" and plan.price == 0.0 and plan.unit == "":
        return "Invalid subscription plan"
    else:
        return "Unknown subscription plan"
basic_plan = SubscriptionPlan(name="basic", price=9.99, unit="month")
premium_plan = SubscriptionPlan(name="premium", price=99.99, unit="year")

print(get_plan_details(basic_plan))
print(get_plan_details(premium_plan))
$9.99 per month for one month.
$99.99 per year for one year.

Using match statements:

from dataclasses import dataclass


@dataclass
class SubscriptionPlan:
    name: str
    price: float
    unit: str


def get_plan_details(plan):
    match plan:
        case SubscriptionPlan(name="basic", price=price, unit="month"):
            return f"${price} per month for one month."
        case SubscriptionPlan(name="premium", price=price, unit="year"):
            return f"${price} per year for one year."
        case SubscriptionPlan():
            return "Invalid subscription plan"
        case _:
            return "Unknown subscription plan"
basic_plan = SubscriptionPlan(name="basic", price=9.99, unit="month")
premium_plan = SubscriptionPlan(name="premium", price=99.99, unit="year")

print(get_plan_details(basic_plan))
print(get_plan_details(premium_plan))
$9.99 per month for one month.
$99.99 per year for one year.

2.11.4. Write Union Types as X|Y in Python 3.10#

Before Python 3.10, you need to use typing.Union to declare that a variable can have one of several different types.

from typing import Union

num = 2.3
isinstance(num, Union[int, float])
True

In Python 3.10, you can replace Union[X, Y] with X | Y to simplify the expression.

isinstance(num, int | float)
True

2.11.5. Write Cleaner Python with the Walrus Operatorn#

The walrus operator (:=) in Python 3.8+ allows you to assign a variable in an expression, making your code more readable and efficient. It’s useful in two main scenarios:

  1. Giving a meaningful name to a complex expression for better readability.

  2. Avoiding repeated computations by reusing a variable instead of recomputing the expression.

Let’s consider an example where we want to calculate the radius, area, and volume of a circle given its diameter and height:

from math import pi

diameter = 4
height = 2

Without the walrus operator, we might compute the radius and area multiple times:

circle = {
    "radius": diameter / 2, # computed twice
    "area": pi * (diameter / 2)**2, # computed twice
    "volume": pi * (diameter / 2)**2 * height,
}
2.0

To avoid repeated computations, we can assign the radius and area to variables before creating the dictionary:

radius = diameter / 2
area = pi * radius**2

circle = {
    "radius": radius,
    "area": area,
    "volume": area * height,
}
2.0

To make the code more concise, we can use the walrus operator to assign the radius and area to variables while creating the dictionary.

circle = {
    "radius": (radius := diameter / 2),
    "area": (area := pi * radius**2),
    "volume": area * height,
}

After executing the code with the walrus operator, we can access the assigned variables:

print(radius)
print(area)
2.0
12.566370614359172

2.11.6. Fine-Grained Traceback in Python 3.11#

Having a clear traceback makes it faster to debug your code. Python 3.11 provides fine-grained error locations in tracebacks, enabling developers to quickly identify the exact location of errors.

The following examples illustrate the difference in traceback between Python 3.9 and Python 3.11.

%%writefile trackback_test.py
def greet(name):
    greeting = "Hello, " + name + "!"
    print(greetng) # Error: Typo in variable name

greet("Khuyen")
# Python 3.9
$ python trackback_test.py
Traceback (most recent call last):
  File "/Users/khuyentran/book/Efficient_Python_tricks_and_tools_for_data_scientists/Chapter1/trackback_test.py", line 5, in <module>
    greet("Khuyen")
  File "/Users/khuyentran/book/Efficient_Python_tricks_and_tools_for_data_scientists/Chapter1/trackback_test.py", line 3, in greet
    print(greetng) # Error: Typo in variable name
NameError: name 'greetng' is not defined
# Python 3.11
$ python trackback_test.py
Traceback (most recent call last):
  File "/Users/khuyentran/book/Efficient_Python_tricks_and_tools_for_data_scientists/Chapter1/trackback_test.py", line 5, in <module>
    greet("Khuyen")
  File "/Users/khuyentran/book/Efficient_Python_tricks_and_tools_for_data_scientists/Chapter1/trackback_test.py", line 3, in greet
    print(greetng) # Error: Typo in variable name
          ^^^^^^^
NameError: name 'greetng' is not defined. Did you mean: 'greeting'?