Python etc
6.11K subscribers
18 photos
194 links
Regular tips about Python and programming in general

Owner — @pushtaev

© CC BY-SA 4.0 — mention if repost
Download Telegram
Let's say, you have the following mock:

from unittest.mock import Mock
user = Mock()
user.name = 'Guido'

You fully specified all attributes and methods it should have, and you pass it into the tested code, but then that code uses an attribute that you don't expect it to use:

user.age
# <Mock name='mock.age' id='...'>

Instead of failing with an AttributeError, the mock instead will create a new mock when its unspecified attribute is accessed. To fix it, you can (and should) use the unittest.mock.seal function (introduced in Python 3.7):

from unittest.mock import seal
seal(user)

user.name
# 'Guido'

user.occupation
# AttributeError: mock.occupation
Let's say, you have a typical decorator that returns a new function. Something like this:

def debug(f):
name = f.__name__
def inner(*args, **kwargs):
print(f'called {name} with {args=} and {kwargs=}')
return f(*args, **kwargs)
return inner

@debug
def concat(a: str, b: str) -> str:
return a + b

concat('hello ', 'world')
# called concat with args=('hello ', 'world') and kwargs={}

If you check the type of concat using reveal_type, you'll see that its type is unknown because of the decorator:

reveal_type(concat)
# Revealed type is "Any"

So, we need to properly annotate the decorator. But how?

This is not precise enough (type errors like x: int = concat(1, 2) won't be detected):

from typing import Callable
def debug(f: Callable) -> Callable: ...

This is slightly better but function arguments are still untyped:

from typing import TypeVar

T = TypeVar('T')
def debug(
f: Callable[..., T],
) -> Callable[..., T]: ...

This is type-safe but it requires the decorated function to accept exactly 2 arguments:

A = TypeVar('A')
B = TypeVar('B')
R = TypeVar('R')
def debug(
f: Callable[[A, B], R],
) -> Callable[[A, B], R]: ...

This is type-safe and works on any function but it will report a type error because inner is not guaranteed to have the same type as the passed callable (for example, someone might pass a class that is callable but we return a function):

F = TypeVar('F', bound=Callable)
def debug(f: F) -> F: ...

PEP 612 (landed in Python 3.10) introduced typing.ParamSpec which solves exactly this problem. You can use it to tell type checkers that the decorator returns a new function that accepts exactly the same arguments as the wrapped one:

from typing import Callable, TypeVar, ParamSpec

P = ParamSpec('P')
R = TypeVar('R')

def debug(
f: Callable[P, R],
) -> Callable[P, R]:
def inner(
*args: P.args,
**kwargs: P.kwargs,
) -> R:
...
return f(*args, **kwargs)
return inner

@debug
def concat(a: str, b: str) -> str:
...

reveal_type(concat)
# Revealed type is "def (a: str, b: str) -> str"
In addition to typing.ParamSpec, PEP 612 introduced typing.Concatenate that allows describing decorators that accept fewer or more arguments that the wrapped function:

from typing import Callable, Concatenate, ParamSpec, TypeVar

P = ParamSpec('P')
R = TypeVar('R')

class User: ...
class Request: ...
class Response: ...

def with_user(
f: Callable[Concatenate[User, P], R],
) -> Callable[P, R]:
def inner(*args: P.args, **kwargs: P.kwargs) -> R:
user = User()
return f(user, *args, **kwargs)
return inner

@with_user
def handle_request(
user: User,
request: Request,
) -> Response:
...

request = Request()
response = handle_request(request)
Great news everyone! We extracted all our recent posts as Markdown, organized them, and made them more accessible. Now we have:

* 🌐 Website: pythonetc.orsinium.dev
* 📢 RSS: pythonetc.orsinium.dev/index.xml
* 🧑‍💻️ GitHub: github.com/life4/pythonetc

If you want to write a guest post, just send us a PR on GitHub. The README tells what you can write about and how. Thank you all for staying with us all these years ❤️