<?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: REVSYS</title>
    <description>The latest articles on DEV Community by REVSYS (@revsys).</description>
    <link>https://dev.to/revsys</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%2Forganization%2Fprofile_image%2F1092%2Fda346647-5833-4c96-b23f-6e7f53127004.jpg</url>
      <title>DEV Community: REVSYS</title>
      <link>https://dev.to/revsys</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/revsys"/>
    <language>en</language>
    <item>
      <title>How to Add Django Models to the Wagtail Admin</title>
      <dc:creator>Lacey Williams Henschel</dc:creator>
      <pubDate>Tue, 27 Aug 2019 15:57:14 +0000</pubDate>
      <link>https://dev.to/revsys/how-to-add-django-models-to-the-wagtail-admin-1mdi</link>
      <guid>https://dev.to/revsys/how-to-add-django-models-to-the-wagtail-admin-1mdi</guid>
      <description>&lt;h2&gt;
  
  
  Versions
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Python 3.7 &lt;/li&gt;
&lt;li&gt;Django 2.2 &lt;/li&gt;
&lt;li&gt;Wagtail 2.6 &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When working with Wagtail, you might find that you're using Wagtail Page models for some of your database models, but regular Django models for others. &lt;/p&gt;

&lt;p&gt;A built-in example of this is the Django &lt;code&gt;User&lt;/code&gt; model. When you log into the Wagtail admin, you can see the Django &lt;code&gt;User&lt;/code&gt; model in the &lt;code&gt;Settings&lt;/code&gt; submenu. The &lt;code&gt;User&lt;/code&gt; model is not a Wagtail model; it's the same &lt;code&gt;User&lt;/code&gt; model you see in a Django project that doesn't use Wagtail. Wagtail just exposes it to the Admin for you.&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%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fn4iczjc4zch9pya74ykc.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%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fn4iczjc4zch9pya74ykc.png" alt="Users menu under the Settings menu in the Wagtail admin"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We can do the same thing with our Django models: we can expose them to the Wagtail admin so we don't have to maintain two separate admin interfaces to manage our website content.  &lt;/p&gt;

&lt;p&gt;For this example, let's assume we're working with these models:&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="n"&gt;django.db&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt; 


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Pizza&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Model&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;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CharField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_length&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;toppings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ManyToManyField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Topping&lt;/span&gt;&lt;span class="sh"&gt;"&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;Topping&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Model&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;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CharField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_length&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Adding a single model
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="http://docs.wagtail.io/en/v2.6.1/reference/contrib/modeladmin/" rel="noopener noreferrer"&gt;Wagtail docs&lt;/a&gt; are pretty clear on how to accomplish this, but let's walk through the steps. &lt;/p&gt;

&lt;p&gt;First, make sure &lt;code&gt;wagtail.contrib.modeladmin&lt;/code&gt; is in your &lt;code&gt;INSTALLED_APPS&lt;/code&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="c1"&gt;# settings.py 
&lt;/span&gt;
&lt;span class="n"&gt;INSTALLED_APPS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wagtail.contrib.modeladmin&lt;/span&gt;&lt;span class="sh"&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;Next, in the same app as the model you want to expose to the Wagtail admin, add a file called &lt;code&gt;wagtail_hooks.py&lt;/code&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="c1"&gt;# wagtail_hooks.py
&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;wagtail.contrib.modeladmin.options&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ModelAdmin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;modeladmin_register&lt;/span&gt; 

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;.models&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Pizza&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PizzaAdmin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ModelAdmin&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Pizza&lt;/span&gt; 
    &lt;span class="n"&gt;menu_label&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Pizza&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;  
    &lt;span class="n"&gt;menu_icon&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pick&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; 
    &lt;span class="n"&gt;menu_order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; 
    &lt;span class="n"&gt;add_to_settings_menu&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt; 
    &lt;span class="n"&gt;exclude_from_explorer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt; 
    &lt;span class="n"&gt;list_display&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,)&lt;/span&gt;
    &lt;span class="n"&gt;list_filter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;toppings&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,)&lt;/span&gt;
    &lt;span class="n"&gt;search_fields&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,)&lt;/span&gt;


&lt;span class="nf"&gt;modeladmin_register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PizzaAdmin&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's step through these options in the &lt;code&gt;ModelAdmin&lt;/code&gt; class: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;model&lt;/code&gt;: The name of the model you're adding. &lt;/li&gt;
&lt;li&gt;
&lt;code&gt;menu_label&lt;/code&gt;: Leave this blank to use the &lt;code&gt;verbose_name_plural&lt;/code&gt; from your model. Give it a value to specify a new label for the Wagtail menu.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;menu_icon&lt;/code&gt;: Every menu item in the Wagtail admin has an icon, and you can specify the one you want to use. Here is a &lt;a href="https://thegrouchy.dev/general/2015/12/06/wagtail-streamfield-icons.html" rel="noopener noreferrer"&gt;list of the available icons&lt;/a&gt;. &lt;/li&gt;
&lt;li&gt;
&lt;code&gt;menu_order&lt;/code&gt;: What order you want this model to appear in. 000 is first, 100 is second, etc. Note: if you add multiple models to the admin, you won't get an error if two of them have the same &lt;code&gt;menu_order&lt;/code&gt;; Wagtail will just pick for you. &lt;/li&gt;
&lt;li&gt;
&lt;code&gt;add_to_settings_menu&lt;/code&gt;: Whether you want this menu item to appear in the &lt;strong&gt;Settings&lt;/strong&gt; submenu in the Wagtail admin. &lt;/li&gt;
&lt;li&gt;
&lt;code&gt;exclude_from_explorer&lt;/code&gt;: Set to True if you &lt;strong&gt;do not&lt;/strong&gt; want the explorer (the search box in the admin) to return results from this model. Set to False if you &lt;strong&gt;do&lt;/strong&gt; want the explorer to return results from this model. (It's confusing.) &lt;/li&gt;
&lt;li&gt;
&lt;code&gt;list_display&lt;/code&gt;: Same as the Django admin; list the fields you want to display on the listing page for this model in the Wagtail admin. &lt;/li&gt;
&lt;li&gt;
&lt;code&gt;list_filter&lt;/code&gt;: Same as the Django admin; supply the fields you want to use to filter in the sidebar of the Wagtail admin.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;search_fields&lt;/code&gt;: Same as the Django admin; supply the fields that you want the explorer to use to return search results. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The final step is to register the admin class. Once you've done that and started your server, you'll be able to see your model in the Wagtail admin:&lt;/p&gt;

&lt;p&gt;We can do the same thing with our Django models: we can expose them to the Wagtail admin so we don't have to maintain two separate admin interfaces to manage our website content.  &lt;/p&gt;

&lt;p&gt;For this example, let's assume we're working with these models:&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="n"&gt;django.db&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt; 


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Pizza&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Model&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;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CharField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_length&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;toppings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ManyToManyField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Topping&lt;/span&gt;&lt;span class="sh"&gt;"&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;Topping&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Model&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;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CharField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_length&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Adding a single model
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="http://docs.wagtail.io/en/v2.6.1/reference/contrib/modeladmin/" rel="noopener noreferrer"&gt;Wagtail docs&lt;/a&gt; are pretty clear on how to accomplish this, but let's walk through the steps. &lt;/p&gt;

&lt;p&gt;First, make sure &lt;code&gt;wagtail.contrib.modeladmin&lt;/code&gt; is in your &lt;code&gt;INSTALLED_APPS&lt;/code&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="c1"&gt;# settings.py 
&lt;/span&gt;
&lt;span class="n"&gt;INSTALLED_APPS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wagtail.contrib.modeladmin&lt;/span&gt;&lt;span class="sh"&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;Next, in the same app as the model you want to expose to the Wagtail admin, add a file called &lt;code&gt;wagtail_hooks.py&lt;/code&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="c1"&gt;# wagtail_hooks.py
&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;wagtail.contrib.modeladmin.options&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ModelAdmin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;modeladmin_register&lt;/span&gt; 

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;.models&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Pizza&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PizzaAdmin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ModelAdmin&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Pizza&lt;/span&gt; 
    &lt;span class="n"&gt;menu_label&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Pizza&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;  
    &lt;span class="n"&gt;menu_icon&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pick&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; 
    &lt;span class="n"&gt;menu_order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; 
    &lt;span class="n"&gt;add_to_settings_menu&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt; 
    &lt;span class="n"&gt;exclude_from_explorer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt; 
    &lt;span class="n"&gt;list_display&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,)&lt;/span&gt;
    &lt;span class="n"&gt;list_filter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;toppings&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,)&lt;/span&gt;
    &lt;span class="n"&gt;search_fields&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,)&lt;/span&gt;


&lt;span class="nf"&gt;modeladmin_register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PizzaAdmin&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's step through these options in the &lt;code&gt;ModelAdmin&lt;/code&gt; class: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;model&lt;/code&gt;: The name of the model you're adding. &lt;/li&gt;
&lt;li&gt;
&lt;code&gt;menu_label&lt;/code&gt;: Leave this blank to use the &lt;code&gt;verbose_name_plural&lt;/code&gt; from your model. Give it a value to specify a new label for the Wagtail menu.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;menu_icon&lt;/code&gt;: Every menu item in the Wagtail admin has an icon, and you can specify the one you want to use. Here is a &lt;a href="https://thegrouchy.dev/general/2015/12/06/wagtail-streamfield-icons.html" rel="noopener noreferrer"&gt;list of the available icons&lt;/a&gt;. &lt;/li&gt;
&lt;li&gt;
&lt;code&gt;menu_order&lt;/code&gt;: What order you want this model to appear in. 000 is first, 100 is second, etc. Note: if you add multiple models to the admin, you won't get an error if two of them have the same &lt;code&gt;menu_order&lt;/code&gt;; Wagtail will just pick for you. &lt;/li&gt;
&lt;li&gt;
&lt;code&gt;add_to_settings_menu&lt;/code&gt;: Whether you want this menu item to appear in the &lt;strong&gt;Settings&lt;/strong&gt; submenu in the Wagtail admin. &lt;/li&gt;
&lt;li&gt;
&lt;code&gt;exclude_from_explorer&lt;/code&gt;: Set to True if you &lt;strong&gt;do not&lt;/strong&gt; want the explorer (the search box in the admin) to return results from this model. Set to False if you &lt;strong&gt;do&lt;/strong&gt; want the explorer to return results from this model. (It's confusing.) &lt;/li&gt;
&lt;li&gt;
&lt;code&gt;list_display&lt;/code&gt;: Same as the Django admin; list the fields you want to display on the listing page for this model in the Wagtail admin. &lt;/li&gt;
&lt;li&gt;
&lt;code&gt;list_filter&lt;/code&gt;: Same as the Django admin; supply the fields you want to use to filter in the sidebar of the Wagtail admin.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;search_fields&lt;/code&gt;: Same as the Django admin; supply the fields that you want the explorer to use to return search results. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The final step is to register the admin class. Once you've done that and started your server, you'll be able to see your model in the Wagtail admin:&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%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fnnu9vamd1svpgh770bra.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%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fnnu9vamd1svpgh770bra.png" alt="Pizzas menu in sidebar in the Wagtail admin"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding related models
&lt;/h2&gt;

&lt;p&gt;In our example models, we have two models: &lt;code&gt;Pizza&lt;/code&gt; and &lt;code&gt;Toppings&lt;/code&gt;. We could manually add the &lt;code&gt;Topping&lt;/code&gt; model to the Wagtail admin and have it appear just below the &lt;code&gt;Pizza&lt;/code&gt; model. We just learned how! &lt;/p&gt;

&lt;p&gt;But it's so closely related to the &lt;code&gt;Pizza&lt;/code&gt; model that it might be nice if we were able to relate those two models together in a submenu, kind of like how &lt;strong&gt;Settings&lt;/strong&gt; is its own submenu in the admin that contains Users, Redirects, Sites, etc. &lt;/p&gt;

&lt;p&gt;Go back to &lt;code&gt;wagtail_hooks.py&lt;/code&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="c1"&gt;# wagtail_hooks.py
&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;wagtail.contrib.modeladmin.options&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;ModelAdmin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="n"&gt;ModelAdminGroup&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="n"&gt;modeladmin_register&lt;/span&gt; 
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;.models&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Pizza&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Topping&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PizzaAdmin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ModelAdmin&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
    &lt;span class="n"&gt;menu_order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;000&lt;/span&gt; 
    &lt;span class="bp"&gt;...&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ToppingAdmin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ModelAdmin&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Topping&lt;/span&gt; 
    &lt;span class="n"&gt;menu_label&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Toppings&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;  
    &lt;span class="n"&gt;menu_icon&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;edit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; 
    &lt;span class="n"&gt;menu_order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; 
    &lt;span class="n"&gt;add_to_settings_menu&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt; 
    &lt;span class="n"&gt;exclude_from_explorer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt; 
    &lt;span class="n"&gt;list_display&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,)&lt;/span&gt;
    &lt;span class="n"&gt;search_fields&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Relating our two models together starts off in the same way: we create a class that inherits from &lt;code&gt;ModelAdmin&lt;/code&gt; for each model and identify the necessary attributes like &lt;code&gt;model&lt;/code&gt; and &lt;code&gt;menu_icon&lt;/code&gt; to control things like their listing pages and search behavior. &lt;/p&gt;

&lt;p&gt;Then, we add a new class that inherits from &lt;code&gt;ModelAdminGroup&lt;/code&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="c1"&gt;# wagtail_hooks.py
&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;wagtail.contrib.modeladmin.options&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;ModelAdmin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="n"&gt;ModelAdminGroup&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="n"&gt;modeladmin_register&lt;/span&gt; 
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;.models&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Pizza&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Topping&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PizzaAdmin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ModelAdmin&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
    &lt;span class="n"&gt;menu_order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;000&lt;/span&gt; 
    &lt;span class="bp"&gt;...&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ToppingAdmin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ModelAdmin&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
    &lt;span class="n"&gt;menu_order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&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;PizzaGroup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ModelAdminGroup&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;menu_label&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Pizzas&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; 
    &lt;span class="n"&gt;menu_icon&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pick&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;menu_order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt; 
    &lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PizzaAdmin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ToppingAdmin&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="nf"&gt;modeladmin_register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PizzaGroup&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the &lt;code&gt;PizzaGroup&lt;/code&gt; class, we have some of the same attributes: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;menu_label&lt;/code&gt;: We set what we want this group of related models to be called in the Wagtail admin menu &lt;/li&gt;
&lt;li&gt;
&lt;code&gt;menu_icon&lt;/code&gt;: Which icon we want to use for this menu &lt;/li&gt;
&lt;li&gt;
&lt;code&gt;menu_order&lt;/code&gt;: Where we want this menu to appear in the sidebar, in relation to the other menu items&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We also add a new attribute, &lt;code&gt;items&lt;/code&gt;, where we list which &lt;code&gt;ModelAdmin&lt;/code&gt; classes we want to be part of this group. In our case, we want &lt;code&gt;PizzaAdmin&lt;/code&gt; and &lt;code&gt;ToppingAdmin&lt;/code&gt; to be in this group, so we add those. &lt;/p&gt;

&lt;p&gt;Note the change we made to &lt;code&gt;menu_order&lt;/code&gt; in &lt;code&gt;PizzaAdmin&lt;/code&gt; and &lt;code&gt;ToppingAdmin&lt;/code&gt;: Now those are set to &lt;code&gt;000&lt;/code&gt; and &lt;code&gt;100&lt;/code&gt;. When the &lt;code&gt;ModelAdmin&lt;/code&gt; classes will be part of a group, set the &lt;code&gt;menu_order&lt;/code&gt; how you want them to relate to each other, not to the other menu items in the Wagtail admin. Then set the &lt;code&gt;menu_order&lt;/code&gt; for the &lt;code&gt;ModelAdminGroup&lt;/code&gt; class to the proper value for the order you want it to appear in the side menu in the admin. &lt;/p&gt;

&lt;p&gt;Then we register the whole group, instead of the &lt;code&gt;ModelAdmin&lt;/code&gt; classes individually, to the Wagtail admin. When we reload the admin, we see this:&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%2Fthepracticaldev.s3.amazonaws.com%2Fi%2F0523sil6cswbmqofbgxe.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%2Fthepracticaldev.s3.amazonaws.com%2Fi%2F0523sil6cswbmqofbgxe.png" alt="Pizzas menu in the Wagtail admin that opens to reveal a Pizza and Toppings menu"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On the far left, there is a new menu item &lt;strong&gt;Pizzas&lt;/strong&gt; that expands a submenu. The submenu contains links to the admin interfaces for &lt;strong&gt;Pizzas&lt;/strong&gt; and &lt;strong&gt;Toppings&lt;/strong&gt;! &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: If you have the Django admin enabled and have your models already in the Django admin, this doesn't disable them from the regular Django admin. You are free to access your models in both the Wagtail admin and the Django admin, or at this point you can choose to remove your models from the Django admin (or disable the Django admin altogether, if you prefer). &lt;/p&gt;

&lt;h2&gt;
  
  
  Helpful Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="http://docs.wagtail.io/en/v2.6.1/reference/contrib/modeladmin/" rel="noopener noreferrer"&gt;Wagtail docs on ModelAdmin&lt;/a&gt; &lt;/li&gt;
&lt;li&gt;
&lt;a href="https://thegrouchy.dev/general/2015/12/06/wagtail-streamfield-icons.html" rel="noopener noreferrer"&gt;A List of Wagtail StreamField Icons&lt;/a&gt; &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Special thanks to Jeff Triplett and Jacob Burch for their help with this post.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>wagtail</category>
      <category>django</category>
      <category>python</category>
    </item>
    <item>
      <title>Dataclasses and attrs: when and why</title>
      <dc:creator>Flavio Curella</dc:creator>
      <pubDate>Tue, 20 Aug 2019 16:27:07 +0000</pubDate>
      <link>https://dev.to/revsys/dataclasses-and-attrs-when-and-why-5ea1</link>
      <guid>https://dev.to/revsys/dataclasses-and-attrs-when-and-why-5ea1</guid>
      <description>&lt;p&gt;Python 3.7 introduced &lt;a href="https://docs.python.org/3/library/dataclasses.html"&gt;dataclasses&lt;/a&gt; (&lt;a href="https://www.python.org/dev/peps/pep-0557/"&gt;PEP557&lt;/a&gt;). Dataclasses can be a convenient way to generate classes whose primary goal is to contain values.&lt;/p&gt;

&lt;p&gt;The design of dataclasses is based on the pre-existing &lt;code&gt;attr.s&lt;/code&gt; library. In fact Hynek Schlawack, the very same author of attrs, helped with the writing of PEP557.&lt;/p&gt;

&lt;p&gt;Basically dataclasses are a slimmed-down version of attrs. Whether this is an improvement or not really depends on your specific use-case.&lt;/p&gt;

&lt;p&gt;I think the addition of dataclasses to the standard library makes attrs even more relevant. The way I see it is that one is a subset of the other, and having both options is a good thing. You should probably use both in your project, according to the level of formality you want in that particular piece of code.&lt;/p&gt;

&lt;p&gt;In this article I will show the way I use dataclasses and attrs, why I think you should use both, and why I think attrs is still very relevant.&lt;/p&gt;

&lt;h3&gt;
  
  
  What do they do
&lt;/h3&gt;

&lt;p&gt;Both the standard library's dataclasses and the &lt;code&gt;attrs&lt;/code&gt; library provide a way to define what I'll call "structured data types" (I would put &lt;code&gt;namedtuple&lt;/code&gt;, &lt;code&gt;dict&lt;/code&gt; and &lt;code&gt;typeddict&lt;/code&gt; in the same family)&lt;/p&gt;

&lt;p&gt;PS: There's probably some more correct CS term for them, but I didn't go to CS School, so ¯\&lt;em&gt;(ツ)&lt;/em&gt;/¯&lt;/p&gt;

&lt;p&gt;They are all variations on the same concept: a class representing a data type containing multiple values, each value addressed by some kind of key.&lt;/p&gt;

&lt;p&gt;They also do a few more useful things: they provide ordering, serialization, and a nice string representation. But for the most part, the most useful purpose is adding a certain degree of formalization to a group of values that need to be passed around.&lt;/p&gt;

&lt;h3&gt;
  
  
  An example
&lt;/h3&gt;

&lt;p&gt;I think an example would better illustrate what I use dataclasses and attrs for.&lt;br&gt;
Suppose you want to render a template containing a table. You want to make sure the table has a title, a description, and rows:&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;def&lt;/span&gt; &lt;span class="nf"&gt;render_document&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;title&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;caption&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;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Dict&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;Any&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;template&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;render&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="s"&gt;"title"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"caption"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;caption&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"data"&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="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, suppose you want to render a document, which consists of a title, description, status ("draft", "in review", "approved"), and a list of tables. How would you pass the tables to &lt;code&gt;render_document&lt;/code&gt;?&lt;/p&gt;

&lt;p&gt;You may choose to represent each table as a &lt;code&gt;dict&lt;/code&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="p"&gt;{&lt;/span&gt;
    &lt;span class="s"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"My Table"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"caption"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"2019 Earnings"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"data"&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;span class="s"&gt;"Period"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"QT1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Europe"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"USA"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;467&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"Period"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"QT2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Europe"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;345&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"USA"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;765&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;But how would you express the type annotation for the &lt;code&gt;tables&lt;/code&gt; argument so that it's correct, explicit and simple to understand?&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;def&lt;/span&gt; &lt;span class="nf"&gt;render_document&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;title&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="n"&gt;status&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;tables&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Dict&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;Any&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;template&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;render&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="s"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;title&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="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"tables"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;tables&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;That only gets us to describe the first level if &lt;code&gt;tables&lt;/code&gt;. It doesn't tell us that a &lt;code&gt;Table&lt;/code&gt; has a title, or caption. Instead, you could use a dataclass:&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;@&lt;/span&gt;&lt;span class="n"&gt;dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Table&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;title&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;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Dict&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;Any&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;
    &lt;span class="n"&gt;caption&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&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;render_document&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;title&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="n"&gt;tables&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Table&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;template&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;render&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="s"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;title&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="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"tables"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;tables&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;This way we have type hinting, helping our IDE helping us.&lt;/p&gt;

&lt;p&gt;But we can go one step further, and also provide type validation at runtime. This is where dataclasses stops, and attrs comes in:&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;@&lt;/span&gt;&lt;span class="n"&gt;attr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;object&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;attr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ib&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;validator&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;attr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;validators&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;instance_of&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="c1"&gt;# don't you pass no bytes!
&lt;/span&gt;    &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Dict&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;Any&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;attr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ib&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;validator&lt;/span&gt;&lt;span class="o"&gt;=&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="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;attr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ib&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;validator&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;attr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;validators&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;instance_of&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;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&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;render_document&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;title&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="n"&gt;tables&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Table&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;template&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;render&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="s"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;title&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="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"tables"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;tables&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;Now, suppose we also need to render a "Report", which is a collection of "Document"s. You can probably see where this is going:&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;@&lt;/span&gt;&lt;span class="n"&gt;dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Table&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;title&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;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Dict&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;Any&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;
    &lt;span class="n"&gt;caption&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;

&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;attr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Document&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;object&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;attr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ib&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;validators&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;attr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;validators&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;in_&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"draft"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"in review"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"approved"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;tables&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Table&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;attr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ib&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&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;render_report&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;title&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;documents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Document&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;template&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;render&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="s"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"documents"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;documents&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;Note how I am validating that &lt;code&gt;Document.status&lt;/code&gt; is one of the allowed values. This comes particularly handy when you're building abstractions on top of Django models with a field that uses &lt;code&gt;choices&lt;/code&gt;. Dataclasses can't do that.&lt;/p&gt;

&lt;p&gt;A couple of patterns I keep finding myself in are the following:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Write a function that accepts some arguments&lt;/li&gt;
&lt;li&gt;Group some of the arguments into a &lt;code&gt;tuple&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Hm, I want field names -&amp;gt; &lt;code&gt;namedtuple&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Hm, I want types -&amp;gt; &lt;code&gt;dataclass&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Hm, I want validation -&amp;gt; &lt;code&gt;attrs&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Another situation that happens quite often is this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;write a function that accepts some arguments&lt;/li&gt;
&lt;li&gt;add typing so my IDE can help me out&lt;/li&gt;
&lt;li&gt;oh, by the way, it needs to support a list of those things, not just one at a time!&lt;/li&gt;
&lt;li&gt;refactor to use dataclasses&lt;/li&gt;
&lt;li&gt;This argument can only be one of those values, or

&lt;ol&gt;
&lt;li&gt;I ask myself: How do I make sure other developers are passing the right type and/or values?&lt;/li&gt;
&lt;/ol&gt;


&lt;/li&gt;
&lt;li&gt;switch to attrs&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Sometimes I stop at the dataclasses. Lots of times I get to the attrs step.&lt;/p&gt;

&lt;p&gt;And sometimes, this happens:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;one half of this legacy codebase uses &lt;code&gt;-1&lt;/code&gt; as special value for &lt;code&gt;False&lt;/code&gt;, that other half uses &lt;code&gt;False&lt;/code&gt;. Switch to &lt;code&gt;attr.s&lt;/code&gt; so I can use &lt;code&gt;converter=&lt;/code&gt; to normalize.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Comparison
&lt;/h3&gt;

&lt;p&gt;The two libraries do appear very similar. To get a clearer picture of how they compare, I've made a table of the features I use most:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
  &lt;tr&gt;
    &lt;th&gt;feature&lt;/th&gt;
    &lt;th&gt;dataclasses&lt;/th&gt;
    &lt;th&gt;attrs&lt;/th&gt;
  &lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
  &lt;tr&gt;
    &lt;td&gt;frozen&lt;/td&gt;
    &lt;td&gt;✓&lt;/td&gt;
    &lt;td&gt;✓&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td&gt;defaults&lt;/td&gt;
    &lt;td&gt;✓&lt;/td&gt;
    &lt;td&gt;✓&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td&gt;totuple&lt;/td&gt;
    &lt;td&gt;✓&lt;/td&gt;
    &lt;td&gt;✓&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td&gt;todict&lt;/td&gt;
    &lt;td&gt;✓&lt;/td&gt;
    &lt;td&gt;✓&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td&gt;validators&lt;/td&gt;
    &lt;td&gt;✗&lt;/td&gt;
    &lt;td&gt;✓&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td&gt;converters&lt;/td&gt;
    &lt;td&gt;✗&lt;/td&gt;
    &lt;td&gt;✓&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td&gt;slotted classes&lt;/td&gt;
    &lt;td&gt;✗&lt;/td&gt;
    &lt;td&gt;✓&lt;/td&gt;
  &lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;As you can see, there's a lot of overlap. But the additional features on &lt;code&gt;attrs&lt;/code&gt; provide functionality that I need more often than not.&lt;/p&gt;

&lt;h3&gt;
  
  
  When to use dataclasses
&lt;/h3&gt;

&lt;p&gt;Dataclasses are just about the "shape" of the data.&lt;br&gt;
Choose dataclasses if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You don't care about values in the fields, only their type&lt;/li&gt;
&lt;li&gt;adding a dependency is not trivial&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  When to use attrs
&lt;/h3&gt;

&lt;p&gt;attrs is about the shape &lt;em&gt;and&lt;/em&gt; the values.&lt;br&gt;
Choose attrs if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;you want to validate values. A common case would be the equivalent of a ChoiceField.&lt;/li&gt;
&lt;li&gt;you want to normalize, or sanitize the input&lt;/li&gt;
&lt;li&gt;whenever you want more formalization than dataclasses alone can offer&lt;/li&gt;
&lt;li&gt;you are concerned about memory and performances. &lt;code&gt;attrs&lt;/code&gt; can create &lt;a href="https://www.attrs.org/en/stable/examples.html#slots"&gt;slotted classes&lt;/a&gt;, which are &lt;a href="http://threeofwands.com/attrs-ii-slots/"&gt;optimized by CPython&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I often find myself using dataclasses and later switching to attr.s because the requirements changed or I find out I need to guard against some particular value. I think that's a normal aspect of developing software and what I call "continuous refactoring".&lt;/p&gt;

&lt;h3&gt;
  
  
  Why I like dataclasses
&lt;/h3&gt;

&lt;p&gt;I'm glad dataclasses have been added to the standard library, and I think it's a beneficial addition. It's a very convenient thing to have at your disposal whenever you need.&lt;/p&gt;

&lt;p&gt;For one, it will encourage a more structured style of programming from the beginning.&lt;/p&gt;

&lt;p&gt;But I think the most compelling case is a practical one. Some high-risk corporate environments (eg: financial institutions) require every package to be vetted (with good reason: we've already had incidents of malicious code in libraries). That means that adding attrs is not as simple as adding a line to your &lt;code&gt;requirements.txt&lt;/code&gt;, and will involve waiting on approval from your corpops team. Those developers can use dataclasses right away and their code will immediately benefit from using more formalized data types.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why I like attrs
&lt;/h3&gt;

&lt;p&gt;Most people don't work in such strictly-controlled environments.&lt;/p&gt;

&lt;p&gt;And sure, sometimes you don't need all the features from attrs, but it doesn't hurt having them.&lt;/p&gt;

&lt;p&gt;More often than not, I end up needing them anyway, as I formalize more and more of my code's API. Dataclasses only gets half-way of where I want to go.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;I think dataclasses encompass only a subset of what attrs has to offer. Admittedly, it is a &lt;em&gt;big&lt;/em&gt; subset. But the features that are not covered are important enough and needed often enough that they make attrs not only still relevant and useful, but also necessary.&lt;/p&gt;

&lt;p&gt;In my mind, using both allows developers to progressively refactor their code, moving the contracts amongst functions from loosely-defined arguments all the way up to formally described data structures as the requirements of the app stabilize over time.&lt;/p&gt;

&lt;p&gt;One nice effect of having dataclasses is that now developers are more incentivized to refactor their code toward more formalization. At some point dataclasses is not going to be enough, and that's when developers will refactor to use attrs. In this way, dataclasses actually acts as an &lt;em&gt;introduction&lt;/em&gt; to attrs. I wouldn't be surprised if attrs becomes &lt;em&gt;more&lt;/em&gt; popular thanks to dataclasses.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://docs.python.org/3/library/dataclasses.html"&gt;Documentation on dataclasses&lt;/a&gt; + &lt;a href="https://www.python.org/dev/peps/pep-0557/"&gt;PEP557&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.attrs.org/en/stable/index.html"&gt;Documentation on attrs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.attrs.org/en/stable/why.html#data-classes"&gt;"Why not dataclasses" on attrs' docs&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Acknowledgments + Thanks
&lt;/h2&gt;

&lt;p&gt;I want to thank the following people for revising drafts and providing input and insights:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Hynek Schlawack&lt;/li&gt;
&lt;li&gt;Jacob Kaplan-Moss&lt;/li&gt;
&lt;li&gt;Jacob Burch&lt;/li&gt;
&lt;li&gt;Jeff Triplett&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>python</category>
      <category>dataclasses</category>
      <category>attr</category>
    </item>
    <item>
      <title>Using Different Read and Write Serializers in Django REST Framework</title>
      <dc:creator>Lacey Williams Henschel</dc:creator>
      <pubDate>Tue, 20 Aug 2019 16:24:38 +0000</pubDate>
      <link>https://dev.to/revsys/using-different-read-and-write-serializers-in-django-rest-framework-1cc1</link>
      <guid>https://dev.to/revsys/using-different-read-and-write-serializers-in-django-rest-framework-1cc1</guid>
      <description>&lt;p&gt;&lt;strong&gt;Versions&lt;/strong&gt;: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Python 3.7 &lt;/li&gt;
&lt;li&gt;Django 2.2 &lt;/li&gt;
&lt;li&gt;Django REST Framework 3.10 &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On a recent project, we needed to use different serializers for GET vs. POST/PUT/PATCH requests to our Django REST Framework API. In our case, this was because the GET serializer contained a lot of nested data; for example, it contained expanded fields from other serializers to foreign-key relationships. The requests to update data via the API, though, didn't need these expanded fields.  &lt;/p&gt;

&lt;p&gt;The first way we approached using different serializers for read and update actions was to override &lt;code&gt;get_serializer_class()&lt;/code&gt; on each viewset to decide which serializer to return depending on the action in the request. We returned the "read" serializer for &lt;code&gt;list&lt;/code&gt; and &lt;code&gt;retrieve&lt;/code&gt; actions, and the "update" serializer for everything else. (The full list of API actions is &lt;a href="https://github.com/encode/django-rest-framework/blob/335054a5d36b352a58286b303b608b6bf48152f8/rest_framework/schemas/coreapi.py#L39"&gt;in the DRF codebase&lt;/a&gt;.) But we wound up repeating ourselves across several viewsets, so we wrote a mixin to take care of some of this work for us! &lt;/p&gt;

&lt;p&gt;A mixin is a Python class that contains custom attributes and methods (&lt;a href="https://easyaspython.com/mixins-for-fun-and-profit-cb9962760556?gi=3875fd6a6ff8"&gt;more explanation&lt;/a&gt;). It's not very useful on its own, but when it's inherited into a class, that class has access to the mixin's special attributes and methods.  &lt;/p&gt;

&lt;p&gt;This was our mixin:&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;ReadWriteSerializerMixin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;object&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="s"&gt;"""
    Overrides get_serializer_class to choose the read serializer
    for GET requests and the write serializer for POST requests.

    Set read_serializer_class and write_serializer_class attributes on a
    viewset. 
    """&lt;/span&gt;

    &lt;span class="n"&gt;read_serializer_class&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;write_serializer_class&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_serializer_class&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="k"&gt;if&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;action&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"create"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"update"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"partial_update"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"destroy"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
            &lt;span class="k"&gt;return&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;get_write_serializer_class&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;return&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;get_read_serializer_class&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;get_read_serializer_class&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="k"&gt;assert&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;read_serializer_class&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s"&gt;"'%s' should either include a `read_serializer_class` attribute,"&lt;/span&gt;
            &lt;span class="s"&gt;"or override the `get_read_serializer_class()` method."&lt;/span&gt;
            &lt;span class="o"&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;__class__&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="k"&gt;return&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;read_serializer_class&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_write_serializer_class&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="k"&gt;assert&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;write_serializer_class&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s"&gt;"'%s' should either include a `write_serializer_class` attribute,"&lt;/span&gt;
            &lt;span class="s"&gt;"or override the `get_write_serializer_class()` method."&lt;/span&gt;
            &lt;span class="o"&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;__class__&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="k"&gt;return&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;write_serializer_class&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This mixin defines two new attributes, &lt;code&gt;read_serializer_class&lt;/code&gt; and &lt;code&gt;write_serializer_class&lt;/code&gt;. Each attribute has a corresponding method to catch the error where the mixin is being used, but those attributes haven't been set. The &lt;code&gt;get_*_serializer_class()&lt;/code&gt; methods will raise an &lt;code&gt;AssertionError&lt;/code&gt; if your viewset hasn't set the appropriate attribute or overridden the necessary method. &lt;/p&gt;

&lt;p&gt;The &lt;code&gt;get_serializer_class&lt;/code&gt; method makes the final decision on which serializer to use. For the "update" actions to the API, it returns &lt;code&gt;write_serializer_class&lt;/code&gt;; otherwise it returns &lt;code&gt;read_serializer_class&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;The mixin gets used in a viewset 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;rest_framework&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;viewsets&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;.mixins&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ReadWriteSerializerMixin&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;.models&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;MyModel&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;.serializers&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ModelReadSerializer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ModelWriteSerializer&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MyModelViewSet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ReadWriteSerializerMixin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;viewsets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ModelViewSet&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;queryset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MyModel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;all&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; 
    &lt;span class="n"&gt;read_serializer_class&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ModelReadSerializer&lt;/span&gt; 
    &lt;span class="n"&gt;write_serializer_class&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ModelWriteSerializer&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the viewset &lt;code&gt;MyModelViewSet&lt;/code&gt; has access to the attributes and methods from the mixin &lt;code&gt;ReadWriteSerializerMixin&lt;/code&gt;. This means that when a call is made to the API that uses &lt;code&gt;MyModelViewSet&lt;/code&gt;, the &lt;code&gt;get_serializer_class()&lt;/code&gt; method from &lt;code&gt;ReadWriteSerializerMixin&lt;/code&gt; will automatically be called and will decide, based on the kind of API request being made, which serializer to use. If we needed to make even more granular decisions about the serializer returned (maybe we want to use a more limited serializer for a &lt;code&gt;list&lt;/code&gt; request and one with more data in a &lt;code&gt;retrieve&lt;/code&gt; request), then our viewset can override &lt;code&gt;get_write_serializer_class()&lt;/code&gt; to add that logic. &lt;/p&gt;

&lt;p&gt;Note: Custom DRF actions will contain actions that aren't part of the DRF list of accepted actions (because they are custom actions you're creating), so when you call &lt;code&gt;get_serializer_class&lt;/code&gt; from inside your action method, it will return whatever your "default" serializer class is. In the example above, the "default" serializer is the &lt;code&gt;read_serializer_class&lt;/code&gt; because it's what we return when we fall through the other conditional.  &lt;/p&gt;

&lt;p&gt;Depending on your action, you will want to override &lt;code&gt;get_serializer_class&lt;/code&gt; to change your default method or explicitly account for your custom action. &lt;/p&gt;

&lt;p&gt;Mixins are a DRY (Don't Repeat Yourself) way to add functionality that you wind up needing to use across several viewsets. We hope you get to experiment with using them soon!&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Thanks to Jeff Triplett for his help with this post.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>django</category>
      <category>python</category>
      <category>djangorestframework</category>
      <category>drf</category>
    </item>
  </channel>
</rss>
