Learning Platform
Урок 04.01 · 22 мин
Начальный
VariablesTypesExpressionsMutabilityOperators
Память и железо: как Python-объекты живут в RAM CPython object model: всё есть объект

Откуда начинать

В прошлом модуле мы поставили

uv
, создали проект и запустили print("Hello!"). С этого урока начинается сам язык. Цель модуля — чтобы вы умели читать и писать любой простой Python-скрипт: переменные, типы, коллекции, циклы, функции, исключения. На фундаменте, который мы здесь заложим, дальше будут идиомы, типы, I/O и работа с базой.

Начнём с переменных и типов. Тема выглядит банально, но именно тут джуниоры на ровном месте получают пять разных ошибок: TypeError: unsupported operand, странности с плавающей точкой, баги с

mutability
, путаница между is и ==. Разберёмся раз и навсегда.

Динамические типы — у объектов, не у переменных

В

статически типизированных языках
вроде C или Go тип принадлежит переменной: int x = 5; — это «коробка с меткой int, в неё можно класть только целые числа». В Python переменная — это просто имя, временно указывающее на какой-то объект в памяти. Тип принадлежит объекту, а не имени.

x = 5          # имя x указывает на объект int(5)
x = "hello"    # теперь то же имя указывает на объект str("hello")
x = [1, 2, 3]  # теперь — на объект list

Это не «магия» и не «отсутствие типов» — это другое расположение информации. У каждого объекта в момент исполнения есть точный тип, узнать его можно через type():

x = 5
print(type(x))         # <class 'int'>
print(type("hello"))   # <class 'str'>
print(type(3.14))      # <class 'float'>

Динамическая типизация удобна на скорости разработки и опасна тем, что ошибки типа всплывают только в runtime. Поэтому в современном Python мы будем сразу писать

type hints
— подсказки типов, которые читает
статический анализатор
до запуска:

x: int = 5
name: str = "Анна"
price: float = 99.90

Type hints — это подсказки, не контракт. Интерпретатор их не проверяет, в x: int = "abc" он не упадёт. Но mypy, pyright и IDE подсветят ошибку красным до того, как код доедет до прода. Возвращаться к типам глубже мы будем в модуле 5, а пока — просто пишите аннотации там, где видите аналогичные в этих уроках.

Базовые типы

В Python 3.13 есть шесть встроенных типов, которые покроют 95% ваших задач как junior DE:

Базовые типы Python

Шесть типов, которые встречаются в любом DE-скрипте. Mutable отмечены жёлтым.

int42Целое число произвольной точности — без лимита разрядности
float3.1464-битное число IEEE 754 — двоичная плавающая точка, неточная для долей
boolTrue / FalseПодтип int: True == 1, False == 0
str"hello"Юникод-строка, immutable. UTF-8 при сериализации в bytes.
bytesb'\\x48\\x65'Иммутабельная последовательность байт. Для файлов, сети, бинарных форматов.
NoneTypeNoneЕдинственный объект типа NoneType. Означает «ничего», аналог null в других языках.

Разберём каждый — на DE-примерах.

int — целое число без лимита

Python int не имеет лимита разрядности. В C int — это 32 бита, переполняется на 2^31 - 1. В Python вы можете без проблем держать число с тысячей цифр:

big = 2 ** 1000
print(big)
# 10715086071862673209484250490600018105614048117055336074437503883703510511249361224931983788156958581275946729175531468251871452856923140435984577574698574803934567774824230985421074605062371141877954182153046474983581941267398767559165543946077062914571196477686542167660429831652624386837205668069376

Внутри он растёт автоматически в

bignum
. Это удобно для подсчёта строк в больших таблицах, идентификаторов из Snowflake, агрегатов поверх миллиардов событий.

float — IEEE 754 с известной болью

float в Python — это

IEEE 754
double — 64-битная двоичная плавающая точка. Это значит, что многие десятичные дроби, привычные нам, не представимы точно:

print(0.1 + 0.2)        # 0.30000000000000004
print(0.1 + 0.2 == 0.3) # False

Это не баг Python и не баг конкретной реализации. Так устроена двоичная плавающая точка в любом языке. Что с этим делать как DE:

  • Для денег и любых точных дробей — используйте decimal.Decimal. Это «честная» десятичная арифметика. Медленнее, но точная.
  • Для научных расчётов с известной погрешностью — float нормально, просто никогда не сравнивайте через ==. Сравнивайте через math.isclose(a, b) или abs(a - b) < epsilon.
from decimal import Decimal

total = Decimal("0.1") + Decimal("0.2")
print(total)            # 0.3
print(total == Decimal("0.3"))  # True

Обратите внимание: Decimal("0.1") через строку, не через Decimal(0.1). Иначе мы передадим внутрь уже испорченный float и точность потеряется на входе.

bool — это int

True и False в Python — не отдельный тип, а подтип int:

print(True == 1)        # True
print(False == 0)       # True
print(True + True)      # 2
print(isinstance(True, int))  # True

Это иногда полезно: sum([True, False, True, True]) вернёт 3, что удобно для подсчёта совпавших условий. Чаще — путает. Не пишите True + 1 ради «оптимизации», читать такое больно.

str — всегда юникод

В Python 3 строка str — это последовательность

кодовых точек Unicode
. Это было осознанное решение разработчиков языка в 2008 году: в Python 2 строки были байтами, и из-за этого приходили баги при работе с любым не-латинским текстом. В Python 3 этот класс багов невозможен — пока вы работаете с str, вы работаете с символами.

text: str = "Привет, мир!"
print(len(text))    # 12 (символов, не байт)
print(text[0])      # 'П'

bytes — для файлов и сети

bytes — это последовательность чисел 0..255. Когда Python читает файл в режиме "rb" или получает HTTP-ответ как сырой body, он получает bytes. Чтобы перевести их в str, нужно знать кодировку:

raw: bytes = b"\xd0\x9f\xd1\x80\xd0\xb8\xd0\xb2\xd0\xb5\xd1\x82"
text: str = raw.decode("utf-8")
print(text)         # Привет

Подробнее о кодировках — в следующем уроке.

None — отсутствие значения

None — единственный объект типа NoneType. Используется как «значение неизвестно», «функция ничего не вернула», «параметр не задан». Аналог null в SQL или nil в Go.

def find_user(user_id: int) -> str | None:
    if user_id == 1:
        return "Анна"
    return None  # явно: «не нашли»

Type hint str | None (читается «str или None») — современный синтаксис из Python 3.10+, заменяющий старый Optional[str]. Так выглядит идиоматический код 2026 года.

Mutable vs immutable

Это понятие, на котором ломается каждый второй джун при первой встрече с list.

Mutable объекты можно изменить после создания. Список [1, 2, 3] — это коробка, в которую можно докинуть четвёртый элемент или поменять третий. Объект тот же — содержимое поменялось.

Immutable объекты неизменяемы. Строка "abc" после создания — навсегда "abc". «Изменение» строки на самом деле создаёт новый объект:

text = "abc"
text = text + "d"   # создан новый объект "abcd", text теперь указывает на него
Mutable vs immutable

Что разрешает менять «на месте», а что нет.

Mutableизменяется на месте
list[1, 2, 3]append, insert, remove, sort — меняют исходный объект
dict{'k': 1}d['k'] = 2, d.update(...) — меняют исходный объект
set{1, 2, 3}add, remove, discard — меняют исходный объект
bytearraybytearray(b'ab')Mutable-версия bytes. Редко нужна junior'у.
Immutableнельзя изменить
int / float / bool42Все числа immutable. x = x + 1 создаёт новый объект.
str"abc"Любая «модификация» создаёт новую строку.
bytesb'\\x01\\x02'Иммутабельная последовательность байт.
tuple(1, 2, 3)Аналог list, но без модификаций.
frozensetfrozenset({1, 2})Immutable-версия set. Можно класть в ключи dict.

Почему это важно для DE на каждом дне работы:

records = [{"id": 1, "amount": 100}]

def add_processed_flag(rs):
    for r in rs:
        r["processed"] = True   # МУТАЦИЯ — меняем сами записи!

add_processed_flag(records)
print(records)
# [{'id': 1, 'amount': 100, 'processed': True}]  <-- исходный список изменён

Функция получила records, но внутри изменила оригинал, потому что dict — mutable. Это типовой источник багов: «я же не присваивал ничего вверх по стеку, почему данные изменились?». Изменились — потому что вы передали ссылку на mutable-объект, а функция его поменяла.

Защита — либо явно копировать, либо возвращать новые объекты:

def with_processed_flag(rs: list[dict]) -> list[dict]:
    return [{**r, "processed": True} for r in rs]

Подробнее про {**r, ...} и comprehensions — в уроке 04.

Операторы

Большинство операторов выглядят как в других языках: +, -, *, /, %, ** для возведения в степень. Несколько отличий, которые стоит запомнить:

print(7 / 2)    # 3.5   — / всегда float-деление
print(7 // 2)   # 3     — // целочисленное (floor) деление
print(7 % 2)    # 1     — остаток
print(2 ** 10)  # 1024  — степень

Сравнения работают как ожидаешь, но можно сравнивать в цепочках — это уникальная фишка Python:

x = 5
if 0 < x < 10:     # эквивалент 0 < x and x < 10
    print("в диапазоне")

Логические операторы — словами: and, or, not. Они

короткозамыкающие
и возвращают не True/False, а сам последний вычисленный операнд:

print(1 and 2)      # 2 — оба истинны, возвращён последний
print(0 and 2)      # 0 — первый ложен, второй не вычислялся
print(None or "default")  # 'default' — первый ложен

Удобный приём для подстановки дефолтов: name = user_input or "Anonymous".

Принадлежность — оператор in:

print(3 in [1, 2, 3])      # True
print("py" in "python")    # True
print("name" in {"name": "Anna"})  # True — у dict ищет в ключах

is vs == — два разных вопроса

Это один из любимых вопросов на собеседованиях, и тут много мифов.

  • ==равны ли значения. «У этих двух яблок одинаковый вкус?»
  • isодин и тот же объект в памяти. «Это одно и то же яблоко или два разных, но похожих?»
a = [1, 2, 3]
b = [1, 2, 3]
c = a

print(a == b)   # True — содержимое одинаковое
print(a is b)   # False — разные объекты
print(a is c)   # True — одно и то же

В 95% случаев вы хотите ==. Единственное место, где принято использовать is — сравнение с None, True, False:

if user is None:        # PEP 8 — так правильно
    ...

if user == None:        # формально работает, но не принято
    ...

Причина — None это singleton, у него ровно один объект в памяти, и is чуть быстрее и выразительнее. Аналогично для True и False, но в коде это обычно не нужно — сравнение с булевым лучше писать как if x: или if not x:.

WARNING

Не пишите x is 5 или x is "abc". Для маленьких чисел и коротких строк Python часто кеширует объекты, и is может «как бы работать». Но это деталь реализации CPython, не часть языка. На больших числах или длинных строках сломается без предупреждения. Сравниваете значения — пишите ==.

Упражнение

Откройте Python в проекте: uv run python (REPL). Выполните и объясните результат каждого выражения:

0.1 + 0.2 == 0.3
True + True + True
"abc" * 3
[1, 2] + [3, 4]
[1, 2] * 3
None == False
None == None
1 == 1.0
1 is 1.0
type(2 ** 100) is int
"привет"[0]
b"hello".decode("utf-8")
"hello".encode("utf-8")

Критерии: для каждого выражения вы можете объяснить почему получился такой результат, не «потому что Python так работает», а через концепции из этого урока (mutability, float-арифметика, типы, is vs ==).

В следующем уроке — строки, f-strings и кодировки. Самая частая боль DE: «открыл CSV из 2003 года, а там какие-то крокозябры».

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 8