Strict Mypy

Mypy - Static type checker that aims to combine the benefits of dynamic and static typing

The article is about journey from

[tool.mypy]
python_version = "3.11"
warn_return_any = false
warn_unused_configs = true
ignore_missing_imports = true
strict_optional = true
allow_redefinition = true
namespace_packages = true
disallow_incomplete_defs = true

to

[tool.mypy]
python_version = "3.11"
strict = true

There were several problems on that journey. I want to share solutions for some of them.

Problem 1. Decorators without annotations

Such decorators were loosing type annotations.

def redis_reconnect(func):
    @backoff.on_exception(
        backoff.expo,
        (WatchError, RedisConnectionError, ConnectionResetError, TimeoutError),
        max_tries=settings.redis_connection_tries,
    )
    @functools.wraps(func)
    async def wrapped_method(*args, **kwargs):
        return await func(*args, **kwargs)

    return wrapped_method

So ParamSpec helps with them:

P = typing.ParamSpec("P")
T = typing.TypeVar("T")
    
def redis_reconnect(func: typing.Callable[P, typing.Awaitable[T]]) -> typing.Callable[P, typing.Awaitable[T]]:
	@backoff.on_exception(  
		backoff.expo,
	    (WatchError, RedisConnectionError, ConnectionResetError, TimeoutError),
	    max_tries=settings.redis_connection_tries,
	)
	@functools.wraps(func)
	async def wrapped_method(*args: P.args, **kwargs: P.kwargs) -> T:
		return await func(*args, **kwargs)

	return wrapped_method

Problem 2. Libraries without annotations

Typeshed

First of all, I caught such errors:

error: Skipping analyzing 'redis': found module but no type hints or library stubs
error: Skipping analyzing 'orjson': found module but no type hints or library stubs

This can be fixed, using stubs from https://github.com/python/typeshed

So for errors behind we need to install packages types-redis and types-orjson

py.typed marker

Second of all, we had some inner packages which were typed, but not marked as typed. In order to mark typed package, py.typed needs to be placed in libraries root directory

*-stubs packages

For some packages there is no stub in typeshed, but exists some specified package. For example django has django-stubs

Read PEP to find out more https://peps.python.org/pep-0561/

Problem 3. Unified domain-models for read and write

For example, this model is used for objects to create and for already created objects from database:

import pydantic

class Message(pydantic.BaseModel):
   id: int | None = None
   name: str

And we need to check that id is not None.

Solution is to split the model:

import pydantic

class MessageCreate(pydantic.BaseModel):
   name: str

class Message(pydantic.BaseModel):
   id: int

Some tips on typing annotations

Tip 1. typing.Self is often misused to annotate self argument in method. Here is an example, showing when it can be used:

import typing
class BaseClass:
   @classmethod
   def construct(cls) -> typing.Self
      return cls()

class SomeClass(BaseClass):
   Pass

some_object = SomeClass.construct()
# some_object has SomeClass type

https://peps.python.org/pep-0673/

Tip 2. typing.Iterable is for iterating

import typing

def iterate_over_sth(items: typing.Iterable[str]):
   for x in items:
      ...

Tip 3. typing.Sequence is for index access and getting length of collections

import typing

def fetch_last_item(items: typing.Sequence[str]):
   return items[-1]

Tip 4. To annotate a tuple of strings use:

tupple[str, ...] = ('a', 'h', 'j', 'n', 'm', 'n', 'z')