9

I had namedtuple variable which represents version of application (its number and type). But i want and some restriction to values:

Version = namedtuple("Version", ["app_type", "number"])
version = Version("desktop") # i want only "desktop" and "web" are valid app types
version = Version("deskpop") # i want to protect from such mistakes

My solution for now is primitive class with no methods:

class Version:
    def __init__(self, app_type, number):
        assert app_type in ('desktop', 'web')

        self.app_type = app_type
        self.number = number

Is it pythonic? Is it overkill?

Reblochon Masque
  • 35,405
  • 10
  • 55
  • 80
keran
  • 113
  • 1
  • 5
  • Why are you assigning `Version` to a namedtuple, and also declaring a class with that same variable? – JacobIRR Jun 26 '19 at 19:46
  • 1
    @JacobIRR I assume the class version is an *alternative* implementation – Chris_Rands Jun 26 '19 at 19:56
  • 1
    If the only two valid app types are `"desktop"` and `"web"` consider making an `Enum` class to specify that. Then instead of passing a raw string into your `namedtuple` you pass in a `AppType` enum. – Kyle Parsons Jun 26 '19 at 20:05
  • 4
    A class seems fine. If you were using a namedtuple to save memory, just use `__slots__` – juanpa.arrivillaga Jun 26 '19 at 20:12
  • I agree with @juanpa — class is the way to do it. If you're using Python 3.7+, consider using a `dataclass` — see [Validating detailed types in python dataclasses](https://stackoverflow.com/questions/50563546/validating-detailed-types-in-python-dataclasses). – martineau Jun 26 '19 at 20:19
  • 3
    I would avoid using `assert` in this case. Raising a `ValueError` seems more appropriate here. https://stackoverflow.com/a/945135/10863327 – Kyle Parsons Jun 26 '19 at 20:26
  • 1
    However you do it, consider making `app_type` and [`enum`](https://docs.python.org/3/library/enum.html#module-enum) to limit the permissible values. – martineau Jun 26 '19 at 20:28
  • People ask "is this Pythonic?" instead of asking "is this good OOD?" now. Sigh. – spinkus Jun 26 '19 at 20:31
  • @spinkus I think "pythonic" can be treated as "good OOD in python" – keran Jun 27 '19 at 10:42
  • Not sure that's possible ... ;). – spinkus Jun 27 '19 at 20:25

2 Answers2

10

You could use enum.Enum, and typing.NamedTuple instead of collections.namedtuple:

Maybe something like this:

from typing import NamedTuple
import enum

class AppType(enum.Enum):
    desktop = 0
    web = 1

class Version(NamedTuple):
    app: AppType


v0 = Version(app=AppType.desktop)
v1 = Version(app=AppType.web)

print(v0, v1)

output:

Version(app=<AppType.desktop: 0>) Version(app=<AppType.web: 1>)

A undefined AppType raises an AttributeError:

v2 = Version(app=AppType.deskpoop)

output:

AttributeError: deskpoop
martineau
  • 119,623
  • 25
  • 170
  • 301
Reblochon Masque
  • 35,405
  • 10
  • 55
  • 80
  • 1
    Thank you for reminding use enums. I'll accept this) – keran Jun 27 '19 at 10:45
  • 1
    It may not be important, but using `Version(app=AppType.deskpoop)`, similar to what was done in first two examples, would have resulted in an `AttributeError: deskpoop`. The `NameError` is occurring because you used `Version(AppType=deskpoop)` in the third example. – martineau Jun 27 '19 at 21:37
  • Thank you for that @martineau, good catch. I think it is important, I modified my answer. Incidentally, the `AttributeError` message raised by `Enum` is rather terse: the traceback is okay, but the message could contain the name of the class it was raised from. – Reblochon Masque Jun 28 '19 at 00:48
0

I would disagree with the accepted answer above, since the validation is actually not done in the NamedTuple class itself, but in external class with extra call, and so it is still possible to do invalid initialisation of Version instance with default constructor:

from typing import NamedTuple
import enum

class AppType(enum.Enum):
    desktop = 0
    web = 1

class Version(NamedTuple):
    app: AppType

>>> v=Version(42)
>>> print(v)

   Version(app=42)

>>> v2=Version('whatever you want')
>>> print(v2)

   Version(app='whatever you want')

I haven't found a way to overload or at least to disable the default NamedTuple constructor, to prevent invalid initialisation((

The workaround I'm using - creating an alternative constructor(s), which implement validation. but again nothing prevents 'user' to create invalid object using default NamedTuple constructor:

class Version2(NamedTuple):
    app_type : str
    
    @classmethod
    def new(cls, app_value: str):
        assert app_value in ('desktop', 'web')
        return cls(app_value) 

>>> Version2.new('web')
Version2(app_type='web')

>>> Version2.new('tablet')
Traceback (most recent call last):
    exec(code, self.locals)
  File "<input>", line 1, in <module>
  File "<input>", line 6, in new
AssertionError

>>> Version2('whatever you want')
Version2(app_type='whatever you want')
Fedor
  • 31
  • 5