<?xml version="1.0" ?><entry xml:lang="en-US" xmlns="http://www.w3.org/2005/Atom" xmlns:planet="http://planet.intertwingly.net/"><id>http://blog.ezyang.com/?p=10283</id><link href="http://blog.ezyang.com/2020/10/idiomatic-algebraic-data-types-in-python-with-dataclasses-and-union/" rel="alternate" type="text/html"/><link href="http://blog.ezyang.com/2020/10/idiomatic-algebraic-data-types-in-python-with-dataclasses-and-union/#comments" rel="replies" type="text/html"/><link href="http://blog.ezyang.com/2020/10/idiomatic-algebraic-data-types-in-python-with-dataclasses-and-union/feed/atom/" rel="replies" type="application/atom+xml"/><title xml:lang="en-US">Idiomatic algebraic data types in Python with dataclasses and Union</title><summary xml:lang="en-US">Greetings from 2024! An official pattern matching PEP has been accepted https://peps.python.org/pep-0636/ and is available in Python 3.10. Class patterns are tested using isinstance, with no inheritance structure necessary, making the pattern described in this post 100% forward compatible to real pattern matching. One of the features I miss most in non-Haskell programming languages is […]</summary><content type="xhtml" xml:lang="en-US"><div xmlns="http://www.w3.org/1999/xhtml"><div class="document">



<p><strong>Greetings from 2024!</strong>  An official pattern matching PEP has been accepted <a class="reference external" href="https://peps.python.org/pep-0636/">https://peps.python.org/pep-0636/</a> and is available in Python 3.10.  Class patterns are tested using isinstance, with no inheritance structure necessary, making the pattern described in this post 100% forward compatible to real pattern matching.</p>
<hr class="docutils"/>
<p>One of the features I miss most in non-Haskell programming languages is algebraic data types (ADT).  ADTs fulfill a similar role to objects in other languages, but with more restrictions: objects are an open universe, where clients can implement new subclasses that were not known at definition time; ADTs are a closed universe, where the definition of an ADT specifies precisely all the cases that are possible.  We often think of restrictions of a bad thing, but in the case of ADTs, the restriction of being a closed universe makes programs easier to understand (a fixed set of cases to understand, as opposed to a potentially infinite set of cases) and allows for new modes of expression (pattern matching). ADTs make it really easy to accurately model your data structures; they encourage you to go for precise types that make illegal states unrepresentable. Still, it is generally not a good idea to try to manually reimplement your favorite Haskell language feature in every other programming language you use, and so for years I've suffered in Python under the impression that ADTs were a no go.</p>
<p>Recently, however, I have noticed that a number of new features in Python 3 have made it possible to use objects in the same style of ADTs, in idiomatic Python with virtually no boilerplate.  The key features:</p>
<ul class="simple">
<li>A structural static type checking system with mypy; in particular, the ability to declare <tt class="docutils literal">Union</tt> types, which let you represent values that could be one of a fixed set of other types, and the ability to refine the type of a variable by performing an <tt class="docutils literal">isinstance</tt> check on it.</li>
<li>The dataclasses library, which allows you to conveniently define (possibly immutable) structures of data without having to write boilerplate for the constructor.</li>
</ul>
<p>The key idea: define each constructor as a dataclass, put the constructors together into an ADT using a Union type, and use <tt class="docutils literal">isinstance</tt> tests to do pattern matching on the result. The result is just as good as an ADT (or better, perhaps; their structural nature bears more similarity to OCaml's polymorphic variants).</p>
<p>Here's how it works.  Let's suppose that you want to define an algebraic data type with two results:</p>
<pre class="literal-block">data Result
   = OK Int
   | Failure String

showResult :: Result -&gt; String
showResult (OK result) = show result
showResult (Failure msg) = &quot;Failure: &quot; ++ msg
</pre>
<p>First, we define each constructor as a dataclass:</p>
<pre class="literal-block">from dataclasses import dataclass

@dataclass(frozen=True)
class OK:
    result: int

@dataclass(frozen=True)
class Failure:
    msg: str
</pre>
<p>Using the automatically generated constructors from dataclasses, we can construct values of these dataclasses using <tt class="docutils literal">OK(2)</tt> or <tt class="docutils literal"><span class="pre">Failure(&quot;something</span> wrong&quot;)</tt>. Next, we define a type synonym for the union of these two classes:</p>
<pre class="literal-block">Result = Union[OK, Failure]
</pre>
<p>Finally, we can do pattern matching on Result by doing <tt class="docutils literal">isinstance</tt> tests:</p>
<pre class="literal-block">def assert_never(x: NoReturn) -&gt; NoReturn:
    raise AssertionError(&quot;Unhandled type: {}&quot;.format(type(x).__name__))

def showResult(r: Result) -&gt; str:
    if isinstance(r, OK):
        return str(r.result)
    elif isinstance(r, Failure):
        return &quot;Failure: &quot; + r.msg
    else:
        assert_never(r)
</pre>
<p><tt class="docutils literal">assert_never</tt> is a <a class="reference external" href="https://github.com/python/typing/issues/735">well known trick</a> for doing exhaustiveness checking in mypy.  If we haven't covered all cases with enough <tt class="docutils literal">isinstance</tt> checks, mypy will complain that <tt class="docutils literal">assert_never</tt> was given a type like <tt class="docutils literal">UnhandledCtor</tt> when it expected <tt class="docutils literal">NoReturn</tt> (which is the uninhabited type in Python).</p>
<p>That's all there is to it.  As an extra bonus, this style of writing unions is compatible with the <a class="reference external" href="https://www.python.org/dev/peps/pep-0634/">structured pattern matching PEP</a>, if it actually gets accepted. I've been using this pattern to good effect in our recent rewrite of PyTorch's code generator. If you have the opportunity to work in a statically typed Python codebase, give this style of code a try!</p>
</div></div></content><updated planet:format="October 14, 2020 06:08 PM">2020-10-14T18:08:54Z</updated><published planet:format="October 14, 2020 06:08 PM">2020-10-14T18:08:54Z</published><category scheme="http://blog.ezyang.com" term="Haskell"/><category scheme="http://blog.ezyang.com" term="Python"/><author><name>Edward Z. Yang</name><uri>http://ezyang.com</uri></author><source><id>http://blog.ezyang.com/feed/atom/</id><link href="http://blog.ezyang.com" rel="alternate" type="text/html"/><link href="http://blog.ezyang.com/feed/atom/" rel="self" type="application/atom+xml"/><subtitle xml:lang="en-US">the arc of software bends towards understanding</subtitle><title xml:lang="en-US">ezyang’s blog</title><updated planet:format="December 24, 2024 04:29 AM">2024-12-24T04:29:58Z</updated><planet:format>atom10</planet:format><planet:bozo>false</planet:bozo><planet:css-id>edward-z-yang</planet:css-id><planet:items_per_page>60</planet:items_per_page><planet:encoding>utf-8</planet:encoding><planet:name>Edward Z. Yang</planet:name><planet:days_per_page>0</planet:days_per_page><planet:http_last_modified>Sat, 4 Jan 2025 23:32:14 GMT</planet:http_last_modified><planet:http_status>200</planet:http_status></source></entry>