asyncio.TaskGroup

In previous versions, this was how to schedule a collection of async tasks and await on them.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import asyncio
import random

async def task(tag: str):
    seconds: int = random.randrange(1, 5)
    await asyncio.sleep(seconds)
    print({"tag": tag, "sleep": seconds})

async def main():
    tasks: list[asyncio.Future] = [task(tag=tag) for tag in range(10)]
    await asyncio.gather(*tasks)

if __name__ == "__main__":
    asyncio.run(main())

Now we can simply enter into a TaskGroup context.

1
2
3
4
async def main():
    async with asyncio.TaskGroup() as tg:
        for tag in range(10):
            tg.create_task(task(tag=tag))

tomllib

In previous versions, we needed to use an external library to load and manipulate toml formats. Now we can utilize tomllib straight out of the box.

1
2
3
4
import tomllib

with open("sample.toml", "rb") as f:
    data = tomllib.load(f)

contextlib.chdir

In previous versions, we had to keep a historic record to switch into a directory and back.

1
2
3
4
5
6
7
import os

if __name__ == "__main__":
    previous: str = os.getcwd()
    os.chdir("..")
    # something
    os.chdir(previous)

Now we can simply contextualize it.

1
2
3
4
5
import contextlib

if __name__ == "__main__":
    with contextlib.chdir(".."):
        # something

typing.Self

In previous versions, we couldn’t directly reference the class as return type.

1
2
3
4
5
6
class Object(object):

    def __init__(self): ...

    def method(self): -> ??
        return self

For example, we would get NameError when running the following code.

1
2
3
4
5
class Object(object):

    # 'Object' not defined yet at the time of evaluation.
    def method(self) -> Object:
        return self

Now we are able to reference using Self without having to resort to a workaround of declaring a new TypeVar or T to represent the class.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
from typing import Self

class Object(object):

    def __init__(self, x):
        self.x = x

    def instance_method(self) -> Self:
        return self

    @classmethod
    def class_method(cls, x) -> Self:
        return cls(x=x)

functools.singledispatch

In previous versions, function overloading was not possible - in the conventional sense. We would “imitate” the overloading behavior by implementing enumerated function parameters and conditionally outputing the result. The downside is that this increases complexity of supposed “overloaded” function and keeping track of the logical bifurication.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
from typing import Iterable

def add(x, y):
    # Add two numbers.
    if isinstance(x, int) and isinstance(y, int):
        return x + y

    # Merge two iterables.
    elif isinstance(x, Iterable) and isinstance(y, Iterable):
        return type(x)((*x, *y))

    # ...
    raise NotImplementedError

if __name__ == "__main__":
    add(1, 2)  # int
    add([1, 2, 3], [4, 5, 6])  # list

Now we are able to utilize functools.singledispatch and type signatures to behave similarly.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
from functools import singledispatch

@singledispatch
def add(x, y): ...

@add.register
def f(x: int | float, y: int | float):
    return x + y

@add.register
def f(x: tuple | list | set, y: tuple | list | set):
    return (*x, *y)