Support for sealed classes
Hi Typing-SIG, Just wanting to revive this thread <https://mail.python.org/archives/list/typing-sig@python.org/thread/QSCT2N4RF...>. I recently came across a use case for sealed classes in the process of making my mahjong game engine library <https://github.com/Kenny2github/mahjong> (still WIP), and was disappointed that no such concept was available. Specifically, mahjong/qna.py <https://github.com/Kenny2github/mahjong/blob/master/mahjong/qna.py> defines the Q&A API, where the idea is to check against each question subclass (see example in mahjong/__main__.py <https://github.com/Kenny2github/mahjong/blob/master/mahjong/__main__.py#L59-...>) and answer the question with the correct type. Currently I can mimic sealed classes (in the typechecking sense) by defining an alias to a Union of the subclasses and annotating with that instead of the base class. By doing this, I can make assert_never(question) typecheck if I apply this patch: diff --git a/mahjong/__main__.py b/mahjong/__main__.py index b8da955..dfa760e 100644 --- a/mahjong/__main__.py +++ b/mahjong/__main__.py @@ -27,6 +27,7 @@ for p in game.players: #''' #''' # uncomment this opening triple quote when commenting out one elsewhere import sys +from typing_extensions import assert_never from mahjong.game import Game, Hand from mahjong import qna @@ -115,11 +116,13 @@ while question is not None: elif isinstance(question, qna.HandEnding): if isinstance(question, qna.Goulash): print('Goulash! Nobody wins. Starting next game...') - else: + elif isinstance(question, (qna.DealerWon, qna.NormalHandEnding)): print('Player #%s won with %s (%s faan; %s points; %s)! Starting next game...' % ( question.winner.seat.value, ','.join(map(str, question.choice)), question.faan()[0], *question.points(1) )) + else: + assert_never(question) question = question.answer() print('Game Over!') #''' On the other hand, if I remove the Union alias and change all references to HandEndingType to refer to HandEnding, Pylance reports the following:
Argument of type "HandEnding" cannot be assigned to parameter "__arg" of type "NoReturn" in function "assert_never" Type "HandEnding" cannot be assigned to type "NoReturn"
This problem is not solved by making HandEnding an ABC, because the fundamental issue is that other subclasses could be defined outside of the file that would pass an isinstance(question, qna.HandEnding) check. Being able to mark HandEnding and UserIO as sealed would allow the assert_never(question) call to typecheck without having to re-enumerate the subclasses in a Union alias. In addition, what would further help my use case is if subclasses of sealed classes were themselves sealable. Marking the PlayeredIO and ArrivedIO subclasses of UserIO as sealed would allow an assert_never(question) in the UserIO if-elif tower to typecheck as well. (The latter is supported by all languages that support sealed classes, but I feel like it's worth explicitly mentioning.) Additional properties I believe sealed classes should have: - Though I only have contrived examples for this, I feel like sealed classes should *not* be automatically marked abstract. A class that is marked sealed alone should be treated like a Union of the subclasses *and the base class*, to allow for the possibility of directly instantiating the base class. If the base class is not to be included in the Union, it should be explicitly and additionally marked abstract, which would then preclude it from being instantiated and thus exclude it from the sealed list. (Since Unions fold subclasses into their parent classes, this means that in practice the difference between an abstract sealed class and a non-abstract sealed class is that the former is equivalent to a Union of its subclasses, while the latter acts like a regular type and merely prevents further direct subclassing from typechecking.) - Following on, sealed abstract subclasses should not be included in the sealed list, as they are not directly instantiable. However, non-sealed abstract subclasses should still be included, because they can be subclassed outside of the file, so while no instance of the class itself will ever exist, an instance of a second-level subclass might. - Subclasses of sealed classes that are not sealed should not be automatically final. That is, if A is sealed, and B, C, and D inherit from it in the same file, it should be typecheckable to inherit from B, C, or D from another file unless they are explicitly marked final. As far as I can tell, this is consistent with all three languages mentioned above. Note: All final classes are sealed classes, since final is just sealed with no subclasses. However, sealed classes are not necessarily final. Prior art: - PEP 622 <https://peps.python.org/pep-0622/#sealed-classes-as-algebraic-data-types>, before it was split, would have introduced sealed classes. However, it implied that sealed classes would also be automatically abstract, because the examples did not include the base class in the equivalent Union. As mentioned above, I believe abstractness and sealing should be distinct. - John T. Hagen (CC'd, hope you don't mind) made a draft PEP <https://github.com/johnthagen/sealed-typing-pep> that includes some nice additional details. However, since its core idea is taken from PEP 622, it also suffers from the same conflation of abstractness and sealing. (It also isn't a proper fork of the python/peps <https://github.com/python/peps> repository, as demanded by the PEP submission process <https://peps.python.org/pep-0001/#submitting-a-pep>.) - Kotlin, Scala, and Java (as of certain versions) support sealed as a language construct. However, Kotlin forces sealing to imply abstractness <https://kotlinlang.org/docs/sealed-classes.html#:~:text=A%20sealed%20class%2...>, while Scala <https://scastie.scala-lang.org/4PaP8zaGS5iNwqYQVks2Hg> and Java <https://openjdk.org/jeps/409#:~:text=public%20sealed%20class%20Rectangle> support the distinction between sealed class and sealed abstract class. (Links are to evidence.) This at least shows precedent for distinguishing between them. User-facing implementation and open questions: - Guido has already expressed his preference for a decorator over a base class or metaclass, since decorators can combine while metaclasses can't. I'd tend to agree on the merits. - Currently, @typing.final does not do any runtime prevention of subclassing. Does this mean @typing.sealed shouldn't either? It probably wouldn't be too much of a hassle to add a runtime=True parameter to the decorator (or something) that inserts an __init_subclass__ or something, but it could also just be left as a typechecking-only thing. I'm willing to draft up a proper PEP for this if that's appropriate. Beyond that, though, I just want to get some feedback, discussion, and thoughts going (again). Hopefully I can at least accomplish that! Best regards, AbyxDev; https://abyx.dev
Hi Typing-SIG, Just wanting to revive this thread <https://mail.python.org/archives/list/typing-sig@python.org/thread/QSCT2N4RF...>. I recently came across a use case for sealed classes in the process of making my mahjong game engine library <https://github.com/Kenny2github/mahjong> (still WIP), and was disappointed that no such concept was available. Specifically, mahjong/qna.py <https://github.com/Kenny2github/mahjong/blob/master/mahjong/qna.py> defines the Q&A API, where the idea is to check against each question subclass (see example in mahjong/__main__.py <https://github.com/Kenny2github/mahjong/blob/master/mahjong/__main__.py#L59-...>) and answer the question with the correct type. Currently I can mimic sealed classes (in the typechecking sense) by defining an alias to a Union of the subclasses and annotating with that instead of the base class. By doing this, I can make assert_never(question) typecheck if I apply this patch: diff --git a/mahjong/__main__.py b/mahjong/__main__.py index b8da955..dfa760e 100644 --- a/mahjong/__main__.py +++ b/mahjong/__main__.py @@ -27,6 +27,7 @@ for p in game.players: #''' #''' # uncomment this opening triple quote when commenting out one elsewhere import sys +from typing_extensions import assert_never from mahjong.game import Game, Hand from mahjong import qna @@ -115,11 +116,13 @@ while question is not None: elif isinstance(question, qna.HandEnding): if isinstance(question, qna.Goulash): print('Goulash! Nobody wins. Starting next game...') - else: + elif isinstance(question, (qna.DealerWon, qna.NormalHandEnding)): print('Player #%s won with %s (%s faan; %s points; %s)! Starting next game...' % ( question.winner.seat.value, ','.join(map(str, question.choice)), question.faan()[0], *question.points(1) )) + else: + assert_never(question) question = question.answer() print('Game Over!') #''' On the other hand, if I remove the Union alias and change all references to HandEndingType to refer to HandEnding, Pylance reports the following:
Argument of type "HandEnding" cannot be assigned to parameter "__arg" of type "NoReturn" in function "assert_never" Type "HandEnding" cannot be assigned to type "NoReturn"
This problem is not solved by making HandEnding an ABC, because the fundamental issue is that other subclasses could be defined outside of the file that would pass an isinstance(question, qna.HandEnding) check. Being able to mark HandEnding and UserIO as sealed would allow the assert_never(question) call to typecheck without having to re-enumerate the subclasses in a Union alias. In addition, what would further help my use case is if subclasses of sealed classes were themselves sealable. Marking the PlayeredIO and ArrivedIO subclasses of UserIO as sealed would allow an assert_never(question) in the UserIO if-elif tower to typecheck as well. (The latter is supported by all languages that support sealed classes, but I feel like it's worth explicitly mentioning.) Additional properties I believe sealed classes should have: - Though I only have contrived examples for this, I feel like sealed classes should *not* be automatically marked abstract. A class that is marked sealed alone should be treated like a Union of the subclasses *and the base class*, to allow for the possibility of directly instantiating the base class. If the base class is not to be included in the Union, it should be explicitly and additionally marked abstract, which would then preclude it from being instantiated and thus exclude it from the sealed list. (Since Unions fold subclasses into their parent classes, this means that in practice the difference between an abstract sealed class and a non-abstract sealed class is that the former is equivalent to a Union of its subclasses, while the latter acts like a regular type and merely prevents further direct subclassing from typechecking.) - Following on, sealed abstract subclasses should not be included in the sealed list, as they are not directly instantiable. However, non-sealed abstract subclasses should still be included, because they can be subclassed outside of the file, so while no instance of the class itself will ever exist, an instance of a second-level subclass might. - Subclasses of sealed classes that are not sealed should not be automatically final. That is, if A is sealed, and B, C, and D inherit from it in the same file, it should be typecheckable to inherit from B, C, or D from another file unless they are explicitly marked final. As far as I can tell, this is consistent with all three languages mentioned above. Note: All final classes are sealed classes, since final is just sealed with no subclasses. However, sealed classes are not necessarily final. Prior art: - PEP 622 <https://peps.python.org/pep-0622/#sealed-classes-as-algebraic-data-types>, before it was split, would have introduced sealed classes. However, it implied that sealed classes would also be automatically abstract, because the examples did not include the base class in the equivalent Union. As mentioned above, I believe abstractness and sealing should be distinct. - John T. Hagen (CC'd, hope you don't mind) made a draft PEP <https://github.com/johnthagen/sealed-typing-pep> that includes some nice additional details. However, since its core idea is taken from PEP 622, it also suffers from the same conflation of abstractness and sealing. (It also isn't a proper fork of the python/peps <https://github.com/python/peps> repository, as demanded by the PEP submission process <https://peps.python.org/pep-0001/#submitting-a-pep>.) - Kotlin, Scala, and Java (as of certain versions) support sealed as a language construct. However, Kotlin forces sealing to imply abstractness <https://kotlinlang.org/docs/sealed-classes.html#:~:text=A%20sealed%20class%2...>, while Scala <https://scastie.scala-lang.org/4PaP8zaGS5iNwqYQVks2Hg> and Java <https://openjdk.org/jeps/409#:~:text=public%20sealed%20class%20Rectangle> support the distinction between sealed class and sealed abstract class. (Links are to evidence.) This at least shows precedent for distinguishing between them. User-facing implementation and open questions: - Guido has already expressed his preference for a decorator over a base class or metaclass, since decorators can combine while metaclasses can't. I'd tend to agree on the merits. - Currently, @typing.final does not do any runtime prevention of subclassing. Does this mean @typing.sealed shouldn't either? It probably wouldn't be too much of a hassle to add a runtime=True parameter to the decorator (or something) that inserts an __init_subclass__ or something, but it could also just be left as a typechecking-only thing. I'm willing to draft up a proper PEP for this if that's appropriate. Beyond that, though, I just want to get some feedback, discussion, and thoughts going (again). Hopefully I can at least accomplish that! Best regards, AbyxDev; https://abyx.dev P.S. If anyone sees this twice, it's because I'm not familiar with the mechanics of mailing lists - sorry!
Hi AbyxDev, Maybe you and John T Hagen can revive the PEP together? I think there are plenty of people who would like to see sealed classes appear in Python's type system, though there are probably also plenty who are opposed. A PEP would be a good way to ferret out the best design and figure out how to deal with the opposition (or maybe they have a point and the PEP can be drafted and then withdrawn). --Guido On Wed, Jul 27, 2022 at 2:13 PM AbyxDev <ken@abyx.dev> wrote:
Hi Typing-SIG,
Just wanting to revive this thread <https://mail.python.org/archives/list/typing-sig@python.org/thread/QSCT2N4RF...>. I recently came across a use case for sealed classes in the process of making my mahjong game engine library <https://github.com/Kenny2github/mahjong> (still WIP), and was disappointed that no such concept was available. Specifically, mahjong/qna.py <https://github.com/Kenny2github/mahjong/blob/master/mahjong/qna.py> defines the Q&A API, where the idea is to check against each question subclass (see example in mahjong/__main__.py <https://github.com/Kenny2github/mahjong/blob/master/mahjong/__main__.py#L59-...>) and answer the question with the correct type.
Currently I can mimic sealed classes (in the typechecking sense) by defining an alias to a Union of the subclasses and annotating with that instead of the base class. By doing this, I can make assert_never(question) typecheck if I apply this patch:
diff --git a/mahjong/__main__.py b/mahjong/__main__.py index b8da955..dfa760e 100644 --- a/mahjong/__main__.py +++ b/mahjong/__main__.py @@ -27,6 +27,7 @@ for p in game.players: #''' #''' # uncomment this opening triple quote when commenting out one elsewhere import sys +from typing_extensions import assert_never from mahjong.game import Game, Hand from mahjong import qna
@@ -115,11 +116,13 @@ while question is not None: elif isinstance(question, qna.HandEnding): if isinstance(question, qna.Goulash): print('Goulash! Nobody wins. Starting next game...') - else: + elif isinstance(question, (qna.DealerWon, qna.NormalHandEnding)): print('Player #%s won with %s (%s faan; %s points; %s)! Starting next game...' % ( question.winner.seat.value, ','.join(map(str, question.choice)), question.faan()[0], *question.points(1) )) + else: + assert_never(question) question = question.answer() print('Game Over!') #'''
On the other hand, if I remove the Union alias and change all references to HandEndingType to refer to HandEnding, Pylance reports the following:
Argument of type "HandEnding" cannot be assigned to parameter "__arg" of type "NoReturn" in function "assert_never" Type "HandEnding" cannot be assigned to type "NoReturn"
This problem is not solved by making HandEnding an ABC, because the fundamental issue is that other subclasses could be defined outside of the file that would pass an isinstance(question, qna.HandEnding) check.
Being able to mark HandEnding and UserIO as sealed would allow the assert_never(question) call to typecheck without having to re-enumerate the subclasses in a Union alias. In addition, what would further help my use case is if subclasses of sealed classes were themselves sealable. Marking the PlayeredIO and ArrivedIO subclasses of UserIO as sealed would allow an assert_never(question) in the UserIO if-elif tower to typecheck as well. (The latter is supported by all languages that support sealed classes, but I feel like it's worth explicitly mentioning.)
Additional properties I believe sealed classes should have:
- Though I only have contrived examples for this, I feel like sealed classes should *not* be automatically marked abstract. A class that is marked sealed alone should be treated like a Union of the subclasses *and the base class*, to allow for the possibility of directly instantiating the base class. If the base class is not to be included in the Union, it should be explicitly and additionally marked abstract, which would then preclude it from being instantiated and thus exclude it from the sealed list. (Since Unions fold subclasses into their parent classes, this means that in practice the difference between an abstract sealed class and a non-abstract sealed class is that the former is equivalent to a Union of its subclasses, while the latter acts like a regular type and merely prevents further direct subclassing from typechecking.) - Following on, sealed abstract subclasses should not be included in the sealed list, as they are not directly instantiable. However, non-sealed abstract subclasses should still be included, because they can be subclassed outside of the file, so while no instance of the class itself will ever exist, an instance of a second-level subclass might. - Subclasses of sealed classes that are not sealed should not be automatically final. That is, if A is sealed, and B, C, and D inherit from it in the same file, it should be typecheckable to inherit from B, C, or D from another file unless they are explicitly marked final. As far as I can tell, this is consistent with all three languages mentioned above.
Note: All final classes are sealed classes, since final is just sealed with no subclasses. However, sealed classes are not necessarily final.
Prior art:
- PEP 622 <https://peps.python.org/pep-0622/#sealed-classes-as-algebraic-data-types>, before it was split, would have introduced sealed classes. However, it implied that sealed classes would also be automatically abstract, because the examples did not include the base class in the equivalent Union. As mentioned above, I believe abstractness and sealing should be distinct. - John T. Hagen (CC'd, hope you don't mind) made a draft PEP <https://github.com/johnthagen/sealed-typing-pep> that includes some nice additional details. However, since its core idea is taken from PEP 622, it also suffers from the same conflation of abstractness and sealing. (It also isn't a proper fork of the python/peps <https://github.com/python/peps> repository, as demanded by the PEP submission process <https://peps.python.org/pep-0001/#submitting-a-pep> .) - Kotlin, Scala, and Java (as of certain versions) support sealed as a language construct. However, Kotlin forces sealing to imply abstractness <https://kotlinlang.org/docs/sealed-classes.html#:~:text=A%20sealed%20class%2...>, while Scala <https://scastie.scala-lang.org/4PaP8zaGS5iNwqYQVks2Hg> and Java <https://openjdk.org/jeps/409#:~:text=public%20sealed%20class%20Rectangle> support the distinction between sealed class and sealed abstract class. (Links are to evidence.) This at least shows precedent for distinguishing between them.
User-facing implementation and open questions:
- Guido has already expressed his preference for a decorator over a base class or metaclass, since decorators can combine while metaclasses can't. I'd tend to agree on the merits. - Currently, @typing.final does not do any runtime prevention of subclassing. Does this mean @typing.sealed shouldn't either? It probably wouldn't be too much of a hassle to add a runtime=True parameter to the decorator (or something) that inserts an __init_subclass__ or something, but it could also just be left as a typechecking-only thing.
I'm willing to draft up a proper PEP for this if that's appropriate. Beyond that, though, I just want to get some feedback, discussion, and thoughts going (again). Hopefully I can at least accomplish that!
Best regards, AbyxDev; https://abyx.dev _______________________________________________ Typing-sig mailing list -- typing-sig@python.org To unsubscribe send an email to typing-sig-leave@python.org https://mail.python.org/mailman3/lists/typing-sig.python.org/ Member address: guido@python.org
-- --Guido van Rossum (python.org/~guido) *Pronouns: he/him **(why is my pronoun here?)* <http://feministing.com/2015/02/03/how-using-they-as-a-singular-pronoun-can-c...>
AbyxDev, if you read this, I got a bounce from your mailserver. If you're going to be that reclusive maybe this isn't the right forum for you? :-) On Wed, Jul 27, 2022 at 8:22 PM Guido van Rossum <guido@python.org> wrote:
Hi AbyxDev,
Maybe you and John T Hagen can revive the PEP together?
I think there are plenty of people who would like to see sealed classes appear in Python's type system, though there are probably also plenty who are opposed. A PEP would be a good way to ferret out the best design and figure out how to deal with the opposition (or maybe they have a point and the PEP can be drafted and then withdrawn).
--Guido
On Wed, Jul 27, 2022 at 2:13 PM AbyxDev <ken@abyx.dev> wrote:
Hi Typing-SIG,
Just wanting to revive this thread <https://mail.python.org/archives/list/typing-sig@python.org/thread/QSCT2N4RF...>. I recently came across a use case for sealed classes in the process of making my mahjong game engine library <https://github.com/Kenny2github/mahjong> (still WIP), and was disappointed that no such concept was available. Specifically, mahjong/qna.py <https://github.com/Kenny2github/mahjong/blob/master/mahjong/qna.py> defines the Q&A API, where the idea is to check against each question subclass (see example in mahjong/__main__.py <https://github.com/Kenny2github/mahjong/blob/master/mahjong/__main__.py#L59-...>) and answer the question with the correct type.
Currently I can mimic sealed classes (in the typechecking sense) by defining an alias to a Union of the subclasses and annotating with that instead of the base class. By doing this, I can make assert_never(question) typecheck if I apply this patch:
diff --git a/mahjong/__main__.py b/mahjong/__main__.py index b8da955..dfa760e 100644 --- a/mahjong/__main__.py +++ b/mahjong/__main__.py @@ -27,6 +27,7 @@ for p in game.players: #''' #''' # uncomment this opening triple quote when commenting out one elsewhere import sys +from typing_extensions import assert_never from mahjong.game import Game, Hand from mahjong import qna
@@ -115,11 +116,13 @@ while question is not None: elif isinstance(question, qna.HandEnding): if isinstance(question, qna.Goulash): print('Goulash! Nobody wins. Starting next game...') - else: + elif isinstance(question, (qna.DealerWon, qna.NormalHandEnding)): print('Player #%s won with %s (%s faan; %s points; %s)! Starting next game...' % ( question.winner.seat.value, ','.join(map(str, question.choice)), question.faan()[0], *question.points(1) )) + else: + assert_never(question) question = question.answer() print('Game Over!') #'''
On the other hand, if I remove the Union alias and change all references to HandEndingType to refer to HandEnding, Pylance reports the following:
Argument of type "HandEnding" cannot be assigned to parameter "__arg" of type "NoReturn" in function "assert_never" Type "HandEnding" cannot be assigned to type "NoReturn"
This problem is not solved by making HandEnding an ABC, because the fundamental issue is that other subclasses could be defined outside of the file that would pass an isinstance(question, qna.HandEnding) check.
Being able to mark HandEnding and UserIO as sealed would allow the assert_never(question) call to typecheck without having to re-enumerate the subclasses in a Union alias. In addition, what would further help my use case is if subclasses of sealed classes were themselves sealable. Marking the PlayeredIO and ArrivedIO subclasses of UserIO as sealed would allow an assert_never(question) in the UserIO if-elif tower to typecheck as well. (The latter is supported by all languages that support sealed classes, but I feel like it's worth explicitly mentioning.)
Additional properties I believe sealed classes should have:
- Though I only have contrived examples for this, I feel like sealed classes should *not* be automatically marked abstract. A class that is marked sealed alone should be treated like a Union of the subclasses *and the base class*, to allow for the possibility of directly instantiating the base class. If the base class is not to be included in the Union, it should be explicitly and additionally marked abstract, which would then preclude it from being instantiated and thus exclude it from the sealed list. (Since Unions fold subclasses into their parent classes, this means that in practice the difference between an abstract sealed class and a non-abstract sealed class is that the former is equivalent to a Union of its subclasses, while the latter acts like a regular type and merely prevents further direct subclassing from typechecking.) - Following on, sealed abstract subclasses should not be included in the sealed list, as they are not directly instantiable. However, non-sealed abstract subclasses should still be included, because they can be subclassed outside of the file, so while no instance of the class itself will ever exist, an instance of a second-level subclass might. - Subclasses of sealed classes that are not sealed should not be automatically final. That is, if A is sealed, and B, C, and D inherit from it in the same file, it should be typecheckable to inherit from B, C, or D from another file unless they are explicitly marked final. As far as I can tell, this is consistent with all three languages mentioned above.
Note: All final classes are sealed classes, since final is just sealed with no subclasses. However, sealed classes are not necessarily final.
Prior art:
- PEP 622 <https://peps.python.org/pep-0622/#sealed-classes-as-algebraic-data-types>, before it was split, would have introduced sealed classes. However, it implied that sealed classes would also be automatically abstract, because the examples did not include the base class in the equivalent Union. As mentioned above, I believe abstractness and sealing should be distinct. - John T. Hagen (CC'd, hope you don't mind) made a draft PEP <https://github.com/johnthagen/sealed-typing-pep> that includes some nice additional details. However, since its core idea is taken from PEP 622, it also suffers from the same conflation of abstractness and sealing. (It also isn't a proper fork of the python/peps <https://github.com/python/peps> repository, as demanded by the PEP submission process <https://peps.python.org/pep-0001/#submitting-a-pep>.) - Kotlin, Scala, and Java (as of certain versions) support sealed as a language construct. However, Kotlin forces sealing to imply abstractness <https://kotlinlang.org/docs/sealed-classes.html#:~:text=A%20sealed%20class%2...>, while Scala <https://scastie.scala-lang.org/4PaP8zaGS5iNwqYQVks2Hg> and Java <https://openjdk.org/jeps/409#:~:text=public%20sealed%20class%20Rectangle> support the distinction between sealed class and sealed abstract class. (Links are to evidence.) This at least shows precedent for distinguishing between them.
User-facing implementation and open questions:
- Guido has already expressed his preference for a decorator over a base class or metaclass, since decorators can combine while metaclasses can't. I'd tend to agree on the merits. - Currently, @typing.final does not do any runtime prevention of subclassing. Does this mean @typing.sealed shouldn't either? It probably wouldn't be too much of a hassle to add a runtime=True parameter to the decorator (or something) that inserts an __init_subclass__ or something, but it could also just be left as a typechecking-only thing.
I'm willing to draft up a proper PEP for this if that's appropriate. Beyond that, though, I just want to get some feedback, discussion, and thoughts going (again). Hopefully I can at least accomplish that!
Best regards, AbyxDev; https://abyx.dev _______________________________________________ Typing-sig mailing list -- typing-sig@python.org To unsubscribe send an email to typing-sig-leave@python.org https://mail.python.org/mailman3/lists/typing-sig.python.org/ Member address: guido@python.org
-- --Guido van Rossum (python.org/~guido) *Pronouns: he/him **(why is my pronoun here?)* <http://feministing.com/2015/02/03/how-using-they-as-a-singular-pronoun-can-c...>
-- --Guido van Rossum (python.org/~guido) *Pronouns: he/him **(why is my pronoun here?)* <http://feministing.com/2015/02/03/how-using-they-as-a-singular-pronoun-can-c...>
Hi John, see below - On Wed, Jul 27, 2022 at 11:22 PM Guido van Rossum <guido@python.org> wrote:
Hi AbyxDev,
Maybe you and John T Hagen can revive the PEP together?
I think there are plenty of people who would like to see sealed classes appear in Python's type system, though there are probably also plenty who are opposed. A PEP would be a good way to ferret out the best design and figure out how to deal with the opposition (or maybe they have a point and the PEP can be drafted and then withdrawn).
--Guido
On Wed, Jul 27, 2022 at 2:13 PM AbyxDev <ken@abyx.dev> wrote:
- snip -
Do you have any preferences for going about this? I'll let this ruminate for a few days so that you can give me a shout if you want in; if I don't hear from you by then I'll just go ahead and write up a full PEP myself. Thanks! -- Best regards, AbyxDev; https://abyx.dev
I'm strongly negative on the idea of sealed classes. I've explained my reasoning in this thread: https://mail.python.org/archives/list/typing-sig@python.org/thread/7TB36OWSW.... As you point out at the top of this thread, there's already a good solution to this problem using unions. This solution is already supported by the Python type system, and it works with all type checkers. --- Eric Traut Contributor to Pyright & Pylance Microsoft
Thanks for the thread link, Eric. Somehow my searching missed that one. That sheds a lot of light on John and David's involvement in the discussion (and on the "rejected ideas" section of John's original PEP draft). On Thu, Jul 28, 2022 at 2:40 AM Eric Traut <eric@traut.com> wrote:
As you point out at the top of this thread, there's already a good solution to this problem using unions. This solution is already supported by the Python type system, and it works with all type checkers.
What I point out at the top of the thread is that there is already a solution. The point of my post, though, is that the solution is not a good one. However, I will read the thread you linked in full before strengthening any opinions - lots of interesting discussion there to see. -- Best regards, AbyxDev; https://abyx.dev
I'm looping in David, who did a lot of work on the current draft PEP. On Wed, Jul 27, 2022, 11:22 PM Guido van Rossum <guido@python.org> wrote:
Hi AbyxDev,
Maybe you and John T Hagen can revive the PEP together?
I think there are plenty of people who would like to see sealed classes appear in Python's type system, though there are probably also plenty who are opposed. A PEP would be a good way to ferret out the best design and figure out how to deal with the opposition (or maybe they have a point and the PEP can be drafted and then withdrawn).
--Guido
On Wed, Jul 27, 2022 at 2:13 PM AbyxDev <ken@abyx.dev> wrote:
Hi Typing-SIG,
Just wanting to revive this thread <https://mail.python.org/archives/list/typing-sig@python.org/thread/QSCT2N4RF...>. I recently came across a use case for sealed classes in the process of making my mahjong game engine library <https://github.com/Kenny2github/mahjong> (still WIP), and was disappointed that no such concept was available. Specifically, mahjong/qna.py <https://github.com/Kenny2github/mahjong/blob/master/mahjong/qna.py> defines the Q&A API, where the idea is to check against each question subclass (see example in mahjong/__main__.py <https://github.com/Kenny2github/mahjong/blob/master/mahjong/__main__.py#L59-...>) and answer the question with the correct type.
Currently I can mimic sealed classes (in the typechecking sense) by defining an alias to a Union of the subclasses and annotating with that instead of the base class. By doing this, I can make assert_never(question) typecheck if I apply this patch:
diff --git a/mahjong/__main__.py b/mahjong/__main__.py index b8da955..dfa760e 100644 --- a/mahjong/__main__.py +++ b/mahjong/__main__.py @@ -27,6 +27,7 @@ for p in game.players: #''' #''' # uncomment this opening triple quote when commenting out one elsewhere import sys +from typing_extensions import assert_never from mahjong.game import Game, Hand from mahjong import qna
@@ -115,11 +116,13 @@ while question is not None: elif isinstance(question, qna.HandEnding): if isinstance(question, qna.Goulash): print('Goulash! Nobody wins. Starting next game...') - else: + elif isinstance(question, (qna.DealerWon, qna.NormalHandEnding)): print('Player #%s won with %s (%s faan; %s points; %s)! Starting next game...' % ( question.winner.seat.value, ','.join(map(str, question.choice)), question.faan()[0], *question.points(1) )) + else: + assert_never(question) question = question.answer() print('Game Over!') #'''
On the other hand, if I remove the Union alias and change all references to HandEndingType to refer to HandEnding, Pylance reports the following:
Argument of type "HandEnding" cannot be assigned to parameter "__arg" of type "NoReturn" in function "assert_never" Type "HandEnding" cannot be assigned to type "NoReturn"
This problem is not solved by making HandEnding an ABC, because the fundamental issue is that other subclasses could be defined outside of the file that would pass an isinstance(question, qna.HandEnding) check.
Being able to mark HandEnding and UserIO as sealed would allow the assert_never(question) call to typecheck without having to re-enumerate the subclasses in a Union alias. In addition, what would further help my use case is if subclasses of sealed classes were themselves sealable. Marking the PlayeredIO and ArrivedIO subclasses of UserIO as sealed would allow an assert_never(question) in the UserIO if-elif tower to typecheck as well. (The latter is supported by all languages that support sealed classes, but I feel like it's worth explicitly mentioning.)
Additional properties I believe sealed classes should have:
- Though I only have contrived examples for this, I feel like sealed classes should *not* be automatically marked abstract. A class that is marked sealed alone should be treated like a Union of the subclasses *and the base class*, to allow for the possibility of directly instantiating the base class. If the base class is not to be included in the Union, it should be explicitly and additionally marked abstract, which would then preclude it from being instantiated and thus exclude it from the sealed list. (Since Unions fold subclasses into their parent classes, this means that in practice the difference between an abstract sealed class and a non-abstract sealed class is that the former is equivalent to a Union of its subclasses, while the latter acts like a regular type and merely prevents further direct subclassing from typechecking.) - Following on, sealed abstract subclasses should not be included in the sealed list, as they are not directly instantiable. However, non-sealed abstract subclasses should still be included, because they can be subclassed outside of the file, so while no instance of the class itself will ever exist, an instance of a second-level subclass might. - Subclasses of sealed classes that are not sealed should not be automatically final. That is, if A is sealed, and B, C, and D inherit from it in the same file, it should be typecheckable to inherit from B, C, or D from another file unless they are explicitly marked final. As far as I can tell, this is consistent with all three languages mentioned above.
Note: All final classes are sealed classes, since final is just sealed with no subclasses. However, sealed classes are not necessarily final.
Prior art:
- PEP 622 <https://peps.python.org/pep-0622/#sealed-classes-as-algebraic-data-types>, before it was split, would have introduced sealed classes. However, it implied that sealed classes would also be automatically abstract, because the examples did not include the base class in the equivalent Union. As mentioned above, I believe abstractness and sealing should be distinct. - John T. Hagen (CC'd, hope you don't mind) made a draft PEP <https://github.com/johnthagen/sealed-typing-pep> that includes some nice additional details. However, since its core idea is taken from PEP 622, it also suffers from the same conflation of abstractness and sealing. (It also isn't a proper fork of the python/peps <https://github.com/python/peps> repository, as demanded by the PEP submission process <https://peps.python.org/pep-0001/#submitting-a-pep>.) - Kotlin, Scala, and Java (as of certain versions) support sealed as a language construct. However, Kotlin forces sealing to imply abstractness <https://kotlinlang.org/docs/sealed-classes.html#:~:text=A%20sealed%20class%2...>, while Scala <https://scastie.scala-lang.org/4PaP8zaGS5iNwqYQVks2Hg> and Java <https://openjdk.org/jeps/409#:~:text=public%20sealed%20class%20Rectangle> support the distinction between sealed class and sealed abstract class. (Links are to evidence.) This at least shows precedent for distinguishing between them.
User-facing implementation and open questions:
- Guido has already expressed his preference for a decorator over a base class or metaclass, since decorators can combine while metaclasses can't. I'd tend to agree on the merits. - Currently, @typing.final does not do any runtime prevention of subclassing. Does this mean @typing.sealed shouldn't either? It probably wouldn't be too much of a hassle to add a runtime=True parameter to the decorator (or something) that inserts an __init_subclass__ or something, but it could also just be left as a typechecking-only thing.
I'm willing to draft up a proper PEP for this if that's appropriate. Beyond that, though, I just want to get some feedback, discussion, and thoughts going (again). Hopefully I can at least accomplish that!
Best regards, AbyxDev; https://abyx.dev _______________________________________________ Typing-sig mailing list -- typing-sig@python.org To unsubscribe send an email to typing-sig-leave@python.org https://mail.python.org/mailman3/lists/typing-sig.python.org/ Member address: guido@python.org
-- --Guido van Rossum (python.org/~guido) *Pronouns: he/him **(why is my pronoun here?)* <http://feministing.com/2015/02/03/how-using-they-as-a-singular-pronoun-can-c...>
Maybe you and John T Hagen can revive the PEP together?
The feedback from the last discussion [1] fell into three camps: (1) Yes, we want sealed. (2) No, we think Union is fine. (3) Sure, if we have to, but we would rather have full ADT enums (aka Rust-style enums). Due the strength of position 3, we were planning on rewriting the draft PEP to feature ADT enums. And then if that fails, retrying with the simpler `sealed` PEP. But if you think we it would be better to just submit the `sealed` PEP as is or submit dueling PEPs, we will do that. [1] https://mail.python.org/archives/list/typing-sig@python.org/thread/7TB36OWSW...
From my perspective, the "sealed class" proposal is a no-go. It violates a key tenet that lazy type evaluators like pyright depend upon for efficient type evaluation. Namely, it requires the type analysis of a full file (or at least all of the classes defined within that file) to discover all of the subclasses of the sealed class. If you pursue the sealed class approach, it's quite likely that pyright and pylance will abstain from implementing it. So my recommendation is to pursue options 2 or 3. -- Eric Traut Contributor to Pyright & Pylance Microsoft
I think threatening that a type checker won't implement a feature is not a good way of supporting an argument. That said, I'm curious to learn more about ADTs / Rust-style enums and what shape they would take in Python. On Thu, Jul 28, 2022 at 9:32 AM Eric Traut <eric@traut.com> wrote:
From my perspective, the "sealed class" proposal is a no-go. It violates a key tenet that lazy type evaluators like pyright depend upon for efficient type evaluation. Namely, it requires the type analysis of a full file (or at least all of the classes defined within that file) to discover all of the subclasses of the sealed class. If you pursue the sealed class approach, it's quite likely that pyright and pylance will abstain from implementing it.
So my recommendation is to pursue options 2 or 3.
-- Eric Traut Contributor to Pyright & Pylance Microsoft _______________________________________________ Typing-sig mailing list -- typing-sig@python.org To unsubscribe send an email to typing-sig-leave@python.org https://mail.python.org/mailman3/lists/typing-sig.python.org/ Member address: guido@python.org
-- --Guido van Rossum (python.org/~guido) *Pronouns: he/him **(why is my pronoun here?)* <http://feministing.com/2015/02/03/how-using-they-as-a-singular-pronoun-can-c...>
I don’t think this was a threat, I think this was more a statement about how we *cannot* support it with a lazy architecture (and the architecture isn’t going to change given that Pylance is first and foremost a language server that needs to react swiftly to edits). On Thu, Jul 28, 2022 at 9:58 AM Guido van Rossum <guido@python.org> wrote:
I think threatening that a type checker won't implement a feature is not a good way of supporting an argument.
That said, I'm curious to learn more about ADTs / Rust-style enums and what shape they would take in Python.
On Thu, Jul 28, 2022 at 9:32 AM Eric Traut <eric@traut.com> wrote:
From my perspective, the "sealed class" proposal is a no-go. It violates a key tenet that lazy type evaluators like pyright depend upon for efficient type evaluation. Namely, it requires the type analysis of a full file (or at least all of the classes defined within that file) to discover all of the subclasses of the sealed class. If you pursue the sealed class approach, it's quite likely that pyright and pylance will abstain from implementing it.
So my recommendation is to pursue options 2 or 3.
-- Eric Traut Contributor to Pyright & Pylance Microsoft _______________________________________________ Typing-sig mailing list -- typing-sig@python.org To unsubscribe send an email to typing-sig-leave@python.org https://mail.python.org/mailman3/lists/typing-sig.python.org/ Member address: guido@python.org
-- --Guido van Rossum (python.org/~guido) *Pronouns: he/him **(why is my pronoun here?)* <http://feministing.com/2015/02/03/how-using-they-as-a-singular-pronoun-can-c...> _______________________________________________ Typing-sig mailing list -- typing-sig@python.org To unsubscribe send an email to typing-sig-leave@python.org https://mail.python.org/mailman3/lists/typing-sig.python.org/ Member address: gram@geekraver.com
@Eric Traut: would it be possible to avoid type analysis of a full file if sealed was introduced as a context manager? ``` with typing.sealed: class PytestDeprecationWarning(DeprecationWarning): pass class PytestRemovedIn8Warning(PytestDeprecationWarning): pass class PytestReturnNotNoneWarning(PytestRemovedIn8Warning): pass class CustomWarning(PytestReturnNotNoneWarning): # type error here pass ```
@Thomas Grainger, that approach would make things even worse for a lazy (just-in-time) type evaluator. The problem is that when the type analyzer is told that a variable is of type `DeprecationWarning`, it needs to somehow determine that `DeprecationWarning` should be treated as though it is a union of other classes that derive from `DeprecationWarning` for purposes of type narrowing and type exhaustion checking. That's the basic problem we're trying to solve here. With the `@sealed` (decorator) approach, a lazy analyzer would at least know that `DeprecationWarning` is sealed and should be treated specially. It would then need to examine all of the class declarations in the file (perhaps hundreds or thousands in the worst case) to determine which of them derive from the sealed base class. That means the type evaluation performance when using a `@sealed` class could be really bad, but type analysis of non-sealed classes would be unaffected. With the context manager approach, the type analyzer wouldn't even know that `DeprecationWarning` is a sealed class to begin with. It would need to assume that _any_ class is potentially sealed and always analyze all of the classes in a file to determine which ones derive from that class. The fact that some of these class declarations are located within a `with` statement body could be used as an optimization clue, but any class that is declared within a file that has other classes declared within a `with` statement would need to be analyzed as though it is potentially sealed.
On Thu, Jul 28, 2022 at 7:55 AM David Hagen <david@drhagen.com> wrote:
Due the strength of position 3, we were planning on rewriting the draft PEP to feature ADT enums. And then if that fails, retrying with the simpler `sealed` PEP. But if you think we it would be better to just submit the `sealed` PEP as is or submit dueling PEPs, we will do that.
If duelling PEPs are a healthy option, I think that might be the best idea here, mostly because I personally am conflicted on implementation options. Elaboration follows. Let me get this out of the way first - the currently possible Union solutions are less than ideal. Using a Union exclusively is a complete no-go for my use case (I need inheritance), and using a Union + base class is confusing, clunky, and difficult to maintain. As Daniel Cooper put it <https://mail.python.org/archives/list/typing-sig@python.org/message/6U6R43DU...> in the last thread:
it feels inelegant to have 2 types that conceptually represent the same point in the type hierarchy, and can create confusing when trying to use one in an invalid way. It also creates an opportunity for those points to, unintentionally diverge.
That is, it's confusing if you accidentally use the wrong type (not easy to use), and easy for the types to accidentally diverge (not easy to maintain). Anyway. Back to implementation options. My use case strongly relies on being able to inherit from the base class - I need to be able to use UserIO.answer() and HandEnding.answer(), since all subclasses call the super method. That means as-is, all of the enum-style ADT implementations would fail to support my use case, since the containing class' name is not bound until the class body finishes executing, preventing inheritance. (Another option would be to define a base class separate from the enum class for method implementations, but that has the same duality drawbacks as a Union + base class.) However, if mechanics were introduced to allow classes defined inside another class' body to inherit from the containing class, or at least for that to be the case for ADTs, then I would not be opposed to the enum-style implementation. My only peeve would be the fact that, by its nature, this would be the only case where inheriting from a class inside its body makes sense. An example of how I might use enum-style sealed classes: @dataclass class UserIO(ABC): """A question to be answered.""" gen: Generator def answer(self, ans=None): """Answer the question. Args: ans: The answer. See specific subclasses for details. """ return _answer(self.gen, ans) @dataclass class ReadyQ(UserIO): """The round has just ended, are you ready to continue to the next one?""" def answer(self): """Answer to continue.""" return super().answer() Pros: - Makes limitation to the same file natural. In Guido's words, it's "just like [how] all methods of a class must be defined in the same file". - Familiar to Rust devs. - Includes the potential for runtime enforcement, if that is desirable: do not allow subclassing the base class if the subclass is not defined within the base class' body. Cons: - Potentially unnatural to Python devs (classes are rarely defined inside other classes). - Requires code changes for regular inheritance to work. Questions: - Would this allow for the base class to be instantiated? As I've expressed before, I believe it should be possible unless marked otherwise as an ABC. ---------- If we leave out the enum-style implementations, then two types of implementations remain: The implementation prevalent in other languages, where inheritance restrictions are based on defining file; and implementations that involve listing the allowed subclasses. I am sympathetic to Eric Traut's objections to file-based inheritance restrictions, though moreso from the "moralistic" grounds that it would be an unnatural case of restricting things by file. (Enum-style ADTs also restrict by file, but that's fine, as explained above.) However, the advantage of file-based restrictions is that there is no need to specify the names of the subclasses twice. (The fact that the base class name is specified twice is irrelevant to me - that's part of inheritance, which is a necessary property in my use case.) The obvious disadvantage of listing the allowed subclasses is the converse of the advantage of file-based inheritance restrictions - it's cumbersome to specify the names twice. However, I am okay with that, because my main gripe with the Union + base class method is that it uses two different types depending on context; the re-specification is only a nit. We already have precedent for double-specifying names - the module attribute __all__. In fact, I would like to suggest a slight modification of the __sealed__ proposal that David came up with <https://mail.python.org/archives/list/typing-sig@python.org/message/ANCISLRU...> - a *class* attribute __all__: class Tree: ... __all__ = [ 'Node', 'Leaf', ] ... class Node(Tree): ... class Leaf(Tree): ... (Note that the difference between my suggestion and David's proposal is that David uses a class annotation while mine uses a class attribute.) Pros: - Familiar to anyone who has used module level __all__. - Like the module attribute, it uses strings from the outset, removing the requirement for a "from __future__ import annotations" statement. - Has a similar connotation to __all__ in that it limits a set of names to fewer than there otherwise would be. (For a module, only these names instead of all non-underscore-prefixed ones; for a class, only these names instead of all potential user-defined ones.) - Includes the potential for runtime enforcement, if that is desirable: a) do not allow classes with a name not in that list to (directly) inherit from the base class; b) once a class has been defined that (directly) inherits from the base class, do not allow more classes with the same name to be defined. The latter rule prevents people from working around the sealing by defining another subclass with the same name, so long as the name is claimed by the (e.g.) package author. - No code changes needed to preserve inheritance. - Natural to Python developers (dunder attributes on classes are well-established). - Allows for base class to be instantiated unless marked otherwise as an ABC. Cons: - Requires specifying names twice, though this has precedent. - Using the name __all__ could maybe lead to people thinking it defines all the names exported by the class (the direct equivalent to the module attribute), rather than its actual purpose. This is solvable by using David's original name, __sealed__, or maybe the ever-so-slightly clearer __sealed_to__. ---------- With all that out of the way, I now have questions for various people. - John and David, from what I understand, you guys see enum-style as the way to go for your PEP. After my email, is that still the case? (I'd imagine so, considering your basis was support, but I want to make sure.) - Eric, would you be more amenable to the __all__-style sealed classes I describe? (For that matter, I don't see any thoughts from you on David's __sealed__-style sealed classes. If my suggestion is distasteful to you, what do you think of his?) - Guido, do you have any recommendations re: whether or not to submit duelling PEPs? Long email, sorry! -- Best regards, AbyxDev; https://abyx.dev P.S. John and David, am I right in guessing that you're brothers?
Hi, I am one of the people cheering for Rust-style/Enum-style ADTs (although, as some people have pointed out to me, the term 'ADTs' is a little too broad; we're talking about sum types in particular). I just want to point out that one of the main benefits to this approach is the ability to have singletons (like in PEP 345 enums) alongside classes. I'd be sad if a proposal would be accepted that doesn't support this. I think currently you can get close with a union of classes and literals but it's very awkward. The sealed approach feels unnatural for this, no? In an PEP 345 enum, all the singletons automatically inherit from the wrapper class. Could we just do a PEP 345 Enum++ that, in addition to singletons, can include classes that also automatically inherit from the wrapper? In general I think taking PEP 345 a step further would be a natural way to introduce better sum types to Python. When you say you needed sum types for your engine, is https://github.com/Kenny2github/mahjong/blob/57a4cc4cc879dae2c0baa6c40a5458e... what you're referring to?
On Fri, Jul 29, 2022 at 4:42 PM Tin Tvrtković <tinchester@gmail.com> wrote:
I just want to point out that one of the main benefits to this approach is the ability to have singletons (like in PEP 345 enums) alongside classes.
Is PEP 345 the PEP you intended to refer to? That PEP is about Python package metadata and I don't see any references to "singletons" or "enums" there.
In an PEP 345 enum, all the singletons automatically inherit from the wrapper class. Could we just do a PEP 345 Enum++ that, in addition to singletons, can include classes that also automatically inherit from the wrapper? In general I think taking PEP 345 a step further would be a natural way to introduce better sum types to Python.
Assuming by "PEP 345 enum" you mean enums in the standard library enum module <https://docs.python.org/3/library/enum.html>, I think you've roughly restated the first of the two options I was conflicted between, though I guess you're thinking of some magic in a new Enum type that could optionally make contained classes automagically inherit from the wrapper, rather than changing naming semantics to allow explicitly inheriting from a class within its body. I'd probably also be amenable to your suggestion if that's the case, depending on the answer to my question about base class instantiability. The point of my email was to mention the two options and ask if submitting a duelling PEP for each would be appropriate. I have some clarifying questions for your suggestion, assuming I've understood it right. Given the below example: class Tree(SumEnum, inherit=True): # or whatever class Leaf: ... class Node(Mixin): ... 1. Are you saying that the would Leaf implicitly become class Leaf(Tree)? 2. Is Node allowed? 3. If so, is it implicitly class Node(Tree, Mixin) or class Node(Mixin, Tree)? When you say you needed sum types for your engine, is
https://github.com/Kenny2github/mahjong/blob/57a4cc4cc879dae2c0baa6c40a5458e... what you're referring to?
Correct, that and HandEndingType, near the end. -- Best regards, AbyxDev; https://abyx.dev
On Fri, Jul 29, 2022 at 11:05 PM AbyxDev <ken@abyx.dev> wrote:
Is PEP 345 the PEP you intended to refer to? That PEP is about Python package metadata and I don't see any references to "singletons" or "enums" there.
Apologies, I had a brain fart. What I'm trying to refer to is PEP 435 (which ultimately became the standard library enum module), not 345.
I have some clarifying questions for your suggestion, assuming I've understood it right. Given the below example:
class Tree(SumEnum, inherit=True): # or whatever class Leaf: ... class Node(Mixin): ...
1. Are you saying that the would Leaf implicitly become class Leaf(Tree)? 2. Is Node allowed? 3. If so, is it implicitly class Node(Tree, Mixin) or class Node(Mixin, Tree)?
I don't know, let's discuss it.
Given a super simple existing enum: ``` class MyEnum(enum.Enum): A = "a" def test(self) -> str: return str(self) ``` it's already the case that `isinstance(MyEnum.A, MyEnum)` and `MyEnum.A.test() == 'MyEnum.A'` so teeeeechnically (if you squint) there's precedent for the answers to 1) to be 'yes'. I could also see 2) being true too, and for 3) my gut feeling would be `Node(Mixin, Tree)`. But this is just me spitballing; I'd love to hear what other folks would think of this.
One way I had imagined this implemented is the "sum" equivalent of `dataclass`'s "product" like ``` @dataclass(sum=True) class MySumType: a: None # This field is like an `Enum` (singleton) field and has an implicit default of `None` b: int c: str = "a default" # generated init def __init__(self, *args, **kwargs): assert len(args) + len(kwargs) == 1 if args: setattr(self, args[0], get_default_or_raise(args[0])) else: name, value = kwargs.items().next() assert name in field_names setattr(self, name, value) a1 = MySumType(a=None) a2 = MySumType("a") # Any field with a default can be given by string name b = MySumType(b=5) c1 = MySumType(c="hello") c2 = MySumType("c") error1 = MySumType(b=1, c="bad") match c1: case MySumType(a=_): print("A") case MySumType(b=my_int): print(my_int + 1) case MySumType(c=my_str): print(f"{my_str} world") ``` This works nicely at runtime based on my limited experiments, but would need special type checker support like there currently is for "product" dataclasses.
My only peeve would be the fact that, by its nature, this would be the only case where inheriting from a class inside its body makes sense.
Back in the original thread, I proposed one more solution [1] to the "can't inherit from a class inside its body". Namely, once the outer class is done building, find each class defined in the body, build a copy of that class with the outer class injected into its bases, and overwrite the old class member with the new class. [1] https://mail.python.org/archives/list/typing-sig@python.org/message/HT2Q6GWS...
On Sat, Jul 30, 2022, 6:12 AM David Hagen <david@drhagen.com> wrote:
once the outer class is done building, find each class defined in the body, build a copy of that class with the outer class injected into its bases, and overwrite the old class member with the new class.
I believe this is what Tin Tvrtkovic had in mind with regards to extending the Enum class or possibly adding a new class in the enum module. Best regards, AbyxDev; https://abyx.dev
The consistent application of good design discipline by Jukka, Guido, and other contributors to the original Python static type system makes it possible today for a type checker to determine the minimal set of types that another type depends upon without analyzing a full source file or set of source files. All of the type features that have been added to the Python type system since then have been designed in a way that retains this ability. Pyright relies on this to deliver good performance for language server features like completion suggestions. It performs the minimal type analysis required to deliver an on-demand answer, usually within 10s of milliseconds. The `@sealed` proposal would be the first type feature that deviates from this design principle and would require all classes in a file to be analyzed before the type of the sealed class could be determined. Pylance, the language server built on top of pyright, is actively used by more than 4M Python developers. The vast majority of those developers do not care about static type checking, but they do care about accurate and fast completion suggestions, signature help, semantic highlighting, semantic search and rename, inlined documentation, and other interactive convenience features that rely on fast type analysis. The `@sealed` proposal has the potential to negatively impact the development experience for this large group of developers while providing a small benefit to a small group of developers. When we’re considering new typing features (or any language features, for that matter), we should take into account both the benefits of a proposal but also the harm it could do, and we should optimize for the greatest good. At least that’s my philosophy and my motivation for pushing back so hard on the `@sealed` proposal. The `__sealed__` or `__all__` proposals address the concern I highlight above because they explicitly list the subclasses associated with the sealed base class. However, I find these proposals to be inelegant and cumbersome. They violate a basic rule of object-oriented programming: a base class should not have knowledge of its subclasses. And in effect, they invent a new (and cumbersome) way to define a union type. To review, here is David’s proposal for `__sealed__`: ```python form __future__ import annotations class Tree: __sealed__: Node | Leaf class Node(Tree): ... class Leaf(Tree): ... ``` This is effectively telling a type checker to treat the symbol `Tree` as a union of `Node` and `Leaf`, but it’s using a new and unfamiliar way to declare this intent. The `__sealed__` example above is equivalent to the following code sample, which doesn’t require any new type features for users to understand or for type checkers to implement. It is no more work to maintain, and it doesn’t require any cumbersome stringified type references. ```python class _TreeBase: ... class Node(_TreeBase): ... class Leaf(_TreeBase): … Tree = Node | Leaf ``` Note that I’ve used an underscore for the name of the base class to indicate that it is a private implementation detail of this file and should not be used on its own. Only the union type (`Tree`) should be used for static typing purposes. The enum-based proposals are also problematic in that they invent a new and unfamiliar way to represent inheritance. They also muddy the concept of an enumeration, which has historically been used for an enumerated set of values, not an enumerate set of types. We already have a mechanism in the type system today to represent an enumerated set of types; it's called a union. Several of you have indicated that the union solution doesn’t meet your needs, but the counterproposals seem to be to be equivalent semantically, and the maintenance burden seems equivalent too. I’m struggling to understand why the union solution doesn't meet your needs but these other proposals do. @AbyxDev said:
Using a Union exclusively is a complete no-go for my use case (I need inheritance), and using a Union + base class is confusing, clunky, and difficult to maintain.
Can you elaborate? The union approach cleanly supports inheritance using the standard inheritance mechanisms. I don’t understand why you think it’s clunky or difficult to maintain. I use this union pattern regularly in my code, and I find it neither clunky nor difficult to maintain. Since the union mechanism is already fully supported in today’s type system, I encourage you to try it for your own use cases. Once you’ve tried it, perhaps you can report back on whether you still think it’s cumbersome and why. The union solution has the added benefit that it allows you to define different subsets of types through the use of different unions. Here’s a practical example from pyright's parser implementation (which is written in TypeScript, but the concepts are the same). https://github.com/microsoft/pyright/blob/74d43236f6fad8300fc4304c970857027b.... Here I define a type called `ParseNode` that is a union of all possible parse node types. All of these subtypes inherit from `ParseNodeBase`. You can see that I also define other unions that include a subset of these parse node types. For example, the `ExpressionNode` union (https://github.com/microsoft/pyright/blob/74d43236f6fad8300fc4304c970857027b...) includes only the parse node types that represent expressions. I find this to be both flexible and easy to maintain. It fully supports type exhaustion, which makes my code robust and immune to an entire class of regressions. Let’s back up and make sure that we understand the problem we’re trying to solve and the requirements that we’ve collected so far. The original problem statement (https://mail.python.org/archives/list/typing-sig@python.org/thread/QSCT2N4RF...) called for a mechanism that supports type exhaustion in if/else towers and match statements. Here are the requirements and desirables that I think we've collected so far: 1. The mechanism must produce a type that supports type exhaustion checks in if/else towers and match statements. 2. The mechanism must support inheritance. 3. The mechanism must not require full analysis of a source file to determine the type of a single class. 4. Preferably, the code should be easy to maintain — in particular, if new subclasses are added, they should either be automatically discovered or it should be possible for a type checker to guide the developer to do what is needed. 5. Preferably, the solution should not invent new and unfamiliar ways to express existing concepts in Python like inheritance or union types. Am I missing anything here? -- Eric traut Contributor to Pyright & Pylance Microsoft
Hey Eric, Thanks for your thoughts, they were insightful. On Sat, Jul 30, 2022 at 6:52 PM Eric Traut <eric@traut.com> wrote:
Here are the requirements and desirables that I think we've collected so
far:
1. The mechanism must produce a type that supports type exhaustion checks in if/else towers and match statements. 2. The mechanism must support inheritance. 3. The mechanism must not require full analysis of a source file to determine the type of a single class. 4. Preferably, the code should be easy to maintain — in particular, if new subclasses are added, they should either be automatically discovered or it should be possible for a type checker to guide the developer to do what is needed. 5. Preferably, the solution should not invent new and unfamiliar ways to express existing concepts in Python like inheritance or union types.
Am I missing anything here?
How about: * the mechanism must support a combination of types and values Thinking about this and your point about unions essentially being sufficient; what I want can be achieved today with a union of an enum and some classes. The simplest case to consider is how `Optional` would be implemented in a different way if `None` did not exist. ``` class OptionalEnum(Enum): NONE = auto() T = TypeVar("T") @dataclass class Some(Generic[T]): value: T Optional: TypeAlias = Some[T] | OptionalEnum def takes_opt(t: Optional[int]) -> None: match t: case OptionalEnum.NONE: print("got none") case Some(v): print(f"got some: {v}") case _: assert_never(t) ``` That's pretty good and works today. The problem is a user of this needs to import 3 things (OptionalEnum, Some, and Optional) to write a function similar to `takes_opt`. Maybe we could just add some syntax sugar for creating unions with attributes? Something like: ``` with create_union(T) as Optional: @Optional.register class OptionalEnum(Enum): NONE = auto() @Optional.register @dataclass class Some(Generic[T]): value: T # Here, Optional is equivalent to `OptionalEnum | Some` but with the two classes as extra attributes on itself. def takes_opt(t: Optional[int]) -> None: match t: case Optional.OptionalEnum.NONE: print("got none") case Optional.Some(v): print(f"got some: {v}") case _: assert_never(t) ``` this would require minimal changes to the type checkers, right? I'd be happy with something like this. * the mechanism should work in a PEP 695 world Probably worth mentioning.
[It is] possible today for a type checker to determine the minimal set of types that another type depends upon without analyzing a full source file or set of source files
I will admit that the way sealed works is weird. And I'll take your word for it that pyright can understand a type without processing the file in which that type is defined. That's actually really cool and not something I would expect to be possible with Python.
Only the union type (`Tree`) should be used for static typing purposes.
With your design in pyright, does autocompleting a variable of type `Tree` suggest just methods defined on `_TreeBase`? I ask because I get all the methods of `Node` and `Leaf` in PyCharm, which does not happen when autocompleting a variable of type `_TreeBase`. Theoretically, this is resolvable by finding the common base types of `Node` and `Leaf` (i.e. `_TreeBase`), but making it easier for type checkers to understand this is one of the motivations of mine in pursuing this feature.
Since the union mechanism is already fully supported in today’s type system, I encourage you to try it for your own use cases. Once you’ve tried it, perhaps you can report back on whether you still think it’s cumbersome and why.
The Union type can be made to work for static analysis. But there is no rescuing its total absence of runtime capabilities. Python could be fixed so that Union types work as cases in match, but there is no fixing `Tree` so that it exposes the class methods of `_TreeBase`. For example, I use a serialization library that inspects the type annotations to automatically generator serializers. If I use `Tree`, there is no way to customize the serialization of a `Tree` because there is no where to put the class methods that the serialization library calls.
David Hagen wrote:
The Union type can be made to work for static analysis. But there is no rescuing its total absence of runtime capabilities. Python could be fixed so that Union types work as cases in match, but there is no fixing `Tree` so that it exposes the class methods of `_TreeBase`. For example, I use a serialization library that inspects the type annotations to automatically generator serializers. If I use `Tree`, there is no way to customize the serialization of a `Tree` because there is no where to put the class methods that the serialization library calls.
This sounds like an implementation detail of the serialization library you use. I also heavily use a runtime serialization library based on types. It's approach is every type defines a serializer + deserializer. Basic types have parsers defined in the library. Common type constructs like Union/dataclass/namedtuple automatically derive a parser too. But all parsers for types are overridable. In the library I use you can do, from config_lib import register A = Union[X, Y] @register(A) class _(Parser[A]): def from_config(self, config): ... def to_config(self, obj): ... # Usage is like below. The expected type is specified mainly # to handle polymorphism/generics more easily + be # friendly for static type checkers and avoid from_config(obj) needing Any. to_config(Tree, obj) from_config(Tree, conf) This allows arbitrary customization of any type's parser. Most of the time default parser works well, but I have used this flexibility occasionally.
Mehdi2277 wrote:
The Union type can be made to work for static analysis. But there is no rescuing its total absence of runtime capabilities. Python could be fixed so that Union types work as cases in match, but there is no fixing `Tree` so that it exposes the class methods of `_TreeBase`. For example, I use a serialization library that inspects the type annotations to automatically generator serializers. If I use `Tree`, there is no way to customize the serialization of a `Tree` because there is no where to put the class methods that the serialization library calls.
This sounds like an implementation detail of the serialization library you use. I also heavily use a runtime serialization library based on types. It's approach is every type defines a serializer + deserializer. Basic types have parsers defined in the library. Common type constructs like Union/dataclass/namedtuple automatically derive a parser too. But all parsers for types are overridable. In the library I use you can do, [workaround]
I think this reflects the motivation of the pro-sealed and pro-enum camps pretty well. We have some base classes that we would like to use with match. The only current solution is to hide the base class and replace it with a Union. Ok, that makes `match` work, but Tree.to_data and Tree.from_data are now gone. Well, I guess there is a workaround where I strip the methods off and register them with the serialization library like Tree is a builtin type. I hope it is understandable why this some people might consider this a very unsatisfactory solution.
On Sun, Jul 31, 2022 at 01:54:42AM -0000, David Hagen wrote:
With your design in pyright, does autocompleting a variable of type `Tree` suggest just methods defined on `_TreeBase`? I ask because I get all the methods of `Node` and `Leaf` in PyCharm, which does not happen when autocompleting a variable of type `_TreeBase`. Theoretically, this is resolvable by finding the common base types of `Node` and `Leaf` (i.e. `_TreeBase`),
Surely PyCharm is correct here and this is not a problem to be resolved? If your variable is either a Node or a Leaf (and which is not known until runtime), then the list of possible methods while editing has to be taken from all the methods of Node plus all the methods of Lead, and not just the methods they have in common inherited from _TreeBase.
The Union type can be made to work for static analysis. But there is no rescuing its total absence of runtime capabilities.
I don't understand that.
there is no fixing `Tree` so that it exposes the class methods of `_TreeBase`.
If Node or Leaf inherit from _TreeBase, then they expose the methods they inherit from _TreeBase. Don't they? That's certainly how rlcompleter and IDLE work, so I would be shocked if more sophisticated IDEs could do less.
If I use `Tree`, there is no way to customize the serialization of a `Tree` because there is no where to put the class methods that the serialization library calls.
How about in _TreeBase? -- Steve
With your design in pyright, does autocompleting a variable of type `Tree` suggest just methods defined on `_TreeBase`? I ask because I get all the methods of `Node` and `Leaf` in PyCharm, which does not happen when autocompleting a variable of type `_TreeBase`. Theoretically, this is resolvable by finding the common base types of `Node` and `Leaf` (i.e. `_TreeBase`),
Surely PyCharm is correct here and this is not a problem to be resolved?
If your variable is either a Node or a Leaf (and which is not known until runtime), then the list of possible methods while editing has to be taken from all the methods of Node plus all the methods of Lead, and not just the methods they have in common inherited from _TreeBase.
If the type system is sound, the methods on Leaf are not valid to call on an instance of Tree. Even though a Tree could be a Leaf, it might instead be a Node, which would be an error.
Steven D'Aprano wrote:
The Union type can be made to work for static analysis. But there is no rescuing its total absence of runtime capabilities.
I don't understand that.
there is no fixing `Tree` so that it exposes the class methods of `_TreeBase`.
If Node or Leaf inherit from _TreeBase, then they expose the methods they inherit from _TreeBase. Don't they?
That's certainly how rlcompleter and IDLE work, so I would be shocked if more sophisticated IDEs could do less.
If I use `Tree`, there is no way to customize the serialization of a `Tree` because there is no where to put the class methods that the serialization library calls.
How about in _TreeBase?
The <i>class methods</i> in _TreeBase are not available in Tree and I cannot think of a straightforward way to change Union to make this work. And class methods are just one example of things that work on _TreeBase that do not work on Tree. This is what I mean by "no rescuing its total absence of runtime capabilities".
On Sat, Jul 30, 2022, 12:52 PM Eric Traut <eric@traut.com> wrote:
The `@sealed` proposal would be the first type feature that deviates from this design principle and would require all classes in a file to be analyzed before the type of the sealed class could be determined.
Which is why I agree that the decorator approach isn't ideal, though I would find it convenient. However, see below for why I don't think all classes need to be analyzed. Pylance, the language server built on top of pyright, is actively used by
more than 4M Python developers. The vast majority of those developers do not care about static type checking, but they do care about accurate and fast completion suggestions, signature help, semantic highlighting, semantic search and rename, inlined documentation, and other interactive convenience features that rely on fast type analysis. The `@sealed` proposal has the potential to negatively impact the development experience for this large group of developers while providing a small benefit to a small group of developers.
I don't see how any sealed class proposal (decorator or not) negatively impacts the audience you describe. The vast majority of developers do not care about static type checking, so in the vast majority of cases, they would never encounter sealed classes, no? Or, if you consider someone using a library that contains sealed classes, to use the example of my library, if the dev is not performing static type checking, no lookup of all subclasses within the file needs to be made. Autocompletion in each rung of the if-else/match-case isinstance() ladder would easily be able to look up methods from the specific subclass being matched; autocompletion outside the ladder would use the base class. Finding the type of one of the subclasses works as normal; you don't need to know what other subclasses there are to know what the parent of a class is. The only case where all subclasses must be looked up, as far as I can think of off the top of my head, is for an exhaustiveness check at the end of the ladder (not performed if not typechecking, which is the majority of cases), or if revealing the type of the argument to each isinstance() call in the ladder. If that last case is slower, what of it? It's rare, and I have faith that the Pylance/Pyright devs can keep the lookup time unnoticeable. The `__sealed__` or `__all__` proposals address the concern I highlight
above because they explicitly list the subclasses associated with the sealed base class. However, I find these proposals to be inelegant and cumbersome. They violate a basic rule of object-oriented programming: a base class should not have knowledge of its subclasses.
Then why has Java implemented it in precisely this way? That is, by listing the names of the allowed subclasses. (Not a challenge, I just know little about Java's design philosophies. However, I've typically considered Java a very strongly and traditionally object-oriented language.) This is effectively telling a type checker to treat the symbol `Tree` as a
union of `Node` and `Leaf`,
Minor correction: Under my scheme, if Tree is not an ABC, Tree becomes a union of Node, Leaf, and an invariant version of Tree itself, since instantiating Tree directly is allowed. You are entirely correct if Tree is in fact an ABC, or if consensus decides that all sealed classes are automatically abstract. Note that I’ve used an underscore for the name of the base class to
indicate that it is a private implementation detail of this file and should not be used on its own. Only the union type (`Tree`) should be used for static typing purposes.
How do you propose to enable isinstance() checks on Tree in versions of Python that don't accept unions as a class argument, if you intend the real base class (the usual argument of such checks) to be private? The enum-based proposals are also problematic in that they invent a new and
unfamiliar way to represent inheritance.
Yeah, this is essentially what I meant in my last long email when I said "Potentially unnatural to Python devs". They also muddy the concept of an enumeration, which has historically been
used for an enumerated set of values, not an enumerate set of types.
This is why if enums are how we do this, I'm more in favor of making a new Enum subclass that additionally modifies member types, rather than adding the functionality to Enum itself. That would keep the concept of an enumeration the same, while introducing ADTs as a new thing that is "like" an enumeration. I’m struggling to understand why the union solution doesn't meet your needs
but these other proposals do.
That's fair; this is a hairy topic and I'm having probably about as much trouble explaining as you are understanding. @AbyxDev said:
- snip -
Can you elaborate? The union approach cleanly supports inheritance using
the standard inheritance mechanisms.
The Union + base class approach supports inheritance, though I would not say it does so "cleanly" (more on this later). When I said "I need inheritance" I was intending to discount the approach of defining multiple classes with no common base class and using only a Union to unify them. Since the union mechanism is already fully supported in today’s type
system, I encourage you to try it for your own use cases. Once you’ve tried it, perhaps you can report back on whether you still think it’s cumbersome and why.
You must have missed the part in my first email to this list that mentioned I was already using the Union + base class approach in my code. I wrote my initial email after finding that approach less than ideal.
I don’t understand why you think it’s clunky or difficult to maintain. I
use this union pattern regularly in my code, and I find it neither clunky nor difficult to maintain.
Let me try again to explain, because evidently I'm doing a poor job of it. The clunkiness arises from the fundamental fact that this method uses two things to represent one. If you use the wrong member of the pair, you end up with non-exhaustiveness errors that are difficult to explain (if you use the base class instead of the union), or actual runtime errors (if you try to check instancehood with a union in the currently most widespread versions of Python). There's also the unexplainable subjective measure of "sealed classes come more naturally; union + base class feels more like a recipe to be memorized". It's difficult to maintain mostly because of the relisting of subclasses, which makes it easy to forget to sync the list with the actual subclasses, but also because the name duality makes refactoring somewhat more tedious (e.g. if you rename the type, you now have two things to rename, not one; you can't avoid renaming the implementation base class by hiding it because users need something to isinstance() against).
The union solution has the added benefit that it allows you to define
different subsets of types through the use of different unions.
Here’s a practical example from pyright's parser implementation (which is
written in TypeScript, but the concepts are the same). https://github.com/microsoft/pyright/blob/74d43236f6fad8300fc4304c970857027b.... Here I define a type called `ParseNode` that is a union of all possible parse node types. All of these subtypes inherit from `ParseNodeBase`. You can see that I also define other unions that include a subset of these parse node types. For example, the `ExpressionNode` union ( https://github.com/microsoft/pyright/blob/74d43236f6fad8300fc4304c970857027b...) includes only the parse node types that represent expressions.
This could just as easily be done with a sealed ParseNode ABC, which the sealed ExpressionNode ABC inherits, which the relevant individual parse node classes then inherit. ParseNode would then be treated like a union of all parse node types, and ExpressionNode would be treated like a union of expression node types, without having to relist the node types, rename both ParseNode and ParseNodeBase if the need arises to rename one of them, or remind oneself of when to use the base class vs the union. Here are the requirements and desirables that I think we've collected so
far:
1. The mechanism must produce a type that supports type exhaustion checks
in if/else towers and match statements.
Yep.
2. The mechanism must support inheritance.
Yep.
3. The mechanism must not require full analysis of a source file to determine the type of a single class.
As described above, I doubt that even the @sealed proposal would fail this requirement. I also think this is more of a desirable, since as I mentioned, cases where all subclasses actually do have to be known for some type operation should be few and far between. 4. Preferably, the code should be easy to maintain — in particular, if new
subclasses are added, they should either be automatically discovered or it should be possible for a type checker to guide the developer to do what is needed.
Yep. Though the latter is not ideal, but it's still acceptable. 5. Preferably, the solution should not invent new and unfamiliar ways to
express existing concepts in Python like inheritance or union types.
Personally agreed, though I feel like the Enum camp would have something to say about the advantages of combining enum singletons with enum classes, as seen in Rust and other languages. Am I missing anything here?
I think following on from the previous statement, 6. Preferably, the mechanism should be familiar to users of languages with existing implementations of sealed classes. Definitely a nice-to-have and not a strict requirement though, given that Python's syntax is almost never familiar to users of other languages anyway. Otherwise, no additional requirements immediately come to mind. Hopefully I've clarified myself better! I spent over an hour writing this on a plane so forgive any typographical mistakes or accidental slights you may perceive. I assure you I respect your expertise and experience. I'll be attending the upcoming typing meetup if you'd like to discuss in real-time. Best regards, AbyxDev; https://abyx.dev
I’m sorry, folks. I find the current “he-said-she-said” form of the discussion hard to follow. Maybe we can pick a concrete example, show what it looks like in each proposal (including problems), and then discuss the pros and cons of each version? —Guido -- --Guido (mobile)
On Sat, Jul 30, 2022 at 9:50 PM Guido van Rossum <guido@python.org> wrote:
I’m sorry, folks. I find the current “he-said-she-said” form of the discussion hard to follow. Maybe we can pick a concrete example, show what it looks like in each proposal (including problems), and then discuss the pros and cons of each version?
Sure, I've compiled a gist of whatever pros and cons I can think of. I did a gist to have full formatting control - hope that's acceptable. https://gist.github.com/Kenny2github/3f475e48ef4ef750359f71e22c158f9a Anyone, please feel free to note items I've missed and I'll do my best to update the gist with your additions. (I'm sure there's a better venue for this, but this is what I could come up with on the spur of the moment I had the idea.) -- Best regards, AbyxDev; https://abyx.dev
The one problem I see with the Union-pattern (apart from the fact that you can't match on a Union) is that it's quite verbose. I think that's not so bad when the members of the Union are complex classes with methods and so on, but the verbosity becomes apparent when the members are just simple Tuples. Take this example: from typing import NewType, Tuple, TypeAlias ColorName = NewType("ColorName", str) RGB = NewType("RGB", Tuple[int, int, int]) Color: TypeAlias = ColorName | RGB It would be nice if that could be written in a shorter way. Maybe like this? Color: TypeAlias = str as ColorName | Tuple[int, int, int] as RGB The advantage is that you don't have to repeat the name of the variant. Though, NewType is actually not the best to use here because it doesn't work with pattern matching. The following does *not* work with the above NewType-definition: def as_string(m: Color) -> str: match m: case ColorName(name): return name case RGB((r, g, b)): return f"red: {r}, green: {g}, blue: {b}" So, the augmented Union syntax would need to create dataclasses or similar in order to work with pattern matching. -Thomas
participants (12)
-
AbyxDev -
David Hagen -
Eric Traut -
Graham Wheeler -
Guido van Rossum -
jagduley@gmail.com -
John Hagen -
Mehdi2277 -
Steven D'Aprano -
Thomas Grainger -
Thomas Kehrenberg -
Tin Tvrtković