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)

shutil.make_archive

In previous versions of shutil.make_archive, we needed to temporarily switch the current working directory to archive file/directory (e.g. zip, tar, bztar, xztar, etc.) to its relative paths - similar to the following shell pattern:

1
2
3
(
  cd /path/to/archive && { archive }
)

Now we can just pass the new root_dir argument.

tempfile.NamedTemporaryFile’s delete_on_close

This is one of my favorite new features. In UNIX, the tempfile.NamedTemporaryFile generates a random file under the /tmp directory to facilitate ephemeral writes and reads (like caching). However, in the old days, we needed to keep track of the filenames if we ever wanted to manually delete them. With the new delete_on_close boolean parameter, we can now offload this responsibility with .close().

os.PIDFD_NONBLOCK

In this version, a new flag to os.pidfd_open(pid: int, flags: int) has been added to expose PIDFD_NONBLOCK to Python (a.k.a. opening a file descriptor to a running process in a non-blocking mode).

1
2
3
4
5
// Modules/posixmodule.c

#ifdef PIDFD_NONBLOCK
    if (PyModule_AddIntMacro(m, PIDFD_NONBLOCK)) return -1;
#endif

Not sure what I’ll be using this for yet, but will be keeping it in mind.

math.sumprod

Instead of doing this:

1
2
3
4
p = [1, 2, 3, 4, 5]
q = [5, 4, 3, 2, 1]

sum(list(map(lambda x, y: x * y, p, q)))  # 35

We can just do this:

1
2
3
4
p = [1, 2, 3, 4, 5]
q = [5, 4, 3, 2, 1]

math.sumprod(p, q)  # 35

typing.Hashable and typing.Sized

I use these type hint notations in my codebase - but with 3.12, the naming convention has been changed to collections.abc.Hashable and collections.abc.Sized.

get_event_loop()

The asyncio.get_event_loop(), which returns the event loop of the current async context, will now DeprecationWarning if there is no context event loop.

typing.runtime_checkable

The runtime_checkable decorator will now freeze the members of typing.Protocol.

wstr and wstr_length

From Unicode objects, the wstr and wstr_length have been removed. This reduces the object size by 8 or 16 Bytes on 64-bit platforms.

re.sub

Great news for regular expression lovers like myself! Regex-based substitutions will be faster by 2~3x for expressions containing group references (like \1).

perf

The perf profilers will now report function names in the trace logs.