Вопрос: Как реализовать колоду карт через классы на python?

Лучший ответ:
Это у вас вышел совсем плохой варант.
Классы ради классов. Вообще в первую очередь нужно решать задачу, а не придумывать как бы применить тут наследование или полморфизм.
В вашем случае лишне сложности ни к чему. Они только добавляют проблем, но ни одной не решают.

Сделайте карту универсальной, сделайте ее на основе typing.NnamedTuple.
Пусть у каждой карты будет весь набор атрбутов, но только один какой-то будет ненулевым. Не нужно тут наследования, ведь у вас не используется полиморфизм.

В вашей реализации не получится сделать две колоды или больше. Колода у вас - это синглтон получается. Такое себе решение.
На сколько я понмаю предметную область, в колоде важна обычно не столько группировка карт, сколько их порядок. Так и храните карты в инкапсулированном списке.
Порядок карт не атрибут карт, он должен храниться в колоде. 
Вполне можно держать в инстансе колоды в качестве индекса для быстрой группировки специальный словарь, но это сомнительная оптимизация на таком этапе. Сколько будет в вашей колоде? Является ли вообще бутылочным гордышком проблема группировки? Почему нельзя ее решать на даном этапе просто понятной фильтрацией?
В общем старайтесь применять новые изученные фичи аккуратно и тоько там, где это на самом деле решает проблемы.
Интересное упражнение - придумывать пример, где изученная концепция дейтсивтельно к месту. Нследование  - это не про ваш вот этот пример с картами. По крайней мере на текущем уровне постановки задачи

Вот пример:
import datetime
import itertools
from typing import NamedTuple


class Card(NamedTuple):
    name: str = None
    phys_damage: int = 0
    hp: int = 0
    mage_damage: int = 0
    armor: int = 0
    add_hp: int = 0

    def __repr__(self):
        """repr у нас будет показывать только аргументы, которые отличаются от умолчательных"""
        defaults = type(self)._field_defaults
        params = ', '.join(
            f'{k}={v!r}' 
            for k, v in self._asdict().items() 
            if k in defaults and v != defaults[k]
        )
        return f'{type(self).__name__}({params})'

    def __str__(self):
        return f'<{self.name}#{id(self)}>'
        
    @property
    def total_hp(self):
        return self.hp + self.add_hp

    @property
    def group(self):
        return max(
            (self.mage_damage, 'Intelegence'),
            (self.add_hp, 'Strength'),
            (self.armor, 'Agility'),
        )[1]
    

class Deck():
    def __init__(self, deck_name="Колода", date=None, cards=None):
        self.deck_name = deck_name
        self.date = date or datetime.now()
        self.cards = cards or []

    def __iter__(self):
        return iter(self.cards)
    
    def by_group(self):
        return itertools.groupby(self, lambda card: card.group)

my_deck = Deck("Моя колода", "24.05.2022", [
    Card("Течис", phys_damage=32, hp=600, mage_damage=100),
    Card("Земеля", phys_damage=40, hp=600, mage_damage=60),
    Card("Рудге", phys_damage=80, hp=600, add_hp=150),
    Card("Крип", phys_damage=60, hp=600, armor=10),
])


for group, cards in my_deck.by_group():
    print(f'# {group}:')
    for card in cards:
        print(f'\t{card!r}')

Худший ответ:
Для проверки свойств карты я-бы использовал представление кода карты как битовое число. Биткарта.
К примеру - карты делятся на красные и черные масти. Это один бит. Далее цвета делятся на трефы, пики 
и т.п. Еще один бит. Тоесть двумя битами от 00 до 11 мы полностью кодируем масть. И ранг карты в игре
(он может менятся в зависимости от вида игры) можно закодировать еще четырьмя битами. Итого шесть бит.
Плюс еще отдельно остается джокер как карта универсал. Для нее можно ввести один бит масти как признак
того что масть игнорируется например. Итого 7 бит. Такой код карты удобно сортировать например или представлять различные проверки условий.