skip to Main Content

This is a question about best practice – consider I have the following base class module, base.py:

from __future__ import annotations
from typing import TYPE_CHECKING

if type_checking:
    from redis import Redis


class Base:
    redis: Redis

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

Assuming I want a class that inherits from Base – which of the following is considered better and why? Is there another way I didn’t think of?

(1.) seems cleaner but in (2.) I declare redis: Redis explicitly, which might be better for type-checking tools

1.

from base import Base

class Sub(Base):
    user_id: int

    def __init__(self, redis, user_id):
        super().__init__(redis)
        self.user_id = user_id
from __future__ import annotations
from typing import TYPE_CHECKING
from base import Base

if TYPE_CHECKING:
    from redis import Redis

class Sub(Base):
    redis: Redis
    user_id: int

    def __init__(self, redis, user_id):
        super().__init__(redis)
        self.user_id = user_id

2

Answers


  1. As @deceze mentioned, type-checking tools should understand your inheritance model and handle the hint appropriately.

    In terms of "best practices", your option 1 is perfectly fine since you don’t have to re-declare redis.

    If you want something cleaner, another option is to use dataclasses

    from dataclasses import dataclass
    
    @dataclass
    class Base:
        redis: Redis
    
    
    @dataclass
    class Sub(Base):
        user_id: int
    

    Reference docs: https://docs.python.org/3/library/dataclasses.html

    Login or Signup to reply.
  2. As covered, not re-declaring things in the subclass is preferable. Since we’re talking about best practices, though, I’ll also suggest that if you’re not using dataclasses and your instance attributes are being set in your __init__, that’s where you should put your type annotations instead:

    class Base:
        def __init__(self, redis: Redis):
            self.redis = redis
    
    class Sub(Base):
        def __init__(self, redis: Redis, user_id: int):
            super().__init__(redis)
            self.user_id = user_id
    

    This is briefer since you don’t need to declare each attribute in multiple places, and it gives you correct typechecking when you construct an instance:

    s = Sub(Redis(), 42)
    reveal_type(s.redis)    # note: Revealed type is "Redis"
    reveal_type(s.user_id)  # note: Revealed type is "builtins.int"
    t = Sub("redis", 42)    # error: Argument 1 to "Sub" has incompatible type "str"; expected "Redis"
    

    If the parameters aren’t annotated, then you get the "correct" revealed type on the attributes, but it’s very easy for them to have the wrong actual types at runtime:

    t = Sub("redis", 42)   # no error (oops!)
    reveal_type(t.redis)   # note: Revealed type is "Redis"
    print(type(t.redis))   # <class 'str'>  (oops!)
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search