<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Roman Imankulov</title>
    <description>The latest articles on DEV Community by Roman Imankulov (@imankulov).</description>
    <link>https://dev.to/imankulov</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F399526%2Fbd3af65e-bbe9-43f9-a19e-23ed890bd6a6.jpeg</url>
      <title>DEV Community: Roman Imankulov</title>
      <link>https://dev.to/imankulov</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/imankulov"/>
    <language>en</language>
    <item>
      <title>Refiner</title>
      <dc:creator>Roman Imankulov</dc:creator>
      <pubDate>Thu, 10 Aug 2023 13:04:57 +0000</pubDate>
      <link>https://dev.to/imankulov/refiner-59o4</link>
      <guid>https://dev.to/imankulov/refiner-59o4</guid>
      <description>&lt;p&gt;I created a &lt;a href="https://refiner.roman.pt"&gt;Refiner&lt;/a&gt;, an open-source project that automatically fixes grammar and stylistic errors. It can also adjust the tone and formatting.&lt;/p&gt;

&lt;p&gt;That is addictively useful. Now, pretty much all my git commit messages, comments to Jira tickets, and Slack comments pass through the wisdom of this tool. The post you are reading has also gone through a series of refinements.&lt;/p&gt;

&lt;h2&gt;
  
  
  How does it help me?
&lt;/h2&gt;

&lt;p&gt;It helps minimize any non-native speaker quirks. Sometimes, I struggle with complex grammar constructions or end up using expressions that don't quite exist. I'm sure my English writing has a noticeable Russian accent, even if I can't hear it. The refiner helps me make my writing sound more natural to native speakers.&lt;/p&gt;

&lt;p&gt;Also, it's a lifesaver when I'm tired. There are times when my brain power is low and I can't express my thoughts clearly. My writing ends up sounding clumsy. For example, after a long day of research, when I need to share my findings with colleagues. Also, when I'm tired, my text can unintentionally come across as pessimistic or aggressive. The "friendly" and "positive" refiners help me make my text more appropriate without sounding fake.&lt;/p&gt;

&lt;p&gt;It even does some of the work for me. I recently added an experimental "Finish the sentence" feature. Sometimes, we write things that are easy to predict. When I catch myself writing one of these clichés, I stop my sentence midway and add XXX to let the refiner complete it. Surprisingly, it works well for things like installation instructions and model definitions.&lt;/p&gt;

&lt;h2&gt;
  
  
  How does it work?
&lt;/h2&gt;

&lt;p&gt;The refiner is basically a wrapper around OpenAI GPT. It takes the prompt, sends the request to OpenAI, and then formats the response to show the difference.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why not use ChatGPT?
&lt;/h2&gt;

&lt;p&gt;Initially, I used ChatGPT for this task, but I encountered two issues:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I had to repeat the same prompt over and over again, with slight contextual modifications.&lt;/li&gt;
&lt;li&gt;More importantly, I had to proofread the entire text generated by GPT to make sure it didn't add any nonsensical stuff and still "sounded like me."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With Refiner, I save time by not having to type the prompt and only reviewing the highlighted parts of the text.&lt;/p&gt;

&lt;h2&gt;
  
  
  How can you use it?
&lt;/h2&gt;

&lt;p&gt;Since it's an open-source tool, you can run it yourself. The source code is available at &lt;a href="https://github.com/imankulov/refiner"&gt;https://github.com/imankulov/refiner&lt;/a&gt;. The only thing you need to pass is an OpenAI API token.&lt;/p&gt;

&lt;p&gt;I also set up a testing installation at &lt;a href="https://refiner.roman.pt/"&gt;https://refiner.roman.pt/&lt;/a&gt;, where you can test it for free without SMS and registration. At least, for now.&lt;/p&gt;

&lt;h2&gt;
  
  
  How much does it cost?
&lt;/h2&gt;

&lt;p&gt;I was initially concerned about the cost of making it free and public. How much would it be worth for me to use it regularly?&lt;/p&gt;

&lt;p&gt;I estimated the number of used tokens &lt;a href="https://platform.openai.com/tokenizer"&gt;here&lt;/a&gt;, and checked the prices &lt;a href="https://openai.com/pricing"&gt;here&lt;/a&gt;. Given this text as an example, it has roughly 600 tokens. Instructions take about 100-200 tokens depending on the number of refiners. Therefore, reformatting the text would take about 800 input tokens and 600 output tokens.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The cost of editing this text is less than a quarter of a cent.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I'm fine with running the tool without any guardrails, as long as it's just me, my friends, and anyone who stumbled upon this blog post and finds the tool helpful.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>openai</category>
      <category>nextjs</category>
    </item>
    <item>
      <title>Django Plausible Proxy</title>
      <dc:creator>Roman Imankulov</dc:creator>
      <pubDate>Wed, 27 Apr 2022 10:34:15 +0000</pubDate>
      <link>https://dev.to/imankulov/django-plausible-proxy-4f3f</link>
      <guid>https://dev.to/imankulov/django-plausible-proxy-4f3f</guid>
      <description>&lt;p&gt;I released &lt;a href="https://github.com/imankulov/django-plausible-proxy/"&gt;Django Plausible Proxy&lt;/a&gt;, a Django application to &lt;strong&gt;proxy requests&lt;/strong&gt; and &lt;strong&gt;send server-side events&lt;/strong&gt; to &lt;a href="https://plausible.io/"&gt;Plausible Analytics&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Plausible is a lightweight and open-source web analytics platform, a privacy-friendly alternative to Google Analytics. I started using it for Django side projects about a month ago and loved its minimalistic interface and "exactly what I need" type of reports.&lt;/p&gt;

&lt;p&gt;Initially, the integration code lived inside the project, but as it grew in functionality, I extracted it in a separate package that is available on &lt;a href="https://pypi.org/project/django-plausible-proxy/"&gt;PyPI&lt;/a&gt; and &lt;a href="https://github.com/imankulov/django-plausible-proxy/"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Proxying&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Proxying allows a project owner concerned about missing data to see a complete picture. See &lt;a href="https://plausible.io/docs/proxy/introduction"&gt;Adblockers and using a proxy for analytics&lt;/a&gt; for the detailed outline of the problem and solution.&lt;/p&gt;

&lt;p&gt;When installed and configured in &lt;code&gt;settings.py&lt;/code&gt; and &lt;code&gt;urls.py&lt;/code&gt;, the app proxies the HTTP requests as such:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://&amp;lt;yourdomain.com&amp;gt;/js/script.js -&amp;gt; https://plausible.io/js/script.js
https://&amp;lt;yourdomain.com&amp;gt;/api/event    -&amp;gt; https://plausible.io/api/event
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Server-side events&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Sending events from Python code lets you keep track of events that can't be recorded otherwise, such as API requests. In the code, it looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;plausible_proxy&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;send_custom_event&lt;/span&gt;
&lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="n"&gt;send_custom_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Register"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"plan"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Premium"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>django</category>
      <category>analytics</category>
      <category>python</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Less red tape on reviewing PRs with the Ship / Show / Ask strategy</title>
      <dc:creator>Roman Imankulov</dc:creator>
      <pubDate>Mon, 27 Dec 2021 15:02:24 +0000</pubDate>
      <link>https://dev.to/imankulov/less-red-tape-on-reviewing-prs-with-the-ship-show-ask-strategy-3bmd</link>
      <guid>https://dev.to/imankulov/less-red-tape-on-reviewing-prs-with-the-ship-show-ask-strategy-3bmd</guid>
      <description>&lt;p&gt;The PR model discourages quick iterations. Effectively, it kills small refactorings and documentation improvements. &lt;/p&gt;

&lt;p&gt;When PR approvals are obligatory, I build hierarchies of PRs or isolate refactorings in separate commits. Still, I feel it's too complicated, and often I choose not to do anything.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.rouanw.com/"&gt;Rouan Wilsenach&lt;/a&gt; observes that not all code changes were born equal and require the same ceremony before the merge:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;"Ship" -- No PR. Small and safe changes that can go directly to the mainline.&lt;/li&gt;
&lt;li&gt;"Show" -- Open a PR and merge when automated tests pass. Then notify others to share your knowledge or ask for improvement advice. Btw, @gitlab has a magic checkbox "Merge when pipeline succeeds".&lt;/li&gt;
&lt;li&gt;"Ask" -- Open a PR and wait for the review. It works well when you're not sure you're taking the right approach and need a second opinion.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The code author has the freedom to decide in which category the code change falls, which sounds like unprecedented blasphemy in teams with trust issues.&lt;/p&gt;

&lt;p&gt;On the other hand, I worked in a team where we naturally applied this approach. We felt incredibly productive and nothing bad happened.&lt;/p&gt;

&lt;p&gt;Read more about this approach and how to make it work for your team.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://martinfowler.com/articles/ship-show-ask.html"&gt;Ship / Show / Ask&lt;/a&gt;&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>github</category>
      <category>culture</category>
      <category>teamwork</category>
    </item>
    <item>
      <title>Don't let dicts spoil your code</title>
      <dc:creator>Roman Imankulov</dc:creator>
      <pubDate>Mon, 27 Dec 2021 14:27:13 +0000</pubDate>
      <link>https://dev.to/imankulov/dont-let-dicts-spoil-your-code-35nd</link>
      <guid>https://dev.to/imankulov/dont-let-dicts-spoil-your-code-35nd</guid>
      <description>&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--IwTD7Bh---/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rkhxzazb8rd7x9mzakb8.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--IwTD7Bh---/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rkhxzazb8rd7x9mzakb8.jpg" alt="Balance" width="880" height="587"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;How often do your simple prototypes or ad-hoc scripts turn into fully-fledged applications?&lt;/p&gt;

&lt;p&gt;The simplicity of organic code growth has a flip side: it becomes too hard to maintain. The proliferation of dicts as primary data structures is a clear signal of tech debt in your code. Fortunately, modern Python provides many viable alternatives to plain dicts.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's wrong with dicts?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Dicts are opaque
&lt;/h3&gt;

&lt;p&gt;Functions that accept dicts are a nightmare to extend and modify. Usually, to change the function that takes a dictionary, you must manually trace the calls back to the roots, where this dict was created. There is often more than one call path, and if a program grows without a plan, you'll likely have discrepancies in the dict structures.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dicts are mutable
&lt;/h3&gt;

&lt;p&gt;Changing dict values to fit a specific workflow is tempting, and programmers often abuse this functionality. In-place mutations may have different names: pre-processing, populating, enriching, data massage, etc. The result is the same. This manipulation hinders the structure of your data and makes it dependent on the workflow of your application.&lt;/p&gt;

&lt;p&gt;Not only do dicts allow you to change their data, but they also allow you to change the very structure of objects. You can add or delete fields or change their types at will. Resorting to this is the worst felony you can commit to your data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Treat dicts as the wire format
&lt;/h2&gt;

&lt;p&gt;A common source of dicts in the code is deserializing from JSON. For example, from a third-party API response.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"https://api.github.com/repos/imankulov/empty"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;'id'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;297081773&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="s"&gt;'node_id'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;'MDEwOlJlcG9zaXRvcnkyOTcwODE3NzM='&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="s"&gt;'name'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;'empty'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="s"&gt;'full_name'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;'imankulov/empty'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="s"&gt;'private'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;A dict, returned from the API.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Make a habit of treating dicts as a "wire format" and convert them immediately to data structures providing semantics.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--e1qnKvsl--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vid3tv4gosdor46htz1o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--e1qnKvsl--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vid3tv4gosdor46htz1o.png" alt="Use serializer and deserializer to convert between wire format and internal representation" width="880" height="258"&gt;&lt;/a&gt;&lt;/p&gt;
Use serializer and deserializer to convert between wire format and internal representation



&lt;p&gt;The implementation is straightforward.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Define your domain models. A domain model is simply a class in your application.&lt;/li&gt;
&lt;li&gt;Fetch and deserialize in the same step.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In Domain-Driven Design (DDD), this pattern is known as the anti-corruption layer. On top of semantic clarity, domain models provide a natural layer that decouples the exterior architecture from your application's business logic.&lt;/p&gt;

&lt;p&gt;Two implementations of a function retrieving repository info from GitHub:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;❌ Returning a dict&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;requests&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_repo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repo_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="s"&gt;"""Return repository info by its name."""&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s"&gt;"https://api.github.com/repos/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;repo_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output of the function is opaque and needlessly verbose. The format is defined outside of your code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;get_repo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"imankulov/empty"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;'id'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;297081773&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="s"&gt;'node_id'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;'MDEwOlJlcG9zaXRvcnkyOTcwODE3NzM='&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="s"&gt;'name'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;'empty'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="s"&gt;'full_name'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;'imankulov/empty'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="s"&gt;'private'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="c1"&gt;# Dozens of lines with unnecessary attributes, URLs, etc.
&lt;/span&gt; &lt;span class="c1"&gt;# ...
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;✅ Returning a domain model&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;GitHubRepo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="s"&gt;"""GitHub repository."""&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;owner&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;owner&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;description&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;full_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="s"&gt;"""Get the repository full name."""&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;owner&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_repo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repo_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;GitHubRepo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="s"&gt;"""Return repository info by its name."""&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s"&gt;"https://api.github.com/repos/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;repo_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;GitHubRepo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"owner"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s"&gt;"login"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;get_repo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"imankulov/empty"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;GitHubRepo&lt;/span&gt; &lt;span class="n"&gt;at&lt;/span&gt; &lt;span class="mh"&gt;0x103023520&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;While the example below has more code, this solution is better than the previous one if we maintain and extend the codebase.&lt;/p&gt;

&lt;p&gt;Let's see what the differences are.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The data structure is clearly defined, and we can document it with as many details as necessary.&lt;/li&gt;
&lt;li&gt;The class also has a method &lt;code&gt;full_name()&lt;/code&gt; implementing some class-specific business logic. Unlike dicts, data models allow you to co-locate the code and data.&lt;/li&gt;
&lt;li&gt;GitHub API's dependency is isolated in the function &lt;code&gt;get_repo()&lt;/code&gt;. The GitHubRepo object doesn't need to know anything about the external API and how objects are created. This way, you can modify the deserializer independently from the model or add new ways of creating objects: from pytest fixtures, the GraphQL API, the local cache, etc.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;☝️ Ignore fields coming from the API if you don't need them. Keep only those that you use.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In many cases, you can and should ignore most of the fields coming from the API, adding only the fields that the application uses. Not only duplicating the fields is a waste of time, but it also makes the class structure rigid, making it hard to adopt changes in the business logic or add support to the new version of the API. From the point of view of testing, fewer fields mean fewer headaches in instantiating the objects.&lt;/p&gt;

&lt;h2&gt;
  
  
  Streamline model creation
&lt;/h2&gt;

&lt;p&gt;Wrapping dicts require creating a lot of classes. You can simplify your work by employing a library that makes "better classes" for you.&lt;/p&gt;

&lt;h3&gt;
  
  
  Consider dataclasses
&lt;/h3&gt;

&lt;p&gt;Starting from version 3.7, Python provides &lt;a href="https://docs.python.org/3/library/dataclasses.html"&gt;Data Classes&lt;/a&gt;. The &lt;code&gt;dataclasses&lt;/code&gt; module of the standard library provides a decorator and functions for automatically adding generated special methods such as &lt;code&gt;__init__()&lt;/code&gt; and &lt;code&gt;__repr__()&lt;/code&gt; to your classes. Therefore, you write less boilerplate code.&lt;/p&gt;

&lt;p&gt;I use dataclasses for small projects or scripts where I don't want to introduce extra dependencies. That's how the &lt;code&gt;GitHubRepo&lt;/code&gt; model looks like with dataclasses.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;dataclasses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;

&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;dataclass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frozen&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;GitHubRepo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="s"&gt;"""GitHub repository."""&lt;/span&gt;
    &lt;span class="n"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;full_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="s"&gt;"""Get the repository full name."""&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;owner&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When I create Data Classes, my Data Classes are almost always defined as frozen. Instead of modifying an object, I create a new instance with &lt;code&gt;dataclasses.replace()&lt;/code&gt;. Read-only attributes bring peace of mind to a developer, reading and maintaining your code.&lt;/p&gt;

&lt;h3&gt;
  
  
  ... or consider Pydantic
&lt;/h3&gt;

&lt;p&gt;Recently &lt;a href="https://pydantic-docs.helpmanual.io/"&gt;Pydantic&lt;/a&gt;, a third-party data-validation library became my go-to choice for model definition. Compared with dataclasses, they are much more powerful. I especially like their serializers and deserializers, automatic type conversions, and custom validators.&lt;/p&gt;

&lt;p&gt;Serializers simplify storing records to external storage, for example, for caching. Type conversions are especially helpful when converting a complex hierarchical JSON to a hierarchy of objects. And validators are helpful for everything else.&lt;/p&gt;

&lt;p&gt;With Pydantic, the same model can look like this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;pydantic&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BaseModel&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;GitHubRepo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="s"&gt;"""GitHub repository."""&lt;/span&gt;
    &lt;span class="n"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;

    &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;frozen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;full_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="s"&gt;"""Get the repository full name."""&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;owner&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can find some examples of me using Pydantic, in my post &lt;a href="https://roman.pt/posts/time-series-caching/"&gt;Time Series Caching with Python and Redis&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  For legacy codebase, annotate dicts as TypeDict
&lt;/h2&gt;

&lt;p&gt;Python 3.8 introduced so-called &lt;a href="https://www.python.org/dev/peps/pep-0589/"&gt;TypedDicts&lt;/a&gt;. In runtime, they behave like regular dicts but provide extra information about their structure for developers, type validators, and IDEs.&lt;/p&gt;

&lt;p&gt;If you come across the dict-heavy legacy code and can't refactor everything yet, at least you can annotate your dicts as typed ones.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;TypedDict&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;GitHubRepo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TypedDict&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="s"&gt;"""GitHub repository."""&lt;/span&gt;
    &lt;span class="n"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;

&lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;GitHubRepo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s"&gt;"owner"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"imankulov"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"empty"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"An empty repository"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Below, I provide two screenshots from PyCharm to show how adding typing information can streamline your development experience with the IDE and protect you against errors.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--s5W2u_yI--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/g23qhyc0h3idgc8t5mjd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--s5W2u_yI--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/g23qhyc0h3idgc8t5mjd.png" alt="PyCharm knows about the value type and provides autocomplete" width="650" height="196"&gt;&lt;/a&gt;&lt;/p&gt;
PyCharm knows about the value type and provides autocomplete



&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--qm5VftgG--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nhyc1o53oscs0pab7gjl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--qm5VftgG--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nhyc1o53oscs0pab7gjl.png" alt="PyCharm knows about the missing key and issues a warning" width="500" height="92"&gt;&lt;/a&gt;&lt;/p&gt;
PyCharm knows about the missing key and issues a warning



&lt;h2&gt;
  
  
  For key-value stores, annotate dicts as mappings
&lt;/h2&gt;

&lt;p&gt;A legitimate use-case of dict is a key-value store where all the values have the same type, and keys are used to look up the value by key.&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;colors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s"&gt;"red"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"#FF0000"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"pink"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"#FFC0CB"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"purple"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"#800080"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
A dict, used as a mapping.


&lt;p&gt;When instantiating or passing such dict to a function, consider hiding implementation details by annotating the variable type as Mapping or MutableMapping. On the one hand, it may sound like overkill. Dict is default and by far the most common implementation of a MutableMapping. On the other hand, by annotating a variable with mapping, you can specify the types for keys and values. Besides, in the case of a Mapping type, you send a clear message that an object is supposed to be immutable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I defined a color mapping and annotated a function. Notice how the function uses the operation allowed for dicts but disallowed for Mapping instances.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# file: colors.py
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Mapping&lt;/span&gt;

&lt;span class="n"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Mapping&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s"&gt;"red"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"#FF0000"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"pink"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"#FFC0CB"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"purple"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"#800080"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;add_yellow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Mapping&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;
    &lt;span class="n"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"yellow"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"#FFFF00"&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"__main__"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;add_yellow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Despite wrong types, no issues in runtime.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;python colors.py
&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s1"&gt;'red'&lt;/span&gt;: &lt;span class="s1"&gt;'#FF0000'&lt;/span&gt;, &lt;span class="s1"&gt;'pink'&lt;/span&gt;: &lt;span class="s1"&gt;'#FFC0CB'&lt;/span&gt;, &lt;span class="s1"&gt;'purple'&lt;/span&gt;: &lt;span class="s1"&gt;'#800080'&lt;/span&gt;, &lt;span class="s1"&gt;'yellow'&lt;/span&gt;: &lt;span class="s1"&gt;'#FFFF00'&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To check the validity, I can use mypy, which raises an error.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;mypy colors.py
colors.py:11: error: Unsupported target &lt;span class="k"&gt;for &lt;/span&gt;indexed assignment &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Mapping[str, str]"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
Found 1 error &lt;span class="k"&gt;in &lt;/span&gt;1 file &lt;span class="o"&gt;(&lt;/span&gt;checked 1 &lt;span class="nb"&gt;source &lt;/span&gt;file&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Take dicts under control
&lt;/h2&gt;

&lt;p&gt;Keep an eye on your dicts. Don't let them take control of your application. As with every piece of technical debt, the further you postpone introducing proper data structures, the more complex the transition becomes.&lt;/p&gt;

</description>
      <category>python</category>
      <category>programming</category>
    </item>
    <item>
      <title>Structure Flask project with convention over configuration</title>
      <dc:creator>Roman Imankulov</dc:creator>
      <pubDate>Wed, 05 May 2021 08:42:45 +0000</pubDate>
      <link>https://dev.to/imankulov/structure-flask-project-with-convention-over-configuration-22ff</link>
      <guid>https://dev.to/imankulov/structure-flask-project-with-convention-over-configuration-22ff</guid>
      <description>&lt;p&gt;When people ask me what a should beginner Python developer choose, Flask or Django, to start their new web project, all other things being equal, I always recommend Django.&lt;/p&gt;

&lt;p&gt;Unlike Flask,  Django is opinionated. In other words, every time you need to make a choice in Flask, Django has the answer for you. Among others, Django outlines the practical project structure out of the box.&lt;/p&gt;

&lt;p&gt;With Django, you split your project into applications. The applications themselves have a well-defined structure: views.py, models.py, all those things.  What maintains and enforces the design is the &lt;a href="https://en.wikipedia.org/wiki/Convention_over_configuration"&gt;convention over configuration&lt;/a&gt; approach: you put your code to specific well-known locations, and the framework discovers them.&lt;/p&gt;

&lt;p&gt;Flask doesn’t impose anything like that. You can start with a single file. When the application grows, it’s up to you to provide the application structure. As a result, the app can quickly turn into an unmaintainable mess.&lt;/p&gt;

&lt;p&gt;Official Flask documentation, following the non-opinionated principle, leaves it for developers to decide. Chapters &lt;a href="https://flask.palletsprojects.com/en/1.1.x/patterns/packages/"&gt;Larger Applications&lt;/a&gt; &lt;a href="https://flask.palletsprojects.com/en/1.1.x/blueprints/"&gt;Modular Applications with Blueprints&lt;/a&gt; don’t cover the topic entirely. To close the gap, people created their own guidelines. Google “flask project structure” to find a plethora of variants and suggestions. For example, there is a chapter &lt;a href="https://blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-xv-a-better-application-structure"&gt;a better application structure&lt;/a&gt; in the monumental Flask Mega-tutorial.&lt;/p&gt;

&lt;p&gt;What bugs me about any Flask solution is the lack of support for conventions. If you’re like me, you have a function &lt;code&gt;app()&lt;/code&gt; that manually imports and configures all the things from all the packages and blueprints. Looks familiar? This is dirty.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# file: myproject/app.py
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;app&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;myproject.users.controller&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;blueprint&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;users_blueprint&lt;/span&gt;
    &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;myproject.projects.controller&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;blueprint&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;projects_blueprint&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;
    &lt;span class="c1"&gt;# Initialize extensions
&lt;/span&gt;    &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;init_app&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;flask_app&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="p"&gt;...&lt;/span&gt;
    &lt;span class="c1"&gt;# Register blueprints
&lt;/span&gt;    &lt;span class="n"&gt;flask_app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;register_blueprint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;users_blueprint&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;flask_app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;register_blueprint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;projects_blueprint&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;For every extension, you call &lt;code&gt;init_app&lt;/code&gt;. For every application with a well-defined structure, you make a dance, adding a couple of lines with imports and registrations.&lt;/p&gt;

&lt;p&gt;To somehow address the issue, I created a Python package &lt;a href="https://github.com/imankulov/roman-discovery"&gt;roman-discovery&lt;/a&gt;. The package lets you declaratively define the conventions of your application and run a &lt;code&gt;discover()&lt;/code&gt; function to apply their rules. It's not specific to Flask, but I created it primarily with Flask in mind.&lt;/p&gt;

&lt;p&gt;For example, assuming that you store all the blueprints in the &lt;code&gt;myproject/&amp;lt;app&amp;gt;/controllers.py&lt;/code&gt;, that’s how you can automatically register all of them.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;flask&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Blueprint&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;roman_discovery&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ObjectRule&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;discover&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;roman_discovery.matchers&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;MatchByPattern&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;MatchByType&lt;/span&gt;

&lt;span class="n"&gt;blueprint_loader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ObjectRule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Flask blueprints loader"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;module_matches&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;MatchByPattern&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s"&gt;"myproject.*.controllers"&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
    &lt;span class="n"&gt;object_matches&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;MatchByType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Blueprint&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;object_action&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;flask_app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;register_blueprint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;discover&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;import_path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"myproject"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rules&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;blueprint_loader&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;There is a &lt;a href="https://github.com/imankulov/roman-discovery/blob/main/roman_discovery/flask.py"&gt;roman_discovery.flask&lt;/a&gt; module you can use as a source of inspiration, or, if you don’t mind applying my conventions, use it as is.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;roman_disovery.flask&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;discover_flask&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Flask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__name__&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;from_object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"myproject.config"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;discover_flask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"myproject"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The latest line will do the following.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Scan &lt;code&gt;myproject/*/controllers.py&lt;/code&gt; and &lt;code&gt;myproject/*/controllers/*.py&lt;/code&gt; to find blueprints and attach them to the Flask application.&lt;/li&gt;
&lt;li&gt;Import all files in &lt;code&gt;myproject/*/models.py&lt;/code&gt; and &lt;code&gt;myproject/*/models/*.py&lt;/code&gt; to help flask-migrate find all the SQLAlchemy models to create migrations.&lt;/li&gt;
&lt;li&gt;Scan all files in &lt;code&gt;myproject/*/cli.py&lt;/code&gt; and &lt;code&gt;myproject/*/cli/*.py&lt;/code&gt; to find &lt;code&gt;flask.cli.AppGroup&lt;/code&gt; instances and attach them to Flask’s CLI.&lt;/li&gt;
&lt;li&gt;Scan top-level &lt;code&gt;myproject/services.py&lt;/code&gt;, find all the instances that have &lt;code&gt;init_app()&lt;/code&gt; methods, and call &lt;code&gt;obj.init_app(app=app)&lt;/code&gt; for each of them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It’s still new, lacks documentation, examples, and tests, but I hope it can already become helpful for you.&lt;/p&gt;

&lt;p&gt;Follow &lt;a href="https://github.com/imankulov/roman-discovery/"&gt;roman-discovery on GitHub&lt;/a&gt;, also to know why the package has an uncommon “roman-” prefix.&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--566lAguM--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/imankulov"&gt;
        imankulov
      &lt;/a&gt; / &lt;a href="https://github.com/imankulov/deescovery"&gt;
        deescovery
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Discover packages and classes in a python project
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;h1&gt;
Deescovery&lt;/h1&gt;
&lt;h2&gt;
Micro-framework initialization problem&lt;/h2&gt;
&lt;p&gt;Micro-framework-based projects are clean while they're small. Every micro-framework codebase I've seen, has a mess in the project initialization. With time, &lt;code&gt;create_app()&lt;/code&gt; becomes filled with ad-hoc settings, imports-within-functions, and plug-in initializations.&lt;/p&gt;
&lt;p&gt;The Application Factory Pattern, proposed, for example, in the &lt;a href="https://flask.palletsprojects.com/en/2.0.x/patterns/appfactories/" rel="nofollow"&gt;official Flask documentation&lt;/a&gt;, and the &lt;a href="https://blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-xv-a-better-application-structure" rel="nofollow"&gt;Flask Mega-Tutorial&lt;/a&gt;, legitimize this approach.&lt;/p&gt;
&lt;p&gt;The nature of &lt;code&gt;create_app()&lt;/code&gt; leaves no place for the &lt;a href="https://blog.cleancoder.com/uncle-bob/2014/05/12/TheOpenClosedPrinciple.html" rel="nofollow"&gt;open-closed principle&lt;/a&gt;. We update this module every time we add a new plug-in, a new blueprint, or a new package.&lt;/p&gt;
&lt;div class="highlight highlight-source-python position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;&lt;span class="pl-c"&gt;# myproject/__ini__.py&lt;/span&gt;
&lt;span class="pl-c"&gt;#&lt;/span&gt;
&lt;span class="pl-c"&gt;# A common Flask application. The code is based on the Flask Mega-Tutorial.&lt;/span&gt;
&lt;span class="pl-k"&gt;def&lt;/span&gt; &lt;span class="pl-en"&gt;create_app&lt;/span&gt;(&lt;span class="pl-s1"&gt;config_class&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-v"&gt;Config&lt;/span&gt;):
    &lt;span class="pl-s1"&gt;app&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-v"&gt;Flask&lt;/span&gt;(&lt;span class="pl-s1"&gt;__name__&lt;/span&gt;)
    &lt;span class="pl-s1"&gt;app&lt;/span&gt;.&lt;span class="pl-s1"&gt;config&lt;/span&gt;.&lt;span class="pl-en"&gt;from_object&lt;/span&gt;(&lt;span class="pl-s1"&gt;config_class&lt;/span&gt;)

    &lt;span class="pl-s1"&gt;db&lt;/span&gt;.&lt;span class="pl-en"&gt;init_app&lt;/span&gt;(&lt;span class="pl-s1"&gt;app&lt;/span&gt;)
    &lt;span class="pl-s1"&gt;migrate&lt;/span&gt;.&lt;span class="pl-en"&gt;init_app&lt;/span&gt;(&lt;span class="pl-s1"&gt;app&lt;/span&gt;, &lt;span class="pl-s1"&gt;db&lt;/span&gt;)
    &lt;span class="pl-s1"&gt;login&lt;/span&gt;.&lt;span class="pl-en"&gt;init_app&lt;/span&gt;(&lt;span class="pl-s1"&gt;app&lt;/span&gt;)
    &lt;span class="pl-s1"&gt;mail&lt;/span&gt;.&lt;span class="pl-en"&gt;init_app&lt;/span&gt;(&lt;span class="pl-s1"&gt;app&lt;/span&gt;)
    &lt;span class="pl-s1"&gt;bootstrap&lt;/span&gt;&lt;/pre&gt;…
&lt;/div&gt;
&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/imankulov/deescovery"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;



</description>
      <category>python</category>
      <category>webdev</category>
      <category>django</category>
      <category>flask</category>
    </item>
    <item>
      <title>21 tools to document your Python project</title>
      <dc:creator>Roman Imankulov</dc:creator>
      <pubDate>Wed, 17 Mar 2021 15:53:15 +0000</pubDate>
      <link>https://dev.to/imankulov/21-tools-to-document-your-python-project-11l1</link>
      <guid>https://dev.to/imankulov/21-tools-to-document-your-python-project-11l1</guid>
      <description>&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjrl4qir62fckrenc7l85.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjrl4qir62fckrenc7l85.jpg" alt="Writing tooling is indispensable"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Overview of tools and services to document your Python web application from installation instructions to public API. How to make sure API documentation is in sync with your code. How to serve internal documentation and keep it private.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why documentation matters
&lt;/h2&gt;

&lt;p&gt;One of the best questions you can ask a team before joining it is whether it has any documentation. As for restaurants, dirty bathrooms say dirty kitchen; for software companies, poor documentation smells rusty software design and poor processes.&lt;/p&gt;

&lt;p&gt;Below, I will share what I know about the tools for creating API and project documentation. I will not touch on the questions of processes; it's a separate topic. Here, only tools and services. My default context for a project is a web project exposing an HTTP API and likely written in Python.&lt;/p&gt;

&lt;h2&gt;
  
  
  API documentation
&lt;/h2&gt;

&lt;p&gt;No matter, if the API is public or private, it needs to be documented. We have several options on the plate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dropbox Paper or Google Docs.&lt;/strong&gt; The most straightforward approach is to write all the docs manually. At Doist, for API drafts we used Dropbox Papers and shared them across the team. The advantage of this approach is that you can API-first without any code yet, iterate quickly, don't need to install anything or convert between the formats, and can quickly gather the feedback in-place.&lt;/p&gt;

&lt;p&gt;On the flip side, this approach gets unwieldy quite fast. You end up with multiple Dropbox papers laying around, with different formats, and besides, these documents get out of sync with the actual API quite fast. While convenient, I wouldn't recommend Dropbox Paper or Google Docs for anything but the draft versions of the API.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Froman.pt%2Fposts%2F21-tools-to-document-your-python-project%2Fdropbox.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Froman.pt%2Fposts%2F21-tools-to-document-your-python-project%2Fdropbox.png" alt="API draft in a Dropbox Paper"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;API draft in a Dropbox Paper&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Slate and friends&lt;/strong&gt;. The next step towards a more controlled evolution of the API documentation is a tool like &lt;a href="https://github.com/slatedocs/slate" rel="noopener noreferrer"&gt;Slate&lt;/a&gt; and the API documentation in git. It's still text that you need to write manually in Markdown, but Slate provides a structure, a slick renderer, and lets you publish your API as an independent website.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Froman.pt%2Fposts%2F21-tools-to-document-your-python-project%2Fslate.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Froman.pt%2Fposts%2F21-tools-to-document-your-python-project%2Fslate.png" alt="Todoist API documentation is made with Slate"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Todoist API documentation is made with Slate&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;One problem with Slate is that it couples the code, the UI, and your documentation in a single repository. Their way of installing the tool is cloning the entire repo and replacing their sample content with yours. There is no simple way to upgrade your application to the new version of the API. You can clone again and copy your files if your only changes are only on the documentation side, but if you found yourself tweaking the UI, the upgrade becomes more challenging.&lt;/p&gt;

&lt;p&gt;Another problem, and arguably, a much bigger one, is that it takes significant efforts and diligence to keep the documentation and the actual API in sync. If someone adds a new field to the object, they need to remember to update the documentation accordingly. With Slate, there's not much you can do beyond remembering, but later we'll see how we can address this issue by generating documentation from the code.&lt;/p&gt;

&lt;h2&gt;
  
  
  OpenAPI specification
&lt;/h2&gt;

&lt;p&gt;Who cares about WYSIWYG editors or markdown when you write YAML! Everybody loves YAML, and the OpenAPI ecosystem lets you take advantage of your passion. Let's talk more about OpenAPI and what it brings to the table.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Froman.pt%2Fposts%2F21-tools-to-document-your-python-project%2Fyaml.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Froman.pt%2Fposts%2F21-tools-to-document-your-python-project%2Fyaml.png" alt="Everybody loves yaml"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Everybody loves yaml. &lt;a href="https://sebiwi.github.io/comics/yaml/" rel="noopener noreferrer"&gt;Source&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;OpenAPI in 2021 should be considered as a standard de-facto for API specification language. The #1 reason to adhere to OpenAPI beyond pure love to YAML, is the tooling that lets you write, validate, test, mock, document your API, render it with a website, generate API specifications from code, and generate API clients from specifications.&lt;/p&gt;

&lt;h2&gt;
  
  
  OpenAPI editors
&lt;/h2&gt;

&lt;p&gt;There are two ways to write OpenAPI specifications. The most straightforward approach is to write the specification manually. Coupled with OpenAPI mocking services, it's also the fastest way to unblock your client-side developers and let them work with the API that doesn't exist yet.&lt;/p&gt;

&lt;p&gt;To get the feeling of how OpenAPI looks like, you can start with an online editor at &lt;a href="https://editor.swagger.io/" rel="noopener noreferrer"&gt;editor.swagger.io&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Froman.pt%2Fposts%2F21-tools-to-document-your-python-project%2Fswagger-editor.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Froman.pt%2Fposts%2F21-tools-to-document-your-python-project%2Fswagger-editor.png" alt="Online swagger editor"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Online swagger editor&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I wouldn't recommend using an online editor for anything serious, though. Fortunately, you can have the same functionality in your IDE or editor with an extension.&lt;/p&gt;

&lt;p&gt;When I'm in VScode, I use an &lt;a href="https://marketplace.visualstudio.com/items?itemName=42Crunch.vscode-openapi" rel="noopener noreferrer"&gt;OpenAPI Editor from 42Crunch&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Froman.pt%2Fposts%2F21-tools-to-document-your-python-project%2Fopenapi-vscode.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Froman.pt%2Fposts%2F21-tools-to-document-your-python-project%2Fopenapi-vscode.png" alt="OpenAPI editor in VSCode"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;OpenAPI editor in VSCode&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In PyCharm, I use an &lt;a href="https://plugins.jetbrains.com/plugin/14394-openapi-specifications" rel="noopener noreferrer"&gt;OpenAPI Specifications plugin&lt;/a&gt; from JetBrains. The plugin from 42Crunch is also available if you prefer it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Froman.pt%2Fposts%2F21-tools-to-document-your-python-project%2Fopenapi-pycharm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Froman.pt%2Fposts%2F21-tools-to-document-your-python-project%2Fopenapi-pycharm.png" alt="OpenAPI editor from JetBrains in PyCharm"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;OpenAPI editor from JetBrains in PyCharm&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  OpenAPI renderers
&lt;/h2&gt;

&lt;p&gt;OpenAPI renderers take your YAML specification and turn it into a website with nicely formatted documentation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/swagger-api/swagger-ui" rel="noopener noreferrer"&gt;Swagger UI&lt;/a&gt;&lt;/strong&gt; is the most popular renderer. It goes even further than generating browsable documentation and lets you play with your API right from the browser.&lt;/p&gt;

&lt;p&gt;Swagger UI is purely client-side. It runs a JavaScript code that reads the specification from a remote URL, parses it, and converts it to the interface.&lt;/p&gt;

&lt;p&gt;If you installed an IDE extension, you don't need to install anything else. All extensions come with a command opening the Swagger UI right in the editor window: that's what you've just seen in the previous screenshots. If you want to install it, though, you can find all the different options in their &lt;a href="https://github.com/swagger-api/swagger-ui/blob/master/docs/usage/installation.md" rel="noopener noreferrer"&gt;installation instructions&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you have Docker installed locally, the easiest way to get started with a separate Swagger UI is to run it with a single command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-p&lt;/span&gt; 8080:8080 swaggerapi/swagger-ui
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Swagger UI will be available at &lt;a href="http://localhost:8080" rel="noopener noreferrer"&gt;http://localhost:8080&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/Redocly/redoc" rel="noopener noreferrer"&gt;ReDoc&lt;/a&gt;&lt;/strong&gt;, an alternative to Swagger UI. It has a slick three-column interface, similar to Slate. The API looks cool, but an API playground is provided only in their paid hosted version.&lt;/p&gt;

&lt;p&gt;ReDoc supports extra fields to your attributes, so-called (vendor extensions)[&lt;a href="https://github.com/Redocly/redoc#swagger-vendor-extensions" rel="noopener noreferrer"&gt;https://github.com/Redocly/redoc#swagger-vendor-extensions&lt;/a&gt;]. These fields provide more options to control how to render the documentation. For example, you can add a key &lt;code&gt;x-codeSamples&lt;/code&gt; to your request specs and provide code samples in different programming languages. If you want something similar to Slate but generated automatically, that's probably the closest you can get.&lt;/p&gt;

&lt;p&gt;As with Swagger UI, Docker is the fastest way to run ReDoc:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-p&lt;/span&gt; 8080:80 redocly/redoc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ReDoc will be available at &lt;a href="http://localhost:8080" rel="noopener noreferrer"&gt;http://localhost:8080&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Froman.pt%2Fposts%2F21-tools-to-document-your-python-project%2Fopenapi-redoc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Froman.pt%2Fposts%2F21-tools-to-document-your-python-project%2Fopenapi-redoc.png" alt="ReDoc serving a file from localhost"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;ReDoc serving a file from localhost&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Generate OpenAPI from the code
&lt;/h2&gt;

&lt;p&gt;Some frameworks can generate OpenAPI specs for you. You can stop worrying about the divergence between your specification and the implementation. The spec is built from the code, and it's always up-to-date!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://fastapi.tiangolo.com/" rel="noopener noreferrer"&gt;FastAPI&lt;/a&gt;&lt;/strong&gt; is the framework that comes to mind first. It thinks, lives, and breathes OpenAPI and lets you express &lt;a href="https://fastapi.tiangolo.com/advanced/additional-responses/" rel="noopener noreferrer"&gt;quite advanced OpenAPI constructs&lt;/a&gt; with Python.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.django-rest-framework.org/" rel="noopener noreferrer"&gt;Django-REST-Framework&lt;/a&gt;&lt;/strong&gt; is a framework that can do everything for your API. With this saying, when using it, I found that if you don't use model serializers, it's close to impossible to write a decent API specification from the code. Eventually, I found that writing a plain YAML file from scratch was faster and easier. This way or another, here's the &lt;a href="https://www.django-rest-framework.org/api-guide/schemas/" rel="noopener noreferrer"&gt;schema API guide&lt;/a&gt;, and here's the &lt;a href="https://www.django-rest-framework.org/topics/documenting-your-api/" rel="noopener noreferrer"&gt;guideline on how to render the documentation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://apispec.readthedocs.io/en/latest/" rel="noopener noreferrer"&gt;apispec&lt;/a&gt;&lt;/strong&gt; is not a framework, but a library that provides a Pythonic interface to OpenAPI constructs. It has multiple integrations with different tools and frameworks, including Flask, Pyramid, aiohttp, and Falcon. The list of integrations is available on the &lt;a href="https://github.com/marshmallow-code/apispec/wiki/Ecosystem" rel="noopener noreferrer"&gt;ecosystem&lt;/a&gt; page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Internal documentation
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What to write&lt;/strong&gt;. While the target audience of the API documentation is client-side developers, the primary audience of the internal documentation is the developers of the service itself.&lt;/p&gt;

&lt;p&gt;There, we outline things like installation and configuration instructions, architectural choices, or guiding principles. Internal documentation can help navigate the code, deploy things to production, add a new dependency, apply a database migration, etc.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where to store.&lt;/strong&gt; I prefer to keep the internal service documentation in the same repository as the service itself in a separate directory, like &lt;code&gt;docs&lt;/code&gt;. The closer the documentation is to the code, the less the chance to get it out of sync with the implementation. Don't overthink it. Something as simple as a bunch of markdown files in the directory can work. Other developers can browse the documentation right from their editor or use the GitHub interface.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rendering the internal documentation
&lt;/h2&gt;

&lt;p&gt;The next step in streamlining the experience with the documentation is to turn this documentation into a standalone website. Here, any static site generator can help, but Python world provides a few "traditional" options that are suited the best for the documentation and integrated with other tools and services.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.sphinx-doc.org/" rel="noopener noreferrer"&gt;Sphinx&lt;/a&gt;&lt;/strong&gt; is a standard de-facto in the Python world. Powerful, but a bit on the challenging side to master. With this saying, most of the projects with extensive documentation prefer Sphinx over alternatives.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.mkdocs.org/" rel="noopener noreferrer"&gt;MkDocs&lt;/a&gt;&lt;/strong&gt; tries to make writing documentation a delightful process. Unlike Sphinx, MkDocs uses Markdown under the hood. The ecosystem of Markdown is rich and spans well beyond the scope of Python. Plugins, editors, formatters, converters, everything is at your service. MkDocs configuration is straightforward. In its simplest form, it's a single YAML file with the project name and a list of pages.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# File mkdocs.yml&lt;/span&gt;
&lt;span class="na"&gt;site_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;My Project&lt;/span&gt;
&lt;span class="na"&gt;nav&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Home&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;index.md&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;About&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;about.md&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My choice for Python project documentation is MkDocs with the &lt;a href="https://squidfunk.github.io/mkdocs-material/" rel="noopener noreferrer"&gt;mkdocs-material&lt;/a&gt; theme.&lt;/p&gt;

&lt;h2&gt;
  
  
  Other renderers
&lt;/h2&gt;

&lt;p&gt;The beauty of internal documentation is that you're not limited to doc-specific frameworks; you can use any static site generator. Many of them have themes specifically suitable for documentation websites. I provide below some examples.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://docs.getpelican.com/en/latest/" rel="noopener noreferrer"&gt;Pelican&lt;/a&gt;&lt;/strong&gt;, the #1 static site generator written in Python.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://gohugo.io/" rel="noopener noreferrer"&gt;Hugo&lt;/a&gt;&lt;/strong&gt; is a popular Golang-based framework for building sites. Hugo runs this blog. &lt;a href="https://themes.gohugo.io/tags/documentation/" rel="noopener noreferrer"&gt;Documentation templates&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://v2.docusaurus.io/" rel="noopener noreferrer"&gt;Docusaurus&lt;/a&gt;&lt;/strong&gt; is a static site generator from Facebook. Their core focus is helping to get the documentation right and well, but they made it possible to build any website, as it is a React application.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://nextjs.org/" rel="noopener noreferrer"&gt;Next.js&lt;/a&gt;&lt;/strong&gt; takes another step further away from documentation / static site generators camp. It's not quite a static site generator, but rather a fully-fledged React-based framework that happens to be able &lt;a href="https://nextjs.org/docs/advanced-features/static-html-export" rel="noopener noreferrer"&gt;to export pages to HTML&lt;/a&gt;. If you are a friend of JavaScript and React, it can be an option, but it's overkill as a starting point.&lt;/p&gt;

&lt;h2&gt;
  
  
  Serving the internal documentation
&lt;/h2&gt;

&lt;p&gt;Anything that can serve a static website, can serve your documentation. If you don't want to make your documentation public, opt-in for a solution that can make your documentation private. Below I will provide some solutions that you should fit your needs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://readthedocs.org/" rel="noopener noreferrer"&gt;Read the Docs&lt;/a&gt;&lt;/strong&gt; is the standard de-facto for serving technical documentation, especially popular among Open Source projects. It supports Sphinx and MkDocs out of the box, supports multiple versions of the documentation and localized versions. The project &lt;a href="https://readthedocs.com/" rel="noopener noreferrer"&gt;readthedocs.com&lt;/a&gt; provides commercial support and serves both public and private documentation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://docs.github.com/en/github/working-with-github-pages/getting-started-with-github-pages" rel="noopener noreferrer"&gt;GitHub pages&lt;/a&gt;&lt;/strong&gt; is the natural choice if you keep your projects and documentation on GitHub. Starting with the Pro or Team tariff plans, &lt;a href="https://docs.github.com/en/github/working-with-github-pageschanging-the-visibility-of-your-github-pages-site" rel="noopener noreferrer"&gt;you can make your pages visible to your collaborators only&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://docs.gitlab.com/ee/user/project/pages/" rel="noopener noreferrer"&gt;Gitlab pages&lt;/a&gt;&lt;/strong&gt; provides the same functionality, but unlike GitHub, private websites are available even on the free account. There is a Gitlab group &lt;a href="https://gitlab.com/pages" rel="noopener noreferrer"&gt;pages&lt;/a&gt; with examples of projects sharing documentation, including &lt;a href="https://gitlab.com/pages/mkdocs" rel="noopener noreferrer"&gt;mkdocs&lt;/a&gt;, &lt;a href="https://gitlab.com/pages/sphinx" rel="noopener noreferrer"&gt;Sphinx&lt;/a&gt;, among dozens of others.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.netlify.com/" rel="noopener noreferrer"&gt;netlify&lt;/a&gt;&lt;/strong&gt; integrates well with CI/CD workflows and is free for public sites. The pro version lets creating password-protected sites, and the business plan provides Role-based access control.&lt;/p&gt;

&lt;p&gt;In all three cases, you build your documentation within the CI/CD pipeline and build static pages to the server.&lt;/p&gt;

&lt;h2&gt;
  
  
  My stack
&lt;/h2&gt;

&lt;p&gt;To wrap up the discussion about the documentation, I will share my preferences for the stack.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;API specification format: OpenAPI.&lt;/li&gt;
&lt;li&gt;OpenAPI spec editor: PyCharm.&lt;/li&gt;
&lt;li&gt;To generate the API from the code: FastAPI.&lt;/li&gt;
&lt;li&gt;To render the API: Swagger UI.&lt;/li&gt;
&lt;li&gt;To write documentation: MkDocs.&lt;/li&gt;
&lt;li&gt;To serve documentation: nothing (read from the editor) or Read the Docs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you like it, please share it and tag me. I'm &lt;a href="https://twitter.com/rdotpy" rel="noopener noreferrer"&gt;@rdotpy&lt;/a&gt;. If you want to discuss it, drop me a line. My DMs are open.&lt;/p&gt;

</description>
      <category>python</category>
      <category>webdev</category>
      <category>documentation</category>
      <category>api</category>
    </item>
    <item>
      <title>Linter for Python architecture</title>
      <dc:creator>Roman Imankulov</dc:creator>
      <pubDate>Tue, 23 Feb 2021 07:51:05 +0000</pubDate>
      <link>https://dev.to/imankulov/linter-for-python-architecture-5hf0</link>
      <guid>https://dev.to/imankulov/linter-for-python-architecture-5hf0</guid>
      <description>&lt;p&gt;&lt;em&gt;How do you enforce architecture for your Python and Django projects other than in code reviews or guidelines?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The goal of the architecture is to organize and constrain the dependencies between the components of your project. For example, in a regular Django project, views depend on models, and not vise versa. If you have utility functions, they shouldn't depend on any specificities of your project.&lt;/p&gt;

&lt;p&gt;I am playing with &lt;a href="https://import-linter.readthedocs.io/en/stable/readme.html"&gt;import-linter&lt;/a&gt;, and it looks promising.&lt;/p&gt;

&lt;h2&gt;
  
  
  What import-linter does
&lt;/h2&gt;

&lt;p&gt;In Python projects, we define dependencies by imports. If module A imports module B, then A depends on B. The import-linter lets you specify the rules that declaratively constrain that dependency flow. In a config file, you define so-called contracts. For example, one contract can say, "in my projects, models.py must not have any imports from the views.py."&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[importlinter]&lt;/span&gt;
&lt;span class="py"&gt;root_package&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;myproject&lt;/span&gt;

&lt;span class="nn"&gt;[importlinter:contract:models_views]&lt;/span&gt;
&lt;span class="py"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;Models don't import views&lt;/span&gt;
&lt;span class="py"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;forbidden&lt;/span&gt;
&lt;span class="py"&gt;source_modules&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="err"&gt;myproject.views&lt;/span&gt;
&lt;span class="py"&gt;forbidden_modules&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="err"&gt;myproject.models&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your models.py contains something like &lt;code&gt;from . import views&lt;/code&gt;, the linter raises an error.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--yTNCziee--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tzi8iwism19f36qr54u5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--yTNCziee--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tzi8iwism19f36qr54u5.png" alt="import-linter error message" width="880" height="988"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;At the moment, there are three types of contracts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Forbidden modules: checks that one set of modules are not imported by another set of modules.&lt;/li&gt;
&lt;li&gt;Independence: checks that a set of modules do not depend on each other.&lt;/li&gt;
&lt;li&gt;Layers: enforces a layered architecture, where higher layers may depend on lower layers, but not the other way around.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you can't express your architecture with these contracts, or you find yourself writing too many similar rules, you can write yours, and it's straightforward.&lt;/p&gt;

&lt;h2&gt;
  
  
  How import-linter works
&lt;/h2&gt;

&lt;p&gt;I love the elegance of the model behind the import-linter. To analyze the project, it builds an import graph. Your contract takes it and checks if any edges violate the contact rules. Is there an edge between module X and module Y? Are there any loops? Is there a path from X to Y, etc.?&lt;/p&gt;

&lt;p&gt;Behind the scenes, it uses the library &lt;a href="https://grimp.readthedocs.io/en/stable/readme.html"&gt;grimp&lt;/a&gt; that, in its turn, is based upon &lt;a href="https://networkx.org/"&gt;NetworkX&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Kind of summary
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Things that I like:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;There is no magic in the project, and the graph-based model is both transparent and powerful.&lt;/li&gt;
&lt;li&gt;It's quite fast even on large projects.&lt;/li&gt;
&lt;li&gt;It doesn't enforce defining the architecture for the entire application. On a legacy project, you can start small.&lt;/li&gt;
&lt;li&gt;You can define your own architecture rules with a Python class, and it's straightforward.&lt;/li&gt;
&lt;li&gt;Works with pre-commit.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Things that left me confused or slightly disappointed:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It looks like a limited set of contract types makes you write different contracts for something that logically should be one contract.&lt;/li&gt;
&lt;li&gt;Documentation uses generic like "foo" or "project_one". Opinionated examples for a simple CRUD application or a typical Django or Flask project would help a lot.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I am still at the early stage of adoption but already added it to my &lt;a href="https://github.com/imankulov/cookiecutter-python-project/commit/4ea55cdc1ee07753c5e865b0305acf06de678754"&gt;Python cookiecutter&lt;/a&gt;. Let's see if it sticks.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://import-linter.readthedocs.io/en/stable/"&gt;import-linter documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/seddonym/import-linter"&gt;import-linter on GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://seddonym.me/2019/05/20/meet-import-linter/"&gt;a blog post with the announcement&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://grimp.readthedocs.io/en/stable/readme.html"&gt;grimp&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://networkx.org/"&gt;NetworkX&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>python</category>
      <category>django</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
