1. Концепция: Читаемость vs Однозначность

Небольшое отступление: Если вы только начинаете знакомиться с классами, объектами и магическими методами, приглашаю вас на мой бесплатный курс ООП Python: Часть 1 на Stepik. Базу можно заложить там.

Главное концептуальное различие между этими двумя магическими методами кроется в их целевой аудитории. Простое правило: __str__ пишется для конечных пользователей, а __repr__ — для разработчиков.

  • __str__ (informal string representation): Его главная задача — читаемость. Этот метод отвечает за то, чтобы красиво и понятно описать объект обычным человеческим языком. Вы используете его там, где информацию увидит клиент (например, при генерации отчетов или выводе сообщений в интерфейс). Он должен скрывать лишние технические детали и показывать только суть.

  • __repr__ (official string representation): Его главная задача — однозначность и отсутствие двусмысленностей. Это ваш главный инструмент для логирования и дебага. Глядя на результат __repr__, программист должен исчерпывающе понимать, что это за объект, какого он типа и в каком состоянии находится.

Золотой стандарт Python: в идеальном мире строка, которую возвращает __repr__, должна быть валидным кодом, скопировав который, можно воссоздать точно такой же объект. То есть должно выполняться негласное правило eval(repr(obj)) == obj.

Посмотрим на практике:

class User:
    def __init__(self, user_id, name):
        self.user_id = user_id
        self.name = name

    def __str__(self):
        # Для пользователя: лаконично и красиво
        return f"Пользователь {self.name}"

    def __repr__(self):
        # Для разработчика: строго, как при создании объекта
        return f"User(user_id={self.user_id}, name={repr(self.name)})"

user = User(1, 'Alice')

# Вызываем оба метода напрямую для сравнения
print(str(user))   # Вывод: Пользователь Alice
print(repr(user))  # Вывод: User(user_id=1, name='Alice')

В этом примере __str__ намеренно прячет технический user_id, потому что для "человеческого" вывода он, как правило, не нужен. А вот __repr__ честно вываливает всю структуру наружу, чтобы при отладке мы в консоли точно видели, с каким именно экземпляром класса работаем.

2. Точки вызова: Где какой метод срабатывает

Мало просто написать эти методы, нужно четко понимать, в какой момент интерпретатор дергает каждый из них. Python делает это автоматически в зависимости от контекста.

Когда работает __str__?
Он срабатывает всегда, когда вы явно или неявно просите Python превратить объект в обычный текст:

  • Вызов функции print(obj)

  • Явное приведение типов str(obj)

  • Форматирование строк: f-строки f"{obj}" или метод "{format}".format(obj)

Когда работает __repr__?
Его юзкейсы более специфичны и направлены на отладку:

  • Явный вызов функции repr(obj)

  • Вывод переменной в интерактивной консоли REPL (когда вы просто пишете имя переменной и жмете Enter)

  • Внутри коллекций (Самое важное!)

Остановка на последнем пункте обязательна. Это классические грабли. Если вы положите свои объекты в список (или словарь, или кортеж) и попытаетесь распечатать саму коллекцию через print(my_list), Python вызовет __str__ для самого списка (чтобы нарисовать квадратные скобки), но для каждого элемента внутри него он вызовет **именно __repr__**, а не __str__.

Разработчики языка сделали это намеренно: в коллекциях важна структура и однозначность элементов, чтобы при дебаге большого списка вы точно видели типы данных внутри, а не просто красивые описания.

Посмотрим на практике:
Возьмем наш класс User из предыдущего примера и поместим объекты в список.

user1 = User(1, 'Alice')
user2 = User(2, 'Bob')

users_list = [user1, user2]

# 1. Срабатывает __str__
print(user1) 
# Вывод: Пользователь Alice

# 2. Срабатывает __str__ (внутри f-строки)
print(f"В систему вошел {user1}") 
# Вывод: В систему вошел Пользователь Alice

# 3. Срабатывает __repr__ для элементов списка!
print(users_list) 
# Вывод: [User(user_id=1, name='Alice'), User(user_id=2, name='Bob')]

Если бы для элементов списка вызывался __str__, мы бы увидели [Пользователь Alice, Пользователь Bob]. Вроде бы красиво, но при отладке сложного кода это скроет от нас важные детали (например, user_id). Именно поэтому коллекции всегда опираются на __repr__.

3. Механика фолбэка (Кто кого подменяет)

Что произойдет, если вы реализуете только один из этих двух методов? Интерпретатор Python использует так называемый механизм фолбэка (fallback) — запасной план действий. И этот механизм работает строго в одну сторону.

Сценарий 1: Есть только __repr__
Если Python пытается получить строковое представление объекта для человека (например, через print(obj)), но метод __str__ в классе не реализован, язык **автоматически откатится к __repr__**.

Логика языка здесь проста: «Разработчик не дал мне красивую строку для пользователя, поэтому я покажу ту строгую техническую строку, которая у меня есть. Это лучше, чем ничего».

class OnlyRepr:
    def __repr__(self):
        return "OnlyRepr(состояние='техническое')"

obj_repr = OnlyRepr()

# Вызываем print, который ищет __str__, не находит его и использует __repr__
print(obj_repr) 
# Вывод: OnlyRepr(состояние='техническое')

Сценарий 2: Есть только __str__
А вот в обратную сторону это не работает. Если системе нужен __repr__ (например, вы вывели список объектов или вызвали repr()), а у вас написан только __str__, Python его проигнорирует.

Почему? Потому что __str__ по определению может быть неточным, скрывать детали и нарушать правило однозначности. Для отладки такой вывод опасен. Поэтому Python сдается и возвращает базовое представление объекта — тот самый нечитаемый адрес в памяти, унаследованный от базового класса object.

class OnlyStr:
    def __str__(self):
        return "Красивое описание объекта"

obj_str = OnlyStr()

# Вызываем print для объекта — работает __str__
print(obj_str) 
# Вывод: Красивое описание объекта

# Пытаемся распечатать список (вызывается __repr__ для элементов)
print([obj_str]) 
# Вывод: [<__main__.OnlyStr object at 0x104b8bdf0>] 
# (Python проигнорировал наш __str__!)

Именно незнание этого одностороннего фолбэка чаще всего приводит к ситуациям: «Я же написал метод, почему в списке все равно выводится эта кракозябра с адресом памяти?!».

4. Золотое правило на практике (Best Practice)

На основе всего, как Python вызывает эти методы и как работает односторонний фолбэк, вырисовывается очень простое и элегантное правило для повседневной разработки.

Правило №1: Всегда реализуйте __repr__
Это абсолютный мастхэв (must-have) для любого кастомного класса. Возьмите за привычку писать __repr__ сразу после __init__. Это тот необходимый минимум, который спасет кучу нервов вам и вашим коллегам при отладке, чтении логов и работе в консоли.

Поскольку функция print() умеет откатываться к __repr__, написав только этот один метод, вы убиваете сразу двух зайцев: получаете и отличный инструмент для дебага (внутри коллекций в том числе), и приемлемый вывод при обычном принте объекта. Если у вас есть время написать только один магический метод — пишите __repr__.

Правило №2: Добавляйте __str__ только по необходимости
Не нужно писать __str__ для галочки, если он будет просто дублировать __repr__. Добавляйте его только тогда, когда вам действительно нужен специфический, «человечный» вывод данных, который разительно отличается от строгой технической картины.

Например, для объекта datetime:

  • __repr__ должен показать точную структуру: datetime.datetime(2023, 10, 25, 12, 30) — идеально для разработчика.

  • __str__ должен показать читаемую дату: 2023-10-25 12:30:00 — идеально для пользователя интерфейса.

Итоговый пример идеального баланса:

class Task:
    def __init__(self, task_id, title, is_done=False):
        self.task_id = task_id
        self.title = title
        self.is_done = is_done

    def __repr__(self):
        # База. Строго, понятно, легко воссоздать объект.
        return f"Task(task_id={self.task_id}, title={self.title!r}, is_done={self.is_done})"

    def __str__(self):
        # Опциональное улучшение для UI/CLI.
        status = "✅" if self.is_done else "❌"
        return f"[{status}] {self.title}"

task = Task(42, "Написать статью на Хабр")

# Логируем/дебажим:
print(repr(task)) 
# Вывод: Task(task_id=42, title='Написать статью на Хабр', is_done=False)

# Показываем пользователю в консольной утилите:
print(task) 
# Вывод: [❌] Написать статью на Хабр

Анонсы новых статей, полезные материалы, а так же если в процессе у вас возникнут сложности, обсудить их или задать вопрос по этой статье можно в моём Telegram-сообществе. Смело заходите, если что-то пойдет не так, — постараемся разобраться вместе.