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)
|
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)
|