This is a two-part question, but the second part is dependent on the first part.
For educational purposes, I am trying to implement an abstract base class and test suite for groups (the concept from abstract algebra). Part of the definition of an algebraic group is equivalent to a type constraint, and I want to implement that type constraint on an ABC, and have something complain if the methods on the concrete classes don't conform to that constraint.
I've got a first-pass implementation for this for the group of Boolean values under logical and
, but there are at least two things wrong with it, and I'm hoping you can help me fix it.
from __future__ import annotations
from abc import ABC, abstractmethod
class AbsGroup(ABC):
@abstractmethod
def op(self, other: AbsGroup) -> AbsGroup: # <-- Line-of-interest #1
pass
class Bool(AbsGroup):
def __init__(self, val="False"):
if val not in ["True", "False"]:
raise ValueError("Invalid Bool value %s" % val)
self.val = val
def op(self, other):
"""Logical AND"""
if self.val == "True" and other.val == "True": # <-- Line-of-interest #2
return Bool("True")
return Bool("False")
def __eq__(self, other):
return self.val == other.val
def __repr__(self):
return self.val
Firstly: Line-of-interest #1 is what's doing the type-constraint work, but the current implementation is wrong. It only checks that the method receives and returns an AbsGroup
instance. This could be any AbsGroup
instance. I want it to check that for the concrete class it gets inherited by, it receives and returns an instance of that concrete class (so in the case of Bool
it receives and returns an instance of Bool
). The point of the exercise is to do this in one location, rather than having to set it specifically on each concrete class. I presume this is done with some type-hinting generics that are a little bit deeper than I've yet to delve with regard to type-hinting. How do I do this?
Secondly: how do I check the concrete method is complying with the abstract type hint? The type inspector in my IDE (PyCharm) complains at Line-of-interest #2, because it's expecting other
to be of type AbsGroup
, which doesn't have a val
attribute. This is expected, and would go away if I could figure out the solution to the first problem, but my IDE is the only thing I can find that notices this discrepancy. mypy
is silent on the matter by default, as are flake8 and pylint. It's great that PyCharm is on the ball, but if I wanted to incorporate this into a workflow, what command would I have to run that would fail in the event of my concrete method not complying with the abstract signature?
First tip: If
mypy
doesnt tell you enough, trymypy --strict
.You correctly realized that the type annotation for
op
in the base class is not restrictive enough and in fact would be incompatible with the child class.Take a look at this not-working example.
I annotated
op
inBool
with the correct type but now mypy complains:You have two options: Either make the base annotation even less restrictive (
Any
) or make your class aGeneric
one:This involves several steps:
T
(looks similar to generic type variables in other languages)Generic[T]
making it a generic classop
method to take and return aT
AbsGroup[Bool]
(in C++ this is known as CRTP)This silences
mypy --strict
and PyCharm correctly infers the return type ofop
.Edit:
The previous child class definition looked like this
class Bool(AbsGroup[Bool]): ...
without quotes. But this does not work and will throw aNameError
when creating the class:This is expected behaviour as written in PEP 563.
So the quotes are still required in this case even though we used the future import.
Just a note: why are you using string symbols for the boolean values? There are already two perfectly working instances called
True
andFalse
. This will make your code much simpler. E.g. the check in the constructor can be simplified toif type(val) is bool
(I would not useisinstance
here since you dont wantval
to be a custom type, probably?).