__str__ и __repr__:
  super().__str__() или super().__repr__() для вызова родительского метода.__str__ или __repr__ косвенно.repr() или str() напрямую, а не f-строки.Предотвращение бесконечных рекурсий в методах __str__ и __repr__ критически важно, так как они часто используются для отладки и логирования. Неправильная реализация может привести к переполнению стека и падению программы.
Проблема: Бесконечная рекурсия возникает, когда реализация __str__ или __repr__ вызывает саму себя напрямую или косвенно.
Основные стратегии предотвращения:
Использовать super().__str__() или super().__repr__():  Вместо прямой работы с атрибутами текущего объекта, можно вызвать реализацию метода у родительского класса.  Это позволяет избежать непосредственного вызова текущего метода.
class MyClass:
    def __init__(self, value):
        self.value = value
    def __str__(self):
        return f"MyClass({self.value})" # Потенциально опасно, если MyClass вызывает str(self)
class MySubClass(MyClass):
    def __init__(self, value, extra):
        super().__init__(value)
        self.extra = extra
    def __str__(self):
        return f"MySubClass({self.value}, {self.extra})" # Безопасно
    def __repr__(self):
      return f"MySubClass(value={self.value}, extra={self.extra})" #Безопасно
Использовать object.__str__() или object.__repr__(): Если у класса нет явно заданного родителя, можно использовать реализацию, предоставленную базовым классом object. Это гарантирует, что метод не будет вызывать саму себя.
class MyClass:
    def __init__(self, value):
        self.value = value
    def __str__(self):
        return object.__str__(self)  # Безопасно, вызов базового object.__str__()
    def __repr__(self):
        return f"<MyClass object at {hex(id(self))}>" # Безопасно
Избегать использования str(self) или repr(self) внутри методов:  Эти вызовы, даже косвенные, приводят к рекурсивному вызову текущего метода.  Вместо этого, обращайтесь напрямую к атрибутам объекта и форматируйте их.
class MyClass:
    def __init__(self, value):
        self.value = value
    def __str__(self):
        # ОПАСНО:  вызывает рекурсию
        # return f"Value: {str(self)}"
        # БЕЗОПАСНО: напрямую форматирует атрибут
        return f"Value: {self.value}"
    def __repr__(self):
        # ОПАСНО: вызывает рекурсию
        # return f"MyClass({repr(self)})"
        #БЕЗОПАСНО:
        return f"MyClass(value={self.value})"
Ограничить глубину рекурсии (менее предпочтительный метод): Хотя это и не лучшее решение, можно временно увеличить лимит рекурсии с помощью `sys.setrecursionlimit()`, но это просто откладывает проблему, а не решает её. Это обычно является признаком плохого дизайна.
import sys
class MyClass:
    def __init__(self, value):
        self.value = value
    def __str__(self):
        # Неправильный подход:  использование sys.setrecursionlimit - это "костыль"
        # sys.setrecursionlimit(10000) #Очень плохо.
        # return f"Value: {str(self)}" # Вызовет рекурсию, но теперь больше лимит.
        return f"Value: {self.value}" # Правильный подход
Лучшие практики:
__repr__ должен давать недвусмысленное строковое представление объекта, которое в идеале можно использовать для его воссоздания (например, в виде вызова конструктора).__str__ должен давать удобное для пользователя строковое представление.__repr__ должна быть всегда безопасной (не вызывать исключений). Если ее невозможно сделать безопасной, лучше предоставить fallback-реализацию, использующую object.__repr____str__ и __repr__.__str__ и __repr__, особенно при наследовании.