The post has been translated automatically. Original language: Russian
When people say that Python is slow, they usually recall loops, GIL, or the absence of JIT in the usual sense for other languages. But in practice, Python code in production very often slows down and bloats up memory, not because of the “language in general”, but because of one much quieter reason: too expensive objects.
This is a problem that many people notice too late. The service seems to be working. ETL passes. The parser collects the data. The bot responds. But the memory grows, the GC lives its own life, the cache swells, and then it turns out that the application is drowning not in mathematics, but in millions of convenient but heavy Python objects.
And this is already more interesting than another argument about “Python vs Go".
The main idea: in Python, you pay not only for the data, but also for the form in which you store it.
Let's take a regular class:
class User:
def __init__(self, user_id: int, name: str, active: bool):
self.user_id = user_id
self.name = name
self.active = activeIt looks harmless. But each instance of such a class usually drags __dict__ with it, which means:
- dynamic attribute storage,
- additional memory,
- unnecessary flexibility, which is often not needed at all.
It doesn't matter at a dozen sites. In hundreds of thousands and millions, it is already very important.
And here Python has one of the most underrated tools: __slots__.
What changes __slots__
__slots__ tells the interpreter: “This object has a fixed set of attributes. You don't need to create a regular dictionary for each instance.”
Example:
class User:
__slots__ = ("user_id", "name", "active")
def __init__(self, user_id: int, name: str, active: bool):
self.user_id = user_id
self.name = name
self.active = activeWhat it gives:
- less memory per object;
- less pressure on GC;
- more compact storage;
- sometimes access to attributes is slightly faster.
Important: this is not a silver bullet. But in systems where you keep a lot of the same type of entities in memory, the difference can be very noticeable.
Typical places where it's really useful:
- ETL and data pipelines;
- parsers and crawlers;
- event systems;
- in-memory indexes;
- bots and high-load backend;
- analytical intermediate structures;
- large caches.
Why is this non-trivial?
Because Python has a very high price for convenience.
An ordinary Python object is not just a “container for three fields”. This is an object with metadata, dynamics, links, dictionaries, the ability to add properties on the fly and generally live the most free life.
And if there are tens of thousands of such objects in the code, everything is fine. If there are millions, you suddenly start paying a huge tax for something that you didn't really need.
Here is a classic anti-example:
records = []
for i in range(1_000_000):
records.append({
"id": i,
"name": f"user_{i}",
"active": i % 2 == 0,
})It's convenient. But a dictionary list is a very expensive memory structure.
If the scheme is fixed, it is almost always worth at least asking yourself the question: is dict needed here, or is it better to use:
- dataclass(slots=True),
- a regular class with __slots__,
- namedtuple,
- tuple,
- array,
- numpy/pandas, if the task is really tabular.
A very good practical compromise: dataclass(slots=True)
One of the best features of modern Python is not to choose between “beautiful code” and “normal memory".
You can write it like this:
from dataclasses import dataclass
@dataclass(slots=True)
class User:
user_id: int
name: str
active: boolAnd get:
- a readable model;
- typed fields;
- automatic __init__;
- convenient repr;
- the object is more compact than that of a regular class.
This is one of those cases where Python allows you to make code both pleasant and practical at the same time.
But when __slots__ should not be used
That's where adult conversation begins.
__slots__ are not needed simply because “that's what optimizers recommend.”
He's not the best choice if:
- the object needs dynamic attributes;
- the library expects a regular __dict__;
- Do you use monkey patching often?;
- The model lives in a very dynamic environment;
- you save microscopic bytes against the background of network queries and SQL.
That is, __slots__ is not always useful, but when you have:
- there are many objects of the same type;
- fixed scheme;
- memory and overhead are really important.
If the application is an API binding above the database, where everything rests on the network and PostgreSQL, you may not notice the difference at all.
Where Python projects lose the most without benefit
There are several typical scenarios.
1. A list of dictionaries instead of structured entities
Very often, data is dragged as a list[dict], although the set of fields is fixed from the first day. This is convenient at the start, but expensive at scale.
2. Redundant intermediate objects
The code is beautiful, “functional”, with a bunch of transformations — and then it turns out that one pipeline creates millions of temporary entities.
3. An attempt to treat everything with micro-optimization
The developer starts arguing which is faster — list comprehension or map, and the main problem is that the application holds 5 times more objects in memory than it needs.
That's why talking about data structure is usually more useful than talking about syntax microoptimization.
Practical checklist
If a Python service unexpectedly eats up memory or behaves heavily, I would check this:
1. Do you have the correct data structure at all?
Isn't there a dict where the fixed model should be?
2. Do you have too many objects?
Sometimes the problem is not the size of the object, but the number of objects.
3. Is it possible to make the model more compact?
Through:
- dataclass(slots=True),
- __slots__,
- simpler containers.
4. Are there any unnecessary intermediate collections?
Sometimes a generator is more useful than a list not because of the “Python speed”, but because of the memory.
5. Have you measured?
Without tracemalloc, sys.getsizeof, memory_profiler, and normal profiling, talking about performance almost always turns into guessing games.
The most important thing
Python is really not the cheapest language in terms of overhead. But very often it is “slow” and “gluttonous” not because it is Python, but because we choose too expensive default structures.
And this, in my opinion, is one of the most interesting features of the language: in Python, performance is often determined not by how you write a loop, but by what exactly you store in memory and in what form.
Sometimes one dataclass(slots=True) gives a project more benefit than ten hours of arguments about optimizing comprehension.
Когда говорят, что Python медленный, обычно вспоминают циклы, GIL или отсутствие JIT в привычном для других языков смысле. Но на практике в продакшене Python-код очень часто тормозит и раздувает память не из-за “языка вообще”, а из-за одной гораздо более тихой причины: слишком дорогих объектов.
Это та проблема, которую многие замечают слишком поздно. Сервис вроде работает. ETL проходит. Парсер собирает данные. Бот отвечает. Но память растёт, GC живёт своей жизнью, кэш распухает, а потом выясняется, что приложение тонет не в математике, а в миллионах удобных, но тяжёлых Python-объектов.
И вот это уже интереснее, чем очередной спор про “Python vs Go”.
Главная мысль: в Python вы платите не только за данные, но и за форму, в которой их храните
Возьмём обычный класс:
class User:
def __init__(self, user_id: int, name: str, active: bool):
self.user_id = user_id
self.name = name
self.active = activeВыглядит безобидно. Но каждый экземпляр такого класса обычно тащит за собой __dict__, а значит:
- динамическое хранение атрибутов,
- дополнительную память,
- лишнюю гибкость, которая часто вообще не нужна.
На десятке объектов это не важно. На сотнях тысяч и миллионах — уже очень важно.
И вот тут у Python есть один из самых недооценённых инструментов: __slots__.
Что меняет __slots__
__slots__ говорит интерпретатору: “У этого объекта фиксированный набор атрибутов. Не нужно создавать обычный словарь для каждого экземпляра”.
Пример:
class User:
__slots__ = ("user_id", "name", "active")
def __init__(self, user_id: int, name: str, active: bool):
self.user_id = user_id
self.name = name
self.active = activeЧто это даёт:
- меньше памяти на каждый объект;
- меньше давления на GC;
- более компактное хранение;
- иногда чуть более быстрый доступ к атрибутам.
Важно: это не серебряная пуля. Но в системах, где вы держите в памяти много однотипных сущностей, разница может быть очень заметной.
Типичные места, где это реально полезно:
- ETL и data pipelines;
- парсеры и краулеры;
- системы событий;
- in-memory индексы;
- боты и high-load backend;
- аналитические промежуточные структуры;
- большие кэши.
Почему это нетривиально
Потому что у Python очень высокая цена удобства.
Обычный Python-объект — это не просто “контейнер для трёх полей”. Это объект с метаданными, динамикой, ссылками, словарями, возможностью на лету дописывать свойства и вообще жить максимально свободной жизнью.
И если в коде таких объектов десятки тысяч, всё хорошо. Если миллионы — вы внезапно начинаете платить огромный налог за то, что вам на самом деле не было нужно.
Вот классический антипример:
records = []
for i in range(1_000_000):
records.append({
"id": i,
"name": f"user_{i}",
"active": i % 2 == 0,
})Это удобно. Но список словарей — очень дорогая структура по памяти.
Если схема фиксированная, почти всегда стоит хотя бы задать себе вопрос: а нужен ли тут dict, или лучше использовать:
- dataclass(slots=True),
- обычный класс со __slots__,
- namedtuple,
- tuple,
- array,
- numpy/pandas, если задача действительно табличная.
Очень хороший практический компромисс: dataclass(slots=True)
Одна из лучших возможностей современного Python — не выбирать между “красивым кодом” и “нормальной памятью”.
Можно написать так:
from dataclasses import dataclass
@dataclass(slots=True)
class User:
user_id: int
name: str
active: boolИ получить:
- читаемую модель;
- типизированные поля;
- автоматический __init__;
- удобный repr;
- компактнее объект, чем у обычного класса.
Это один из тех случаев, когда Python позволяет сделать код и приятным, и практичным одновременно.
Но когда __slots__ использовать не надо
Вот где начинается взрослый разговор.
__slots__ не нужен просто потому, что “так советуют оптимизаторы”.
Он не лучший выбор, если:
- объекту нужны динамические атрибуты;
- библиотека ожидает обычный __dict__;
- вы часто используете monkey patching;
- модель живёт в очень динамичной среде;
- вы экономите микроскопические байты на фоне сетевых запросов и SQL.
То есть __slots__ полезен не всегда, а тогда, когда у вас:
- много однотипных объектов;
- фиксированная схема;
- память и накладные расходы действительно важны.
Если приложение — API-обвязка над базой, где всё упирается в сеть и PostgreSQL, вы можете вообще не заметить разницы.
Где Python-проекты теряют больше всего без пользы
Есть несколько типичных сценариев.
1. Список словарей вместо структурированных сущностей
Очень часто данные тащат как list[dict], хотя набор полей фиксирован с первого дня. Это удобно на старте, но дорого на масштабе.
2. Избыточные промежуточные объекты
Код красивый, “функциональный”, с кучей преобразований — а потом оказывается, что один pipeline создаёт миллионы временных сущностей.
3. Попытка лечить всё micro-оптимизацией
Разработчик начинает спорить, что быстрее — list comprehension или map, а основная проблема в том, что приложение держит в памяти в 5 раз больше объектов, чем нужно.
Именно поэтому разговор про структуру данных обычно полезнее, чем разговор про микрооптимизацию синтаксиса.
Практический чек-лист
Если Python-сервис неожиданно ест память или ведёт себя тяжеловато, я бы проверял вот это:
1. У вас вообще правильная структура данных?
Не dict ли там, где должна быть фиксированная модель?
2. У вас слишком много объектов?
Иногда проблема не в размере объекта, а в их количестве.
3. Можно ли сделать модель компактнее?
Через:
- dataclass(slots=True),
- __slots__,
- более простые контейнеры.
4. Нет ли лишних промежуточных коллекций?
Иногда генератор полезнее списка не из-за “скорости Python”, а из-за памяти.
5. Вы измеряли?
Без tracemalloc, sys.getsizeof, memory_profiler и нормального профилирования разговор о производительности почти всегда превращается в угадайку.
Самое важное
Python действительно не самый дешёвый язык по накладным расходам. Но очень часто он “медленный” и “прожорливый” не потому, что он Python, а потому, что мы выбираем слишком дорогие структуры по умолчанию.
И это, на мой взгляд, одна из самых интересных особенностей языка: в Python производительность часто определяется не тем, как вы пишете цикл, а тем, что именно вы храните в памяти и в какой форме.
Иногда один dataclass(slots=True) даёт проекту больше пользы, чем десять часов споров про оптимизацию comprehension.