<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/"><channel><title>alchemmist — Eosp</title><link>https://alchemmist.xyz/ru/tags/eosp/</link><description>Последние записи в блоге alchemmist</description><generator>Hugo 0.163.3</generator><language>ru</language><atom:link href="https://alchemmist.xyz/ru/tags/eosp/index.xml" rel="self" type="application/rss+xml"/><lastBuildDate>Thu, 12 Feb 2026 11:07:00 +0300</lastBuildDate><item><title>Тестирование на Python</title><link>https://alchemmist.xyz/ru/articles/testing-in-python/</link><pubDate>Thu, 12 Feb 2026 11:07:00 +0300</pubDate><dc:creator>alchemmist</dc:creator><guid>https://alchemmist.xyz/ru/articles/testing-in-python/</guid><description>Эта статья-интенсив по тестированию на Python подробно объясняет, зачем нужны автотесты, какие виды тестов существуют, и как строится пирамида тестирования. Вы познакомитесь с подходом TDD и циклом Red -&amp;gt; Green -&amp;gt; Refactor, узнаете, когда использовать мок-объекты, а когда нет, и получите практические навыки работы с pytest — от юнит-тестов до интеграционных и E2E-тестов. Примеры кода, рекомендации по структуре тестов и советы по качественной архитектуре тестового покрытия помогут вам начать писать надёжный и поддерживаемый Python-код с тестами.</description><content:encoded><![CDATA[<p>Эта статья-интенсив по тестированию на Python подробно объясняет,
зачем нужны автотесты, какие виды тестов существуют, и как
строится пирамида тестирования. Вы познакомитесь с подходом TDD
и циклом Red -&gt; Green -&gt; Refactor, узнаете, когда использовать
мок-объекты, а когда нет, и получите практические навыки работы
с pytest — от юнит-тестов до интеграционных и E2E-тестов. Примеры
кода, рекомендации по структуре тестов и советы по качественной
архитектуре тестового покрытия помогут вам начать писать надёжный
и поддерживаемый Python-код с тестами.</p>

<h2 id="правильное-отношение-к-тестам">
  <a class="link" href="#%d0%bf%d1%80%d0%b0%d0%b2%d0%b8%d0%bb%d1%8c%d0%bd%d0%be%d0%b5-%d0%be%d1%82%d0%bd%d0%be%d1%88%d0%b5%d0%bd%d0%b8%d0%b5-%d0%ba-%d1%82%d0%b5%d1%81%d1%82%d0%b0%d0%bc">
    #
  </a>
  Правильное отношение к тестам
</h2>

<p>Вокруг тестов в Python, да и не только, много лишней драматургии.
Обычно есть два крайних мнения. Первое звучит как: &ldquo;без
стопроцентного покрытия вы не разработчик&rdquo;. Второе как: &ldquo;мы
стартап, нам не до тестов&rdquo;. Оба подхода, конечно, имеют право на
существование, но ведут, как правило не туда. В первом случае,
погоня за стопроцентным покрытим с высокой вероятностью может
привести к мусорным тестам, так как метрика покрытия (<em>как бы она
не вычисляллась</em>) не идеальна. Например, никто не запрещяет через
рефликисю получить все возможные объекты в коде, инстанцировать
их, и вообще ничего не проверять, получив стопроцентное покрытие.
Во втором случае, со временем процесс разработки будет становится
все сложнее, а DE (developer experience) ухудшаться из-за рочных
проверок &ldquo;на каждый чих&rdquo;.</p>
<p>Зрелый взгляд на тестирование куда проще. У нас есть система,
которая меняется. У этой системы есть цена ошибки. У этой же
системы есть стоимость проверки. Мы хотим снизить цену ошибки при
вменяемой стоимости проверки. Автотесты решают именно эту задачу.</p>
<p>Важно понимать, что тесты не делают код безошибочным. Они делают
ошибки быстрыми, локализуемыми и дешевыми в исправлении. Когда
ошибка ловится на ноутбуке разработчика через двадцать секунд после
изменения, это одна экономика. Когда ошибка ловится через два дня
в продакшене у реальных пользователей, это почти всегда дороже.</p>

<h2 id="зачем-автотесты-на-практике">
  <a class="link" href="#%d0%b7%d0%b0%d1%87%d0%b5%d0%bc-%d0%b0%d0%b2%d1%82%d0%be%d1%82%d0%b5%d1%81%d1%82%d1%8b-%d0%bd%d0%b0-%d0%bf%d1%80%d0%b0%d0%ba%d1%82%d0%b8%d0%ba%d0%b5">
    #
  </a>
  Зачем автотесты на практике
</h2>

<p>Чаще всего называют три причины. Защита от регрессий, документация
поведения и уверенность при рефакторинге. Это правильные слова,
но чтобы они не были абстрактными, нужно приземлить их на реальные
сценарии.</p>
<p>Представим сервис заказов. В него добавляют поддержку промокодов.
Разработчик изменяет логику расчета итоговой цены и случайно
ломает правило бесплатной доставки для VIP-клиентов. Если тестов
нет, баг уезжает в прод, поддержка получает жалобы, менеджер
поднимает инцидент, команда срочно откатывает релиз. Если тесты
есть, падает конкретный тест расчета доставки, и проблема
решается до релиза.</p>
<p>Документация поведения в тестах особенно заметна в долгоживущих
проектах. Через полгода после запуска фичи никто уже не помнит,
почему именно в случае отмененного заказа и частичного возврата
комиссия пересчитывается определенным образом. Тест с понятным
названием и явным assertion отвечает на этот вопрос быстрее, чем
любой коммит в истории.</p>
<p>Уверенность при рефакторинге вообще стоит считать отдельной
инженерной валютой. Код без рефакторинга стареет и тянет проект
вниз. Код с рефакторингом без тестов превращается в поле
экспериментов с высоким риском. Код с тестами позволяет улучшать
архитектуру постепенно и безопасно.</p>

<h2 id="что-именно-мы-тестируем-уровни-и-назначение">
  <a class="link" href="#%d1%87%d1%82%d0%be-%d0%b8%d0%bc%d0%b5%d0%bd%d0%bd%d0%be-%d0%bc%d1%8b-%d1%82%d0%b5%d1%81%d1%82%d0%b8%d1%80%d1%83%d0%b5%d0%bc-%d1%83%d1%80%d0%be%d0%b2%d0%bd%d0%b8-%d0%b8-%d0%bd%d0%b0%d0%b7%d0%bd%d0%b0%d1%87%d0%b5%d0%bd%d0%b8%d0%b5">
    #
  </a>
  Что именно мы тестируем: уровни и назначение
</h2>

<!-- TODO: replace link to einglish wiki -->
<p>Слово &ldquo;тест&rdquo; без указания уровня мало что означает. Один тест
может проверять <a href="https://ru.wikipedia.org/wiki/%D0%A7%D0%B8%D1%81%D1%82%D0%BE%D1%82%D0%B0_%D1%84%D1%83%D0%BD%D0%BA%D1%86%D0%B8%D0%B8">чистую
функцию</a>
за миллисекунды, другой гонять браузерный сценарий несколько
минут. И это нормально, если мы понимаем, зачем каждый из них
нужен.</p>
<p>Юнит-тест проверяет маленький фрагмент поведения в изоляции.
Обычно это функция или небольшой класс без обращения к сети,
диску и базе данных. Главная ценность юнита в скорости и точности
сигнала. Если он упал, область поиска проблемы очень узкая.</p>
<p>Интеграционный тест проверяет стык нескольких модулей или слоев.
Обычно здесь уже участвуют реальные адаптеры, БД, очередь,
внешний HTTP-клиент через тестовый стенд. Такой тест медленнее,
но он ловит ошибки контрактов и конфигураций, которые юнитами не
поймать.</p>
<p>E2E тест проверяет сквозной пользовательский путь. Это не тест
метода. Это тест бизнес-сценария целиком. Например,
&ldquo;регистрация -&gt; подтверждение почты -&gt; создание заказа -&gt;
оплата&rdquo;. Такие тесты самые дорогие в поддержке, но на даже
небольшое их количество, покрывающее критические участки системы,
дают большую увернность в надёжности.</p>
<p>Регрессионные тесты не отдельный технический уровень,
а назначение. Любой тест становится регрессионным, когда он
защищает уже согласованное поведение от повторной поломки.</p>

<h2 id="пирамида-тестирования-как-модель-затрат">
  <a class="link" href="#%d0%bf%d0%b8%d1%80%d0%b0%d0%bc%d0%b8%d0%b4%d0%b0-%d1%82%d0%b5%d1%81%d1%82%d0%b8%d1%80%d0%be%d0%b2%d0%b0%d0%bd%d0%b8%d1%8f-%d0%ba%d0%b0%d0%ba-%d0%bc%d0%be%d0%b4%d0%b5%d0%bb%d1%8c-%d0%b7%d0%b0%d1%82%d1%80%d0%b0%d1%82">
    #
  </a>
  Пирамида тестирования как модель затрат
</h2>

<p>Пирамида тестирования это рабочая модель стоимости проверки.
Внизу много дешевых и быстрых проверок, выше меньше и дороже, на
вершине совсем немного самых дорогих проверок на уровне бизнеса.</p>
<p>Если команда пишет почти одни E2E, она быстро сталкивается
с медленным feedback loop. Каждый запуск долгий, окружение
хрупкое, тесты нестабильны. Разработчики начинают реже запускать
их локально, а CI превращается в место сюрпризов.</p>
<p>Если команда пишет только юниты, она теряет защиту на стыках.
Модули по отдельности могут быть безупречны, но в интеграции
сломаться из-за формата данных, конфигурации или миграций.</p>
<p>Здоровая стратегия почти всегда выглядит как широкий фундамент из
юнитов, умеренный слой интеграционных проверок и ограниченный, но
тщательно подобранный набор E2E на критический путь продукта. Но
в зависимости от специфики проекта, количество и соотношение всех
видов тестов может менятсья и это нормально.</p>

<h2 id="архитектура-и-тестируемость-связаны-напрямую">
  <a class="link" href="#%d0%b0%d1%80%d1%85%d0%b8%d1%82%d0%b5%d0%ba%d1%82%d1%83%d1%80%d0%b0-%d0%b8-%d1%82%d0%b5%d1%81%d1%82%d0%b8%d1%80%d1%83%d0%b5%d0%bc%d0%be%d1%81%d1%82%d1%8c-%d1%81%d0%b2%d1%8f%d0%b7%d0%b0%d0%bd%d1%8b-%d0%bd%d0%b0%d0%bf%d1%80%d1%8f%d0%bc%d1%83%d1%8e">
    #
  </a>
  Архитектура и тестируемость связаны напрямую
</h2>

<p>Тестируемость редко появляется случайно. Она почти всегда является
следствием архитектурных решений. Чем лучше разделены
ответственности в коде, тем проще писать тесты и тем дешевле их
поддерживать.</p>
<p>Плохой пример обычно выглядит как склеивание бизнес-логики,
инфраструктуры и побочных эффектов в одном методе.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">PaymentService</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">pay</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">user_id</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">amount</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">db</span> <span class="o">=</span> <span class="n">PostgresClient</span><span class="p">(</span><span class="s2">&#34;postgres://prod&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">gateway</span> <span class="o">=</span> <span class="n">StripeClient</span><span class="p">(</span><span class="s2">&#34;secret&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">metrics</span> <span class="o">=</span> <span class="n">MetricsClient</span><span class="p">(</span><span class="s2">&#34;statsd://metrics&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="n">user</span> <span class="o">=</span> <span class="n">db</span><span class="o">.</span><span class="n">fetch_user</span><span class="p">(</span><span class="n">user_id</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="ow">not</span> <span class="n">user</span><span class="o">.</span><span class="n">active</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="k">raise</span> <span class="ne">ValueError</span><span class="p">(</span><span class="s2">&#34;inactive user&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="n">gateway</span><span class="o">.</span><span class="n">charge</span><span class="p">(</span><span class="n">user</span><span class="o">.</span><span class="n">card_token</span><span class="p">,</span> <span class="n">amount</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">db</span><span class="o">.</span><span class="n">save_payment</span><span class="p">(</span><span class="n">user_id</span><span class="p">,</span> <span class="n">amount</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">metrics</span><span class="o">.</span><span class="n">increment</span><span class="p">(</span><span class="s2">&#34;payments.success&#34;</span><span class="p">)</span>
</span></span></code></pre></div><p>Здесь тесту сложно выбрать уровень. Для юнит-теста слишком много
внешнего мира. Для интеграционного слишком много разных
ответственностей сразу.</p>
<p>Лучший пример отделяет бизнес-решение от инфраструктуры и
внедряет зависимости извне.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">PaymentService</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">users_repo</span><span class="p">,</span> <span class="n">payments_repo</span><span class="p">,</span> <span class="n">gateway</span><span class="p">,</span> <span class="n">metrics</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">users_repo</span> <span class="o">=</span> <span class="n">users_repo</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">payments_repo</span> <span class="o">=</span> <span class="n">payments_repo</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">gateway</span> <span class="o">=</span> <span class="n">gateway</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">metrics</span> <span class="o">=</span> <span class="n">metrics</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">pay</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">user_id</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">amount</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">user</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">users_repo</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">user_id</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="ow">not</span> <span class="n">user</span><span class="o">.</span><span class="n">active</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="k">raise</span> <span class="ne">ValueError</span><span class="p">(</span><span class="s2">&#34;inactive user&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">gateway</span><span class="o">.</span><span class="n">charge</span><span class="p">(</span><span class="n">user</span><span class="o">.</span><span class="n">card_token</span><span class="p">,</span> <span class="n">amount</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">payments_repo</span><span class="o">.</span><span class="n">save</span><span class="p">(</span><span class="n">user_id</span><span class="p">,</span> <span class="n">amount</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">metrics</span><span class="o">.</span><span class="n">increment</span><span class="p">(</span><span class="s2">&#34;payments.success&#34;</span><span class="p">)</span>
</span></span></code></pre></div><p>Теперь юнит-тест может проверять решения домена через фейковые
зависимости, а интеграционные тесты отдельно проверят репозиторий,
шлюз и метрики. Такой код проще развивать и проще покрывать.</p>

<h2 id="red---green---refactor">
  <a class="link" href="#red---green---refactor">
    #
  </a>
  Red -&gt; Green -&gt; Refactor
</h2>

<p>Red означает, что мы сначала формулируем ожидаемое поведение через
падающий тест. Green означает, что мы пишем минимальный код,
достаточный для прохождения. Refactor означает, что мы улучшаем
структуру кода, не меняя поведение.</p>
<p>Рассмотрим короткий пример. Пусть нужна функция,
которая разбивает сумму по месяцам и последнюю копейку отдает
в последний месяц.</p>
<p>На фазе Red можно начать так:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">test_allocate_even_months_with_remainder</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># 1000 cents over 3 months -&gt; 333, 333, 334</span>
</span></span><span class="line"><span class="cl">    <span class="k">assert</span> <span class="n">allocate_cents</span><span class="p">(</span><span class="mi">1000</span><span class="p">,</span> <span class="mi">3</span><span class="p">)</span> <span class="o">==</span> <span class="p">[</span><span class="mi">333</span><span class="p">,</span> <span class="mi">333</span><span class="p">,</span> <span class="mi">334</span><span class="p">]</span>
</span></span></code></pre></div><p>На фазе Green пишем минимальную реализацию:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">allocate_cents</span><span class="p">(</span><span class="n">total</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">months</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">list</span><span class="p">[</span><span class="nb">int</span><span class="p">]:</span>
</span></span><span class="line"><span class="cl">    <span class="n">base</span> <span class="o">=</span> <span class="n">total</span> <span class="o">//</span> <span class="n">months</span>
</span></span><span class="line"><span class="cl">    <span class="n">remainder</span> <span class="o">=</span> <span class="n">total</span> <span class="o">%</span> <span class="n">months</span>
</span></span><span class="line"><span class="cl">    <span class="n">result</span> <span class="o">=</span> <span class="p">[</span><span class="n">base</span><span class="p">]</span> <span class="o">*</span> <span class="n">months</span>
</span></span><span class="line"><span class="cl">    <span class="n">result</span><span class="p">[</span><span class="o">-</span><span class="mi">1</span><span class="p">]</span> <span class="o">+=</span> <span class="n">remainder</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">result</span>
</span></span></code></pre></div><p>На фазе Refactor добавляем защиту от некорректного ввода,
делаем имя переменных яснее, добавляем тесты на крайние случаи.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">pytest</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">allocate_cents</span><span class="p">(</span><span class="n">total</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">months</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">list</span><span class="p">[</span><span class="nb">int</span><span class="p">]:</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">months</span> <span class="o">&lt;=</span> <span class="mi">0</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">raise</span> <span class="ne">ValueError</span><span class="p">(</span><span class="s2">&#34;months must be positive&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">total</span> <span class="o">&lt;</span> <span class="mi">0</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">raise</span> <span class="ne">ValueError</span><span class="p">(</span><span class="s2">&#34;total must be non-negative&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">base</span> <span class="o">=</span> <span class="n">total</span> <span class="o">//</span> <span class="n">months</span>
</span></span><span class="line"><span class="cl">    <span class="n">remainder</span> <span class="o">=</span> <span class="n">total</span> <span class="o">%</span> <span class="n">months</span>
</span></span><span class="line"><span class="cl">    <span class="n">schedule</span> <span class="o">=</span> <span class="p">[</span><span class="n">base</span><span class="p">]</span> <span class="o">*</span> <span class="n">months</span>
</span></span><span class="line"><span class="cl">    <span class="n">schedule</span><span class="p">[</span><span class="o">-</span><span class="mi">1</span><span class="p">]</span> <span class="o">+=</span> <span class="n">remainder</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">schedule</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">test_allocate_one_month</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="k">assert</span> <span class="n">allocate_cents</span><span class="p">(</span><span class="mi">500</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span> <span class="o">==</span> <span class="p">[</span><span class="mi">500</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">test_allocate_zero_total</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="k">assert</span> <span class="n">allocate_cents</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">4</span><span class="p">)</span> <span class="o">==</span> <span class="p">[</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">test_allocate_invalid_months</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="k">with</span> <span class="n">pytest</span><span class="o">.</span><span class="n">raises</span><span class="p">(</span><span class="ne">ValueError</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="n">allocate_cents</span><span class="p">(</span><span class="mi">100</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span>
</span></span></code></pre></div><p>Именно так TDD удерживает фокус на контракте и предотвращает
переусложнение раньше времени.</p>

<h2 id="скорость-обратной-связи">
  <a class="link" href="#%d1%81%d0%ba%d0%be%d1%80%d0%be%d1%81%d1%82%d1%8c-%d0%be%d0%b1%d1%80%d0%b0%d1%82%d0%bd%d0%be%d0%b9-%d1%81%d0%b2%d1%8f%d0%b7%d0%b8">
    #
  </a>
  Скорость обратной связи
</h2>

<p>Команда ускоряется не тогда, когда быстрее печатает код.
Команда ускоряется тогда, когда быстрее получает надежный ответ
на вопрос &ldquo;изменение корректно или нет&rdquo;.</p>
<p>Маленькие изолированные тесты дают сигнал за секунды.
Тяжелые интеграционные проверки дают сигнал за минуты.
E2E иногда дают сигнал за десятки минут. Все уровни нужны,
но последовательность их использования критична.</p>
<p>В рабочем режиме разработчик обычно запускает локально быстрый
пакет, потом нужные интеграционные тесты по задаче, и уже после
этого отдает изменения в CI, где гоняется полный набор.</p>
<p>Когда эта дисциплина есть, количество &ldquo;красных&rdquo; сюрпризов в CI
существенно падает. А значит, падает и время ожидания,
и количество контекстных переключений.</p>

<h2 id="london-school-и-classicist-school-в-реальном-проекте">
  <a class="link" href="#london-school-%d0%b8-classicist-school-%d0%b2-%d1%80%d0%b5%d0%b0%d0%bb%d1%8c%d0%bd%d0%be%d0%bc-%d0%bf%d1%80%d0%be%d0%b5%d0%ba%d1%82%d0%b5">
    #
  </a>
  London school и classicist school в реальном проекте
</h2>

<p>Спор между школами тестирования существует давно и часто подается
как выбор одного лагеря. На практике полезнее понимать, в чем
сила каждой школы и где ее границы.</p>
<p>London school чаще использует моки и проверяет взаимодействия.
Если ваш код это оркестратор нескольких внешних вызовов,
этот подход дает очень точный контроль над тем, кто и как был
вызван.</p>
<p>Classicist school чаще проверяет состояние и поведение через
реальные объекты без агрессивного мокинга. Если у вас много
чистой доменной логики, такой стиль обычно делает тесты более
устойчивыми к рефакторингу.</p>
<p>Давайте посмотрим на один и тот же кейс двумя стилями.</p>
<p>В mock-heavy подходе:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">unittest.mock</span> <span class="kn">import</span> <span class="n">Mock</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">register_user</span><span class="p">(</span><span class="n">repo</span><span class="p">,</span> <span class="n">email_sender</span><span class="p">,</span> <span class="n">email</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">user_id</span> <span class="o">=</span> <span class="n">repo</span><span class="o">.</span><span class="n">create</span><span class="p">(</span><span class="n">email</span><span class="o">=</span><span class="n">email</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">email_sender</span><span class="o">.</span><span class="n">send_welcome</span><span class="p">(</span><span class="n">user_id</span><span class="o">=</span><span class="n">user_id</span><span class="p">,</span> <span class="n">email</span><span class="o">=</span><span class="n">email</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">test_register_user_calls_dependencies</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="n">repo</span> <span class="o">=</span> <span class="n">Mock</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="n">repo</span><span class="o">.</span><span class="n">create</span><span class="o">.</span><span class="n">return_value</span> <span class="o">=</span> <span class="mi">101</span>
</span></span><span class="line"><span class="cl">    <span class="n">email_sender</span> <span class="o">=</span> <span class="n">Mock</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">register_user</span><span class="p">(</span><span class="n">repo</span><span class="p">,</span> <span class="n">email_sender</span><span class="p">,</span> <span class="s2">&#34;alice@example.com&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">repo</span><span class="o">.</span><span class="n">create</span><span class="o">.</span><span class="n">assert_called_once_with</span><span class="p">(</span><span class="n">email</span><span class="o">=</span><span class="s2">&#34;alice@example.com&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">email_sender</span><span class="o">.</span><span class="n">send_welcome</span><span class="o">.</span><span class="n">assert_called_once_with</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">user_id</span><span class="o">=</span><span class="mi">101</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">email</span><span class="o">=</span><span class="s2">&#34;alice@example.com&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span>
</span></span></code></pre></div><p>В state-oriented подходе:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">InMemoryRepo</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">items</span> <span class="o">=</span> <span class="p">[]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">create</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">email</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">int</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">user_id</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">items</span><span class="p">)</span> <span class="o">+</span> <span class="mi">1</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">items</span><span class="o">.</span><span class="n">append</span><span class="p">({</span><span class="s2">&#34;id&#34;</span><span class="p">:</span> <span class="n">user_id</span><span class="p">,</span> <span class="s2">&#34;email&#34;</span><span class="p">:</span> <span class="n">email</span><span class="p">})</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="n">user_id</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">InMemoryEmailSender</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">sent</span> <span class="o">=</span> <span class="p">[]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">send_welcome</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">user_id</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">email</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">sent</span><span class="o">.</span><span class="n">append</span><span class="p">({</span><span class="s2">&#34;user_id&#34;</span><span class="p">:</span> <span class="n">user_id</span><span class="p">,</span> <span class="s2">&#34;email&#34;</span><span class="p">:</span> <span class="n">email</span><span class="p">})</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">test_register_user_changes_state</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="n">repo</span> <span class="o">=</span> <span class="n">InMemoryRepo</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="n">sender</span> <span class="o">=</span> <span class="n">InMemoryEmailSender</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">register_user</span><span class="p">(</span><span class="n">repo</span><span class="p">,</span> <span class="n">sender</span><span class="p">,</span> <span class="s2">&#34;alice@example.com&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">assert</span> <span class="n">repo</span><span class="o">.</span><span class="n">items</span> <span class="o">==</span> <span class="p">[{</span><span class="s2">&#34;id&#34;</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span> <span class="s2">&#34;email&#34;</span><span class="p">:</span> <span class="s2">&#34;alice@example.com&#34;</span><span class="p">}]</span>
</span></span><span class="line"><span class="cl">    <span class="k">assert</span> <span class="n">sender</span><span class="o">.</span><span class="n">sent</span> <span class="o">==</span> <span class="p">[{</span><span class="s2">&#34;user_id&#34;</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span> <span class="s2">&#34;email&#34;</span><span class="p">:</span> <span class="s2">&#34;alice@example.com&#34;</span><span class="p">}]</span>
</span></span></code></pre></div><p>Оба теста полезны. Первый лучше контролирует контракт вызовов.
Второй лучше переживает рефакторинг внутренней реализации.</p>

<h2 id="моки-и-стабы-когда-помогают-а-когда-ломают-картину">
  <a class="link" href="#%d0%bc%d0%be%d0%ba%d0%b8-%d0%b8-%d1%81%d1%82%d0%b0%d0%b1%d1%8b-%d0%ba%d0%be%d0%b3%d0%b4%d0%b0-%d0%bf%d0%be%d0%bc%d0%be%d0%b3%d0%b0%d1%8e%d1%82-%d0%b0-%d0%ba%d0%be%d0%b3%d0%b4%d0%b0-%d0%bb%d0%be%d0%bc%d0%b0%d1%8e%d1%82-%d0%ba%d0%b0%d1%80%d1%82%d0%b8%d0%bd%d1%83">
    #
  </a>
  Моки и стабы: когда помогают, а когда ломают картину
</h2>

<p>Мок нужен там, где без него тест становится нестабильным,
дорогим или слишком широким. Хороший признак полезного мокинга
это изоляция внешнего мира, а не изоляция собственной логики.</p>
<p>Разберем пример с внешним API курса валют. В production-коде мы
делаем HTTP-запрос, но в юнит-тесте хотим проверить реакцию на
ответы API без реальной сети.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">requests</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">fetch_rate</span><span class="p">(</span><span class="n">base</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">quote</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">float</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">resp</span> <span class="o">=</span> <span class="n">requests</span><span class="o">.</span><span class="n">get</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;https://rates.example.com/latest&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">params</span><span class="o">=</span><span class="p">{</span><span class="s2">&#34;base&#34;</span><span class="p">:</span> <span class="n">base</span><span class="p">,</span> <span class="s2">&#34;quote&#34;</span><span class="p">:</span> <span class="n">quote</span><span class="p">},</span>
</span></span><span class="line"><span class="cl">        <span class="n">timeout</span><span class="o">=</span><span class="mi">3</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">data</span> <span class="o">=</span> <span class="n">resp</span><span class="o">.</span><span class="n">json</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="nb">float</span><span class="p">(</span><span class="n">data</span><span class="p">[</span><span class="s2">&#34;rate&#34;</span><span class="p">])</span>
</span></span></code></pre></div><p>Юнит-тест с mock:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">unittest.mock</span> <span class="kn">import</span> <span class="n">Mock</span><span class="p">,</span> <span class="n">patch</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">test_fetch_rate_parses_response</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="n">fake_response</span> <span class="o">=</span> <span class="n">Mock</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="n">fake_response</span><span class="o">.</span><span class="n">json</span><span class="o">.</span><span class="n">return_value</span> <span class="o">=</span> <span class="p">{</span><span class="s2">&#34;rate&#34;</span><span class="p">:</span> <span class="s2">&#34;95.4&#34;</span><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">with</span> <span class="n">patch</span><span class="p">(</span><span class="s2">&#34;requests.get&#34;</span><span class="p">,</span> <span class="n">return_value</span><span class="o">=</span><span class="n">fake_response</span><span class="p">)</span> <span class="k">as</span> <span class="n">get_mock</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">rate</span> <span class="o">=</span> <span class="n">fetch_rate</span><span class="p">(</span><span class="s2">&#34;USD&#34;</span><span class="p">,</span> <span class="s2">&#34;RUB&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">assert</span> <span class="n">rate</span> <span class="o">==</span> <span class="mf">95.4</span>
</span></span><span class="line"><span class="cl">    <span class="n">get_mock</span><span class="o">.</span><span class="n">assert_called_once</span><span class="p">()</span>
</span></span></code></pre></div><p>Тут мок оправдан. Мы не тестируем <code>requests</code>, мы тестируем,
как наша функция обрабатывает ответ.</p>
<p>Но если мы начнем мокать каждый метод собственного репозитория,
каждую внутреннюю функцию и каждый <code>if</code>, тесты станут хрупкими.
Они будут падать от технического рефакторинга, даже если
поведение не изменилось.</p>

<h2 id="unittest-pytest-nose2-что-выбирать-в-2026-году">
  <a class="link" href="#unittest-pytest-nose2-%d1%87%d1%82%d0%be-%d0%b2%d1%8b%d0%b1%d0%b8%d1%80%d0%b0%d1%82%d1%8c-%d0%b2-2026-%d0%b3%d0%be%d0%b4%d1%83">
    #
  </a>
  unittest, pytest, nose2: что выбирать в 2026 году
</h2>

<p>Встроенный <code>unittest</code> остается рабочим вариантом, особенно там,
где важна минимизация зависимостей и строгая стандартизация.</p>
<p><code>pytest</code> в повседневной разработке обычно выигрывает за счет
читаемости, параметризации, фикстур и удобной экосистемы.
Для большинства новых Python-проектов это практический выбор по
умолчанию.</p>
<p><code>nose</code> стоит рассматривать как исторический этап экосистемы.
<code>nose2</code> жив, но в новых кодовых базах он встречается редко,
потому что <code>pytest</code> решает типичные задачи проще и с большей
поддержкой сообщества.</p>

<h2 id="aaa-и-структура-теста-которую-удобно-читать-через-год">
  <a class="link" href="#aaa-%d0%b8-%d1%81%d1%82%d1%80%d1%83%d0%ba%d1%82%d1%83%d1%80%d0%b0-%d1%82%d0%b5%d1%81%d1%82%d0%b0-%d0%ba%d0%be%d1%82%d0%be%d1%80%d1%83%d1%8e-%d1%83%d0%b4%d0%be%d0%b1%d0%bd%d0%be-%d1%87%d0%b8%d1%82%d0%b0%d1%82%d1%8c-%d1%87%d0%b5%d1%80%d0%b5%d0%b7-%d0%b3%d0%be%d0%b4">
    #
  </a>
  AAA и структура теста, которую удобно читать через год
</h2>

<p>Одна из самых простых техник, которая сильно повышает
поддерживаемость тестов, это явная структура Arrange, Act,
Assert. Даже без комментариев она удерживает тест коротким
и линейным.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">calculate_total</span><span class="p">(</span><span class="n">price</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">quantity</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">discount</span><span class="p">:</span> <span class="nb">float</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">int</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">subtotal</span> <span class="o">=</span> <span class="n">price</span> <span class="o">*</span> <span class="n">quantity</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="nb">int</span><span class="p">(</span><span class="n">subtotal</span> <span class="o">*</span> <span class="p">(</span><span class="mi">1</span> <span class="o">-</span> <span class="n">discount</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">test_calculate_total_with_discount</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="n">price</span> <span class="o">=</span> <span class="mi">500</span>
</span></span><span class="line"><span class="cl">    <span class="n">quantity</span> <span class="o">=</span> <span class="mi">2</span>
</span></span><span class="line"><span class="cl">    <span class="n">discount</span> <span class="o">=</span> <span class="mf">0.1</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">result</span> <span class="o">=</span> <span class="n">calculate_total</span><span class="p">(</span><span class="n">price</span><span class="p">,</span> <span class="n">quantity</span><span class="p">,</span> <span class="n">discount</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">assert</span> <span class="n">result</span> <span class="o">==</span> <span class="mi">900</span>
</span></span></code></pre></div><p>Здесь тест легко сканируется глазами. Подготовка данных,
действие, проверка. Ничего лишнего.</p>
<p>Если тест содержит слишком много действий и проверок, это часто
сигнал, что он пытается проверить слишком много разных сценариев
сразу. Лучше разделить такой тест на несколько маленьких.</p>

<h2 id="фикстуры-setupteardown-и-контроль-над-состоянием">
  <a class="link" href="#%d1%84%d0%b8%d0%ba%d1%81%d1%82%d1%83%d1%80%d1%8b-setupteardown-%d0%b8-%d0%ba%d0%be%d0%bd%d1%82%d1%80%d0%be%d0%bb%d1%8c-%d0%bd%d0%b0%d0%b4-%d1%81%d0%be%d1%81%d1%82%d0%be%d1%8f%d0%bd%d0%b8%d0%b5%d0%bc">
    #
  </a>
  Фикстуры, setup/teardown и контроль над состоянием
</h2>

<p>Фикстуры в <code>pytest</code> решают ключевую проблему дублирования
подготовки данных и окружения. Но их легко превратить в
непрозрачную магию, если прятать внутри слишком много логики.</p>
<p>Сравним плохой и хороший подход.</p>
<p>Плохо, когда фикстура делает слишком много:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">pytest</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nd">@pytest.fixture</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">app_everything</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># создает БД, поднимает сервис, загружает данные,</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># настраивает очередь, запускает фоновые воркеры</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="p">{</span><span class="s2">&#34;warning&#34;</span><span class="p">:</span> <span class="s2">&#34;fixture is too broad&#34;</span><span class="p">}</span>
</span></span></code></pre></div><p>Хорошо, когда фикстуры узкие и ясные по назначению:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">pytest</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nd">@pytest.fixture</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">user_email</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="s2">&#34;alice@example.com&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nd">@pytest.fixture</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">in_memory_repo</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="k">class</span> <span class="nc">Repo</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">            <span class="bp">self</span><span class="o">.</span><span class="n">items</span> <span class="o">=</span> <span class="p">[]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="k">def</span> <span class="nf">save</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">email</span><span class="p">:</span> <span class="nb">str</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">            <span class="bp">self</span><span class="o">.</span><span class="n">items</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">email</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">Repo</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">test_repo_save</span><span class="p">(</span><span class="n">in_memory_repo</span><span class="p">,</span> <span class="n">user_email</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">in_memory_repo</span><span class="o">.</span><span class="n">save</span><span class="p">(</span><span class="n">user_email</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">assert</span> <span class="n">in_memory_repo</span><span class="o">.</span><span class="n">items</span> <span class="o">==</span> <span class="p">[</span><span class="s2">&#34;alice@example.com&#34;</span><span class="p">]</span>
</span></span></code></pre></div><p>Чем яснее фикстуры, тем проще понимать, откуда в тесте взялись
данные и почему они именно такие.</p>

<h2 id="рекомендации-по-юнит-тестам-через-конкретные-примеры">
  <a class="link" href="#%d1%80%d0%b5%d0%ba%d0%be%d0%bc%d0%b5%d0%bd%d0%b4%d0%b0%d1%86%d0%b8%d0%b8-%d0%bf%d0%be-%d1%8e%d0%bd%d0%b8%d1%82-%d1%82%d0%b5%d1%81%d1%82%d0%b0%d0%bc-%d1%87%d0%b5%d1%80%d0%b5%d0%b7-%d0%ba%d0%be%d0%bd%d0%ba%d1%80%d0%b5%d1%82%d0%bd%d1%8b%d0%b5-%d0%bf%d1%80%d0%b8%d0%bc%d0%b5%d1%80%d1%8b">
    #
  </a>
  Рекомендации по юнит-тестам через конкретные примеры
</h2>

<p>Сильный юнит-тест обычно проверяет один смысловой сценарий.
Рассмотрим функцию нормализации телефона.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">re</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">normalize_phone</span><span class="p">(</span><span class="n">value</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">digits</span> <span class="o">=</span> <span class="n">re</span><span class="o">.</span><span class="n">sub</span><span class="p">(</span><span class="sa">r</span><span class="s2">&#34;\D&#34;</span><span class="p">,</span> <span class="s2">&#34;&#34;</span><span class="p">,</span> <span class="n">value</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">digits</span><span class="p">)</span> <span class="o">==</span> <span class="mi">11</span> <span class="ow">and</span> <span class="n">digits</span><span class="o">.</span><span class="n">startswith</span><span class="p">(</span><span class="s2">&#34;8&#34;</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="n">digits</span> <span class="o">=</span> <span class="s2">&#34;7&#34;</span> <span class="o">+</span> <span class="n">digits</span><span class="p">[</span><span class="mi">1</span><span class="p">:]</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">digits</span><span class="p">)</span> <span class="o">!=</span> <span class="mi">11</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">raise</span> <span class="ne">ValueError</span><span class="p">(</span><span class="s2">&#34;invalid phone&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="s2">&#34;+&#34;</span> <span class="o">+</span> <span class="n">digits</span>
</span></span></code></pre></div><p>Плохой тест на такую функцию часто пытается проверить сразу
пять форматов и две ошибки в одном кейсе. Хороший набор тестов
разделяет сценарии.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">pytest</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">test_normalize_phone_ru_local_prefix</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="k">assert</span> <span class="n">normalize_phone</span><span class="p">(</span><span class="s2">&#34;8 (999) 000-00-00&#34;</span><span class="p">)</span> <span class="o">==</span> <span class="s2">&#34;+79990000000&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">test_normalize_phone_already_international</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="k">assert</span> <span class="n">normalize_phone</span><span class="p">(</span><span class="s2">&#34;+7 999 000 00 00&#34;</span><span class="p">)</span> <span class="o">==</span> <span class="s2">&#34;+79990000000&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">test_normalize_phone_invalid_length</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="k">with</span> <span class="n">pytest</span><span class="o">.</span><span class="n">raises</span><span class="p">(</span><span class="ne">ValueError</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="n">normalize_phone</span><span class="p">(</span><span class="s2">&#34;123&#34;</span><span class="p">)</span>
</span></span></code></pre></div><p>Теперь при падении точно видно, какой именно сценарий сломан.</p>

<h2 id="как-организовать-test-suite-проекта-чтобы-им-реально-пользовались">
  <a class="link" href="#%d0%ba%d0%b0%d0%ba-%d0%be%d1%80%d0%b3%d0%b0%d0%bd%d0%b8%d0%b7%d0%be%d0%b2%d0%b0%d1%82%d1%8c-test-suite-%d0%bf%d1%80%d0%be%d0%b5%d0%ba%d1%82%d0%b0-%d1%87%d1%82%d0%be%d0%b1%d1%8b-%d0%b8%d0%bc-%d1%80%d0%b5%d0%b0%d0%bb%d1%8c%d0%bd%d0%be-%d0%bf%d0%be%d0%bb%d1%8c%d0%b7%d0%be%d0%b2%d0%b0%d0%bb%d0%b8%d1%81%d1%8c">
    #
  </a>
  Как организовать test suite проекта, чтобы им реально пользовались
</h2>

<p>Рабочий test suite начинается с понятной структуры каталогов и
режимов запуска. Когда уровни тестов разделены, разработчику проще
выбирать нужный пакет под конкретную задачу.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line"><span class="cl">project/
</span></span><span class="line"><span class="cl">  app/
</span></span><span class="line"><span class="cl">    domain/
</span></span><span class="line"><span class="cl">    application/
</span></span><span class="line"><span class="cl">    infrastructure/
</span></span><span class="line"><span class="cl">  tests/
</span></span><span class="line"><span class="cl">    unit/
</span></span><span class="line"><span class="cl">    integration/
</span></span><span class="line"><span class="cl">    e2e/
</span></span><span class="line"><span class="cl">  pyproject.toml
</span></span></code></pre></div><p>В <code>pyproject.toml</code> удобно зафиксировать маркеры и общие настройки
<code>pytest</code>, чтобы команда запускала тесты единообразно.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-toml" data-lang="toml"><span class="line"><span class="cl"><span class="p">[</span><span class="nx">tool</span><span class="p">.</span><span class="nx">pytest</span><span class="p">.</span><span class="nx">ini_options</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="nx">addopts</span> <span class="p">=</span> <span class="s2">&#34;-ra -q&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nx">testpaths</span> <span class="p">=</span> <span class="p">[</span><span class="s2">&#34;tests&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="nx">markers</span> <span class="p">=</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">  <span class="s2">&#34;integration: tests with real infrastructure&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s2">&#34;e2e: end-to-end scenarios&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">]</span>
</span></span></code></pre></div><p>Тогда локально можно запускать быстрые тесты отдельно от тяжелых.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">pytest tests/unit
</span></span><span class="line"><span class="cl">pytest -m integration
</span></span><span class="line"><span class="cl">pytest -m e2e
</span></span></code></pre></div><p>Ключевой момент в том, что suite должен помогать разработчику,
а не наказывать его долгими непредсказуемыми прогонами.</p>

<h2 id="интеграционные-тесты-на-примере-api-и-базы-данных">
  <a class="link" href="#%d0%b8%d0%bd%d1%82%d0%b5%d0%b3%d1%80%d0%b0%d1%86%d0%b8%d0%be%d0%bd%d0%bd%d1%8b%d0%b5-%d1%82%d0%b5%d1%81%d1%82%d1%8b-%d0%bd%d0%b0-%d0%bf%d1%80%d0%b8%d0%bc%d0%b5%d1%80%d0%b5-api-%d0%b8-%d0%b1%d0%b0%d0%b7%d1%8b-%d0%b4%d0%b0%d0%bd%d0%bd%d1%8b%d1%85">
    #
  </a>
  Интеграционные тесты на примере API и базы данных
</h2>

<p>Разберем практический пример с FastAPI и SQLite,
где мы хотим проверить полный путь &ldquo;HTTP-запрос -&gt; запись в БД -&gt;
HTTP-ответ&rdquo;.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">fastapi</span> <span class="kn">import</span> <span class="n">FastAPI</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">pydantic</span> <span class="kn">import</span> <span class="n">BaseModel</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">sqlite3</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">app</span> <span class="o">=</span> <span class="n">FastAPI</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">ItemIn</span><span class="p">(</span><span class="n">BaseModel</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">name</span><span class="p">:</span> <span class="nb">str</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">get_conn</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="n">conn</span> <span class="o">=</span> <span class="n">sqlite3</span><span class="o">.</span><span class="n">connect</span><span class="p">(</span><span class="s2">&#34;test.db&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">conn</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="s2">&#34;CREATE TABLE IF NOT EXISTS items(name TEXT)&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">conn</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nd">@app.post</span><span class="p">(</span><span class="s2">&#34;/items&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">create_item</span><span class="p">(</span><span class="n">payload</span><span class="p">:</span> <span class="n">ItemIn</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">conn</span> <span class="o">=</span> <span class="n">get_conn</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="n">conn</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="s2">&#34;INSERT INTO items(name) VALUES (?)&#34;</span><span class="p">,</span> <span class="p">(</span><span class="n">payload</span><span class="o">.</span><span class="n">name</span><span class="p">,))</span>
</span></span><span class="line"><span class="cl">    <span class="n">conn</span><span class="o">.</span><span class="n">commit</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="n">conn</span><span class="o">.</span><span class="n">close</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="p">{</span><span class="s2">&#34;ok&#34;</span><span class="p">:</span> <span class="kc">True</span><span class="p">}</span>
</span></span></code></pre></div><p>Интеграционный тест:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">fastapi.testclient</span> <span class="kn">import</span> <span class="n">TestClient</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">sqlite3</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">client</span> <span class="o">=</span> <span class="n">TestClient</span><span class="p">(</span><span class="n">app</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">test_create_item_persists_to_db</span><span class="p">(</span><span class="n">tmp_path</span><span class="p">,</span> <span class="n">monkeypatch</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">db_path</span> <span class="o">=</span> <span class="n">tmp_path</span> <span class="o">/</span> <span class="s2">&#34;items.db&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">get_test_conn</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">        <span class="n">conn</span> <span class="o">=</span> <span class="n">sqlite3</span><span class="o">.</span><span class="n">connect</span><span class="p">(</span><span class="n">db_path</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">conn</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="s2">&#34;CREATE TABLE IF NOT EXISTS items(name TEXT)&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="n">conn</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">monkeypatch</span><span class="o">.</span><span class="n">setattr</span><span class="p">(</span><span class="s2">&#34;main.get_conn&#34;</span><span class="p">,</span> <span class="n">get_test_conn</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">response</span> <span class="o">=</span> <span class="n">client</span><span class="o">.</span><span class="n">post</span><span class="p">(</span><span class="s2">&#34;/items&#34;</span><span class="p">,</span> <span class="n">json</span><span class="o">=</span><span class="p">{</span><span class="s2">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;book&#34;</span><span class="p">})</span>
</span></span><span class="line"><span class="cl">    <span class="k">assert</span> <span class="n">response</span><span class="o">.</span><span class="n">status_code</span> <span class="o">==</span> <span class="mi">200</span>
</span></span><span class="line"><span class="cl">    <span class="k">assert</span> <span class="n">response</span><span class="o">.</span><span class="n">json</span><span class="p">()</span> <span class="o">==</span> <span class="p">{</span><span class="s2">&#34;ok&#34;</span><span class="p">:</span> <span class="kc">True</span><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">conn</span> <span class="o">=</span> <span class="n">sqlite3</span><span class="o">.</span><span class="n">connect</span><span class="p">(</span><span class="n">db_path</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">rows</span> <span class="o">=</span> <span class="n">conn</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="s2">&#34;SELECT name FROM items&#34;</span><span class="p">)</span><span class="o">.</span><span class="n">fetchall</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="n">conn</span><span class="o">.</span><span class="n">close</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">assert</span> <span class="n">rows</span> <span class="o">==</span> <span class="p">[(</span><span class="s2">&#34;book&#34;</span><span class="p">,)]</span>
</span></span></code></pre></div><p>Этот тест уже не юнит. Он проверяет интеграцию HTTP-слоя,
валидации и хранилища.</p>

<h2 id="e2e-на-примере-playwright">
  <a class="link" href="#e2e-%d0%bd%d0%b0-%d0%bf%d1%80%d0%b8%d0%bc%d0%b5%d1%80%d0%b5-playwright">
    #
  </a>
  E2E на примере Playwright
</h2>

<p>E2E полезно показывать на коротких, но бизнес-значимых сценариях.
Например, логин пользователя.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">playwright.sync_api</span> <span class="kn">import</span> <span class="n">sync_playwright</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">test_login_e2e</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="k">with</span> <span class="n">sync_playwright</span><span class="p">()</span> <span class="k">as</span> <span class="n">p</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">browser</span> <span class="o">=</span> <span class="n">p</span><span class="o">.</span><span class="n">chromium</span><span class="o">.</span><span class="n">launch</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">        <span class="n">page</span> <span class="o">=</span> <span class="n">browser</span><span class="o">.</span><span class="n">new_page</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="n">page</span><span class="o">.</span><span class="n">goto</span><span class="p">(</span><span class="s2">&#34;http://localhost:8000/login&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">page</span><span class="o">.</span><span class="n">fill</span><span class="p">(</span><span class="s2">&#34;input[name=&#39;email&#39;]&#34;</span><span class="p">,</span> <span class="s2">&#34;alice@example.com&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">page</span><span class="o">.</span><span class="n">fill</span><span class="p">(</span><span class="s2">&#34;input[name=&#39;password&#39;]&#34;</span><span class="p">,</span> <span class="s2">&#34;qwerty&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">page</span><span class="o">.</span><span class="n">click</span><span class="p">(</span><span class="s2">&#34;button[type=&#39;submit&#39;]&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="n">page</span><span class="o">.</span><span class="n">wait_for_url</span><span class="p">(</span><span class="s2">&#34;**/dashboard&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">assert</span> <span class="n">page</span><span class="o">.</span><span class="n">locator</span><span class="p">(</span><span class="s2">&#34;text=Welcome&#34;</span><span class="p">)</span><span class="o">.</span><span class="n">is_visible</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="n">browser</span><span class="o">.</span><span class="n">close</span><span class="p">()</span>
</span></span></code></pre></div><p>В этом тесте мы не проверяем внутреннюю реализацию логина.
Мы проверяем, что пользователь действительно может пройти
критический путь.</p>

<h2 id="база-данных-в-тестах-миграции-фикстуры-rollback">
  <a class="link" href="#%d0%b1%d0%b0%d0%b7%d0%b0-%d0%b4%d0%b0%d0%bd%d0%bd%d1%8b%d1%85-%d0%b2-%d1%82%d0%b5%d1%81%d1%82%d0%b0%d1%85-%d0%bc%d0%b8%d0%b3%d1%80%d0%b0%d1%86%d0%b8%d0%b8-%d1%84%d0%b8%d0%ba%d1%81%d1%82%d1%83%d1%80%d1%8b-rollback">
    #
  </a>
  База данных в тестах: миграции, фикстуры, rollback
</h2>

<p>С БД в тестах есть одно правило, которое нельзя нарушать.
Каждый тест должен быть независим по состоянию.</p>
<p>Один из рабочих шаблонов для SQLAlchemy это транзакция на тест,
которая откатывается после выполнения.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">pytest</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">sqlalchemy</span> <span class="kn">import</span> <span class="n">create_engine</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">sqlalchemy.orm</span> <span class="kn">import</span> <span class="n">sessionmaker</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nd">@pytest.fixture</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">db_session</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="n">engine</span> <span class="o">=</span> <span class="n">create_engine</span><span class="p">(</span><span class="s2">&#34;sqlite:///:memory:&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">Session</span> <span class="o">=</span> <span class="n">sessionmaker</span><span class="p">(</span><span class="n">bind</span><span class="o">=</span><span class="n">engine</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">session</span> <span class="o">=</span> <span class="n">Session</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">tx</span> <span class="o">=</span> <span class="n">session</span><span class="o">.</span><span class="n">begin</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">yield</span> <span class="n">session</span>
</span></span><span class="line"><span class="cl">    <span class="k">finally</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">tx</span><span class="o">.</span><span class="n">rollback</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">        <span class="n">session</span><span class="o">.</span><span class="n">close</span><span class="p">()</span>
</span></span></code></pre></div><p>Такой подход дает чистое состояние на каждый тест и сохраняет
высокую скорость прогонов.</p>
<p>Если в проекте используются миграции, их нужно включать в процесс
подготовки тестовой БД, иначе тесты начнут жить в схеме,
которая отличается от production.</p>

<h2 id="приватные-методы-когда-тестировать-напрямую-а-когда-нет">
  <a class="link" href="#%d0%bf%d1%80%d0%b8%d0%b2%d0%b0%d1%82%d0%bd%d1%8b%d0%b5-%d0%bc%d0%b5%d1%82%d0%be%d0%b4%d1%8b-%d0%ba%d0%be%d0%b3%d0%b4%d0%b0-%d1%82%d0%b5%d1%81%d1%82%d0%b8%d1%80%d0%be%d0%b2%d0%b0%d1%82%d1%8c-%d0%bd%d0%b0%d0%bf%d1%80%d1%8f%d0%bc%d1%83%d1%8e-%d0%b0-%d0%ba%d0%be%d0%b3%d0%b4%d0%b0-%d0%bd%d0%b5%d1%82">
    #
  </a>
  Приватные методы: когда тестировать напрямую, а когда нет
</h2>

<p>По умолчанию лучше тестировать поведение через публичный API.
Это делает тесты устойчивее к рефакторингу.</p>
<p>Рассмотрим пример.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">TaxCalculator</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">calculate</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">amount</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">int</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="n">amount</span> <span class="o">+</span> <span class="bp">self</span><span class="o">.</span><span class="n">_tax</span><span class="p">(</span><span class="n">amount</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">_tax</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">amount</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">int</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="nb">int</span><span class="p">(</span><span class="n">amount</span> <span class="o">*</span> <span class="mf">0.2</span><span class="p">)</span>
</span></span></code></pre></div><p>Стабильный тест проверяет <code>calculate</code>, а не <code>_tax</code>.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">test_calculate_adds_tax</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="n">calc</span> <span class="o">=</span> <span class="n">TaxCalculator</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="k">assert</span> <span class="n">calc</span><span class="o">.</span><span class="n">calculate</span><span class="p">(</span><span class="mi">100</span><span class="p">)</span> <span class="o">==</span> <span class="mi">120</span>
</span></span></code></pre></div><p>Если завтра <code>_tax</code> будет заменен на сложную стратегию
с льготами, тест публичного поведения останется валидным,
если контракт не изменился.</p>
<p>Прямое тестирование приватного метода может быть временно
оправдано в legacy-коде, но как постоянная практика оно
обычно повышает хрупкость тестов.</p>

<h2 id="время-в-тестах-и-детерминизм">
  <a class="link" href="#%d0%b2%d1%80%d0%b5%d0%bc%d1%8f-%d0%b2-%d1%82%d0%b5%d1%81%d1%82%d0%b0%d1%85-%d0%b8-%d0%b4%d0%b5%d1%82%d0%b5%d1%80%d0%bc%d0%b8%d0%bd%d0%b8%d0%b7%d0%bc">
    #
  </a>
  Время в тестах и детерминизм
</h2>

<p>Время это одна из самых частых причин flaky-тестов.
Если тест зависит от <code>datetime.now()</code>, он может вести себя
по-разному в разные дни, часы и timezone.</p>
<p><code>freezegun</code> решает это элегантно.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">datetime</span> <span class="kn">import</span> <span class="n">datetime</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">freezegun</span> <span class="kn">import</span> <span class="n">freeze_time</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">current_period_start</span><span class="p">()</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">now</span> <span class="o">=</span> <span class="n">datetime</span><span class="o">.</span><span class="n">now</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">now</span><span class="o">.</span><span class="n">replace</span><span class="p">(</span><span class="n">day</span><span class="o">=</span><span class="mi">1</span><span class="p">,</span> <span class="n">hour</span><span class="o">=</span><span class="mi">0</span><span class="p">,</span> <span class="n">minute</span><span class="o">=</span><span class="mi">0</span><span class="p">,</span> <span class="n">second</span><span class="o">=</span><span class="mi">0</span><span class="p">,</span> <span class="n">microsecond</span><span class="o">=</span><span class="mi">0</span><span class="p">)</span><span class="o">.</span><span class="n">isoformat</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nd">@freeze_time</span><span class="p">(</span><span class="s2">&#34;2026-02-12 10:00:00&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">test_current_period_start</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="k">assert</span> <span class="n">current_period_start</span><span class="p">()</span> <span class="o">==</span> <span class="s2">&#34;2026-02-01T00:00:00&#34;</span>
</span></span></code></pre></div><p>Такой тест детерминирован и не зависит от машины запуска.</p>

<h2 id="плотный-блок-про-pytest-от-базы-до-полезных-встроенных-фикстур">
  <a class="link" href="#%d0%bf%d0%bb%d0%be%d1%82%d0%bd%d1%8b%d0%b9-%d0%b1%d0%bb%d0%be%d0%ba-%d0%bf%d1%80%d0%be-pytest-%d0%be%d1%82-%d0%b1%d0%b0%d0%b7%d1%8b-%d0%b4%d0%be-%d0%bf%d0%be%d0%bb%d0%b5%d0%b7%d0%bd%d1%8b%d1%85-%d0%b2%d1%81%d1%82%d1%80%d0%be%d0%b5%d0%bd%d0%bd%d1%8b%d1%85-%d1%84%d0%b8%d0%ba%d1%81%d1%82%d1%83%d1%80">
    #
  </a>
  Плотный блок про pytest: от базы до полезных встроенных фикстур
</h2>

<p>Начнем с базового примера с <code>assert</code>.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">multiply</span><span class="p">(</span><span class="n">a</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">b</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">int</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">a</span> <span class="o">*</span> <span class="n">b</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">test_multiply_basic</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="k">assert</span> <span class="n">multiply</span><span class="p">(</span><span class="mi">3</span><span class="p">,</span> <span class="mi">4</span><span class="p">)</span> <span class="o">==</span> <span class="mi">12</span>
</span></span></code></pre></div><p>Параметризация позволяет расширять покрытие без копипаста.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">pytest</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nd">@pytest.mark.parametrize</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;a,b,expected&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="p">[</span>
</span></span><span class="line"><span class="cl">        <span class="p">(</span><span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mi">6</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">10</span><span class="p">,</span> <span class="mi">0</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="p">(</span><span class="o">-</span><span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="o">-</span><span class="mi">6</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="p">],</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">test_multiply_parametrized</span><span class="p">(</span><span class="n">a</span><span class="p">,</span> <span class="n">b</span><span class="p">,</span> <span class="n">expected</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="k">assert</span> <span class="n">multiply</span><span class="p">(</span><span class="n">a</span><span class="p">,</span> <span class="n">b</span><span class="p">)</span> <span class="o">==</span> <span class="n">expected</span>
</span></span></code></pre></div><p>Проверка исключений:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">pytest</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">divide</span><span class="p">(</span><span class="n">a</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">b</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">float</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">b</span> <span class="o">==</span> <span class="mi">0</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">raise</span> <span class="ne">ZeroDivisionError</span><span class="p">(</span><span class="s2">&#34;division by zero&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">a</span> <span class="o">/</span> <span class="n">b</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">test_divide_zero_raises</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="k">with</span> <span class="n">pytest</span><span class="o">.</span><span class="n">raises</span><span class="p">(</span><span class="ne">ZeroDivisionError</span><span class="p">,</span> <span class="k">match</span><span class="o">=</span><span class="s2">&#34;division by zero&#34;</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="n">divide</span><span class="p">(</span><span class="mi">10</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span>
</span></span></code></pre></div><p><code>tmp_path</code> полезен для файловых сценариев.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">save_report</span><span class="p">(</span><span class="n">path</span><span class="p">,</span> <span class="n">text</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">path</span><span class="o">.</span><span class="n">write_text</span><span class="p">(</span><span class="n">text</span><span class="p">,</span> <span class="n">encoding</span><span class="o">=</span><span class="s2">&#34;utf-8&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">test_save_report</span><span class="p">(</span><span class="n">tmp_path</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">report</span> <span class="o">=</span> <span class="n">tmp_path</span> <span class="o">/</span> <span class="s2">&#34;report.txt&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">save_report</span><span class="p">(</span><span class="n">report</span><span class="p">,</span> <span class="s2">&#34;ok&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">assert</span> <span class="n">report</span><span class="o">.</span><span class="n">read_text</span><span class="p">(</span><span class="n">encoding</span><span class="o">=</span><span class="s2">&#34;utf-8&#34;</span><span class="p">)</span> <span class="o">==</span> <span class="s2">&#34;ok&#34;</span>
</span></span></code></pre></div><p><code>monkeypatch</code> помогает временно менять окружение и атрибуты.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">os</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">service_url</span><span class="p">()</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">os</span><span class="o">.</span><span class="n">getenv</span><span class="p">(</span><span class="s2">&#34;SERVICE_URL&#34;</span><span class="p">,</span> <span class="s2">&#34;https://prod.example.com&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">test_service_url_from_env</span><span class="p">(</span><span class="n">monkeypatch</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">monkeypatch</span><span class="o">.</span><span class="n">setenv</span><span class="p">(</span><span class="s2">&#34;SERVICE_URL&#34;</span><span class="p">,</span> <span class="s2">&#34;https://test.example.com&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">assert</span> <span class="n">service_url</span><span class="p">()</span> <span class="o">==</span> <span class="s2">&#34;https://test.example.com&#34;</span>
</span></span></code></pre></div><p>В реальных проектах такие встроенные инструменты снимают много
болезненного ручного кода в тестах.</p>

<h2 id="интеграция-типизации-и-тестов-две-сетки-безопасности-лучше-одной">
  <a class="link" href="#%d0%b8%d0%bd%d1%82%d0%b5%d0%b3%d1%80%d0%b0%d1%86%d0%b8%d1%8f-%d1%82%d0%b8%d0%bf%d0%b8%d0%b7%d0%b0%d1%86%d0%b8%d0%b8-%d0%b8-%d1%82%d0%b5%d1%81%d1%82%d0%be%d0%b2-%d0%b4%d0%b2%d0%b5-%d1%81%d0%b5%d1%82%d0%ba%d0%b8-%d0%b1%d0%b5%d0%b7%d0%be%d0%bf%d0%b0%d1%81%d0%bd%d0%be%d1%81%d1%82%d0%b8-%d0%bb%d1%83%d1%87%d1%88%d0%b5-%d0%be%d0%b4%d0%bd%d0%be%d0%b9">
    #
  </a>
  Интеграция типизации и тестов: две сетки безопасности лучше одной
</h2>

<p>Тесты и статический анализ закрывают разные области риска.
Type checker ловит типовые несоответствия до запуска.
Тесты ловят поведенческие и интеграционные ошибки при выполнении.</p>
<p>Например, type checker может заранее подсветить ситуацию,
когда функция обещает вернуть <code>int</code>, а фактически возвращает
<code>float</code> или <code>None</code>. Тесты в этот момент еще даже не запускались.</p>
<p>В CI зрелого проекта обычно запускают и type checker,
и pytest. Это дает плотный и быстрый контур качества.
Сначала отсекаются структурные ошибки, затем проверяется
поведение.</p>

<h2 id="cicd-как-сделать-тесты-частью-процесса-а-не-пожеланием">
  <a class="link" href="#cicd-%d0%ba%d0%b0%d0%ba-%d1%81%d0%b4%d0%b5%d0%bb%d0%b0%d1%82%d1%8c-%d1%82%d0%b5%d1%81%d1%82%d1%8b-%d1%87%d0%b0%d1%81%d1%82%d1%8c%d1%8e-%d0%bf%d1%80%d0%be%d1%86%d0%b5%d1%81%d1%81%d0%b0-%d0%b0-%d0%bd%d0%b5-%d0%bf%d0%be%d0%b6%d0%b5%d0%bb%d0%b0%d0%bd%d0%b8%d0%b5%d0%bc">
    #
  </a>
  CI/CD: как сделать тесты частью процесса, а не пожеланием
</h2>

<p>Если тесты живут только локально, их ценность ограничена.
Кто-то запускает их, кто-то нет, а качество становится вопросом
дисциплины отдельных людей. CI снимает этот риск, потому что
проверки становятся обязательным этапом потока.</p>
<p>Для GitHub Actions минимальный рабочий пайплайн может выглядеть так:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">tests</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">on</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">pull_request</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">push</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">branches</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">main]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">jobs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">test</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">runs-on</span><span class="p">:</span><span class="w"> </span><span class="l">ubuntu-latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">steps</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">actions/checkout@v4</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">actions/setup-python@v5</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">with</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">python-version</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;3.12&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="l">pip install -r requirements.txt</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="l">pip install pytest pytest-cov pyright</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="l">pyright</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="l">pytest tests/unit tests/integration --cov=app --cov-fail-under=80</span><span class="w">
</span></span></span></code></pre></div><p>Для GitLab CI логика та же:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">stages</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="l">test</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">test</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">stage</span><span class="p">:</span><span class="w"> </span><span class="l">test</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">python:3.12</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">script</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">pip install -r requirements.txt</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">pip install pytest pytest-cov pyright</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">pyright</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">pytest tests/unit tests/integration --cov=app --cov-fail-under=80</span><span class="w">
</span></span></span></code></pre></div><p>Ключевая инженерная мысль здесь простая. Неважно, какая платформа
CI выбрана. Важно, что правила качества автоматизированы и
единообразны для всех.</p>

<h2 id="частые-анти-паттерны-из-за-которых-тесты-теряют-ценность">
  <a class="link" href="#%d1%87%d0%b0%d1%81%d1%82%d1%8b%d0%b5-%d0%b0%d0%bd%d1%82%d0%b8-%d0%bf%d0%b0%d1%82%d1%82%d0%b5%d1%80%d0%bd%d1%8b-%d0%b8%d0%b7-%d0%b7%d0%b0-%d0%ba%d0%be%d1%82%d0%be%d1%80%d1%8b%d1%85-%d1%82%d0%b5%d1%81%d1%82%d1%8b-%d1%82%d0%b5%d1%80%d1%8f%d1%8e%d1%82-%d1%86%d0%b5%d0%bd%d0%bd%d0%be%d1%81%d1%82%d1%8c">
    #
  </a>
  Частые анти-паттерны, из-за которых тесты теряют ценность
</h2>

<p>Один из самых неприятных анти-паттернов это тестирование внутренней
реализации вместо внешнего поведения. Такой тест привязывается
к тому, как код написан сейчас, и ломается при любом рефакторинге,
даже если контракт не менялся.</p>
<p>Второй анти-паттерн это смешивание уровней в одном тесте.
Тест называется юнитом, но внутри идет в сеть, использует
реальную БД, зависит от текущего времени и случайности.
Результат предсказуем: долгий и нестабильный прогон.</p>
<p>Третий анти-паттерн это flaky-тесты, которые иногда красные,
иногда зеленые без изменений в коде. Как только команда привыкает
к мысли &ldquo;этот тест иногда сам по себе падает&rdquo;, доверие к тестовому
пакету рушится.</p>
<p>Четвертый анти-паттерн это попытка покрыть абсолютно все подряд,
включая чужие библиотеки и тривиальные прокси-методы без
бизнес-смысла. Покрытие растет, но полезный сигнал не растет.</p>

<h2 id="реальный-сценарий-red---green---refactor-на-короткой-функции">
  <a class="link" href="#%d1%80%d0%b5%d0%b0%d0%bb%d1%8c%d0%bd%d1%8b%d0%b9-%d1%81%d1%86%d0%b5%d0%bd%d0%b0%d1%80%d0%b8%d0%b9-red---green---refactor-%d0%bd%d0%b0-%d0%ba%d0%be%d1%80%d0%be%d1%82%d0%ba%d0%be%d0%b9-%d1%84%d1%83%d0%bd%d0%ba%d1%86%d0%b8%d0%b8">
    #
  </a>
  Реальный сценарий Red -&gt; Green -&gt; Refactor на короткой функции
</h2>

<p>Сделаем еще один компактный TDD-сценарий, на этот раз про валидацию
пароля. Требование такое: пароль должен быть длиной не меньше
восьми символов, содержать хотя бы одну цифру и хотя бы одну
букву.</p>
<p>Сначала Red:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">test_validate_password_ok</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="k">assert</span> <span class="n">validate_password</span><span class="p">(</span><span class="s2">&#34;abc12345&#34;</span><span class="p">)</span> <span class="ow">is</span> <span class="kc">True</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">test_validate_password_too_short</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="k">assert</span> <span class="n">validate_password</span><span class="p">(</span><span class="s2">&#34;a1&#34;</span><span class="p">)</span> <span class="ow">is</span> <span class="kc">False</span>
</span></span></code></pre></div><p>Green, минимальная реализация:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">validate_password</span><span class="p">(</span><span class="n">value</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">bool</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">value</span><span class="p">)</span> <span class="o">&lt;</span> <span class="mi">8</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="kc">False</span>
</span></span><span class="line"><span class="cl">    <span class="n">has_digit</span> <span class="o">=</span> <span class="nb">any</span><span class="p">(</span><span class="n">ch</span><span class="o">.</span><span class="n">isdigit</span><span class="p">()</span> <span class="k">for</span> <span class="n">ch</span> <span class="ow">in</span> <span class="n">value</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">has_alpha</span> <span class="o">=</span> <span class="nb">any</span><span class="p">(</span><span class="n">ch</span><span class="o">.</span><span class="n">isalpha</span><span class="p">()</span> <span class="k">for</span> <span class="n">ch</span> <span class="ow">in</span> <span class="n">value</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">has_digit</span> <span class="ow">and</span> <span class="n">has_alpha</span>
</span></span></code></pre></div><p>Refactor и расширение тестов:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">pytest</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nd">@pytest.mark.parametrize</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;value,expected&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="p">[</span>
</span></span><span class="line"><span class="cl">        <span class="p">(</span><span class="s2">&#34;abc12345&#34;</span><span class="p">,</span> <span class="kc">True</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="p">(</span><span class="s2">&#34;abcdefgh&#34;</span><span class="p">,</span> <span class="kc">False</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="p">(</span><span class="s2">&#34;12345678&#34;</span><span class="p">,</span> <span class="kc">False</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="p">(</span><span class="s2">&#34;ab12&#34;</span><span class="p">,</span> <span class="kc">False</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="p">],</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">test_validate_password_parametrized</span><span class="p">(</span><span class="n">value</span><span class="p">,</span> <span class="n">expected</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="k">assert</span> <span class="n">validate_password</span><span class="p">(</span><span class="n">value</span><span class="p">)</span> <span class="ow">is</span> <span class="n">expected</span>
</span></span></code></pre></div><p>Это простой пример, но он хорошо показывает идею.
Тесты сначала формулируют контракт, потом поддерживают
безопасные изменения.</p>

<h2 id="что-делать-уже-завтра-в-реальном-проекте">
  <a class="link" href="#%d1%87%d1%82%d0%be-%d0%b4%d0%b5%d0%bb%d0%b0%d1%82%d1%8c-%d1%83%d0%b6%d0%b5-%d0%b7%d0%b0%d0%b2%d1%82%d1%80%d0%b0-%d0%b2-%d1%80%d0%b5%d0%b0%d0%bb%d1%8c%d0%bd%d0%be%d0%bc-%d0%bf%d1%80%d0%be%d0%b5%d0%ba%d1%82%d0%b5">
    #
  </a>
  Что делать уже завтра в реальном проекте
</h2>

<p>Если в проекте тестов мало или они хаотичны, не нужно пытаться
переписать все за одну итерацию. Практичнее начать с маленького,
но системного шага. Выделить критический бизнес-путь и закрыть его
базовым набором проверок на трех уровнях: быстрые юниты,
несколько интеграционных тестов на ключевые контракты и один-два
E2E сценария на пользовательскую ценность.</p>
<p>Параллельно стоит зафиксировать минимальные правила качества в CI.
Запуск юнитов, запуск интеграционных тестов, запуск type checker,
порог покрытия, блокировка merge при красном пайплайне. Это уже
создает каркас процесса, который можно постепенно усиливать.</p>
<p>По мере роста кода нужно следить не только за числом тестов,
но и за их полезностью. Каждый тест должен отвечать на вопрос,
какой риск он закрывает. Когда тесты начинают жить своей жизнью
и проверять не риск, а случайные детали, их ценность быстро
снижается.</p>

<h2 id="вместо-финала">
  <a class="link" href="#%d0%b2%d0%bc%d0%b5%d1%81%d1%82%d0%be-%d1%84%d0%b8%d0%bd%d0%b0%d0%bb%d0%b0">
    #
  </a>
  Вместо финала
</h2>

<p>Тестирование в Python не является бюрократией, если к нему
подходить как к инженерному инструменту, а не как к формальному
требованию. Тесты помогают быстрее двигаться, потому что дают
предсказуемый сигнал, удерживают систему в рабочем состоянии
и позволяют безопасно менять код, который уже приносит бизнесу
ценность.</p>
<p>Команда, которая умеет писать понятные юниты, осмысленные
интеграционные тесты и ограниченный набор E2E на критический путь,
обычно выпускает изменения чаще и спокойнее. Не потому, что у нее
&ldquo;больше тестов&rdquo;, а потому что у нее лучше управляется риск.</p>
<p>И в этом главный смысл всей темы. Тесты нужны не ради тестов.
Они нужны ради управляемой разработки.</p>
]]></content:encoded><category>python</category><category>testing</category><category>eosp</category></item><item><title>Типизация в Python</title><link>https://alchemmist.xyz/ru/articles/typing-python/</link><pubDate>Wed, 11 Feb 2026 16:02:00 +0300</pubDate><dc:creator>alchemmist</dc:creator><guid>https://alchemmist.xyz/ru/articles/typing-python/</guid><description>В этой статье рассматриваются два ключевых вопроса. Зачем использовать типизацию в языке Python, который позволяет писать без неё? И как писать типизированный код на Python правильно? Постараемся быстро познакомиться со всеми необходимыми инструментами, чтобы после прочтения вы уже могли начать осознанно писать свои программы типизированными, ведь это совсем не сложно!
# Динамика vs Статика Итак, что же такое типизация? Новым это понятие может оказаться только для питонистов, ведь большинство классических языков таких как C, Java, Rust и многие другие исходно были созданы, как языки со статической типизацией. Но что это означает? Давайте рассмотрим небольшой пример на C:</description><content:encoded><![CDATA[<p>В этой статье рассматриваются два ключевых вопроса. Зачем
использовать типизацию в языке Python, который позволяет писать
без неё? И как писать типизированный код на Python правильно?
Постараемся быстро познакомиться со всеми необходимыми
инструментами, чтобы после прочтения вы уже могли начать
осознанно писать свои программы типизированными, ведь это совсем
не сложно!</p>

<h2 id="динамика-vs-статика">
  <a class="link" href="#%d0%b4%d0%b8%d0%bd%d0%b0%d0%bc%d0%b8%d0%ba%d0%b0-vs-%d1%81%d1%82%d0%b0%d1%82%d0%b8%d0%ba%d0%b0">
    #
  </a>
  Динамика vs Статика
</h2>

<p>Итак, что же такое типизация? Новым это понятие может оказаться
только для питонистов, ведь большинство классических языков таких
как C, Java, Rust и многие другие исходно были созданы, как языки
со статической типизацией. Но что это означает? Давайте рассмотрим
небольшой пример на C:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="kt">int</span> <span class="nf">sum</span><span class="p">(</span><span class="kt">int</span> <span class="n">a</span><span class="p">,</span> <span class="kt">int</span> <span class="n">b</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">a</span> <span class="o">+</span> <span class="n">b</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kt">int</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nf">printf</span><span class="p">(</span><span class="s">&#34;%d</span><span class="se">\n</span><span class="s">&#34;</span><span class="p">,</span> <span class="nf">sum</span><span class="p">(</span><span class="mi">10</span><span class="p">,</span> <span class="mi">20</span><span class="p">));</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1">// printf(&#34;%d\n&#34;, sum(&#34;10&#34;, 20));
</span></span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>Такой код работает и выводит число <code>30</code>. Но обратите внимание,
что последняя строка закомментирована. Если мы раскомментируем
её и опять попробуем скомпилировать программу, то получим
примерно вот такой лог ошибки:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line"><span class="cl">error: passing argument 1 of ‘sum’ makes integer from pointer without cast
</span></span><span class="line"><span class="cl">   10 |     printf(&#34;%d\n&#34;, sum(&#34;10&#34;, 20));
</span></span><span class="line"><span class="cl">      |                        ^~~~
</span></span><span class="line"><span class="cl">      |                        |
</span></span><span class="line"><span class="cl">      |                        char *
</span></span><span class="line"><span class="cl">note: expected ‘int’ but argument is of type ‘char *’
</span></span></code></pre></div><p>Лог сообщает нам, что параметр функции, ожидая <code>int</code>, получил
аргумент типа <code>char *</code> (<em>для упрощения можем считать это
эквивалентом строки</em>). На первый взгляд ничего удивительного, для
нас с вами — питонистов, в этом нет. Ведь вот такой код на Python
тоже упал бы с ошибкой:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">sum</span><span class="p">(</span><span class="n">a</span><span class="p">,</span> <span class="n">b</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">a</span> <span class="o">+</span> <span class="n">b</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="nb">sum</span><span class="p">(</span><span class="s2">&#34;10&#34;</span><span class="p">,</span> <span class="mi">20</span><span class="p">))</span> <span class="c1"># TypeError</span>
</span></span></code></pre></div><p>В чем же разница, спросите вы? Давайте немного подправим оба этих
примера на C и на Python. Попробуем вызвать функцию <code>sum</code> от двух
строк:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="nb">sum</span><span class="p">(</span><span class="s2">&#34;10&#34;</span><span class="p">,</span> <span class="s2">&#34;20&#34;</span><span class="p">))</span> <span class="c1"># &gt; 1020</span>
</span></span></code></pre></div><p>Здесь мы не получаем никаких ошибок, потому что действует
полиморфизм, а для строк операция сложения тоже реализована. Но
что будет в C?</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="kt">int</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nf">printf</span><span class="p">(</span><span class="s">&#34;%d</span><span class="se">\n</span><span class="s">&#34;</span><span class="p">,</span> <span class="nf">sum</span><span class="p">(</span><span class="s">&#34;10&#34;</span><span class="p">,</span> <span class="s">&#34;20&#34;</span><span class="p">));</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>Такую программу не выйдет даже скомпилировать. Мы опять получим
точно такой же лог ошибки, который был ранее. Обратите внимание
на то, как мы определяли функцию <code>sum</code> в языке C. Там мы явно
указали типы входных аргументов как <code>int</code>. Это означает, что
аргумент любого другого типа нельзя передать в эту функцию. Это
и называется статической типизацией. Также статическая типизация
обязывает указать тип для каждой переменной и запрещает менять
типы переменных после определения оных. То есть тип фиксирован,
статичен.</p>
<p>Вторая же группа языков называется динамически типизированными.
Это такие языки как Python, Lua, JavaScript и другие. В них,
соответственно, тип переменной строго не фиксирован и может
меняться в ходе исполнения программы.</p>

<h2 id="преимущества-типизации">
  <a class="link" href="#%d0%bf%d1%80%d0%b5%d0%b8%d0%bc%d1%83%d1%89%d0%b5%d1%81%d1%82%d0%b2%d0%b0-%d1%82%d0%b8%d0%bf%d0%b8%d0%b7%d0%b0%d1%86%d0%b8%d0%b8">
    #
  </a>
  Преимущества типизации
</h2>

<p>Пора перейти к вопросу, зачем типизация нам нужна, если в том же
Python всё и так работает отлично. Во-первых, это скорость. На
низком уровне (<em>чем бы он ни был представлен</em>) нам в любом случае
нужно знать типы переменных. А тот факт, что мы можем позволить
себе, их не выставлять, лишь означает, что кто-то делает это за
нас (<em>например, виртуальная машина</em>), а это в свою очередь
требует ресурсов. Отсюда и картина, которую мы наблюдаем
в рейтингах языков по скорости:</p>
<p><img
    src="/images/pl-rating_hu_24d9f448f6699b56.webp" srcset="/images/pl-rating_hu_37edec30c72a3477.webp 360w, /images/pl-rating_hu_24d9f448f6699b56.webp 720w, /images/pl-rating_hu_e59670835513d670.webp 1080w, /images/pl-rating_hu_f117cd474e83bcca.webp 1440w" sizes="900px"
    width="720"
    height="486"
    class="content-img" style="width:900px"
    alt=""
    loading="lazy"
    decoding="async"
    data-zoom-src="/images/pl-rating_hu_f117cd474e83bcca.webp"
  /></p>
<p>Python в этом
<a href="https://github.com/niklas-heer/speed-comparison">рейтинге</a>,
кстати говоря, на последнем месте. Может быть типизированный
Python как раз поможет нам исправить этот печальный факт?
К сожалению,
<a href="https://bernsteinbear.com/blog/typed-python/#fnref:simple-annotations">нет</a>.
Поскольку Python как не был исконно, так по сей день остается,
<strong>не</strong> статически типизированным языком. Аннотации типов в Python
остаются опциональными, их можно не указывать. И сам
интерпретатор Python в runtime не проверят их.</p>
<p>Тут мы переходим ко второму преимуществу, которое открывают для
нас типы. И это качество кода.</p>
<blockquote class="markdown-blockquote">
  <p>&ldquo;Цель типизации в Python — помочь инструментам разработки искать
ошибки в кодовых базах на Python с помощью статического
анализа, то есть не выполняя тестов кода.&rdquo;</p>
<p>Лусиану Рамальо, &ldquo;Python. К вершинам мастерства&rdquo;</p>

</blockquote>
<p>Расширяя эту мысль, мы можем уточнить, что цели типизации это:</p>
<ul>
<li>Раннее выявление ошибок — до runtime-а, до падения кода на
продакшене</li>
<li>&ldquo;Экстракция&rdquo; тестов — правильная типизация помогает уменьшить
количество тестов, писать и поддерживать которые сложнее, чем
типы. В тестах остаётся тестировать бизнес логику, а не
банальное несоответствие примитивов.</li>
<li>Улучшение читаемости кода — <a href="https://peps.python.org/pep-0020/">PEP
20</a> говорит нам о том, что
&ldquo;явное лучше неявного&rdquo;. Когда мы читаем код, нам достаточно
увидеть сигнатуру функции, не вчитываясь в её реализацию</li>
<li>Упрощение разработки в IDE — больше подсказок
и предупреждений о потенциальных ошибках.</li>
<li>Повышение качества архитектуры — типы &ldquo;заставляют&rdquo;
проектировать правильные абстракции.</li>
</ul>

<h2 id="как-писать-типизированно">
  <a class="link" href="#%d0%ba%d0%b0%d0%ba-%d0%bf%d0%b8%d1%81%d0%b0%d1%82%d1%8c-%d1%82%d0%b8%d0%bf%d0%b8%d0%b7%d0%b8%d1%80%d0%be%d0%b2%d0%b0%d0%bd%d0%bd%d0%be">
    #
  </a>
  Как писать типизированно
</h2>

<p>Перед тем, как переходить к коду, давайте разберёмся с тремя
важными понятиями: интерфейс, абстрактный класс и протокол. В чем
разница? Давайте по порядку:</p>
<ul>
<li><strong>Интерфейс</strong> — это класс, у которого все методы абстрактные,
то есть не содержат деталей реализации.</li>
<li><strong>Абстрактный класс</strong> — это класс, у которого, помимо
абстрактных методов, есть еще и реализованные методы.</li>
<li><strong>Протокол</strong> — это неявный интерфейс.</li>
</ul>
<p>Первые два понятия должны быть понятны. А на последнем давайте
остановимся. В Python реализация паттерна интерфейса работает
через наследование, то есть, класс, реализующий интерфейс,
наследуется от этого класса-интерфейса. При этом класс, который
реализует протокол, не наследуется от него и вообще может о нем
не знать.</p>

<h3 id="примитивы">
  <a class="link" href="#%d0%bf%d1%80%d0%b8%d0%bc%d0%b8%d1%82%d0%b8%d0%b2%d1%8b">
    ##
  </a>
  Примитивы
</h3>

<p>Давайте наконец посмотрим на то, как же писать типизированный
код. Начнем с простейшего примера функции, которая
мультиплицирует переданную ей строку $n$ раз.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">multi_string</span><span class="p">(</span><span class="n">string</span><span class="p">,</span> <span class="n">n</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">string</span> <span class="o">*</span> <span class="n">n</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="n">multi_string</span><span class="p">(</span><span class="s2">&#34;cat&#34;</span><span class="p">,</span> <span class="mi">3</span><span class="p">))</span> <span class="c1"># &gt; catcatcat</span>
</span></span></code></pre></div><p>Это простейшая функция, которая принимает на вход строку и число,
а возвращает строку, полученную путём конкатенации исходной
строки с самой собой $n$ раз. Давайте типизируем эту функцию!</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">multi_string</span><span class="p">(</span><span class="n">string</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">n</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">string</span> <span class="o">*</span> <span class="n">n</span>
</span></span></code></pre></div><p>Синтаксис простой: типы для входных параметров мы подписываем
через двоеточие, а выходной тип с помощью стрелочки. Теперь наши
редакторы кода (IDE) будут подсвечивать для нас ошибку,
в случае если мы неправильно передадим входные или неправильно
обработаем выходные значения функции:</p>
<p><img src="/images/typing-demo-1.gif" class="content-img" style="width:700px" alt="" loading="lazy" decoding="async" /></p>
<p>Таким образом можно проаннотировать все классические примитивы
— <code>str</code>, <code>int</code>, <code>bytes</code>, <code>float</code>, <code>Decimal</code>, <code>bool</code>.</p>

<h3 id="объединение-типов">
  <a class="link" href="#%d0%be%d0%b1%d1%8a%d0%b5%d0%b4%d0%b8%d0%bd%d0%b5%d0%bd%d0%b8%d0%b5-%d1%82%d0%b8%d0%bf%d0%be%d0%b2">
    ##
  </a>
  Объединение типов
</h3>

<p>В реальности, конечно, бывают и более сложные кейсы, когда
функция может принимать и работать с разными типами, но все же
не любыми. В таких случаях можно использовать объединение типов:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">normalize</span><span class="p">(</span><span class="n">data</span><span class="p">:</span> <span class="nb">str</span> <span class="o">|</span> <span class="nb">bytes</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">data</span><span class="p">,</span> <span class="nb">bytes</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="n">data</span><span class="o">.</span><span class="n">decode</span><span class="p">(</span><span class="s2">&#34;utf-8&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">data</span>
</span></span></code></pre></div><p>В примере выше функция <code>normalize</code> принимает на вход либо строку,
либо последовательность байт и на выход всегда возвращает строку
в кодировке <code>utf-8</code>. Через <code>union</code> (<code>|</code>) можно перечислить любое
количество типов, но важно понимать, где это уместно. Если вам
хочется написать простыню из объединения десяти типов, стоит
хорошенько подумать. О том как такие ситуации разрешаются, мы
скажем ниже.</p>
<p>Объединение также можно использовать и в указании типа
возвращаемого значения. Но использовать это стоит только для
спецификации опциональности. Например:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">parse_int</span><span class="p">(</span><span class="n">value</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">int</span> <span class="o">|</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="ow">not</span> <span class="n">value</span><span class="o">.</span><span class="n">isdigit</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="kc">None</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="nb">int</span><span class="p">(</span><span class="n">value</span><span class="p">)</span>
</span></span></code></pre></div><p>По сути, тут мы говорим, что результат функции опционален. Она
может вернуть <code>int</code>, либо не вернуть его, если что-то пошло не
так. Однако, аннотировать возвращаемое значение как <code>int | str</code>
или любым другим подобным образом, где мы просто объединяем два
совершенно разных типа, считается плохой практикой. Потому что
в таком случае непонятно, как обрабатывать результат этой
функции. В результате функции мы можем ожидать либо конкретную
реализацию, про которую нам точно известны её методы и атрибуты,
либо <code>None</code>. Отступление от этого, как правило, будет приводить
к усложнению кода.</p>

<h3 id="спецификация-коллекций">
  <a class="link" href="#%d1%81%d0%bf%d0%b5%d1%86%d0%b8%d1%84%d0%b8%d0%ba%d0%b0%d1%86%d0%b8%d1%8f-%d0%ba%d0%be%d0%bb%d0%bb%d0%b5%d0%ba%d1%86%d0%b8%d0%b9">
    ##
  </a>
  Спецификация коллекций
</h3>

<p>Помимо примитивов мы конечно же можем аннотировать и коллекции.
Но лучше не ограничиваться простым <code>data: list</code>, а специфицировать
и содержание коллекции тоже. Это можно сделать, используя
синтаксис квадратных скобок. Давайте приведём несколько примеров:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">format_user</span><span class="p">(</span><span class="n">user</span><span class="p">:</span> <span class="nb">tuple</span><span class="p">[</span><span class="nb">str</span><span class="p">,</span> <span class="nb">int</span><span class="p">])</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">name</span><span class="p">,</span> <span class="n">score</span> <span class="o">=</span> <span class="n">user</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="sa">f</span><span class="s2">&#34;</span><span class="si">{</span><span class="n">name</span><span class="si">}</span><span class="s2">: </span><span class="si">{</span><span class="n">score</span><span class="si">}</span><span class="s2"> points&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">average</span><span class="p">(</span><span class="n">values</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="nb">float</span><span class="p">])</span> <span class="o">-&gt;</span> <span class="nb">float</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">values</span><span class="p">)</span> <span class="o">==</span> <span class="mi">0</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="mi">0</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="nb">sum</span><span class="p">(</span><span class="n">values</span><span class="p">)</span> <span class="o">/</span> <span class="nb">len</span><span class="p">(</span><span class="n">values</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">total_count</span><span class="p">(</span><span class="n">counters</span><span class="p">:</span> <span class="nb">dict</span><span class="p">[</span><span class="nb">str</span><span class="p">,</span> <span class="nb">int</span><span class="p">])</span> <span class="o">-&gt;</span> <span class="nb">int</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="nb">sum</span><span class="p">(</span><span class="n">counters</span><span class="o">.</span><span class="n">values</span><span class="p">())</span>
</span></span></code></pre></div><p>Это, конечно, не покрывает все возможные сценарии, которые могут
возникнуть в реальном продакшн коде. В секциях
<a href="/ru/articles/typing-python/#typeddict">TypedDict</a>, <a href="/ru/articles/typing-python/#namedtuple">NamedTuple</a>,
<a href="/ru/articles/typing-python/#dataclass">Dataclass</a> мы расширим наш инструментарий.</p>

<h3 id="mapping-и-mutablemapping">
  <a class="link" href="#mapping-%d0%b8-mutablemapping">
    ##
  </a>
  Mapping и MutableMapping
</h3>

<p><code>Mapping</code> и <code>MutableMapping</code> — это абстрактные классы для
словари-подобных структур. <code>Mapping</code> гарантирует только чтение
(ключи, значения, итерацию), а <code>MutableMapping</code> говорит, что
объект можно изменять.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">collections.abc</span> <span class="kn">import</span> <span class="n">Mapping</span><span class="p">,</span> <span class="n">MutableMapping</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">read_config</span><span class="p">(</span><span class="n">cfg</span><span class="p">:</span> <span class="n">Mapping</span><span class="p">[</span><span class="nb">str</span><span class="p">,</span> <span class="nb">str</span><span class="p">])</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">cfg</span><span class="p">[</span><span class="s2">&#34;DATABASE_URL&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">patch_config</span><span class="p">(</span><span class="n">cfg</span><span class="p">:</span> <span class="n">MutableMapping</span><span class="p">[</span><span class="nb">str</span><span class="p">,</span> <span class="nb">str</span><span class="p">])</span> <span class="o">-&gt;</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">cfg</span><span class="p">[</span><span class="s2">&#34;DEBUG&#34;</span><span class="p">]</span> <span class="o">=</span> <span class="s2">&#34;1&#34;</span>
</span></span></code></pre></div><p>Если функция только читает данные, указывайте <code>Mapping</code>. Если
меняет — <code>MutableMapping</code>. Это маленькая, но важная подсказка для
читателя и статического анализатора.</p>

<h3 id="namedtuple">
  <a class="link" href="#namedtuple">
    ##
  </a>
  NamedTuple
</h3>

<p>Когда нужно описать структуру с фиксированным набором полей и
одновременно сохранить поведение кортежа, удобно использовать
<code>NamedTuple</code>. Это неизменяемый тип данных, который можно
индексировать и при этом читать поля по именам.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">typing</span> <span class="kn">import</span> <span class="n">NamedTuple</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">User</span><span class="p">(</span><span class="n">NamedTuple</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="nb">id</span><span class="p">:</span> <span class="nb">int</span>
</span></span><span class="line"><span class="cl">    <span class="n">username</span><span class="p">:</span> <span class="nb">str</span>
</span></span><span class="line"><span class="cl">    <span class="n">score</span><span class="p">:</span> <span class="nb">int</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">print_user</span><span class="p">(</span><span class="n">user</span><span class="p">:</span> <span class="n">User</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="sa">f</span><span class="s2">&#34;</span><span class="si">{</span><span class="n">user</span><span class="o">.</span><span class="n">username</span><span class="si">}</span><span class="s2"> (</span><span class="si">{</span><span class="n">user</span><span class="o">.</span><span class="n">id</span><span class="si">}</span><span class="s2">) = </span><span class="si">{</span><span class="n">user</span><span class="o">.</span><span class="n">score</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span></code></pre></div><p><code>NamedTuple</code> хорошо подходит для компактных структур данных,
которые не должны изменяться после создания. Если вам нужны
изменяемые поля и более богатое поведение, лучше выбрать
<code>dataclass</code> или обычный класс.</p>

<h3 id="typeddict">
  <a class="link" href="#typeddict">
    ##
  </a>
  TypedDict
</h3>

<p>Если же ваша структура — это обычный словарь, но вы хотите
типизировать ожидаемые ключи и значения, используйте <code>TypedDict</code>.
Такой тип описывает форму словаря и работает только на уровне
статического анализа.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">typing</span> <span class="kn">import</span> <span class="n">TypedDict</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">User</span><span class="p">(</span><span class="n">TypedDict</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="nb">id</span><span class="p">:</span> <span class="nb">int</span>
</span></span><span class="line"><span class="cl">    <span class="n">username</span><span class="p">:</span> <span class="nb">str</span>
</span></span><span class="line"><span class="cl">    <span class="n">email</span><span class="p">:</span> <span class="nb">str</span> <span class="o">|</span> <span class="kc">None</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">send_email</span><span class="p">(</span><span class="n">user</span><span class="p">:</span> <span class="n">User</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="o">...</span>
</span></span></code></pre></div><p>Это особенно полезно, когда данные приходят из JSON или другого
динамического источника, но вы хотите строгий контракт по ключам.
Если часть ключей опциональна, опишите их явно, чтобы не терять
проверку на уровне типов.</p>

<h3 id="dataclass">
  <a class="link" href="#dataclass">
    ##
  </a>
  Dataclass
</h3>

<p><code>dataclass</code> — это удобный способ описать &ldquo;контейнер данных&rdquo; с
инициализатором, сравнениями и читаемым <code>repr</code>. Такой класс
изменяем, если не указать обратное, и отлично подходит для домена
или DTO.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">dataclasses</span> <span class="kn">import</span> <span class="n">dataclass</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nd">@dataclass</span>
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">User</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="nb">id</span><span class="p">:</span> <span class="nb">int</span>
</span></span><span class="line"><span class="cl">    <span class="n">username</span><span class="p">:</span> <span class="nb">str</span>
</span></span><span class="line"><span class="cl">    <span class="n">email</span><span class="p">:</span> <span class="nb">str</span> <span class="o">|</span> <span class="kc">None</span> <span class="o">=</span> <span class="kc">None</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">normalize</span><span class="p">(</span><span class="n">user</span><span class="p">:</span> <span class="n">User</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">User</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">user</span><span class="o">.</span><span class="n">username</span> <span class="o">=</span> <span class="n">user</span><span class="o">.</span><span class="n">username</span><span class="o">.</span><span class="n">lower</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">user</span>
</span></span></code></pre></div><p><code>dataclass</code> хорош, когда нужна структура, которая может
изменяться, и когда важна ясная модель данных. Если объект
должен быть неизменяемым, используйте <code>@dataclass(frozen=True)</code>.</p>

<h3 id="enum">
  <a class="link" href="#enum">
    ##
  </a>
  Enum
</h3>

<p><code>Enum</code> помогает описать закрытый набор значений. Это полезно,
когда у поля есть строго ограниченный набор допустимых вариантов,
и вы хотите, чтобы типы не позволяли случайные строки.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">enum</span> <span class="kn">import</span> <span class="n">Enum</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">Status</span><span class="p">(</span><span class="n">Enum</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">NEW</span> <span class="o">=</span> <span class="s2">&#34;new&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="n">DONE</span> <span class="o">=</span> <span class="s2">&#34;done&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="n">FAILED</span> <span class="o">=</span> <span class="s2">&#34;failed&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">is_done</span><span class="p">(</span><span class="n">status</span><span class="p">:</span> <span class="n">Status</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">bool</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">status</span> <span class="ow">is</span> <span class="n">Status</span><span class="o">.</span><span class="n">DONE</span>
</span></span></code></pre></div><p>Такой подход удобен для статусов, ролей, флагов, режимов работы,
то есть любого &ldquo;перечислимого&rdquo; доменного значения.</p>

<h3 id="кастомные-классы">
  <a class="link" href="#%d0%ba%d0%b0%d1%81%d1%82%d0%be%d0%bc%d0%bd%d1%8b%d0%b5-%d0%ba%d0%bb%d0%b0%d1%81%d1%81%d1%8b">
    ##
  </a>
  Кастомные классы
</h3>

<p>Конечно же система типов позволяет специфицировать не только
примитивы, но и наши собственные или библиотечные классы.
Например:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">User</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="nb">id</span><span class="p">:</span> <span class="nb">int</span>
</span></span><span class="line"><span class="cl">    <span class="n">username</span><span class="p">:</span> <span class="nb">str</span>
</span></span><span class="line"><span class="cl">    <span class="n">email</span><span class="p">:</span> <span class="nb">str</span>
</span></span><span class="line"><span class="cl">    <span class="n">friends</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="n">User</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">hand_shake</span><span class="p">(</span><span class="n">user1</span><span class="p">:</span> <span class="n">User</span><span class="p">,</span> <span class="n">user2</span><span class="p">:</span> <span class="n">User</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">user1</span><span class="o">.</span><span class="n">friends</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">user2</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">user2</span><span class="o">.</span><span class="n">friends</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">user1</span><span class="p">)</span>
</span></span></code></pre></div>
<h3 id="абстрактные-классы">
  <a class="link" href="#%d0%b0%d0%b1%d1%81%d1%82%d1%80%d0%b0%d0%ba%d1%82%d0%bd%d1%8b%d0%b5-%d0%ba%d0%bb%d0%b0%d1%81%d1%81%d1%8b">
    ##
  </a>
  Абстрактные классы
</h3>

<p>В Python у нас существует прекрасный модуль <code>collections.abc</code>. Там
уже описан ряд абстрактных классов, которые в 90% случаев закроют
все наши потребности. Они полезны, когда вы хотите описывать
поведение, а не конкретную реализацию. Итак, что же там
представлено? А вот что:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">collections</span><span class="o">.</span><span class="n">abc</span><span class="o">.</span><span class="n">ABCMeta</span>
</span></span><span class="line"><span class="cl"><span class="n">collections</span><span class="o">.</span><span class="n">abc</span><span class="o">.</span><span class="n">AsyncGenerator</span>
</span></span><span class="line"><span class="cl"><span class="n">collections</span><span class="o">.</span><span class="n">abc</span><span class="o">.</span><span class="n">AsyncIterable</span>
</span></span><span class="line"><span class="cl"><span class="n">collections</span><span class="o">.</span><span class="n">abc</span><span class="o">.</span><span class="n">AsyncIterator</span>
</span></span><span class="line"><span class="cl"><span class="n">collections</span><span class="o">.</span><span class="n">abc</span><span class="o">.</span><span class="n">Awaitable</span>
</span></span><span class="line"><span class="cl"><span class="n">collections</span><span class="o">.</span><span class="n">abc</span><span class="o">.</span><span class="n">Buffer</span>
</span></span><span class="line"><span class="cl"><span class="n">collections</span><span class="o">.</span><span class="n">abc</span><span class="o">.</span><span class="n">ByteString</span>
</span></span><span class="line"><span class="cl"><span class="n">collections</span><span class="o">.</span><span class="n">abc</span><span class="o">.</span><span class="n">Callable</span>
</span></span><span class="line"><span class="cl"><span class="n">collections</span><span class="o">.</span><span class="n">abc</span><span class="o">.</span><span class="n">Collection</span>
</span></span><span class="line"><span class="cl"><span class="n">collections</span><span class="o">.</span><span class="n">abc</span><span class="o">.</span><span class="n">Container</span>
</span></span><span class="line"><span class="cl"><span class="n">collections</span><span class="o">.</span><span class="n">abc</span><span class="o">.</span><span class="n">Coroutine</span>
</span></span><span class="line"><span class="cl"><span class="n">collections</span><span class="o">.</span><span class="n">abc</span><span class="o">.</span><span class="n">EllipsisType</span>
</span></span><span class="line"><span class="cl"><span class="n">collections</span><span class="o">.</span><span class="n">abc</span><span class="o">.</span><span class="n">FunctionType</span>
</span></span><span class="line"><span class="cl"><span class="n">collections</span><span class="o">.</span><span class="n">abc</span><span class="o">.</span><span class="n">Generator</span>
</span></span><span class="line"><span class="cl"><span class="n">collections</span><span class="o">.</span><span class="n">abc</span><span class="o">.</span><span class="n">GenericAlias</span>
</span></span><span class="line"><span class="cl"><span class="n">collections</span><span class="o">.</span><span class="n">abc</span><span class="o">.</span><span class="n">Hashable</span>
</span></span><span class="line"><span class="cl"><span class="n">collections</span><span class="o">.</span><span class="n">abc</span><span class="o">.</span><span class="n">ItemsView</span>
</span></span><span class="line"><span class="cl"><span class="n">collections</span><span class="o">.</span><span class="n">abc</span><span class="o">.</span><span class="n">Iterable</span>
</span></span><span class="line"><span class="cl"><span class="n">collections</span><span class="o">.</span><span class="n">abc</span><span class="o">.</span><span class="n">Iterator</span>
</span></span><span class="line"><span class="cl"><span class="n">collections</span><span class="o">.</span><span class="n">abc</span><span class="o">.</span><span class="n">KeysView</span>
</span></span><span class="line"><span class="cl"><span class="n">collections</span><span class="o">.</span><span class="n">abc</span><span class="o">.</span><span class="n">Mapping</span>
</span></span><span class="line"><span class="cl"><span class="n">collections</span><span class="o">.</span><span class="n">abc</span><span class="o">.</span><span class="n">MappingView</span>
</span></span><span class="line"><span class="cl"><span class="n">collections</span><span class="o">.</span><span class="n">abc</span><span class="o">.</span><span class="n">MutableMapping</span>
</span></span><span class="line"><span class="cl"><span class="n">collections</span><span class="o">.</span><span class="n">abc</span><span class="o">.</span><span class="n">MutableSequence</span>
</span></span><span class="line"><span class="cl"><span class="n">collections</span><span class="o">.</span><span class="n">abc</span><span class="o">.</span><span class="n">MutableSet</span>
</span></span><span class="line"><span class="cl"><span class="n">collections</span><span class="o">.</span><span class="n">abc</span><span class="o">.</span><span class="n">Reversible</span>
</span></span><span class="line"><span class="cl"><span class="n">collections</span><span class="o">.</span><span class="n">abc</span><span class="o">.</span><span class="n">Sequence</span>
</span></span><span class="line"><span class="cl"><span class="n">collections</span><span class="o">.</span><span class="n">abc</span><span class="o">.</span><span class="n">Set</span>
</span></span><span class="line"><span class="cl"><span class="n">collections</span><span class="o">.</span><span class="n">abc</span><span class="o">.</span><span class="n">Sized</span>
</span></span><span class="line"><span class="cl"><span class="n">collections</span><span class="o">.</span><span class="n">abc</span><span class="o">.</span><span class="n">ValuesView</span>
</span></span></code></pre></div><p>Как мы видим тут большое количество абстрактных классов.
Большинство из них созданы для описания какого-то свойства
коллекции. Аннотируя с их помощью код, мы можем выражать более
широкие полиморфные границы наших функций. Например:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">collections.abc</span> <span class="kn">import</span> <span class="n">Iterable</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">total</span><span class="p">(</span><span class="n">values</span><span class="p">:</span> <span class="n">Iterable</span><span class="p">[</span><span class="nb">int</span><span class="p">])</span> <span class="o">-&gt;</span> <span class="nb">int</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="nb">sum</span><span class="p">(</span><span class="n">values</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">total</span><span class="p">([</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">])</span>
</span></span><span class="line"><span class="cl"><span class="n">total</span><span class="p">((</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">))</span>
</span></span><span class="line"><span class="cl"><span class="n">total</span><span class="p">({</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">})</span>
</span></span></code></pre></div><p>Иногда можно встретить такие же импорты из модуля <code>typing</code>:
<code>from typing import Iterable, Sequence</code>. Но в реальности это
просто реэкспорт реализаций из <code>collections.abc</code>. Поэтому сейчас
лучше импортировать абстрактные классы напрямую из
<code>collections.abc</code>.</p>

<h3 id="sequence-и-iterable">
  <a class="link" href="#sequence-%d0%b8-iterable">
    ##
  </a>
  Sequence и Iterable
</h3>

<p>Эти два типа часто путают. <code>Iterable</code> гарантирует только то, что
объект можно перебирать в цикле. Никаких индексов, длины или
упорядоченности тут не обещается. <code>Sequence</code> же, кроме
возможности итерации, гарантирует наличие индексации и длины,
то есть <code>__getitem__</code> и <code>__len__</code>. Из этого вытекает разница
в том, что можно безопасно делать с объектом.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">collections.abc</span> <span class="kn">import</span> <span class="n">Iterable</span><span class="p">,</span> <span class="n">Sequence</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">sum_any</span><span class="p">(</span><span class="n">values</span><span class="p">:</span> <span class="n">Iterable</span><span class="p">[</span><span class="nb">int</span><span class="p">])</span> <span class="o">-&gt;</span> <span class="nb">int</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">total</span> <span class="o">=</span> <span class="mi">0</span>
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="n">v</span> <span class="ow">in</span> <span class="n">values</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">total</span> <span class="o">+=</span> <span class="n">v</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">total</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">head</span><span class="p">(</span><span class="n">values</span><span class="p">:</span> <span class="n">Sequence</span><span class="p">[</span><span class="nb">int</span><span class="p">])</span> <span class="o">-&gt;</span> <span class="nb">int</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">values</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span>
</span></span></code></pre></div><p>Функция <code>sum_any</code> примет и список, и кортеж, и генератор.
Функция <code>head</code> уже не сможет принять генератор, потому что у него
нет индексации, и мы не можем написать <code>values[0]</code>. Поэтому,
когда вам важно только итерироваться — используйте <code>Iterable</code>,
а если вы опираетесь на индексацию или длину — <code>Sequence</code>.</p>

<h3 id="callable">
  <a class="link" href="#callable">
    ##
  </a>
  Callable
</h3>

<p><code>Callable</code> используется, когда функция принимает другую функцию.
Это особенно важно для коллбеков, обработчиков событий и функций
высшего порядка.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">collections.abc</span> <span class="kn">import</span> <span class="n">Callable</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">apply</span><span class="p">(</span><span class="n">values</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="nb">int</span><span class="p">],</span> <span class="n">fn</span><span class="p">:</span> <span class="n">Callable</span><span class="p">[[</span><span class="nb">int</span><span class="p">],</span> <span class="nb">int</span><span class="p">])</span> <span class="o">-&gt;</span> <span class="nb">list</span><span class="p">[</span><span class="nb">int</span><span class="p">]:</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="p">[</span><span class="n">fn</span><span class="p">(</span><span class="n">v</span><span class="p">)</span> <span class="k">for</span> <span class="n">v</span> <span class="ow">in</span> <span class="n">values</span><span class="p">]</span>
</span></span></code></pre></div><p>Если сигнатура заранее неизвестна, можно использовать
<code>Callable[..., ReturnType]</code>, но это стоит делать как крайний
вариант.</p>

<h3 id="дженерики-generics">
  <a class="link" href="#%d0%b4%d0%b6%d0%b5%d0%bd%d0%b5%d1%80%d0%b8%d0%ba%d0%b8-generics">
    ##
  </a>
  Дженерики (generics)
</h3>

<p>Generics — это параметризованные обобщённые типы. Проще говоря,
это типы, которые сами принимают типы. Это важно, когда вы хотите
сохранить связь между входом и выходом, а не потерять её в <code>Any</code>.
Например, функция <code>first</code> возвращает элемент того же типа, что и
внутри переданной коллекции:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">collections.abc</span> <span class="kn">import</span> <span class="n">Sequence</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">typing</span> <span class="kn">import</span> <span class="n">TypeVar</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">T</span> <span class="o">=</span> <span class="n">TypeVar</span><span class="p">(</span><span class="s2">&#34;T&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">first</span><span class="p">(</span><span class="n">items</span><span class="p">:</span> <span class="n">Sequence</span><span class="p">[</span><span class="n">T</span><span class="p">])</span> <span class="o">-&gt;</span> <span class="n">T</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">items</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span>
</span></span></code></pre></div><p>Без дженериков нам пришлось бы писать <code>Sequence[Any]</code> и терять
тип результата. А с дженериками анализатор знает, что если мы
передали <code>Sequence[str]</code>, то вернётся <code>str</code>. Это особенно важно
для коллекций, фабрик и репозиториев, где один и тот же код
обрабатывает разные типы.</p>
<p>Также полезно знать, что у <code>TypeVar</code> есть параметр <code>bound</code>,
который позволяет ограничить те типы, которые могут попадать
в дженерик:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">collections.abc</span> <span class="kn">import</span> <span class="n">Hashable</span><span class="p">,</span> <span class="n">Iterable</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">typing</span> <span class="kn">import</span> <span class="n">TypeVar</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">HashableT</span> <span class="o">=</span> <span class="n">TypeVar</span><span class="p">(</span><span class="s2">&#34;HashableT&#34;</span><span class="p">,</span> <span class="n">bound</span><span class="o">=</span><span class="n">Hashable</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">mode</span><span class="p">(</span><span class="n">data</span><span class="p">:</span> <span class="n">Iterable</span><span class="p">[</span><span class="n">HashableT</span><span class="p">])</span> <span class="o">-&gt;</span> <span class="n">HashableT</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="o">...</span>
</span></span></code></pre></div>
<h3 id="literal">
  <a class="link" href="#literal">
    ##
  </a>
  Literal
</h3>

<p><code>Literal</code> позволяет зафиксировать конкретные допустимые значения,
а не просто базовый тип. Это полезно, когда у параметра есть
закрытый набор режимов, статусов или ключей.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">typing</span> <span class="kn">import</span> <span class="n">Literal</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">export_report</span><span class="p">(</span><span class="nb">format</span><span class="p">:</span> <span class="n">Literal</span><span class="p">[</span><span class="s2">&#34;csv&#34;</span><span class="p">,</span> <span class="s2">&#34;json&#34;</span><span class="p">])</span> <span class="o">-&gt;</span> <span class="nb">bytes</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="o">...</span>
</span></span></code></pre></div><p>Сигнатура выше сразу задаёт контракт: третьего формата тут нет.
Это делает API понятнее и позволяет анализатору ловить опечатки
вроде <code>&quot;jsno&quot;</code> до запуска.</p>

<h3 id="статические-анализаторы">
  <a class="link" href="#%d1%81%d1%82%d0%b0%d1%82%d0%b8%d1%87%d0%b5%d1%81%d0%ba%d0%b8%d0%b5-%d0%b0%d0%bd%d0%b0%d0%bb%d0%b8%d0%b7%d0%b0%d1%82%d0%be%d1%80%d1%8b">
    ##
  </a>
  Статические анализаторы
</h3>

<p>И наконец пришло время познакомиться с тем, кто &ldquo;оживляет&rdquo;
типизацию в Python: со статическими анализаторами. Они читают
аннотации, сопоставляют их с кодом и подсвечивают ошибки до
runtime.</p>
<ul>
<li><code>mypy</code> - классический статический анализатор типов для
постепенного внедрения типизации. Сильная сторона: экосистема
плагинов и тонкая настройка строгости по модулям. Слабая
сторона: без настройки может быть либо слишком мягким, либо
слишком шумным.</li>
<li><code>pyright</code> - быстрый и понятный чекер. Сильная сторона: хорошие
диагностические сообщения и быстрый feedback. Слабая сторона:
расширяемость через плагины хуже, чем в <code>mypy</code>.</li>
<li><code>pyrefly</code> - новый быстрый анализатор на Rust. Сильная сторона:
высокая скорость и интеграция с LSP. Слабая сторона: проект
ещё молодой, поэтому часть поведения может меняться.</li>
<li><code>ty</code> - новый Rust-инструмент от Astral (пока beta).
Сильная сторона: скорость и современная архитектура. Слабая
сторона: pre-release стадия, часть возможностей ещё догоняет
зрелые чекеры.</li>
</ul>
<p>Установить и запускать их можно через <code>uv</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">uv tool install mypy
</span></span><span class="line"><span class="cl">uv tool install pyright
</span></span><span class="line"><span class="cl">uv tool install pyrefly
</span></span><span class="line"><span class="cl">uv tool install ty
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">uvx mypy .
</span></span><span class="line"><span class="cl">uvx pyright .
</span></span><span class="line"><span class="cl">uvx pyrefly check
</span></span><span class="line"><span class="cl">uvx ty check
</span></span></code></pre></div><p>Ниже бизнес-пример с намеренными ошибками типизации:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">dataclasses</span> <span class="kn">import</span> <span class="n">dataclass</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">typing</span> <span class="kn">import</span> <span class="n">NewType</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">UserId</span> <span class="o">=</span> <span class="n">NewType</span><span class="p">(</span><span class="s2">&#34;UserId&#34;</span><span class="p">,</span> <span class="nb">int</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nd">@dataclass</span>
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">User</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="nb">id</span><span class="p">:</span> <span class="n">UserId</span>
</span></span><span class="line"><span class="cl">    <span class="n">email</span><span class="p">:</span> <span class="nb">str</span>
</span></span><span class="line"><span class="cl">    <span class="n">is_active</span><span class="p">:</span> <span class="nb">bool</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">discount</span><span class="p">(</span><span class="n">total</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">percent</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">int</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">total</span> <span class="o">-</span> <span class="n">total</span> <span class="o">*</span> <span class="p">(</span><span class="n">percent</span> <span class="o">/</span> <span class="mi">100</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">send_invoice</span><span class="p">(</span><span class="n">user</span><span class="p">:</span> <span class="n">User</span><span class="p">,</span> <span class="n">amount</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="ow">not</span> <span class="n">user</span><span class="o">.</span><span class="n">is_active</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="kc">None</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="sa">f</span><span class="s2">&#34;invoice for </span><span class="si">{</span><span class="n">user</span><span class="o">.</span><span class="n">email</span><span class="si">}</span><span class="s2">: </span><span class="si">{</span><span class="n">amount</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">main</span><span class="p">()</span> <span class="o">-&gt;</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">user</span> <span class="o">=</span> <span class="n">User</span><span class="p">(</span><span class="nb">id</span><span class="o">=</span><span class="mi">42</span><span class="p">,</span> <span class="n">email</span><span class="o">=</span><span class="mi">123</span><span class="p">,</span> <span class="n">is_active</span><span class="o">=</span><span class="s2">&#34;yes&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">total</span> <span class="o">=</span> <span class="n">discount</span><span class="p">(</span><span class="s2">&#34;1000&#34;</span><span class="p">,</span> <span class="mi">10</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">send_invoice</span><span class="p">(</span><span class="n">user</span><span class="p">,</span> <span class="s2">&#34;500&#34;</span><span class="p">)</span>
</span></span></code></pre></div><p>Проверка <code>pyright</code> даст сообщения примерно такого вида:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line"><span class="cl">error: Type &#34;float&#34; is not assignable to return type &#34;int&#34;
</span></span><span class="line"><span class="cl">error: Type &#34;None&#34; is not assignable to return type &#34;str&#34;
</span></span><span class="line"><span class="cl">error: &#34;Literal[42]&#34; is not assignable to &#34;UserId&#34;
</span></span><span class="line"><span class="cl">error: &#34;Literal[123]&#34; is not assignable to &#34;str&#34;
</span></span><span class="line"><span class="cl">error: &#34;Literal[&#39;yes&#39;]&#34; is not assignable to &#34;bool&#34;
</span></span><span class="line"><span class="cl">error: &#34;Literal[&#39;1000&#39;]&#34; is not assignable to &#34;int&#34;
</span></span><span class="line"><span class="cl">error: &#34;Literal[&#39;500&#39;]&#34; is not assignable to &#34;int&#34;
</span></span></code></pre></div><p>Что важно в этом выводе:</p>
<ul>
<li>Ошибки в <code>discount</code> и <code>send_invoice</code> показывают нарушение
контракта функции: сигнатура обещает одно, реализация делает
другое.</li>
<li>Ошибки в <code>main</code> показывают, что граничный слой (ввод/DTO) передаёт
неверные типы в доменную логику.</li>
<li>Сообщение про <code>UserId</code> демонстрирует, зачем <code>NewType</code> полезен в
бизнес-коде: <code>id</code> пользователя нельзя случайно подменить обычным
<code>int</code> без явного решения разработчика.</li>
</ul>

<h3 id="stub-файлы-pyi">
  <a class="link" href="#stub-%d1%84%d0%b0%d0%b9%d0%bb%d1%8b-pyi">
    ##
  </a>
  Stub файлы (.pyi)
</h3>

<p>Иногда хочется типизировать код, к которому нельзя или неудобно
вносить изменения. Например, это сгенерированный код, код
сторонней библиотеки или даже ваш собственный модуль, где вы не
хотите мешать типы с реализацией. Для этого существуют stub
файлы — файлы с расширением <code>.pyi</code>.</p>
<p>Файл <code>.pyi</code> содержит только типовые сигнатуры и не содержит
реализации. Статические анализаторы ищут их рядом с кодом или в
отдельных пакетах <code>types-*</code>. Пример:</p>
<p><code>calc.py</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">add</span><span class="p">(</span><span class="n">a</span><span class="p">,</span> <span class="n">b</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">a</span> <span class="o">+</span> <span class="n">b</span>
</span></span></code></pre></div><p><code>calc.pyi</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">add</span><span class="p">(</span><span class="n">a</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">b</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">int</span><span class="p">:</span> <span class="o">...</span>
</span></span></code></pre></div><p>Так вы можете поддерживать типизацию отдельно от реализации, а
иногда даже без доступа к исходникам.</p>

<h3 id="typealias">
  <a class="link" href="#typealias">
    ##
  </a>
  TypeAlias
</h3>

<p>Когда тип становится сложным, его лучше вынести в алиас, чтобы
сигнатуры были читаемыми. Это особенно полезно для бизнес-терминов
вроде <code>UserId</code>, <code>Currency</code>, <code>Payload</code>. Для этого используют
<code>TypeAlias</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">typing</span> <span class="kn">import</span> <span class="n">TypeAlias</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">UserId</span><span class="p">:</span> <span class="n">TypeAlias</span> <span class="o">=</span> <span class="nb">int</span>
</span></span><span class="line"><span class="cl"><span class="n">Payload</span><span class="p">:</span> <span class="n">TypeAlias</span> <span class="o">=</span> <span class="nb">dict</span><span class="p">[</span><span class="nb">str</span><span class="p">,</span> <span class="nb">str</span> <span class="o">|</span> <span class="nb">int</span> <span class="o">|</span> <span class="nb">float</span><span class="p">]</span>
</span></span></code></pre></div><p>Теперь можно писать:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">send</span><span class="p">(</span><span class="n">user_id</span><span class="p">:</span> <span class="n">UserId</span><span class="p">,</span> <span class="n">payload</span><span class="p">:</span> <span class="n">Payload</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="o">...</span>
</span></span></code></pre></div><p>В Python 3.12+ для того же есть синтаксис <code>type</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="nb">type</span> <span class="n">UserId</span> <span class="o">=</span> <span class="nb">int</span>
</span></span><span class="line"><span class="cl"><span class="nb">type</span> <span class="n">Payload</span> <span class="o">=</span> <span class="nb">dict</span><span class="p">[</span><span class="nb">str</span><span class="p">,</span> <span class="nb">str</span> <span class="o">|</span> <span class="nb">int</span> <span class="o">|</span> <span class="nb">float</span><span class="p">]</span>
</span></span></code></pre></div><p>Это всё тот же алиас, просто короче и читабельнее.</p>

<h3 id="typealias-vs-newtype">
  <a class="link" href="#typealias-vs-newtype">
    ##
  </a>
  TypeAlias vs NewType
</h3>

<p><code>TypeAlias</code> — это просто синоним типа, он не создаёт новый тип.
<code>NewType</code> же создаёт новый тип на уровне статической проверки,
хотя на runtime это всего лишь функция-обёртка. Это полезно,
когда у вас есть логически разные значения одного базового типа,
например <code>UserId</code> и <code>OrderId</code>.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">typing</span> <span class="kn">import</span> <span class="n">NewType</span><span class="p">,</span> <span class="n">TypeAlias</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">UserId</span><span class="p">:</span> <span class="n">TypeAlias</span> <span class="o">=</span> <span class="nb">int</span>
</span></span><span class="line"><span class="cl"><span class="n">OrderId</span> <span class="o">=</span> <span class="n">NewType</span><span class="p">(</span><span class="s2">&#34;OrderId&#34;</span><span class="p">,</span> <span class="nb">int</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">get_user</span><span class="p">(</span><span class="n">user_id</span><span class="p">:</span> <span class="n">UserId</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="o">...</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">get_order</span><span class="p">(</span><span class="n">order_id</span><span class="p">:</span> <span class="n">OrderId</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="o">...</span>
</span></span></code></pre></div><p><code>UserId</code> и <code>int</code> считаются одним и тем же типом, а вот <code>OrderId</code>
уже не совместим с <code>int</code> без явного приведения.</p>

<h2 id="как-правильно-использовать-типизацию">
  <a class="link" href="#%d0%ba%d0%b0%d0%ba-%d0%bf%d1%80%d0%b0%d0%b2%d0%b8%d0%bb%d1%8c%d0%bd%d0%be-%d0%b8%d1%81%d0%bf%d0%be%d0%bb%d1%8c%d0%b7%d0%be%d0%b2%d0%b0%d1%82%d1%8c-%d1%82%d0%b8%d0%bf%d0%b8%d0%b7%d0%b0%d1%86%d0%b8%d1%8e">
    #
  </a>
  Как правильно использовать типизацию
</h2>

<p>Сразу хочу сформировать у вас верную предпосылку относительно
типизации. Точно так же как и у тестов, задача у типов
<strong>падать</strong>, не проходить, крашиться и бесить этим нас. Если они
этого не делают, их можно просто выкинуть. Это означает, что чем
менее снисходительна к нам система типизации, тем лучше для нас.
Потому что это заставляет нас думать о том, как писать надёжный
и безопасный код. Строгость системы типизации (<em>как и у тестов</em>)
помогает выявлять ошибки, но для этого они должны <strong>не
проходить.</strong></p>
<p>И да, писать действительно корректно типизированный код сложно.
Это отдельный навык, который развивается так же, как архитектура
или тестирование. Поэтому нормально начинать с малого и
постепенно усиливать строгость.</p>

<h3 id="возвращение-none">
  <a class="link" href="#%d0%b2%d0%be%d0%b7%d0%b2%d1%80%d0%b0%d1%89%d0%b5%d0%bd%d0%b8%d0%b5-none">
    ##
  </a>
  Возвращение <code>None</code>
</h3>

<p>Также обратите внимание на случай, когда функция ничего не
возвращает:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">print_weather</span><span class="p">(</span><span class="n">weather</span><span class="p">:</span> <span class="n">Weather</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;Weather:&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="n">date</span><span class="p">,</span> <span class="n">data</span> <span class="ow">in</span> <span class="n">weather</span><span class="o">.</span><span class="n">by_days</span><span class="p">()</span><span class="o">.</span><span class="n">items</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="n">date</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;</span><span class="se">\t</span><span class="si">{</span><span class="n">data</span><span class="o">.</span><span class="n">temperature</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;</span><span class="se">\t</span><span class="si">{</span><span class="n">data</span><span class="o">.</span><span class="n">humidity</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;</span><span class="se">\t</span><span class="si">{</span><span class="n">data</span><span class="o">.</span><span class="n">wind_speed</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;========&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">tmp</span> <span class="o">=</span> <span class="n">print_weather</span><span class="p">(</span><span class="n">Weather</span><span class="p">())</span> <span class="o">+</span> <span class="mi">1</span>
</span></span></code></pre></div><p>Как известно в Python, если в функции не стоит <code>return</code>, значит
она ничего не возвращает. Об этом знают и статические анализаторы.
Например, <code>pyright</code> для этого кода, где намеренно в последней
строчке допущена ошибка, даст следующее предупреждение:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line"><span class="cl">error:
</span></span><span class="line"><span class="cl">    Operator &#34;+&#34; not supported for types &#34;None&#34; and &#34;Literal[1]&#34;
</span></span></code></pre></div><p>По этому сообщению становится ясно, что <code>pyright</code> &ldquo;понимает&rdquo;, что
возвращаемый тип функции <code>None</code>. Так значит можно их не
подписывать? Нет, не значит. Во-первых, тут можно просто
сослаться на <a href="https://peps.python.org/pep-0020/">PEP 20</a>, &ldquo;явное
лучше неявного&rdquo;. Во-вторых, стоит учесть специфику
developer experience (DX) при разработке на Python. Все, кто пишут на
Python, знают, что типизация опциональна. И это создаёт
двусмысленность: когда я смотрю на сигнатуру функции, где не
специфицировано возвращаемое значение, я не понимаю, это автор
кода просто не стал её типизировать и на самом деле она возвращает
<strong>не</strong> <code>None</code>, либо там действительно возвращается <code>None</code>.
Разрешается эта двусмысленность только погружением в детали
реализации функции, что усложняет работу с кодом.</p>

<h3 id="вход-и-выход-у-функции">
  <a class="link" href="#%d0%b2%d1%85%d0%be%d0%b4-%d0%b8-%d0%b2%d1%8b%d1%85%d0%be%d0%b4-%d1%83-%d1%84%d1%83%d0%bd%d0%ba%d1%86%d0%b8%d0%b8">
    ##
  </a>
  Вход и выход у функции
</h3>

<p>Продолжим тему возвращаемых значений. Не стоит прибегать
к использованию абстрактных классов из модуля <code>collections.abc</code>,
а также реализованных самостоятельно, для указания возвращаемого
значения из функции. Они предназначены для входных значений,
чтобы сохранить полиморфность функции в максимально широких
границах. Возвращаемое значение должно быть специфицировано
конкретной реализацией, чтобы было <strong>ясно</strong>, как обрабатывать
возвращаемый результат.</p>





<blockquote class="markdown-alert markdown-alert--important">
  <div class="markdown-alert__title">
    
      <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M0 1.75C0 .784.784 0 1.75 0h12.5C15.216 0 16 .784 16 1.75v9.5A1.75 1.75 0 0 1 14.25 13H8.06l-2.573 2.573A1.458 1.458 0 0 1 3 14.543V13H1.75A1.75 1.75 0 0 1 0 11.25Zm1.75-.25a.25.25 0 0 0-.25.25v9.5c0 .138.112.25.25.25h2a.75.75 0 0 1 .75.75v2.19l2.72-2.72a.749.749 0 0 1 .53-.22h6.5a.25.25 0 0 0 .25-.25v-9.5a.25.25 0 0 0-.25-.25Zm7 2.25v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 9a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"/></svg>
    

    
      Important
    
  </div>

  <p>Функция должна более ясно говорить о том, какой конкретный тип
она возвращает, чем принимает.</p>
</blockquote>
<br>
<hr>
<br>
<p>Если вы хотите углубиться в тему, ниже — отличные материалы,
которые помогут настроить инструменты и разобраться в деталях
типовой системы Python.</p>
<ul>
<li><a href="https://to.digital/typed-python/intro/intro.html">Типизированный Python для профессиональной
разработчики</a></li>
<li><a href="https://fastapi.tiangolo.com/python-types/#type-hints-with-metadata-annotations">FastAPI type hints guide (Полезно для
web-разработчиков)</a></li>
<li><a href="https://realpython.com/python-type-checking/">RealPython, Type Checking
Guide</a></li>
<li><a href="https://python-type-challenges.zeabur.app/">Упражнения</a></li>
</ul>
]]></content:encoded><category>python</category><category>eosp</category></item><item><title>Как внести свой вклад</title><link>https://alchemmist.xyz/ru/articles/how-we-contribute/</link><pubDate>Sat, 07 Feb 2026 21:25:00 +0300</pubDate><dc:creator>alchemmist</dc:creator><guid>https://alchemmist.xyz/ru/articles/how-we-contribute/</guid><description>Это краткое руководство для начинающих, о том, как работать с открытым исходным кодом в рамках практик GitHub flow. Все примеры будут с платформой GitHub, но можно использовать и другие хостинги репозиториев, которые предоставляют необходимый функционал для реализации GitHub flow. Здесь вы можете найти классический способ внести свой вклад почти в любой проект. А так же узнать несколько советов и хаков.
Итак, с чего же начинается любой вклад. В идеале, с проблемы. Проблемой можно называть что угодно, в том числе нехватка каких-то новых фич, отсутствие либо неполнота страниц документации по определённому вопросу, какой-то вопрос, либо же банальные баги, которые есть везде. Лично вы проблему ни с чем не спутаете, ведь она обязательно будет сопровождать зудящим чувством, что “что-то подбешивает.” Но будет ли это проблемой для других пользователей и разработчиков? Для того, чтобы это понять и синхронизировать ваше представление с представлениями команды, и существует issue.</description><content:encoded><![CDATA[<p>Это краткое руководство для начинающих, о том, как работать
с открытым исходным кодом в рамках практик GitHub flow. Все
примеры будут с платформой GitHub, но можно использовать и другие
хостинги репозиториев, которые предоставляют необходимый
функционал для реализации GitHub flow. Здесь вы можете найти
классический способ внести свой вклад почти в любой проект. А так
же узнать несколько советов и хаков.</p>
<p>Итак, с чего же начинается любой вклад. В идеале, с проблемы.
Проблемой можно называть что угодно, в том числе нехватка
каких-то новых фич, отсутствие либо неполнота страниц
документации по определённому вопросу, какой-то вопрос, либо же
банальные баги, которые есть везде. Лично вы проблему ни с чем не
спутаете, ведь она обязательно будет сопровождать зудящим
чувством, что &ldquo;что-то подбешивает.&rdquo; Но будет ли это проблемой для
других пользователей и разработчиков? Для того, чтобы это понять
и синхронизировать ваше представление с представлениями команды,
и существует issue.</p>





<blockquote class="markdown-alert markdown-alert--caution">
  <div class="markdown-alert__title">
    
      <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M4.47.22A.749.749 0 0 1 5 0h6c.199 0 .389.079.53.22l4.25 4.25c.141.14.22.331.22.53v6a.749.749 0 0 1-.22.53l-4.25 4.25A.749.749 0 0 1 11 16H5a.749.749 0 0 1-.53-.22L.22 11.53A.749.749 0 0 1 0 11V5c0-.199.079-.389.22-.53Zm.84 1.28L1.5 5.31v5.38l3.81 3.81h5.38l3.81-3.81V5.31L10.69 1.5ZM8 4a.75.75 0 0 1 .75.75v3.5a.75.75 0 0 1-1.5 0v-3.5A.75.75 0 0 1 8 4Zm0 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"/></svg>
    

    
      CONTRIBUTION.md
    
  </div>

  <p>В каждом проекте есть свои собственные устоявшиеся правила
того, как нужно в них участвовать. Эти правило описываются либо
в <code>README.md</code> в отдельном параграфе &ldquo;Contribution&rdquo;, либо
в специальном файле <code>CONTRIBUTION.md</code>. Обязательно прочитайте
его в самом начале. Этот файл имеет первый приоритет. То есть
даже если вы понимаете, что какие-то практики отходят от той
правильной схемы, которую мы рассмотрим ниже, всё равно
следуйте им. Некоторые крупные проекты, такие как <code>Postgres</code>
или <code>linux-kernel</code> вообще до сих пор работает по mail-листам.
Следует уважать те традиции, которые сложились в репозитории,
даже если сейчас это выглядит как legacy.</p>
</blockquote>

<h2 id="1-issue">
  <a class="link" href="#1-issue">
    #
  </a>
  1. Issue
</h2>

<p>Issue представляет из себя тикет, который хранится в репозитории.
Создать его может любой человек, пришедший в репозиторий. Но
я рекомендую вам, перед тем как бросаться описывать, вашу
проблему, сделать несколько попыток по поиску подобных issue,
чтобы не нагружать лишней работой мейнтейнеров репозитория.
Давайте посмотрим как это можно сделать:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">gh issue list --search <span class="s2">&#34;app crashed&#34;</span>
</span></span></code></pre></div><blockquote class="markdown-blockquote">
  <p>Этот и все дальнейшие примеры будут с использованием
официальной утилиты <code>gh</code>. Она позволяет из консоли выполнять
все базовые операции с GitHub. Рекомендую использовать её для
простых операций, для ускорения работы.</p>

</blockquote>
<p>Параметр <code>search</code> позволяет задавать прям в себе добавлять
различные фильтры поиска. Давайте попробуем их добавить:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">gh issue list --search <span class="s2">&#34;app crashed is:open label:bug created:&gt;2024-01-01&#34;</span>
</span></span></code></pre></div><p>Вполне возможно, особенно в крупных проектах, что это позволит
вам выйти на уже заведённый тикет по вашей проблеме и даже найти
в нём решение или ответ. Если же нет, то не стесняйтесь
открывать новый issue. Наилучшим образом его структурировать
поможет схема:</p>
<pre class="mermaid">
  graph LR
    A[Excepted] ---&gt; B(&#34;Action (run/click/etc)&#34;)
    B ---&gt; C[Got]
</pre>

<p>В крупных проектах существуют issue templates, которые помогут
вам оформить issue в соответствии с теми стандартными, которые
приняты в этом проекте. Создать issue можно командой:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">gh issue create
</span></span></code></pre></div><p>Так же отличной практикой является предоставление docker образа,
в котором настроено все окружение, и проблема гарантированно
воспроизводима. Либо хотя бы предоставить лог. Профессиональным
подходом так же будет локализация проблемы, настолько насколько
это возможно. Например, вы получили ошибку в таком файле:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">fruits</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">logistic</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">a</span> <span class="o">=</span> <span class="n">fruits</span><span class="o">.</span><span class="n">Apple</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="n">basket</span> <span class="o">=</span> <span class="n">fruits</span><span class="o">.</span><span class="n">MakeBasket</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="n">basket</span><span class="o">.</span><span class="n">put</span><span class="p">(</span><span class="n">a</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">logistic</span><span class="o">.</span><span class="n">send</span><span class="p">(</span><span class="n">basket</span><span class="p">)</span>
</span></span></code></pre></div><p>Конечно, можно было бы так в issue и написать. Но еще лучше будет
самостоятельно поэкспериментировать с этим кодом и понять, что
именно не так. Например проверить удалось ли добавить яблоко
в корзину:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">basket</span><span class="o">.</span><span class="n">put</span><span class="p">(</span><span class="n">a</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="n">basket</span><span class="o">.</span><span class="n">content</span><span class="p">())</span>
</span></span></code></pre></div><p>Возможно проблема в том, что мы пытаемся отправить пустую
корзину, что будет означать баг в методе <code>put</code>. Либо же все-таки
проблема в методе <code>send</code>. Таким образом, мы срезаем лишнее,
оставляя в сообщении issue суть проблемы.</p>
<p>Помните, о том, что даже факт заведения успешного issue,
например, предложения ценной фичи или обнаружение реального бага
или уязвимости — это уже вклад в проект, и это уже успех.
А неуспехом будет создание дублирующего issue или перегрузка
мейнтейнеров лишним контекстом.</p>

<h2 id="2-fork">
  <a class="link" href="#2-fork">
    #
  </a>
  2. Fork
</h2>

<p>Когда вы поняли какую проблемы вы решаете и убедились, что это
реально проблема, можно переходить к её решению. Для этого стоит
для начала сделать fork репозитория. Это будет, как ценным
подспорьем для вас если вы, например, будете тестировать CI или
просто сильно опережать оригинальный репозиторий, так и позволит
отправлять pull request-ы, ведь вы, скорее всего, не имеете прав
для внесения изменений в оригинальный репозиторий:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">gh repo fork &lt;OWNER&gt;/&lt;REPO&gt;
</span></span></code></pre></div><p>Клонировав fork локально вносите изменения в отдельной ветке,
которую будет удобнее назвать номером issue, который вы пытаетесь
закрыть. Почему? Потому что в коротком лаконичном названии ветки
<strong>далеко</strong> не всегда удастся выразить суть проблемы, на которую,
возможно, ушло несколько месяцев обсуждения в тиките. Поэтому
гораздо удобнее просто на него сослаться, через номер issue:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">git switch -c <span class="m">42</span>
</span></span></code></pre></div><p>В GitHub flow мы не создаем сложную структуру из веток, где
у каждой есть свое значение. Вместо этого у нас есть master-ветка
в которую попадают только прошедшие все проверки изменения.
А попадают они туда из множества, так называемых feature-веток,
которые мы предлагаем тут привязывать к конкретным issue. А раз
в какое-то время мы на master ветке выпускаем релиз. В итоге
получается вполне лакончиная структура:</p>
<pre class="mermaid">
  %%{init: {
  &#34;gitGraph&#34;: {
    &#34;tagLabelColor&#34;: &#34;#ffffff&#34;,
    &#34;tagLabelBackground&#34;: &#34;#d73a49&#34;
  }
}}%%
gitGraph
    commit
    commit
    branch &#34;42&#34;
    checkout &#34;42&#34;
    commit
    commit
    checkout main
    merge &#34;42&#34;
    commit tag: &#34;🚩 v1.1.23&#34;

    branch &#34;44&#34;
    checkout &#34;44&#34;
    commit
    commit
    commit
    checkout main
    merge &#34;44&#34;
    commit tag: &#34;🚩 v1.2.0&#34;
</pre>

<p>Так же не забывайте синхронизировать ваш <code>fork</code>, чтобы не
потерять связь с оригинальным проектом:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">gh repo sync &lt;your-username&gt;/&lt;fork-repo&gt; --branch main
</span></span></code></pre></div>
<h2 id="3-pull-request">
  <a class="link" href="#3-pull-request">
    #
  </a>
  3. Pull request
</h2>

<p>Когда изменения внесены и закоммичены, то можно отправлять pull
request. Удобнее всего просто остаться на вашей ветке и выполнить
команду ниже. <code>gh</code> сам сформирует подходящий pull request:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">gh pr create
</span></span></code></pre></div><p>После создания PR могут автоматически запуститься проверки, если
в репозитории настроен CI. И это отлично. Потому что проверки
упрощают и ускоряют процесс ревью. Вы сразу можете увидеть, что
в вашем коде например не проходят проверки стилей, или, что вы не
написали тесты на свой код и из-за этого упал code coverage. Не
игнорируйте это, а делайте новой коммиты в той же ветки и после
пуша они автоматически добавятся в PR и проверки будут
перезапущены. Мейнтейнер репозитория даже не начнет ревью вашего
PR, пока все проверки в нем не станут зелёными. Проверить статус
pull request-а вы можете командой:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">gh pr status
</span></span></code></pre></div><p>Так же старайтесь сделать аккуратную историю коммитов в pull
request-е, чтобы упросить процесс review. В этом вам помогут
<a href="https://www.geeksforgeeks.org/git/git-squash/">squash</a> коммиты.</p>
<p>Когда вы прорвались через все авто-проверки, не стесняйтесь
обращаться к архитектору репозитория и просить его ревью. Он либо
сам сделает ревью вашего PR, либо делегирует это кому-то из
команды. На GitHub это делается просто через тег: <code>@username</code>.
Часто уведомления о том, что пришёл новый pull request могут быть
отключены, и тем более архитектор не узнает, когда вы внесете все
правки, чтобы пройти CI. Поэтому скромность здесь может привести
к тому, что ваш pull request просто никто не увидит. Тоже,
кстати, касается и issue.</p>
<p>Если вы понимаете, что ваш pull request полностью закрывает тот
issue, который вы решали, то в сообщениях PR можно указать <code>Close \#49</code>, тогда issue будет автоматически закрыт после слияния pull
request-а.</p>

<h2 id="4-merge">
  <a class="link" href="#4-merge">
    #
  </a>
  4. Merge
</h2>

<p>Итак, когда вы прошли все авто-проверки и ручное ревью, то ваш
код попадет в master-ветку репозитория и вы можете считать себя
полноправным контрибьютором в этот проект. Так держать!</p>
<p>Обратите так же внимание на интересную практику, которая, на мой
взгляд, очень профессиональна. Представим что вы обнаружили
и доложили о каком-то баге в issue. Это уже вклад в репозиторий.
Дальше предлагается создать pull request, в котором вы пишете
тест, который воспроизводит этот баг. Тест конечно-же будет
падать. Но вы присылать pull request, внося тем самым этот тест
в состоянии disabled в кодовую базу. Так же можно оставить <code>TODO</code>
метку. И это станет еще большим вкладом в репозиторий, поскольку
вы добавили содержательный тест, а этом всегда очень полезно.
И наконец вторым pull request-ом вы уже присылаете изменения,
которые чинят код так, чтобы он проходил этот тест. И вновь
увеличиваете ваш вклад. Таким образом вы оставили после себя
массу артефактов в репозитории: issue, усиленная система тестов,
и исправленный баг. Это будет отличным примером реализации TDD
(Test Driven Development) подхода.</p>
<br>
<hr>
<br>
<p>Таким образом мы с вами прошли полный цикл от проблемы до вклада
в master-ветку:</p>
<pre class="mermaid">
  flowchart LR
    BUG[&#34;Find a bug / Need feature&#34;] --&gt; ISSUE[&#34;Create Issue&#34;]
    ISSUE --&gt; DISCUSS[&#34;Discussion / Planning&#34;]
    DISCUSS --&gt; PR[&#34;Open Pull Request&#34;]
    PR --&gt; CI[&#34;PR Checks with CI&#34;]
    CI --&gt; REVIEW[&#34;Reviewer Reviews PR&#34;]
    REVIEW --&gt; MERGE[&#34;Merge PR into main&#34;]
    MERGE --&gt; BUG
</pre>

<p>Вы можете проверить свои силы уже сейчас, найдя подходящий issue
с помощью этих прекрасных сервисов:</p>
<ul>
<li><a href="https://goodfirstissue.dev/">Good First Issue</a> — Issue,
которые отлично подходят для новичков.</li>
<li><a href="https://github.com/MunGell/awesome-for-beginners">Awesome for
Beginners</a>
— Подборка проектов для начинающих.</li>
<li><a href="https://up-for-grabs.net/#/">Up For Grabs</a> — Открытые для
решения задачи по разным технологиям.</li>
<li><a href="https://github.com/firstcontributions/first-contributions">First
Contributions</a>
— Репозиторий с манулом с списком открытых проектов.</li>
<li><a href="https://www.firsttimersonly.com/">First Timers Only</a> — Больше
подобных ресурсов.</li>
</ul>
<p>Не забывайте, что вы и сами можете регистрировать свои проекты на
этих ресурсах, чтобы привлекать к себе новых разработчиков!</p>





<blockquote class="markdown-alert markdown-alert--note">
  <div class="markdown-alert__title">
    
      <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"/></svg>
    

    
      GitHub CLI
    
  </div>

  <p>Если вас заинтересовала продуктивная работа с GitHub из консоли
с помощью утилиты <code>gh</code>, обратите внимание на её
<a href="https://cli.github.com/manual/">документацию</a>. А так же на то,
что <code>gh</code> поддерживает расширения, ознакомиться с которыми вы
можете с помощью команды <code>gh ext browse</code> или написать своё!</p>
</blockquote>
<p>Так же можно почитать по теме:</p>
<ul>
<li><a href="https://opensource.guide/how-to-contribute/">Official GitHub
guideline</a></li>
<li><a href="https://navendu.me/posts/pull-requests-like-a-pro/">Pull Requests like a Pro, Navendu
Pottekkat</a></li>
<li><a href="https://dev.to/helloquash/10-common-mistakes-to-avoid-when-contributing-to-open-source-projects-1mna">10 Common Mistakes to Avoid When Contributing to Open Source
Projects</a></li>
<li><a href="https://github-help-wanted.com/open-source/beginners-guide/">Open Source for
Beginners</a></li>
</ul>
]]></content:encoded><category>open-source</category><category>eosp</category></item></channel></rss>