In Python, instance initialization happens in the__init__ method.Generally speaking, you should keep as little logic as possible in it, and you should think about what the class needs and not how it is going to be instantiated.
Passing complex objects into__init__ and then using them to derive data for the class unnecessarily couples your new class with the old class which makes it harder to test and also will cause problems later.
So assuming you use an ORM and want to extract 2D points from a row object, do not write code like this:
classPoint:def__init__(self,database_row):self.x=database_row.xself.y=database_row.ypt=Point(row)
Instead, write aclassmethod that will extract it for you:
@defineclassPoint:x:floaty:float@classmethoddeffrom_row(cls,row):returncls(row.x,row.y)pt=Point.from_row(row)
This is sometimes called anamed constructor or afactory method.
Now, you can instantiatePoints without creating fake row objects in your tests.You can also have as many smart creation helpers as you want.This flexibility is useful because additional data sources tend to appear over time.
For similar reasons, we strongly discourage from patterns like:
pt=Point(**row.attributes)
which couples your classes to the database data model.Try to design your classes in a way that is clean and convenient to use – not based on your database format.The database format can change anytime and you’re stuck with a bad class design that is hard to change.Embrace functions and classmethods as a filter between reality and what’s best for you to work with.
Important
Whileattrs’s initialization concepts (including the following sections about validators and converters) are powerful, they arenot intended to replace a fully-featured serialization or validation system.
We want to help you to write a__init__ that you’d write by hand, but with less boilerplate.
If you look for powerful-yet-unintrusive serialization and validation for yourattrs classes, have a look at our sibling projectcattrs or ourthird-party extensions.
This separation of creating classes and serializing them is a conscious design decision.We don’t think that your business model and your serialization format should be coupled.
One thing people tend to find confusing is the treatment of private attributes that start with an underscore.Although there isa convention that members of an object that start with an underscore should be treated as private, consider that a core feature ofattrs is to automatically create an__init__ method whose arguments correspond to the members.There is no corresponding convention for private arguments: the entire signature of a function is its public interface to be used by callers.
However, it is sometimes useful to accept a public argument when an object is constructed, but treat that attribute as private after the object is created, perhaps to maintain some invariant.As a convenience for this use case, the default behavior ofattrs is that if you specify a member that starts with an underscore, it will strip the underscore from the name when it creates the__init__ method signature:
>>>importinspect>>>fromattrsimportdefine>>>@define...classFileDescriptor:..._fd:int>>>inspect.signature(FileDescriptor.__init__)<Signature (self, fd: int) -> None>
Even if you’re not using this feature, it’s important to be aware of it because it can lead to surprising syntax errors:
>>>@define...classC:..._1:intTraceback (most recent call last):...SyntaxError:invalid syntax
In this case a valid attribute name_1 got transformed into an invalid argument name1.
Whether this feature is useful to you is a matter of taste.If your taste differs, you can use thealias argument toattrs.field() to explicitly set the argument name.This can be used to override private attribute handling, or make other arbitrary changes to__init__ argument names.
>>>fromattrsimportfield>>>@define...classC:..._x:int=field(alias="_x")...y:int=field(alias="distasteful_y")..._1:int=field(alias="underscore1")>>>inspect.signature(C.__init__)<Signature (self, _x: int, distasteful_y: int, underscore1: int) -> None>
Sometimes you don’t want to pass all attribute values to a class.And sometimes, certain attributes aren’t even intended to be passed but you want to allow for customization anyways for easier testing.
This is when default values come into play:
>>>fromattrsimportFactory>>>@define...classC:...a:int=42...b:list=field(factory=list)...c:list=Factory(list)# syntactic sugar for above...d:dict=field()...@d.default...def_any_name_except_a_name_of_an_attribute(self):...return{}>>>C()C(a=42, b=[], c=[], d={})
It’s important that the decorated method – or any other method or property! – doesn’t have the same name as the attribute, otherwise it would overwrite the attribute definition.
Similar to thecommon gotcha with mutable default arguments,default=[] willnot do what you may think it might do:
>>>@define...classC:...x=[]>>>i=C()>>>k=C()>>>i.x.append(42)>>>k.x[42]
This is whyattrs comes with factory options.
Warning
Please note that the decorator based defaults have one gotcha:they are executed when the attribute is set, that means depending on the order of attributes, theself object may not be fully initialized when they’re called.
Therefore you should useself as little as possible.
Even the smartest of us canget confused by what happens if you pass partially initialized objects around.
Another thing that definitelydoes belong in__init__ is checking the resulting instance for invariants.This is whyattrs has the concept of validators.
The most straightforward way is using the attribute’svalidator method as a decorator.
The method has to accept three arguments:
theinstance that’s being validated (akaself),
theattribute that it’s validating, and finally
thevalue that is passed for it.
These values are passed aspositional arguments, therefore their names don’t matter.
If the value does not pass the validator’s standards, it just raises an appropriate exception.
>>>@define...classC:...x:int=field()...@x.validator...def_check_x(self,attribute,value):...ifvalue>42:...raiseValueError("x must be smaller or equal to 42")>>>C(42)C(x=42)>>>C(43)Traceback (most recent call last):...ValueError:x must be smaller or equal to 42
Again, it’s important that the decorated method doesn’t have the same name as the attribute and that theattrs.field() helper is used.
If you want to reuse your validators, you should have a look at thevalidator argument toattrs.field().
It takes either a callable or a list of callables (usually functions) and treats them as validators that receive the same arguments as with the decorator approach.Also as with the decorator approach, they are passed aspositional arguments so you can name them however you want.
Since the validators runafter the instance is initialized, you can refer to other attributes while validating:
>>>importattrs>>>defx_smaller_than_y(instance,attribute,value):...ifvalue>=instance.y:...raiseValueError("'x' has to be smaller than 'y'!")>>>@define...classC:...x=field(validator=[attrs.validators.instance_of(int),...x_smaller_than_y])...y=field()>>>C(x=3,y=4)C(x=3, y=4)>>>C(x=4,y=3)Traceback (most recent call last):...ValueError:'x' has to be smaller than 'y'!
This example demonstrates a convenience shortcut:Passing a list of validators directly is equivalent to passing them wrapped in theattrs.validators.and_ validator and all validators must pass.
attrs won’t intercept your changes to those attributes but you can always callattrs.validate() on any instance to verify that it’s still valid:
When usingattrs.define() orattrs.frozen, however,attrs will run the validators even when setting the attribute.
>>>i=C(4,5)>>>i.x=5Traceback (most recent call last):...ValueError:'x' has to be smaller than 'y'!
attrs ships with a bunch of validators, make sure tocheck them out before writing your own:
>>>@define...classC:...x=field(validator=attrs.validators.instance_of(int))>>>C(42)C(x=42)>>>C("42")Traceback (most recent call last):...TypeError:("'x' must be <type 'int'> (got '42' that is a <type 'str'>).", Attribute(name='x', default=NOTHING, factory=NOTHING, validator=<instance_of validator for type <type 'int'>>, type=None), <type 'int'>, '42')
Of course you can mix and match the two approaches at your convenience.If you use both ways to define validators for an attribute, they are both ran:
>>>@define...classC:...x=field(validator=attrs.validators.instance_of(int))...@x.validator...deffits_byte(self,attribute,value):...ifnot0<=value<256:...raiseValueError("value out of bounds")>>>C(128)C(x=128)>>>C("128")Traceback (most recent call last):...TypeError:("'x' must be <class 'int'> (got '128' that is a <class 'str'>).", Attribute(name='x', default=NOTHING, validator=[<instance_of validator for type <class 'int'>>, <function fits_byte at 0x10fd7a0d0>], repr=True, cmp=True, hash=True, init=True, metadata=mappingproxy({}), type=None, converter=None), <class 'int'>, '128')>>>C(256)Traceback (most recent call last):...ValueError:value out of bounds
Finally, validators can be globally disabled:
>>>attrs.validators.set_disabled(True)>>>C("128")C(x='128')>>>attrs.validators.set_disabled(False)>>>C("128")Traceback (most recent call last):...TypeError:("'x' must be <class 'int'> (got '128' that is a <class 'str'>).", Attribute(name='x', default=NOTHING, validator=[<instance_of validator for type <class 'int'>>, <function fits_byte at 0x10fd7a0d0>], repr=True, cmp=True, hash=True, init=True, metadata=mappingproxy({}), type=None, converter=None), <class 'int'>, '128')
… or within a context manager:
>>>withattrs.validators.disabled():...C("128")C(x='128')>>>C("128")Traceback (most recent call last):...TypeError:("'x' must be <class 'int'> (got '128' that is a <class 'str'>).", Attribute(name='x', default=NOTHING, validator=[<instance_of validator for type <class 'int'>>, <function fits_byte at 0x10fd7a0d0>], repr=True, cmp=True, hash=True, init=True, metadata=mappingproxy({}), type=None, converter=None), <class 'int'>, '128')
Sometimes, it is necessary to normalize the values coming in, thereforeattrs comes with converters.
Attributes can have aconverter function specified, which will be called with the attribute’s passed-in value to get a new value to use.This can be useful for doing type-conversions on values that you don’t want to force your callers to do.
>>>@define...classC:...x=field(converter=int)>>>o=C("1")>>>o.x1>>>o.x="2">>>o.x2
Converters are runbefore validators, so you can use validators to check the final form of the value.
>>>defvalidate_x(instance,attribute,value):...ifvalue<0:...raiseValueError("x must be at least 0.")>>>@define...classC:...x=field(converter=int,validator=validate_x)>>>o=C("0")>>>o.x0>>>C("-1")Traceback (most recent call last):...ValueError:x must be at least 0.
Arguably, you can abuse converters as one-argument validators:
>>>C("x")Traceback (most recent call last):...ValueError:invalid literal for int() with base 10: 'x'
If a converter’s first argument has a type annotation, that type will appear in the signature for__init__.A converter will override an explicit type annotation ortype argument.
>>>defstr2int(x:str)->int:...returnint(x)>>>@define...classC:...x=field(converter=str2int)>>>C.__init__.__annotations__{'return': None, 'x': <class 'str'>}
If you need more control over the conversion process, you can wrap the converter with aattrs.Converter and ask for the instance and/or the field that are being initialized:
>>>defcomplicated(value,self_,field):...returnint(value)*self_.factor+field.metadata["offset"]>>>@define...classC:...factor=5# not an *attrs* field...x=field(...metadata={"offset":200},...converter=attrs.Converter(...complicated,...takes_self=True,takes_field=True...))>>>C("42")C(x=410)
Generally speaking, the moment you realize the need of finer control – than whatattrs offers – over how a class is instantiated, it’s usually best to use aclassmethod factory or to apply thebuilder pattern.
However, sometimes you need to do that one quick thing before or after your class is initialized.For that purpose,attrs offers the following options:
__attrs_pre_init__ is automatically detected and runbeforeattrs starts initializing.If__attrs_pre_init__ takes more than theself argument, theattrs-generated__init__ will call it with the same arguments it received itself.This is useful if you need to inject a call tosuper().__init__() – with or without arguments.
__attrs_post_init__ is automatically detected and runafterattrs is done initializing your instance.This is useful if you want to derive some attribute from others or perform some kind of validation over the whole instance.
__attrs_init__ is written and attached to your classinstead of__init__, ifattrs is told to not write one (wheninit=False or a by a combination ofauto_detect=True and a custom__init__).This is useful if you want full control over the initialization process, but don’t want to set the attributes by hand.
The sole reason for the existence of__attrs_pre_init__ is to give users the chance to callsuper().__init__(), because some subclassing-based APIs require that.
>>>@define...classC:...x:int...def__attrs_pre_init__(self):...super().__init__()>>>C(42)C(x=42)
If you need more control, use the custom init approach described next.
Warning
You never need to usesuper() withattrs classes that inherit from otherattrs classes.Eachattrs class implements an__init__ based on its own fields and those of all its base classes.
You only need this escape hatch when subclassing non-attrs classes.
If you tellattrs to not write an__init__, it will write an__attrs_init__ instead, with the same code that it would have used for__init__.You have full control over the initialization, but also have to type out the types of your arguments etc.Here’s an example of a manual default value:
>>>@define...classC:...x:int......def__init__(self,x:int=42):...self.__attrs_init__(x)>>>C()C(x=42)
>>>@define...classC:...x:int...y:int=field(init=False)...def__attrs_post_init__(self):...self.y=self.x+1>>>C(1)C(x=1, y=2)
Please note that you can’t directly set attributes on frozen classes:
>>>@frozen...classFrozenBroken:...x:int...y:int=field(init=False)...def__attrs_post_init__(self):...self.y=self.x+1>>>FrozenBroken(1)Traceback (most recent call last):...attrs.exceptions.FrozenInstanceError:can't set attribute
If you need to set attributes on a frozen class, you’ll have to resort to thesame trick asattrs and useobject.__setattr__():
>>>@frozen...classFrozen:...x:int...y:int=field(init=False)...def__attrs_post_init__(self):...object.__setattr__(self,"y",self.x+1)>>>Frozen(1)Frozen(x=1, y=2)
Note that youmust not access the hash code of the object in__attrs_post_init__ ifcache_hash=True.
If present, the hooks are executed in the following order:
__attrs_pre_init__ (if present oncurrent class)
For each attribute, in the order it was declared:
default factory
converter
all validators
__attrs_post_init__ (if present oncurrent class)
Notably this means, that you can access all attributes from within your validators, but your converters have to deal with invalid values and have to return a valid value.
One of the most commonattrs questions onStack Overflow is how to have attributes that depend on other attributes.For example if you have an API token and want to instantiate a web client that uses it for authentication.Based on the previous sections, there are two approaches.
The simpler one is using__attrs_post_init__:
@defineclassAPIClient:token:strclient:WebClient=field(init=False)def__attrs_post_init__(self):self.client=WebClient(self.token)
The second one is using a decorator-based default:
@defineclassAPIClient:token:strclient:WebClient=field()# needed! attr.ib works too@client.defaultdef_client_factory(self):returnWebClient(self.token)
That said, and as pointed out in the beginning of the chapter, a better approach would be to have a factory class method:
@defineclassAPIClient:client:WebClient@classmethoddeffrom_token(cls,token:str)->"APIClient":returncls(client=WebClient(token))
This makes the class more testable.
__init_subclass__¶object.__init_subclass__() is a special method that is called when a subclass of the class that defined it is created.
For example:
>>>classBase:...@classmethod...def__init_subclass__(cls):...print(f"Base has been subclassed by{cls}.")>>>classDerived(Base):...passBase has been subclassed by <class 'Derived'>.
Unfortunately, a class decorator-based approach likeattrs (ordataclasses) doesn’t play well with__init_subclass__.Withdict classes, it is runbefore the class has been processed byattrs and in the case ofslotted classes, whereattrs has toreplace the original class,__init_subclass__ is calledtwice: once for the original class and once for theattrs class.
To alleviate this,attrs provides__attrs_init_subclass__ which is also called once the class is done assembling.The base class doesn’t even have to be anattrs class:
>>>classBase:...@classmethod...def__attrs_init_subclass__(cls):...print(f"Base has been subclassed by attrs{cls}.")>>>@define...classDerived(Base):...passBase has been subclassed by attrs <class 'Derived'>.