Установка Python, терминал, первый запуск, traceback, PEP 8, uv и venv, модули, файлы и документация
- Маршрут: Основы · тема 1 из 7
- Навыки:
J1,J5,J7- До урока: входная тема; опыт программирования не требуется. Нужны компьютер, редактор текста и возможность открыть терминал.
- Результат: воспроизводимое окружение, каркас пакета и небольшая файловая обработка данных с осмысленным журналированием.
- Основа: окружение, импорты,
pathlibи файловые форматы. Работа в эксплуатации:pyproject.toml, lock-файл, конфигурация и наблюдаемость. Углубление: границы импорта, пакета, сборки и воспроизводимость окружения.- Подтверждение: запускаемый проект и лог обработки входного файла.
В этом уроке сначала установим Python, запустим первый файл и разберём минимум синтаксиса. Затем перейдём к устройству проекта и окружения, работе с файлами, документации и журналированию. В конце соберём всё в небольшой запускаемый проект. PEP 8 здесь нужен как общий язык команды, а не как набор правил ради правил.
Все команды с
$выполняются в терминале без самого символа$. Блоки кода в разделах про импорты, форматы и логирование — отдельные примеры: не объединяйте их в один файл, если текст явно этого не требует.
Курс рассчитан на CPython 3.12–3.14 и использует uv для воспроизводимого
окружения. CPython — основная реализация языка Python, а uv устанавливает
нужную версию интерпретатора, создаёт изолированное окружение и запускает
команды проекта.
uv и PythonОфициальные команды установки uv:
# macOS и Linux
curl -LsSf https://astral.sh/uv/install.sh | sh# Windows PowerShell
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"Перезапустите терминал и проверьте установку:
uv --version
uv python install 3.14
uv run --python 3.14 python --versionЕсли политика организации запрещает запуск установочного скрипта, выберите
пакетный менеджер из официальной инструкции uv.
Команда python --version может указывать на другой системный Python; в курсе
используйте uv run python, чтобы запускать версию из проекта.
pwd # показать текущий каталог (Windows: cd без аргументов)
ls # показать содержимое (Windows: dir)
mkdir python-start # создать каталог
cd python-start # перейти в негоСоздайте проект и первый файл:
uv init --python 3.14Откройте созданный main.py в редакторе и замените его содержимое:
name = "Студент"
lessons_completed = 0
print(f"Привет, {name}!")
print("Пройдено уроков:", lessons_completed)Запустите файл из каталога проекта:
uv run python main.pyОжидаемый вывод:
Привет, Студент!
Пройдено уроков: 0Интерактивный режим (REPL) удобен для коротких проб:
uv run pythonПосле приглашения >>> введите 2 + 2 и получите 4. Выйти можно командой
exit() или сочетанием Ctrl-D (Ctrl-Z, затем Enter в Windows).
Намеренно уберите закрывающую кавычку в первой строке main.py и снова
запустите файл. Python покажет traceback — сообщение с местом и типом ошибки.
Читайте его снизу вверх:
SyntaxError;File ".../main.py", line 1 указывает файл и номер строки;^ показывает место, рядом с которым парсер обнаружил проблему.Верните кавычку и убедитесь, что команда снова завершается без ошибки.
SyntaxError означает, что Python не смог разобрать текст программы.
Исключения вроде NameError возникают уже во время выполнения; отдельная
тема курса объяснит, какие из них нужно обрабатывать.
Переменная связывает имя со значением. = выполняет присваивание, а ==
сравнивает значения:
price = 120
quantity = 3
total = price * quantity
print(total) # 360
print(total == 360) # TrueОтступы объединяют команды в блок. После строки, открывающей блок, ставится двоеточие:
if total >= 300:
print("Бесплатная доставка")
else:
print("Доставка оплачивается отдельно")Список хранит последовательность, словарь — пары ключ–значение, а цикл for
по очереди получает элементы:
prices = [120, 80, 200]
order = {"id": 17, "paid": True}
for item_price in prices:
print(item_price)
print(order["id"]) # 17Функция даёт имя повторяемому действию. Параметры перечисляются в скобках,
return возвращает результат:
def calculate_total(price, quantity):
return price * quantity
result = calculate_total(120, 3)
print(result) # 360import подключает готовый модуль. Конструкция with гарантирует завершение
работы с ресурсом, а try/except описывает ожидаемую ошибку. Подробные правила
будут в темах про исключения и контекстные менеджеры; здесь достаточно увидеть
назначение:
from pathlib import Path
path = Path("message.txt")
path.write_text("Привет\n", encoding="utf-8")
try:
with path.open(encoding="utf-8") as file:
print(file.read())
except OSError as error:
print(f"Не удалось прочитать файл: {error}")Перед продолжением проверьте себя: вы можете объяснить разницу между редактором,
терминалом и интерпретатором; запустить main.py; найти номер строки в
traceback; предсказать вывод пяти примеров выше. Если нет — повторите этот
раздел: последующие разделы опираются на эти действия.
# snake_case — переменные, функции, методы, модули
user_name = "Alice"
def calculate_total(items): ...
MAX_RETRY_COUNT = 3 # UPPER_SNAKE_CASE — константы
_private_var = "internal" # _ — соглашение о приватности
# PascalCase используют для классов; они подробно появятся в теме ООП:
class UserAccount:
...
# Имена: описательные, не сокращения
# Проблема: calc, n, usr, tmp
# Вариант: calculate_total, count, user, temp_file# Отступы: 4 пробела (не табуляция)
def function():
if condition:
do_something()
# Максимум 79 символов в строке (PEP 8) или 88/99 (Black)
# Перенос с обратным слешем или скобками (предпочтительно):
result = (
some_very_long_variable
+ another_very_long_variable
+ yet_another_variable
)
# Пробелы вокруг операторов:
x = 5 + 3 # Вариант:
x=5+3 # Проблема:
func(a=1, b=2) # Вариант: keyword args — без пробелов
func(a =1, b= 2) # Проблема:
# Пустые строки:
# 2 перед определением класса или функции на верхнем уровне
# 1 внутри класса между методамиpip install black isort ruff
black myfile.py # форматирует код по стандарту Black
isort myfile.py # сортирует импорты
ruff check myfile.py # линтер: находит проблемы
ruff check --fix . # автоисправлениеМодуль — это файл Python, например billing.py. Обычный пакет — каталог с
модулями и файлом __init__.py. Начиная с Python 3.3 существуют и namespace
packages без __init__.py, но для первого проекта явный файл обычно делает
структуру понятнее.
myproject/
├── pyproject.toml
├── src/
│ └── myapp/
│ ├── __init__.py
│ ├── __main__.py
│ └── reports.py
└── tests/import myapp.reports ищет модуль по каталогам из sys.path. В этот список
попадают, среди прочего, каталог запуска, стандартная библиотека,
site-packages активного окружения и пути из PYTHONPATH. Не стоит исправлять
импорты случайным sys.path.append(...): установите проект в окружение или
запускайте модуль через python -m myapp.reports.
# Правильный порядок (isort это делает автоматически):
# 1. Стандартная библиотека
import os
import sys
from pathlib import Path
from typing import Optional
# 2. Сторонние пакеты
import requests
import fastapi
from pydantic import BaseModel
# 3. Локальные модули
from myapp.models import User
from .utils import helper_function
# Не используйте wildcard импорты (засоряют namespace):
from os import * # Проблема: — что именно импортировано?
# Alias для длинных имён:
import numpy as np
import pandas as pd
from datetime import datetime as dtОтносительный импорт начинается с точки и считается от текущего пакета:
from .models import User # соседний модуль
from ..common import settings # пакет уровнем вышеОн уместен внутри пакета. Файл с относительными импортами нужно запускать как
модуль (python -m myapp.reports), а не как отдельный скрипт по пути.
# Проблема:
# a.py: from b import B
# b.py: from a import A ← ImportError!
# Решение 1: Перенести импорт внутрь функции
def create_b():
from b import B # импорт при вызове, не при загрузке модуля
return B()
# Решение 2: TYPE_CHECKING
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from b import B # импорт только при статическом анализе
def process(b: "B") -> None: # строковая аннотация
pass
# Решение 3: Реструктуризация кода
# Вынести общие части в третий модуль common.py__all__ — публичный API модуля# mymodule.py
__all__ = ["PublicClass", "public_function"]
class PublicClass: ...
def public_function(): ...
def _private_function(): ... # не экспортируется
# from mymodule import * — импортирует только то что в __all__venv — стандартный инструментvenv входит в Python и изолирует установленные Python-пакеты. virtualenv
решает ту же задачу как сторонний инструмент и поддерживает дополнительные
сценарии и старые версии Python. Conda управляет не только Python-пакетами, но
и самим Python и нативными библиотеками; её чаще выбирают для data science и
проектов со сложными системными зависимостями. Для этого курса достаточно
venv, которым uv управляет автоматически.
# Создание
python -m venv .venv
# Активация
source .venv/bin/activate # Linux/macOS
.venv\Scripts\activate # Windows
# Деактивация
deactivate
# Управление зависимостями
pip install requests
pip freeze > requirements.txt
pip install -r requirements.txtuv — workflow проектаuv init myproject # создать проект с pyproject.toml
uv add requests # добавить runtime-зависимость
uv add --dev pytest ruff mypy
uv add pydantic-settings # пакет для примера конфигурации ниже
uv remove requests
uv sync --locked # синхронизировать строго по uv.lock
uv run python main.py # выполнить команду в окружении проекта
uv run pytest# pyproject.toml
[project]
name = "myproject"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = [
"requests>=2.32",
"pydantic>=2.0",
"pydantic-settings>=2.0",
]
[dependency-groups]
dev = ["pytest", "ruff", "mypy"]pyproject.toml описывает прямые требования проекта, а uv.lock фиксирует полное разрешение зависимостей. Для библиотек диапазоны совместимости должны отражать реально протестированный API; для приложения CI/deploy используют lock-файл. Poetry, Hatch и PDM остаются допустимыми альтернативами, но курс использует один воспроизводимый workflow, совпадающий с этим проектом.
Команда pip install . собирает и устанавливает текущую версию проекта.
Editable-установка pip install -e . связывает окружение с рабочим исходным
кодом: изменения модулей видны без повторной установки. Такой режим удобен при
разработке, но релизную сборку всё равно нужно проверять обычной установкой.
pyproject.toml служит стандартной точкой обмена между frontend сборки
(pip, uv) и backend (setuptools, Hatchling и другие). Здесь же хранятся
метаданные проекта и прямые зависимости. Точное решение всех транзитивных
зависимостей относится к lock-файлу.
В старых проектах те же настройки могут находиться в исполняемом setup.py
или декларативном setup.cfg. Для нового проекта начинайте с
pyproject.toml; legacy-файлы поддерживайте только там, где их требует
существующая сборка.
Не дублируйте номер версии вручную в нескольких файлах. Для установленного дистрибутива его можно получить из метаданных:
from importlib.metadata import PackageNotFoundError, version
try:
__version__ = version("myproject")
except PackageNotFoundError:
__version__ = "0+unknown"pathlib — современный подходfrom pathlib import Path
# Создание и навигация
project = Path("/Users/alice/projects/myapp")
config = project / "config" / "settings.yaml" # / — оператор!
print(config.name) # "settings.yaml"
print(config.stem) # "settings"
print(config.suffix) # ".yaml"
print(config.parent) # /Users/alice/projects/myapp/config
print(config.exists()) # True/False
print(config.is_file()) # True/False
print(config.is_dir()) # True/False
# Чтение / запись
text = config.read_text(encoding="utf-8")
config.write_text("key: value\n", encoding="utf-8")
data = config.read_bytes()
# Обход директории
for py_file in project.rglob("*.py"):
print(py_file)
for item in project.iterdir():
if item.is_dir():
print(f"Dir: {item.name}")
# Создание
Path("/tmp/new_dir").mkdir(parents=True, exist_ok=True)
# Переименование / перемещение
old = Path("old_name.txt")
new = old.rename("new_name.txt")
old.replace("/other/dir/file.txt") # перезаписывает если существуетpathlib покрывает повседневную работу с путями и небольшими файлами. Для
копирования каталогов, перемещения между файловыми системами и создания архивов
используйте shutil:
import shutil
shutil.copy2("source.txt", "backup/source.txt")
shutil.copytree("assets", "backup/assets", dirs_exist_ok=True)
shutil.move("draft.txt", "archive/draft.txt")
shutil.make_archive("project-backup", "zip", "project")
# Удаляет дерево целиком: путь должен быть проверен заранее.
shutil.rmtree("temporary-build")# Всегда указывайте encoding
with open("data.txt", encoding="utf-8") as f:
content = f.read()
# Большой файл — читаем построчно
with open("huge.log", encoding="utf-8") as f:
for line in f: # итерация без загрузки всего файла
print(line.rstrip("\n"))
# Бинарные файлы
with open("image.png", "rb") as f:
header = f.read(8) # первые 8 байт
# Запись
with open("output.txt", "w", encoding="utf-8") as f:
f.write("Hello\n")
f.writelines(["line1\n", "line2\n"])
# Дозапись
with open("log.txt", "a", encoding="utf-8") as f:
f.write("New log entry\n")Ошибки файловой границы обрабатывайте по ожидаемым типам и только там, где можете восстановиться или добавить контекст:
from pathlib import Path
try:
text = Path("config.json").read_text(encoding="utf-8")
except FileNotFoundError as error:
raise RuntimeError("Файл конфигурации не найден") from error
except UnicodeDecodeError as error:
raise RuntimeError("Конфигурация должна быть в UTF-8") from errorГолый except: перехватывает даже KeyboardInterrupt и SystemExit.
except Exception: уже, но для большинства мест тоже слишком широк: ловите
конкретную ошибку, которую действительно умеете обработать.
import json
data = {"name": "Alice", "age": 30, "scores": [1, 2, 3]}
# Сериализация
json_str = json.dumps(data)
json_pretty = json.dumps(data, indent=2, ensure_ascii=False) # unicode сохраняется
# Десериализация
parsed = json.loads(json_str)
# Файлы
with open("data.json", "w", encoding="utf-8") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
with open("data.json", encoding="utf-8") as f:
loaded = json.load(f)
# Кастомный encoder для нестандартных типов
import datetime
class DateTimeEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, datetime.datetime):
return obj.isoformat()
return super().default(obj)
json.dumps({"ts": datetime.datetime.now()}, cls=DateTimeEncoder)import csv
# Запись
with open("data.csv", "w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=["name", "age", "email"])
writer.writeheader()
writer.writerow({"name": "Alice", "age": 30, "email": "a@b.com"})
writer.writerows([
{"name": "Bob", "age": 25, "email": "b@b.com"},
{"name": "Charlie", "age": 35, "email": "c@b.com"},
])
# Чтение
with open("data.csv", encoding="utf-8") as f:
reader = csv.DictReader(f)
for row in reader:
print(row["name"], row["age"])
# row — dict: {"name": "Alice", "age": "30", ...}
# Внимание: все значения — строки! Нужно явное приведение типовpip install pyyamlimport yaml
config_text = """
database:
host: localhost
port: 5432
name: mydb
debug: true
allowed_hosts:
- localhost
- 127.0.0.1
"""
# Безопасная загрузка (не выполняет Python-код)
config = yaml.safe_load(config_text)
print(config["database"]["port"]) # 5432 (int, не строка!)
# Из файла
with open("config.yaml", encoding="utf-8") as f:
config = yaml.safe_load(f)
# Сохранение
with open("output.yaml", "w", encoding="utf-8") as f:
yaml.dump(config, f, allow_unicode=True, default_flow_style=False)
# Важно: НЕ используйте yaml.load() без Loader — это опасно!
# yaml.load(data) # Проблема: может выполнить произвольный код
# yaml.safe_load(data) # Вариант: безопасноimport tomllib # встроен в Python 3.11+
with open("pyproject.toml", "rb") as f: # обязательно "rb"!
data = tomllib.load(f)
# Или из строки:
text = """
[tool.mypy]
strict = true
python_version = "3.12"
"""
config = tomllib.loads(text)
print(config["tool"]["mypy"]["strict"]) # Truedef calculate_bmi(weight_kg: float, height_m: float) -> float:
"""Вычисляет индекс массы тела (ИМТ).
Args:
weight_kg: Вес в килограммах.
height_m: Рост в метрах. Должен быть положительным.
Returns:
ИМТ = weight_kg / height_m².
Raises:
ValueError: Если height_m <= 0.
Examples:
>>> calculate_bmi(70, 1.75)
22.857142857142858
"""
if height_m <= 0:
raise ValueError(f"height_m must be positive, got {height_m}")
return weight_kg / height_m ** 2Стиль docstring выбирают один раз на весь проект. В Google style используются
секции Args, Returns и Raises. NumPy style оформляет Parameters,
Returns и Raises отдельными секциями с подчёркиваниями. reStructuredText
записывает поля как :param name:, :type name: и :return:; этот формат
часто встречается в проектах на Sphinx.
Docstring хранится в func.__doc__ и показывается через help(func). Если её
нет, __doc__ равен None, а help() всё равно покажет имя и сигнатуру
функции, но не сможет вывести содержательное описание.
doctest — тесты в документацииdef add(a: int, b: int) -> int:
"""Складывает два числа.
>>> add(2, 3)
5
>>> add(-1, 1)
0
>>> add(0, 0)
0
"""
return a + b
# Запуск doctests:
# python -m doctest module.py -v
# pytest --doctest-modules module.pyimport logging
# Базовая конфигурация
logging.basicConfig(
level=logging.DEBUG,
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
# Правильный способ: logger для каждого модуля
logger = logging.getLogger(__name__)
def process_order(order_id: int) -> None:
# Ленивая %-подстановка: строка формируется только при реальной записи,
# а не при каждом вызове (важно для DEBUG в горячих путях).
logger.debug("Processing order %s", order_id)
try:
result = execute_order(order_id)
logger.info("Order %s completed: %s", order_id, result)
except Exception:
logger.exception("Failed to process order %s", order_id)
raise
# Уровни: DEBUG < INFO < WARNING < ERROR < CRITICAL
# Структурированное логирование
import json
import sys
class JSONFormatter(logging.Formatter):
def format(self, record):
return json.dumps({
"time": self.formatTime(record),
"level": record.levelname,
"message": record.getMessage(),
"module": record.module,
})
# Форматтер нужно привязать к handler'у, иначе он не применится:
handler = logging.StreamHandler(sys.stdout)
handler.setFormatter(JSONFormatter())
# force=True заменяет конфигурацию из предыдущего учебного примера.
logging.basicConfig(handlers=[handler], level=logging.INFO, force=True)__name__ == "__main__"# module.py
def main():
print("Running as main program")
def helper():
return 42
# Этот блок выполняется только при прямом запуске:
# python module.py
# НЕ выполняется при: import module
if __name__ == "__main__":
main()
# Зачем: позволяет использовать файл и как модуль, и как скрипт
# import module → можно использовать helper()
# python module.py → запускается main()Для небольшого CLI используйте argparse: он разбирает аргументы, проверяет
типы и сам формирует справку.
import argparse
parser = argparse.ArgumentParser(description="Собрать отчёт")
parser.add_argument("input", help="Путь к исходному JSON")
parser.add_argument("--limit", type=int, default=100)
args = parser.parse_args()import os
from typing import Optional
# os.environ — словарь переменных окружения
database_url = os.environ["DATABASE_URL"] # KeyError если нет
database_url = os.environ.get("DATABASE_URL", "sqlite:///dev.db") # с дефолтом
debug = os.getenv("DEBUG", "0") == "1" # bool
# python-dotenv для .env файлов
# pip install python-dotenv
from dotenv import load_dotenv
load_dotenv(".env") # загружает .env в os.environ
# Pydantic Settings — лучший способ (FastAPI)
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
database_url: str
redis_url: str = "redis://localhost:6379"
debug: bool = False
api_key: Optional[str] = None
model_config = SettingsConfigDict(env_file=".env")
settings = Settings() # автоматически читает из переменных окружения# enumerate — индекс + элемент
for i, item in enumerate(["a", "b", "c"], start=1):
print(f"{i}. {item}")
# zip — параллельный обход (останавливается на коротком)
names = ["Alice", "Bob", "Charlie"]
scores = [85, 92, 78]
for name, score in zip(names, scores):
print(f"{name}: {score}")
# zip_longest (заполняет None или custom fillvalue)
from itertools import zip_longest
# sorted с key
users = [{"name": "Charlie", "age": 30}, {"name": "Alice", "age": 25}]
by_name = sorted(users, key=lambda u: u["name"])
by_age_desc = sorted(users, key=lambda u: u["age"], reverse=True)
# map, filter (предпочитайте comprehensions)
squares = list(map(lambda x: x**2, range(10)))
evens = list(filter(lambda x: x % 2 == 0, range(10)))
# Лучше:
squares = [x**2 for x in range(10)]
evens = [x for x in range(10) if x % 2 == 0]
# any / all
has_admin = any(u["role"] == "admin" for u in users)
all_active = all(u["active"] for u in users)
# vars / dir / type / id
class SomeObject:
pass
obj = SomeObject()
print(vars(obj)) # obj.__dict__
print(dir(obj)) # все атрибуты и методы
print(type(obj)) # <class 'SomeObject'>
print(id(obj)) # уникальный идентификатор на время жизни объекта
# В CPython id() обычно связан с адресом памяти. Язык Python этого не обещает.
# isinstance vs type
isinstance(True, int) # True — bool наследует int
type(True) is int # False — точный тип boolpyproject.toml с зафиксированным
решением из lock-файла.sys.path.Соберите проект, который одинаково запускается из чистого окружения, читает
вход через pathlib и явно задаёт кодировку. Результат основы — команда запуска
и один проверяемый файл, а не снимок экрана из среды разработки.
Сохраните код в probe.py и выполните python probe.py. Отсутствие ошибки
означает, что запись и чтение Unicode воспроизводимы.
import json
import tempfile
from pathlib import Path
with tempfile.TemporaryDirectory() as directory:
report = Path(directory) / "report.json"
payload = {"message": "проверка", "count": 2}
report.write_text(
json.dumps(payload, ensure_ascii=False, sort_keys=True),
encoding="utf-8",
)
assert json.loads(report.read_text(encoding="utf-8")) == payloadПрактика с подсказками: вынесите путь и уровень журналирования в конфигурацию, запретите вывод исходной записи целиком и добавьте отдельные ветви для ошибки чтения и ошибки записи. Сначала напишите проверку, затем меняйте код.
Докажите разницу между python module.py и python -m package.module на
маленьком пакете. В записке отделите правила импорта языка от поведения
конкретной раскладки проекта и среды запуска.
os.path.join(), os.path.exists(), os.listdir() в эквивалент на pathlib.read_config(path: str) -> dict читающий JSON/YAML/TOML в зависимости от расширения файла.calculate() и блоком if __name__ == "__main__". Проверьте что он работает и как import, и как скрипт.pydantic_settings, создайте Settings класс для приложения. Добавьте валидацию: port должен быть от 1024 до 65535./, Path.exists() и
Path.iterdir(), а код не склеивает путь строками.pyyaml.calculate(), прямой
запуск — должен.Handler с уровнями INFO и ERROR; один вызов
logger.error(...) обязан попасть в оба назначения.1024 и 65535, а также ошибки
для 1023 и 65536.Закрепите тему в лаборатории «Воспроизводимая настройка и CLI». Вы отделите TOML, окружение и системные ошибки, а затем докажете, что запуск не зависит от текущего каталога и не раскрывает секреты.
git clone --branch v0.6.2 --depth 1 https://gitlab.potapov.me/courses/python-labs.git
cd python-labs
uv sync --group test
uv run --group test pytest python_basics/testsДалее: Базовые типы данных