Skip to content

← Interfaces with Protocols and ABCs step 4 of 26

Easy Primitives

Build an ABC with an Abstract Property and Classmethod

Build a small family of text codecs on an ABC. Every codec has a name, a from_config factory that builds it from a settings dictionary, and an encode method. The base class supplies label, which uses all three.

codec = RotCodec.from_config({"shift": "2"})
codec.name             # "rot2"
codec.label("abcdef")  # "rot2:cdefab"

Fix the decorator order first

The starter crashes before any test runs:

AttributeError: attribute '__isabstractmethod__' of 'property' objects is not writable

@abstractmethod marks a function as abstract. @property and @classmethod then read that mark from the function they wrap. Decorators apply from the bottom up, so @abstractmethod goes directly above the def:

@property
@abstractmethod
def name(self) -> str: ...

@classmethod
@abstractmethod
def from_config(cls, config: Mapping[str, str]) -> Self: ...

The starter has @abstractmethod on top, so it tries to mark the property object instead, and a property does not allow that.

What to write

Codec has three abstract members and one ordinary method:

  • name, a read-only property that returns a str.
  • from_config(cls, config), a classmethod that returns Self. You met Self in the type hints track. Here it means that RotCodec.from_config returns a RotCodec.
  • encode(self, text: str) -> str.
  • label(text), which returns f"{self.name}:{self.encode(text)}".

RotCodec(shift: int = 1) moves the first shift characters to the end:

  • name is f"rot{shift}".
  • encode returns exactly text[shift:] + text[:shift]. Do not reduce the shift modulo the length. With "abc", a shift of 5 returns "abc", and a shift of -1 returns "cab". Empty text stays empty.
  • from_config returns cls(int(config.get("shift", "1"))), so a missing "shift" means 1.

The starter already has UpperCodec and PartialCodec. PartialCodec leaves out name on purpose. Python lets you define the class, but creating an instance raises TypeError, because an abstract member is still missing.

Then write two functions:

  • try_build(cls: type) -> str calls cls(). It returns "TypeError" if that raises TypeError, and "constructed" otherwise. Keep the parameter as plain type. mypy does not let you pass an abstract class as a type[Codec].
  • solve(kind, config, text) looks up kind in a dict[str, type[Codec]] that maps "rot" to RotCodec and "upper" to UpperCodec. It builds the codec with from_config(config).

What the driver reports

__abstractmethods__ holds the names of the abstract members that a class has not defined yet.

Key Value Expected
label codec.label(text) For example, "upper:" for empty text
codec_abstracts sorted(Codec.__abstractmethods__) ["encode", "from_config", "name"]
partial_abstracts sorted(PartialCodec.__abstractmethods__) ["name"]
partial_build try_build(PartialCodec) "TypeError"
complete_build try_build(UpperCodec) "constructed"