6 ms·
Approximating sum types in Python with Pydantic
- blorenz 2y agoDiscriminated unions are also a wonderful part of the zod library. I use them to overload endpoints for multiple relevant operations.
- roshankhan28 2y ago+1 for the discriminated unions
- myvoiceismypass 2y agoI used to use io-ts heavily but zod is my go to now - and it’s so ergonomic and easy for typescript newbies to pick up and grasp.
- wizerno 2y agoA slightly related discussion on Type Unions in C# from a week ago: https://news.ycombinator.com/item?id=41183240 https://news.ycombinator.com/item?id=41183240
- reubenmorais 2y agoOne caveat of the tip in the "Deduplicating shared variant state" section about including an underspecified discriminator field in the base class, is that it doesn't play well if you're using Literals instead of Enums as the discriminator type. Python does not allow you to narrow a literal type of a field in a subclass, so the following doesn't type check: from typing import Literal class _FrobulatedBase: kind: Literal['foo', 'bar'] value: str class Foo(_FrobulatedBase): kind: Literal['foo'] = 'foo' foo_specific: int class Bar(_FrobulatedBase): kind: Literal['bar'] = 'bar' bar_specific: bool "kind" overrides symbol of same name in class "_FrobulatedBase" Variable is mutable so its type is invariant Override type "Literal['foo']" is not the same as base type "Literal['foo', 'bar']" https://pyright-play.net/?code=GYJw9gtgBALgngBwJYDsDmUkQWEMoAySMApiAIYA2AUNQMaXkDOTUA%2BgGLgBGAro6QAmAIWYkAXNSjSoAa1SDxhYmSoBtAOTAwYDQBooG7uRAaAulJkA3KrwlQmMELXqMWUDjoAUnHv3JCokwkAJSSMnIKSkSkFJSa2rpmUAC8hokaltKJbEwIJHRIwEh0SqgwLgzMrKIgPlxgfAIkImJhWZEoisqx6kYm5qmGxqYdI7n5hcWlUNw6NEA https://pyright-play.net/?code=GYJw9gtgBALgngBwJYDsDmUkQWEMo...
- yorwba 2y ago> it doesn't play well if you're using Literals instead of Enums as the discriminator type The original example code with Enums doesn't type-check either, and for the same reason: If the type checker allowed that, someone could take an object of type Foo, assign it to a variable of type _FrobulatedBase, then use that variable to modify the kind field to 'bar' and now you have an illegal Foo with kind 'bar'.
- woodruffw 2y agomypy typechecks that just fine[1]. However, I think that's possibly a bug :-) -- I agree that narrowing a literal via subclassing is unsound. That's why the example in the blog used `str` for the superclass, not the closure of all `Literal` variants. (I use this pattern pretty extensively in Python codebases that are typechecked with mypy, and I haven't run into many issues with mypy failing to understand the variant shapes -- the exception to this so far has been with `RootModel`, where mypy has needed Pydantic's mypy plugin[2] to understand the relationship between the "root" type and its underlying union. But it's possible that this is essentially unsound as well.) [1]: https://mypy-play.net/?mypy=latest&python=3.12&gist=f35da62ec4ac59f06360a758ca5f58f6 https://mypy-play.net/?mypy=latest&python=3.12&gist=f35da62e... [2]: https://docs.pydantic.dev/latest/integrations/mypy/ https://docs.pydantic.dev/latest/integrations/mypy/
- reubenmorais 2y agoUsing str in the superclass equally unsound and also doesn't type check. There's no good way to do it, as the discriminator type is by definition disjoint between all kinds.
- deleted 2y ago[deleted]
- sevensor 2y agoThe problem here seems to be inheritance. Why share a base class? Why not write Frobulated = Foo|Bar and go about your day writing functions like def frotz(x: Frobulated) -> str: return f”{x.value} is the value of x”
- carderne 2y agoI think it would be useful to differentiate more clearly between what is offered by Python's type system, and what is offered by Pydantic. That is, you can approximate Rusts's enum (sum type) with pure Python using whatever combination of Literal, Enum, Union and dataclasses. For example (more here[1]): @dataclass class Foo: ... @dataclass class Bar: ... Frobulated = Foo | Bar Pydantic adds de/ser, but if you're not doing that then you can get very far without it. (And even if you are, there are lighter-weight options that play with dataclasses like cattrs, pyserde, dataclasses-json). [1] https://threeofwands.com/algebraic-data-types-in-python/ https://threeofwands.com/algebraic-data-types-in-python/
- intalentive 2y agoPydantic offers runtime checks. Also I’d add msgspec to your list at the end. Lightweight and fast, handles validation during decoding.
- mejutoco 2y agoTypeguard too. The @typechecked annotation on any function or method will blow up with an error at runtime if types do not match
- carderne 2y agoGood point, but that's not always desirable. If you have strict type-checking and _aren't_ doing ser/de, it's likely not necessary (eg Rust doesn't do runtime checks).
- nsteel 2y agoThe situation we found where it's still useful is if your app supports extension-type functionality. The 3rd parties writing extensions would ideally be type-checking during development... but they might not bother. Runtime validation becomes useful at the interfaces.
- woodruffw 2y agoYep, someone brought this up on another discussion forum. The post was intended to be explicitly about accomplishing the ser/de half as well, hence the emphasis on Pydantic :-) (Python’s annotated types are very powerful, and you can do this and more with them if you don’t immediately need ser/de! But they also have limitations, e.g. I believe Union wasn’t allowed in isinstance checks or matching until a recent version.)
- jghn 2y agoMeta comment. Something I've wondered of late. I keep seeing these articles pop up and they're trying to recreate ADTs for Python in the manner of Rust. But there's a long history of ADTs in other languages. For instance we don't see threads on recreating Haskell's ADT structures in Python. Is this an artifact of Rust is hype right now, especially on HN? As in the typical reader is more familiar with Rust than Haskell, and thus "I want to do what I'm used to in Rust in Python" is more likely to resonate than "I want to do what I'm used to in Haskell in Python"? At the end of the day it doesn't *really* matter as the underlying construct being modeled is the same. It's the translation layer that I'm wondering about.
- steveklabnik 2y agoI think "hype" has some connotations that I wouldn't necessarily agree with, and I don't think it's as much "on HN" as "people who write Python," but I would agree that I would expect at this point more Python folks to be familiar with Rust than Haskell, and so that to be the reason, yes.
- jghn 2y agoThe reason I said hype is that it's a cycle here. If you go back 10 years every example *would* have been in Haskell. Or perhaps Scala. They were the cool languages of the era. And the topics here painted a picture that their use in the broader world was more common than they really were. And I say that as someone who used both Haskell & Scala in my day job at the time. HN would have you believe that I was the norm, but I very much was not. That's not to say it's bad, or a problem. If it gets more people into these concepts that's great.
- woodruffw 2y ago(Author of the post.) I think so, in the sense that Rust has successfully translated ADTs and other PLT-laden concepts from SML/Haskell into syntax that a large base of engineers finds intuitive. Whether or not that’s hype is a value judgement, but that is the reason I picked it for the example snippet: I figured more people would “get” it with less explanation required :-)
- __mharrison__ 2y agoA pretty good article. Would be a great article if they used real world examples instead of made up "formulated" ones.
- jamincan 2y agoI know that Foo and Frobulator and so on have history in code examples, but I personally find examples with them require more careful reading than examples built on real concepts.
- yen223 2y agoSomething I've learned is that in general, people find it easier to follow concrete examples than abstract ones. I agree with you that the article would have been improved if they'd used real-world examples, e.g. a ContactMethod type that has Address or PhoneNumber or something like that.
- __mharrison__ 2y agoI teach for a living. Simple examples are fine for introducing a concept but students really grok it when they can see it in practice.
- adamc 2y agoThe problem I see with it is this: Now, instead of understanding Python, which is straightforward, you have to understand a bunch about Pydantic and type unions. In a large shop of Python programmers, I would expect many would not follow most of this. Essentially, if this is a feature you must have, Python seems like the wrong language. Maybe if you only need it in spots this makes sense...
- Spivak 2y agoPydantic is truly a godsend to the Python ecosystem. It is a full implementation of "parse don't validate" and does so using Python's existing type declarations. It uses the same forms as dataclasses, SQLAlchemy, and Django that have been part of Python forever so most Python programmers are familiar with it. And the reason you reach for it is that it eliminates whole classes of errors when the boundary between your program and the outside world is only via .model_validate() and .model_dump(). The outside world including 3rd-party API calls. The data either comes back to you exactly like you expect it to, or it errs. It's hundreds of tests that you simply don't have to write. In the same way that SQLite bills itself as the better alternative to fopen(), Pydantic is the better alternative to json.loads()/json.dumps().
- adamc 2y agoI don't think you are wrong and I have at times missed having such an option. But... I saw Java go down this path of cool features that you needed to learn, on top of the basic language, and eventually it took Java to an environment where learning the toolset and environment was complex, and vastly changed the calculus of how approachable the language was. In my mind, anyway, it went from being a useful if incomplete tool to being a more complete language that was not really worth messing with unless you were going to make a big commitment. Every step that takes Python in that direction is a mistake, because if we need to make a huge commitment, Python probably isn't the right language. A large part of the appeal of Python is that it is easy to learn, easy to bring devs up to speed on if they don't know it, easy to debug and understand. That's why people use it despite its performance shortcomings, despite its concurrency issues, etc. (That and the benefit of a large and fairly high quality library.)
- deleted 2y ago[deleted]
- ks2048 2y agoIt's a shame there's so many different names for a set of very related (or identical?) concepts. For example wikipedia says "tagged union" is also known as "variant, variant record, choice type, discriminated union, disjoint union, sum type, or coproduct". [https://en.wikipedia.org/wiki/Tagged_union https://en.wikipedia.org/wiki/Tagged_union]
- deleted 2y ago[deleted]
- nsonha 2y agoAnd people complain that typescript is crazy. I think we just need to acknowledge that typing is hard, especially with what mainstream languages give us.
- gnulinux996 2y agoI feel like with typescript and pydantic taking center stage it seems that the dynamic vs static typing debate finally comes to a close. More and more Java seems to be not that bad after all.
- lolive 2y agoNext steps: immutable data structures and functional programming. #haskell #hereWeCome
- screye 2y ago> immutable data structures Data pipelines & ML workflows are already pseudo immutable and pseudo functional.
- masklinn 2y agoMaybe it has gotten less bad but Java was the main counter-argument and foe to static typing: a poor and inexpressive type system coupled with significant verbosity, Java required a lot of buck and provided very little bang for it. And that is just the langage, ignoring the horrendous best practices. I very much credit Java for having turned a generation away from static typing, dynamic typing did get buoyed by a combination of moore’s law and good press but could never have done it without Java having smothered the other side and being dreck.
- pjmlp 2y agoSomething went very wrong then, given Go's existence, making Java feel like a PhD level language.
- masklinn 2y agoJava never felt like phd level language except for a phd in sucking. Go does have a poor type system, but it has nowhere near the verbosity of early aught Java: local type inference, free functions, any number of (public) types in the same file, closures, type definitions (terse and easy newtyping), iteration (if only for builtin types until 1.23), etc... And that's without considering the cultural side of requiring two different implementations of every type (one interface and one impl) or XML-oriented programming for bindings "improved" by unreliable parsers going through method comments.
- adsharma 2y agopython has been about expressing ideas. Even if the language doesn't support some of the concepts natively, it's useful to express it in python so it could be effectively transpiled into a language that does. This is what py2many needs from a curated subset of python with some enhancements as opposed to inventing a new language. https://github.com/adsharma/adt https://github.com/adsharma/adt contains a small enhancement for @sealed decorator from the excellent upstream repo. https://github.com/py2many/py2many/blob/main/tests/cases/sealed.py https://github.com/py2many/py2many/blob/main/tests/cases/sea... https://github.com/py2many/py2many/blob/main/tests/expected/sealed.rs https://github.com/py2many/py2many/blob/main/tests/expected/...
- LtWorf 2y agoAuthor of typedload here. typedload does this without need to pass a "discriminator" parameter. Just having the types with the same field defined as a literal of different things will suffice. I've also implemented an algorithm to inspect the data and find out the type directly from the literal field, to avoid having to try multiple types when loading a union. Pydantic has also implemented the same strategy afterwards. typedload is faster than pydantic to load tagged unions. It is written in pure python. edit: Also, typedload just uses completely regular dataclasses or attrs. No need for all those different BaseModel, RootModel and understanding when to use them.
- levhawk 2y agoThat looks crazy coming from statically-typed languages. So many hops, efforts, custom structures just to verify types. Come on, modern C# can do all of that out-of-the-box. You define a record/class for DTO and System.Text.Json will either convert it successfully or throw you an exception that will say exactly what the problem was and at what character/field. Combined with much more advanced IntelliSense, development comfort is so much better. But of course, whatever works for you.
- hexane360 2y agoAs an alternative to Pydantic, check out the wonderful mashumaro: https://github.com/Fatal1ty/mashumaro https://github.com/Fatal1ty/mashumaro I've also played around with writing my own dataclass/data conversion library: https://github.com/hexane360/pane https://github.com/hexane360/pane