Откуда начинать
В прошлом модуле мы поставили
print("Hello!"). С этого урока начинается сам язык. Цель модуля — чтобы вы умели читать и писать любой простой Python-скрипт: переменные, типы, коллекции, циклы, функции, исключения. На фундаменте, который мы здесь заложим, дальше будут идиомы, типы, I/O и работа с базой.
Начнём с переменных и типов. Тема выглядит банально, но именно тут джуниоры на ровном месте получают пять разных ошибок: TypeError: unsupported operand, странности с плавающей точкой, баги с
is и ==. Разберёмся раз и навсегда.
Динамические типы — у объектов, не у переменных
В
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 мы будем сразу писать
x: int = 5
name: str = "Анна"
price: float = 99.90
Type hints — это подсказки, не контракт. Интерпретатор их не проверяет, в x: int = "abc" он не упадёт. Но mypy, pyright и IDE подсветят ошибку красным до того, как код доедет до прода. Возвращаться к типам глубже мы будем в модуле 5, а пока — просто пишите аннотации там, где видите аналогичные в этих уроках.
Базовые типы
В Python 3.13 есть шесть встроенных типов, которые покроют 95% ваших задач как junior DE:
Шесть типов, которые встречаются в любом DE-скрипте. Mutable отмечены жёлтым.
Разберём каждый — на DE-примерах.
int — целое число без лимита
Python int не имеет лимита разрядности. В C int — это 32 бита, переполняется на 2^31 - 1. В Python вы можете без проблем держать число с тысячей цифр:
big = 2 ** 1000
print(big)
# 10715086071862673209484250490600018105614048117055336074437503883703510511249361224931983788156958581275946729175531468251871452856923140435984577574698574803934567774824230985421074605062371141877954182153046474983581941267398767559165543946077062914571196477686542167660429831652624386837205668069376
Внутри он растёт автоматически в
float — IEEE 754 с известной болью
float в Python — это
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 — это последовательность
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 теперь указывает на него
Что разрешает менять «на месте», а что нет.
Почему это важно для 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:.
Не пишите 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 года, а там какие-то крокозябры».