Skip to content

← Structural Typing and the Hard Parts step 4 of 24

Easy Primitives

abc.ABC: abstract properties and classmethods, in the right order

@abstractmethod is not a decorator that “marks a method abstract”. It is a decorator that sets __isabstractmethod__ = True on the object handed to it, and ABCMeta later collects every such object into cls.__abstractmethods__. Everything surprising about abstract properties follows from that one sentence.

Decorator order

The builtin descriptor goes outermost; @abstractmethod goes innermost, closest to the function:

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

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

Get it backwards and, on a current CPython, you do not get a subtle bug — you get an immediate

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

at class-creation time, because property and classmethod expose __isabstractmethod__ as a read-only getset. The starter code has the order wrong on purpose. Run it once before you fix it.

The genuinely silent case is different, and worth knowing: put any other wrapping decorator outside @abstractmethod and, unless that wrapper propagates __isabstractmethod__, the member vanishes from __abstractmethods__ and the incomplete subclass constructs happily. abc.update_abstractmethods() (3.10+) exists to recompute the set after a class decorator has rewritten the body.

abstractproperty, abstractclassmethod and abstractstaticmethod have been deprecated since Python 3.3. Do not use them.

Your task

Complete an abstract Codec with:

  • an abstract read-only property name
  • an abstract classmethod from_config(config: Mapping[str, str]) -> Self
  • an abstract method encode(text: str) -> str
  • a concrete template method label(text) returning f"{self.name}:{self.encode(text)}"

and two concrete subclasses, every overriding member carrying @override:

  • RotCodec(shift: int = 1)name is f"rot{shift}"; encode rotates the string left by shift: text[shift:] + text[:shift]. from_config reads config["shift"] as an int, defaulting to 1.
  • UpperCodec()name is "upper"; encode upper-cases.

A third subclass PartialCodec deliberately omits name. Defining it is perfectly legal; building it is not.

def solve(kind: str, config: dict[str, str], text: str) -> dict[str, object]:

dispatches kind ("rot" / "upper") through a dict[str, type[Codec]], builds the codec via from_config, and returns:

key value
"label" codec.label(text)
"codec_abstracts" sorted(Codec.__abstractmethods__)
"partial_abstracts" sorted(PartialCodec.__abstractmethods__)
"partial_build" try_build(PartialCodec)
"complete_build" try_build(UpperCodec)

try_build(cls: type) -> str calls cls() and returns "TypeError" if that raises TypeError, else "constructed".

Why this shape

__abstractmethods__ is the observable artefact of the whole mechanism. If your decorator order is wrong the class will not even build; if a member is silently unregistered the set will be missing an entry; and try_build proves that ABCMeta really does refuse to instantiate — which is exactly the guarantee a Protocol does not give you (see the protocol subclassing problem).