How to force/ensure class attributes are a specific type?
Asked Answered
O

10

85

How do I restrict a class member variable to be a specific type in Python?


Longer version:

I have a class that has several member variables which are set externally to the class. Due to the way they're used, they must be of specific types, either int or list.

If this was C++, I would simply make them private and do type-checking in the 'set' function. Given that that isn't possible, is there any way to restrict the type of the variables so that an error/exception occurs at runtime if they're assigned a value of incorrect type? Or do I need to check their type within every function that uses them?

Ovoviviparous answered 16/2, 2012 at 4:54 Comment(1)
The Pythonic way is to document the class appropriately. If wrong object type is set, the code will fail anyway. On the other hand the user may use a type that wouldn't pass your isinstance checks but otherwise is fine (duck typing).Classy
C
81

You can use a property like the other answers put it - so, if you want to constrain a single attribute, say "bar", and constrain it to an integer, you could write code like this:

class Foo(object):
    def _get_bar(self):
        return self.__bar
    def _set_bar(self, value):
        if not isinstance(value, int):
            raise TypeError("bar must be set to an integer")
        self.__bar = value
    bar = property(_get_bar, _set_bar)

And this works:

>>> f = Foo()
>>> f.bar = 3
>>> f.bar
3
>>> f.bar = "three"
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 6, in _set_bar
TypeError: bar must be set to an integer
>>> 

(There is also a new way of writing properties, using the "property" built-in as a decorator to the getter method - but I prefer the old way, like I put it above).

Of course, if you have lots of attributes on your classes, and want to protect all of them in this way, it starts to get verbose. Nothing to worry about - Python's introspection abilities allow one to create a class decorator that could automate this with a minimum of lines.

def getter_setter_gen(name, type_):
    def getter(self):
        return getattr(self, "__" + name)
    def setter(self, value):
        if not isinstance(value, type_):
            raise TypeError(f"{name} attribute must be set to an instance of {type_}")
        setattr(self, "__" + name, value)
    return property(getter, setter)

def auto_attr_check(cls):
    new_dct = {}
    for key, value in cls.__dict__.items():
        if isinstance(value, type):
            value = getter_setter_gen(key, value)
        new_dct[key] = value
    # Creates a new class, using the modified dictionary as the class dict:
    return type(cls)(cls.__name__, cls.__bases__, new_dct)

And you just use auto_attr_checkas a class decorator, and declar the attributes you want in the class body to be equal to the types the attributes need to constrain too:

...     
... @auto_attr_check
... class Foo(object):
...     bar = int
...     baz = str
...     bam = float
... 
>>> f = Foo()
>>> f.bar = 5; f.baz = "hello"; f.bam = 5.0
>>> f.bar = "hello"
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 6, in setter
TypeError: bar attribute must be set to an instance of <type 'int'>
>>> f.baz = 5
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 6, in setter
TypeError: baz attribute must be set to an instance of <type 'str'>
>>> f.bam = 3 + 2j
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 6, in setter
TypeError: bam attribute must be set to an instance of <type 'float'>
>>> 

    
Consonant answered 17/2, 2012 at 2:6 Comment(4)
Is there a simple way to have multiple type checks, e.g. bar = int or float?Pridgen
Isinstance accepts a tuple of instances - since it is called on this code, you can simply use a tuple of types there - it should just work: ` bar = int, float`Consonant
I love this decorator! Only issue I've found is this does not play nice with MyPy, as MyPy triggers things like "Incompatible types in assignment (expression has type "str", variable has type "Type[str]")"Neologism
This has been written before Python 3.7 came with the "dataclasses" that will support a lot more features than this example, and play nice with MyPy. Also, check for pydantic - pydantic-docs.helpmanual.io - which hold similar functionality, but use tupe annotations thenselves to create the fields.Consonant
C
41

Since Python 3.5, you can use type-hints to indicate that a class attribute should be of a particular type. Then, you could include something like MyPy as part of your continuous integration process to check that all the type contracts are respected.

For example, for the following Python script:

class Foo:
    x: int
    y: int

foo = Foo()
foo.x = "hello"

MyPy would give the following error:

6: error: Incompatible types in assignment (expression has type "str", variable has type "int")

If you want types to be enforced at runtime, you could use the enforce package. From the README:

>>> import enforce
>>>
>>> @enforce.runtime_validation
... def foo(text: str) -> None:
...     print(text)
>>>
>>> foo('Hello World')
Hello World
>>>
>>> foo(5)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/william/.local/lib/python3.5/site-packages/enforce/decorators.py", line 106, in universal
    _args, _kwargs = enforcer.validate_inputs(parameters)
  File "/home/william/.local/lib/python3.5/site-packages/enforce/enforcers.py", line 69, in validate_inputs
    raise RuntimeTypeError(exception_text)
enforce.exceptions.RuntimeTypeError: 
  The following runtime type errors were encountered:
       Argument 'text' was not of type <class 'str'>. Actual type was <class 'int'>.
Chino answered 7/11, 2017 at 15:18 Comment(4)
This is exactly what I was looking for. It enables VS Code to give hints even when I initialize the variable to None in __init__.Electrician
I like your solution. I tried this but got error in import .pyenv/versions/p3/lib/python3.9/site-packages/enforce/parsers.py", line 225, in <module>Sulk
In your example foo.x is an instance attribute, not a class attribute (Foo.x). I guess that the type hint is supposed to be used for both the cases foo.x and Foo.x. Can anyone confirm?Toshiatoshiko
This seems to be the answer I was looking forJulie
H
29

In general, this is not a good idea for the reasons that @yak mentioned in his comment. You are basically preventing the user from supplying valid arguments that have the correct attributes/behavior but are not in the inheritance tree you hard-coded in.

Disclaimer aside, there are a few of options available for what you are trying to do. The main issue is that there are no private attributes in Python. So if you just have a plain old object reference, say self._a, you can not guarantee that the user won't set it directly even though you have provided a setter that does type checking for it. The options below demonstrate how to really enforce the type checking.

Override __setattr__

This method will only be convenient for a (very) small number of attributes that you do this to. The __setattr__ method is what gets called when you use dot notation to assign a regular attribute. For example,

class A:
    def __init__(self, a0):
        self.a = a0

If we now do A().a = 32, it would call A().__setattr__('a', 32) under the hood. In fact, self.a = a0 in __init__ uses self.__setattr__ as well. You can use this to enforce the type check:

 class A:
    def __init__(self, a0):
        self.a = a0
    def __setattr__(self, name, value):
        if name == 'a' and not isinstance(value, int):
            raise TypeError('A.a must be an int')
        super().__setattr__(name, value)

The disadvantage of this method is that you have to have a separate if name == ... for each type you want to check (or if name in ... to check multiple names for a given type). The advantage is that it is the most straightforward way to make it nearly impossible for the user to circumvent the type check.

Make a property

Properties are objects that replace your normal attribute with a descriptor object (usually by using a decorator). Descriptors can have __get__ and __set__ methods that customize how the underlying attribute is accessed. This is sort of like taking the corresponding if branch in __setattr__ and putting it into a method that will run just for that attribute. Here is an example:

class A:
    def __init__(self, a0):
        self.a = a0
    @property
    def a(self):
        return self._a
    @a.setter
    def a(self, value):
        if not isinstance(value, int):
            raise TypeError('A.a must be an int')
        self._a = value

A slightly different way of doing the same thing can be found in @jsbueno's answer.

While using a property this way is nifty and mostly solves the problem, it does present a couple of issues. The first is that you have a "private" _a attribute that the user can modify directly, bypassing your type check. This is almost the same problem as using a plain getter and setter, except that now a is accessible as the "correct" attribute that redirects to the setter behind the scenes, making it less likely that the user will mess with _a. The second issue is that you have a superfluous getter to make the property work as read-write. These issues are the subject of this question.

Create a True Setter-Only Descriptor

This solution is probably the most robust overall. It is suggested in the accepted answer to the question mentioned above. Basically, instead of using a property, which has a bunch of frills and conveniences that you can not get rid of, create your own descriptor (and decorator) and use that for any attributes that require type checking:

class SetterProperty:
    def __init__(self, func, doc=None):
        self.func = func
        self.__doc__ = doc if doc is not None else func.__doc__
    def __set__(self, obj, value):
        return self.func(obj, value)

class A:
    def __init__(self, a0):
        self.a = a0
    @SetterProperty
    def a(self, value):
        if not isinstance(value, int):
            raise TypeError('A.a must be an int')
        self.__dict__['a'] = value

The setter stashes the actual value directly into the __dict__ of the instance to avoid recursing into itself indefinitely. This makes it possible to get the attribute's value without supplying an explicit getter. Since the descriptor a does not have the __get__ method, the search will continue until it finds the attribute in __dict__. This ensures that all sets go through the descriptor/setter while gets allow direct access to the attribute value.

If you have a large number of attributes that require a check like this, you can move the line self.__dict__['a'] = value into the descriptor's __set__ method:

class ValidatedSetterProperty:
    def __init__(self, func, name=None, doc=None):
        self.func = func
        self.__name__ = name if name is not None else func.__name__
        self.__doc__ = doc if doc is not None else func.__doc__
    def __set__(self, obj, value):
        ret = self.func(obj, value)
        obj.__dict__[self.__name__] = value

class A:
    def __init__(self, a0):
        self.a = a0
    @ValidatedSetterProperty
    def a(self, value):
        if not isinstance(value, int):
            raise TypeError('A.a must be an int')

Update

Python3.6 does this for you almost out-of the box: https://docs.python.org/3.6/whatsnew/3.6.html#pep-487-descriptor-protocol-enhancements

TL;DR

For a very small number of attributes that need type-checking, override __setattr__ directly. For a larger number of attributes, use the setter-only descriptor as shown above. Using properties directly for this sort of application introduces more problems than it solves.

Harmless answered 16/12, 2016 at 17:20 Comment(0)
B
4

Note 1: @Blckknght thank you for your fair comment. I missed recursion issue in my much too simple test suite.

Note 2: I wrote this answer when I was at the very beginning of learning Python. Right now I would rather use Python's descriptors, see e.g link1, link2.

Thanks to the previous posts and some thinking, I believe I have figured out a much more user-friendly way of how to restrict a class attribute to be of specific type.

First of all, we create a function, which universally tests for type:

def ensure_type(value, types):
    if isinstance(value, types):
        return value
    else:
        raise TypeError('Value {value} is {value_type}, but should be {types}!'.format(
            value=value, value_type=type(value), types=types))

Then we simply use and apply it in our classes via setter. I think this is relatively simple and follow DRY, especially once you export it to a separate module to feed your whole project. See the example below:

class Product:
    def __init__(self, name, quantity):
        self.name = name
        self.quantity = quantity

    @property
    def name(self):
        return self.__dict__['name']

    @name.setter
    def name(self, value):
        self.__dict__['name'] = ensure_type(value, str)

    @property
    def quantity(self):
        return self.quantity

    @quantity.setter
    def quantity(self, value):
        self.__dict__['quantity'] = ensure_type(value, int)

The tests produce reasonable results. See first the tests:

if __name__ == '__main__':
    from traceback import format_exc

    try:
        p1 = Product(667, 5)
    except TypeError as err:
        print(format_exc(1))

    try:
        p2 = Product('Knight who say...', '5')
    except TypeError as err:
        print(format_exc(1))

    p1 = Product('SPAM', 2)
    p2 = Product('...and Love', 7)
    print('Objects p1 and p2 created successfully!')

    try:
        p1.name = -191581
    except TypeError as err:
        print(format_exc(1))

    try:
        p2.quantity = 'EGGS'
    except TypeError as err:
        print(format_exc(1))

And the tests result:

Traceback (most recent call last):
  File "/Users/BadPhoenix/Desktop/Coding/Coders-Lab/Week-2/WAR_PYT_S_05_OOP/2_Praca_domowa/day-1/stackoverflow.py", line 35, in <module>
    p1 = Product(667, 5)
TypeError: Value 667 is <class 'int'>, but should be <class 'str'>!

Traceback (most recent call last):
  File "/Users/BadPhoenix/Desktop/Coding/Coders-Lab/Week-2/WAR_PYT_S_05_OOP/2_Praca_domowa/day-1/stackoverflow.py", line 40, in <module>
    p2 = Product('Knights who say...', '5')
TypeError: Value 5 is <class 'str'>, but should be <class 'int'>!

Objects p1 and p2 created successfully!

Traceback (most recent call last):
  File "/Users/BadPhoenix/Desktop/Coding/Coders-Lab/Week-2/WAR_PYT_S_05_OOP/2_Praca_domowa/day-1/stackoverflow.py", line 49, in <module>
    p1.name = -191581
TypeError: Value -191581 is <class 'int'>, but should be <class 'str'>!

Traceback (most recent call last):
  File "/Users/BadPhoenix/Desktop/Coding/Coders-Lab/Week-2/WAR_PYT_S_05_OOP/2_Praca_domowa/day-1/stackoverflow.py", line 54, in <module>
    p2.quantity = 'EGGS'
TypeError: Value EGGS is <class 'str'>, but should be <class 'int'>!
Burney answered 12/9, 2017 at 14:19 Comment(1)
The getter functions for your properties will recurse forever. You can't do return self.name, because that just invokes the getter again! You probably should be explicitly looking up your value in self.__dict__, since that's where you're setting it. Another option would be to use a different attribute name (like _name instead of name) for the value.Cestar
R
1

You can do it exactly as you say you said you would do it in C++; make assignment to them go through a setter method, and have the setter method check the type. The concepts of "private state" and "public interfaces" in Python are done with documentation and convention, and it's pretty much impossible to force anyone to use your setter rather than directly assign the variable. But if you give the attributes names beginning with an underscore and document the setters as the way to use your class, that should do it (don't use __names with two underscores; it's almost always more trouble than it's worth unless you're actually in the situation they're designed for, which is clashing attribute names in an inheritance hierarchy). Only particularly obtuse developers will avoid the easy way of using the class the way it's documented to work in favour of figuring out what the internal names are and using them directly; or developers who are frustrated by your class behaving unusually (for Python) and not allowing them to use a custom list-like class in place of a list.

You can use properties, as other answers have described, to do this while still making it look like you're assigning to attributes directly.


Personally, I find attempts to enforce type safety in Python to be pretty useless. Not because I think static type checking is always inferior, but because even if you could add type requirements on your Python variables that worked 100% of the time, they just won't be effective in maintaining the assurance that your program is free of type errors because they will only raise exceptions at runtime.

Think about it; when your statically compiled program successfully compiles with no errors, you know that it is completely free of all the bugs that the compiler can detect (in the case of languages like Haskell or Mercury that's a pretty good guarantee, though still not complete; in the case of languages like C++ or Java... meh).

But in Python, the type error will only be noticed if it is ever executed. This means, even if you could get full static type enforcement everywhere in your program, you need to be regularly executing test suites with 100% code coverage to actually know your program is free of type errors. But if you had regularly executed tests with full coverage you'd know if you had any type errors, even without attempting to enforce types! So the benefit just really doesn't seem worth it to me. You're throwing away Python's strength (flexibility) without gaining more than a trifle in one of its weaknesses (static error detection).

Redden answered 17/2, 2012 at 5:45 Comment(7)
Usually, type is more a guide for the developper when they actually use an object. It helps to pass the good type and inform the dev how to use the object's method properly. Also, it is a lot easier to debug with a "TypeError, wrong value passed to fonction x, need a 'list'" rather than "type int does not have 'append' function". Why would you accept an integer in your function when you really need a list?Elbertina
@Elbertina It's not about accepting an integer when you really need a list. It's about accepting anything that has (suitably well-behaved) append method. Why would you reject my Rope class (specialised list of strings) when the code you've written works perfectly well on it? In one real world project I actually have a module with 5 different variations of dict-like class (with no superclass common to all of them except object). They would be useless if all the library code I'm using explicitly required instances of dict.Redden
"and it's pretty much impossible to force anyone to use your setter rather than directly assign the variable" actually - that is not true n Python. If you make use of descriptors (which are easily materialized with the property helper callable), it would require some ugly hacking around to not use the setter function at all.Consonant
@Consonant My point was that the ugly hacking is always possible (and generally not very difficult), if you've set your mind to it. So it won't stop people playing by the rules declared in your documentation, who aren't trying to access the private variable directly anyway, and it won't stop people who have decided (for wherever reason) that they need to get around the rules.Redden
OO encapsulation is not about preventing willing access to variables as a matter of security. You are controling a full system, in the same process - in Java you can access then with the reflection tools, and C++ gives you raw memory access. In that aspect Python is arguably even better because it does not give one a false sensation of security.Consonant
@Consonant That's exactly what I was saying?Redden
In Python it is very possible to enforce the setter. You just have to define it right.Harmless
G
1

I know this discussion has been settled, but a much simpler solution is to use the Python Structure module show below. This would require you to make a container for your data before you assign a value to it, but it is very effective at keeping the data type static. https://pypi.python.org/pypi/structures

Gramophone answered 30/8, 2017 at 3:52 Comment(0)
C
1

I know this is old but it is the first google result for this question and all of these answers seem overly complicated. At least for python 3 this is the simplest solution:

class dog:
    species = 'Dog' 
    def __init__(self, name, age, weight):  
        self.name = str(name)
        self.age = int(age)
        self.weight = float(weight)

'''The following line tries to create a new object from the 'dog' class and 
passes a string instead of an integer for the age argument'''

newdog = dog('spike','three',60) 

When run an exception is thrown:

ValueError: invalid literal for int() with base 10: 'three'

In Python, primitive data types (int, float, str, booleans) are themselves classes. Thus if you instantiate the class attributes of your class before passing the method parameters during object creation, the argument values will be converted if possible (such as from a int to a float) or an exception will be thrown if the data type cannot be converted (such as from a string to an integer).

Convolute answered 21/11, 2019 at 20:32 Comment(1)
This doesn't answer the question, because it does not verify the type. Instead this code tries to convert it. This is valid for your dog class: newdog = dog(-10, 20.5, 60) and your newdog.age will be 20 and not 20.5, meaning you lose information.Saturn
C
0

You can use same type of property as you mention in C++. You will get help for property from http://adam.gomaa.us/blog/2008/aug/11/the-python-property-builtin/.

Clemens answered 16/2, 2012 at 5:27 Comment(2)
Wouldn't help much. To have a property work that way, you would still need some storage within the instance, which the user could use to bypass the property. The only advantage is that you could make the internal value "private" with the leading underscore convention and not document it. But you could do that with a regular setter just as well.Harmless
Link only answer are bad, and indeed the link is broken.Statocyst
S
0

If you want to copy a lot of packages, It's a bit more simple than everyone is answering:

class test:
    def __init__(self, name: type you want)

or

class test:
    def __init__(self, name: [list of types])

I had this problem and I just read what tkinter done in their files.

Slideaction answered 10/4 at 14:13 Comment(1)
This doesn't constrain anything. It communicates to other developers and certain tools what should be done, but it won't prevent callers from passing in something else.Levison
B
0

I wrote a package on pypi called pyoload which could help you do that. Example:

from pyoload import *

@annotate
class Person:
    age:int
    name:str
    achievements:list[str]

    def __init__(self: Any, name: str, age: int):
        self.name = name


marry = Person('marry', 15)
marry.achievements = [3]

which outputs this:

Traceback (most recent call last):
  File "C:\Users\CHEF SEC\pyoload-test.py", line 14, in <module>
    marry.achievements = [3]
    ^^^^^^^^^^^^^^^^^^
  File "C:\Users\CHEF SEC\AppData\Roaming\Python\Python311-32\site-packages\pyoload\__init__.py", line 452, in new_setter
    raise AnnotationError(
pyoload.AnnotationError: value [3] does not match annotationof attribute: 'achievements':list[str] of object of class __main__.Person

Check it out at pyoload pypi.

Bisect answered 2/6 at 4:35 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.