stdlib: datetime, zoneinfo, json, csv

Работа с временем и сериализация в JSON/CSV - повседневные backend-задачи. В этом уроке - модули datetime и zoneinfo для timezone-aware дат, json для API-сообщений и csv для табличных данных. Открывать файлы будем через знакомые по прошлому уроку open и pathlib.

datetime - даты и время

from datetime import datetime, date, time, timedelta

# Текущее время (naive - без таймзоны)
now = datetime.now()
print(now)   # 2026-05-27 14:30:00.123456

# Конкретная дата
d = date(2026, 5, 27)
print(d)     # 2026-05-27

# Время без даты
t = time(14, 30, 0)

# Из строки
dt = datetime.strptime("2026-05-27 14:30:00", "%Y-%m-%d %H:%M:%S")

# В строку
s = dt.strftime("%Y-%m-%d %H:%M:%S")

strptime - parse строки в datetime, strftime - формат datetime в строку. Format codes:

  • %Y - год 4 цифры
  • %m - месяц с ведущим нулём
  • %d - день
  • %H - часы 24-часовой формат
  • %M - минуты
  • %S - секунды
  • %z - UTC offset
  • %Z - timezone name

ISO 8601 - стандартный формат

# Текущее в ISO формате
dt = datetime.now()
iso = dt.isoformat()
# '2026-05-27T14:30:00.123456'

# Парсинг ISO 8601
dt = datetime.fromisoformat("2026-05-27T14:30:00")
# С Python 3.11+ поддерживает и timezone-aware с Z и +HH:MM

ISO 8601 - предпочтительный формат для API и логов. Сортируется лексикографически, понятен людям и машинам.

timedelta - разница и арифметика

from datetime import timedelta

dt = datetime.now()
yesterday = dt - timedelta(days=1)
in_one_hour = dt + timedelta(hours=1)
in_30_min = dt + timedelta(minutes=30)

# Разница
delta = dt - yesterday
print(delta.days)            # 1
print(delta.total_seconds()) # 86400

timedelta принимает days, hours, minutes, seconds, microseconds, milliseconds, weeks. Можно складывать/вычитать с datetime.

timezone aware - современная практика

from datetime import datetime, timezone

# UTC
now = datetime.now(timezone.utc)
print(now)   # 2026-05-27 14:30:00.123456+00:00

# Naive (без таймзоны)
naive = datetime.now()
# print(naive.timestamp())   # OS-specific интерпретация - опасно!

Правило: всегда используй timezone-aware datetimes. Naive datetimes - источник багов в multi-timezone applications.

zoneinfo - таймзоны (Py 3.9+)

from datetime import datetime
from zoneinfo import ZoneInfo

# Текущее время в Москве
moscow = datetime.now(ZoneInfo("Europe/Moscow"))
print(moscow)   # 2026-05-27 17:30:00+03:00

# Конкретная дата в таймзоне
dt = datetime(2026, 5, 27, 14, 0, tzinfo=ZoneInfo("UTC"))
print(dt.astimezone(ZoneInfo("America/New_York")))
# 2026-05-27 10:00:00-04:00

zoneinfo использует IANA database, корректно обрабатывает переход на летнее время, исторические изменения.

До Python 3.9 использовалась библиотека pytz. Сейчас zoneinfo встроенный и рекомендуется.

Сравнение и форматирование

dt1 = datetime(2026, 5, 27, 14, 0, tzinfo=timezone.utc)
dt2 = datetime(2026, 5, 27, 17, 0, tzinfo=ZoneInfo("Europe/Moscow"))

# Сравнение timezone-aware работает корректно
print(dt1 == dt2)   # True - один момент времени

# Сравнение naive и aware - TypeError
naive = datetime(2026, 5, 27, 14, 0)
# dt1 == naive   # TypeError - нельзя сравнивать

Mixing aware и naive - TypeError. Это спасает от багов: либо везде aware, либо везде naive.

Распространённые задачи

from datetime import datetime, timedelta, timezone

# Сейчас в UTC
now_utc = datetime.now(timezone.utc)

# Начало дня
start_of_day = now_utc.replace(hour=0, minute=0, second=0, microsecond=0)

# Возраст по дате рождения
from datetime import date
birth = date(1990, 5, 15)
today = date.today()
age = (today - birth).days // 365   # упрощённо

# Unix timestamp
ts = now_utc.timestamp()
# Из timestamp
dt = datetime.fromtimestamp(ts, tz=timezone.utc)

# Только дата
just_date = now_utc.date()

json - сериализация

import json

# Object → JSON string
data = {"name": "Alice", "age": 30, "tags": ["python", "backend"]}
json_str = json.dumps(data)
# '{"name": "Alice", "age": 30, "tags": ["python", "backend"]}'

# Pretty
json_str = json.dumps(data, indent=2)

# С Unicode
json_str = json.dumps({"name": "Алиса"}, ensure_ascii=False)
# Без ensure_ascii=False: {"name": "Алиса"}

# JSON string → Python object
data = json.loads('{"name": "Bob"}')

# Из/в файла
with open("data.json", "w") as f:
    json.dump(data, f, indent=2)

with open("data.json") as f:
    data = json.load(f)

dumps/loads для строк (s = string). dump/load для файлов. Это convention для всех serialization модулей в Python.

JSON и нестандартные типы

JSON поддерживает только: str, int, float, bool, None, list, dict. Чтобы сериализовать datetime, UUID, set:

import json
from datetime import datetime
from uuid import UUID

data = {
    "id": UUID("12345678-1234-5678-1234-567812345678"),
    "created": datetime.now(timezone.utc),
    "tags": {"a", "b"},
}

# json.dumps(data)   # TypeError

# Custom encoder
class CustomEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, datetime):
            return obj.isoformat()
        if isinstance(obj, UUID):
            return str(obj)
        if isinstance(obj, set):
            return list(obj)
        return super().default(obj)

json_str = json.dumps(data, cls=CustomEncoder)

Или через default= параметр:

def custom(obj):
    if isinstance(obj, datetime):
        return obj.isoformat()
    if isinstance(obj, UUID):
        return str(obj)
    if isinstance(obj, set):
        return list(obj)
    raise TypeError(f"Not serializable: {type(obj)}")

json.dumps(data, default=custom)

В реальном коде backend часто использует Pydantic или dataclasses_json - они автоматически сериализуют сложные типы, включая dataclass и Enum.

json.tool - CLI форматирование

echo '{"name":"Alice","age":30}' | python -m json.tool
# {
#     "name": "Alice",
#     "age": 30
# }

python -m json.tool < input.json > formatted.json

Удобно для проверки/форматирования JSON в shell.

csv - работа с табличными данными

import csv

# Запись
with open("data.csv", "w", newline="") as f:
    writer = csv.writer(f)
    writer.writerow(["name", "age", "city"])
    writer.writerow(["Alice", 30, "Moscow"])
    writer.writerow(["Bob", 25, "London"])

# Чтение
with open("data.csv") as f:
    reader = csv.reader(f)
    for row in reader:
        print(row)   # список значений

newline="" в open важно - csv модуль сам обрабатывает line endings.

DictReader / DictWriter

Удобнее для именованных полей:

import csv

# Запись с заголовками
with open("data.csv", "w", newline="") as f:
    fields = ["name", "age", "city"]
    writer = csv.DictWriter(f, fieldnames=fields)
    writer.writeheader()
    writer.writerow({"name": "Alice", "age": 30, "city": "Moscow"})
    writer.writerow({"name": "Bob", "age": 25, "city": "London"})

# Чтение как словари
with open("data.csv") as f:
    reader = csv.DictReader(f)
    for row in reader:
        print(f"{row['name']}: {row['age']}")

DictReader первая строка - заголовки, остальные - dict. Удобнее чем индексирование по позиции.

CSV dialect

# Tab-separated
with open("data.tsv", "w", newline="") as f:
    writer = csv.writer(f, delimiter="\t")
    writer.writerow(["a", "b", "c"])

# Excel dialect (default)
csv.writer(f, dialect="excel")

# Кастомный dialect
csv.register_dialect("pipe", delimiter="|", quoting=csv.QUOTE_ALL)
writer = csv.writer(f, dialect="pipe")

CSV не строгий стандарт - разные программы используют разные форматы. dialect помогает работать с конкретным форматом.

Большие CSV файлы

# Чтение построчно - не загружает весь файл в память
with open("huge.csv") as f:
    reader = csv.DictReader(f)
    for row in reader:
        process(row)

csv.reader/DictReader ленивые - читают построчно. Подходит для очень больших файлов.

Для очень крупных и сложных табличных данных лучше pandas или polars. Стандартный csv подходит для простых случаев.

Другие форматы из stdlib

import configparser
config = configparser.ConfigParser()
config.read("config.ini")
print(config["section"]["key"])

import tomllib   # Python 3.11+, только чтение
with open("config.toml", "rb") as f:
    data = tomllib.load(f)

Запись TOML - через сторонние tomli_w или tomlkit. Чтение - в стандартной библиотеке с Python 3.11.

Распространённые ошибки

1. Naive datetime в производственном коде

# Плохо - не таймзонно-aware
dt = datetime.now()
expires = dt + timedelta(hours=1)
# Если сервер в одной таймзоне, клиент в другой - баги

# Хорошо
dt = datetime.now(timezone.utc)
expires = dt + timedelta(hours=1)

Всегда timezone-aware в API, БД, логах.

2. JSON без ensure_ascii=False для нелатинских символов

json.dumps({"name": "Иван"})
# '{"name": "Иван"}'   # читаемо но многословно

json.dumps({"name": "Иван"}, ensure_ascii=False)
# '{"name": "Иван"}'   # компактнее

Для frontend-API и человекочитаемых логов - ensure_ascii=False.

3. csv без newline=""

with open("data.csv", "w") as f:   # без newline
    csv.writer(f).writerow(["a", "b"])
# На Windows может быть двойные line endings

Всегда newline="" при работе с csv.

4. strftime/strptime для парсинга ISO

# Плохо - сложный format string
datetime.strptime("2026-05-27T14:30:00", "%Y-%m-%dT%H:%M:%S")

# Лучше для ISO
datetime.fromisoformat("2026-05-27T14:30:00")

fromisoformat для ISO 8601, strptime для специфичных форматов.

5. Сравнение naive и aware datetimes

naive = datetime(2026, 5, 27)
aware = datetime(2026, 5, 27, tzinfo=timezone.utc)
# naive < aware   # TypeError

Используй единый стиль (aware) везде.

Сравнение с Go

В Go есть time пакет:

now := time.Now()
inOneHour := now.Add(time.Hour)

// Часовые пояса
loc, _ := time.LoadLocation("Europe/Moscow")
moscow := now.In(loc)

// JSON через encoding/json
data, _ := json.Marshal(map[string]any{"name": "Alice"})

Go более строго с типами и timezone-awareness (Time всегда aware). Python flexible (aware и naive) но требует дисциплины. Семантически близко.

Мини-задание

  1. Datetime с таймзоной:
from datetime import datetime, timezone
from zoneinfo import ZoneInfo

# Текущее время UTC и в Москве
utc_now = datetime.now(timezone.utc)
msk_now = utc_now.astimezone(ZoneInfo("Europe/Moscow"))

print(f"UTC: {utc_now.isoformat()}")
print(f"MSK: {msk_now.isoformat()}")

# Через 30 дней
from datetime import timedelta
future = utc_now + timedelta(days=30)
print(f"In 30 days: {future.isoformat()}")
  1. JSON с custom encoder:
import json
from datetime import datetime, timezone
from uuid import uuid4

data = {
    "id": uuid4(),
    "created": datetime.now(timezone.utc),
    "name": "Алиса",
    "tags": {"backend", "python"},
}

def encode(obj):
    if isinstance(obj, datetime):
        return obj.isoformat()
    from uuid import UUID
    if isinstance(obj, UUID):
        return str(obj)
    if isinstance(obj, set):
        return sorted(obj)
    raise TypeError(f"Not serializable: {type(obj)}")

print(json.dumps(data, default=encode, ensure_ascii=False, indent=2))
  1. CSV с DictReader/Writer:
import csv

users = [
    {"name": "Alice", "age": 30, "email": "alice@example.com"},
    {"name": "Bob", "age": 25, "email": "bob@example.com"},
    {"name": "Charlie", "age": 35, "email": "charlie@example.com"},
]

# Запись
with open("users.csv", "w", newline="") as f:
    writer = csv.DictWriter(f, fieldnames=["name", "age", "email"])
    writer.writeheader()
    writer.writerows(users)

# Чтение
with open("users.csv") as f:
    for row in csv.DictReader(f):
        print(f"{row['name']} - {row['email']}")

Что дальше

Освоили работу с датами и сериализацию. В следующем уроке (последнем в Модуле 8) - logging: стандартный механизм журналирования вместо print, конфигурация, обработчики.

Зарегистрируйтесь бесплатно, чтобы пройти квиз, решить задание с автопроверкой и вести прогресс.